feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 画个标 · 核心:表情设计师 AI 编排(Emoji AI)
|
||||
* AI 输出结构化设计决策 JSON(布局/风格/动画/分层)→ 系统负责渲染与导出;
|
||||
* AI 失败/无 Key 时返回 null,由 emojiGen 降级为内置随机池(页面不空白)。
|
||||
*
|
||||
* 设计要点:
|
||||
* - 与 aiDesigner 同模式:buildPrompt → HGB_AI.chat(json) → extractJson → validate → 决策
|
||||
* - validate 把所有输出修正到合法白名单(布局/风格/动画类型/参数),非法即回退
|
||||
* - 纯函数 + 环境分级容错(Node 无头无 HGB_AI 时返回 null,不算失败)
|
||||
*/
|
||||
const HGB_EmojiAI = (function () {
|
||||
'use strict';
|
||||
|
||||
/* ============ Prompt 编排 ============ */
|
||||
|
||||
function layoutList() {
|
||||
if (typeof HGB_EmojiGen !== 'undefined') return HGB_EmojiGen.EMOJI_LAYOUTS.join('/');
|
||||
return 'orb/badge/seal/burst/duo/emblem/hexagon/diamond/arch/geo/techcorner/fullbleed/concentric/centroid';
|
||||
}
|
||||
|
||||
function buildPrompt(opt) {
|
||||
const ind = opt.industry || {};
|
||||
const iconDesc = (opt.icons && opt.icons.length)
|
||||
? opt.icons.map(i => i.name || i.id).join('、')
|
||||
: '(未指定,由 AI 建议行业意象)';
|
||||
const mood = opt.mood ? '用户期望情绪:' + opt.mood + ';' : '';
|
||||
// 情绪-动画映射(生成前置,先审后生成:用户给出期望情绪时优先匹配,避免动效与情绪割裂)
|
||||
const moodMap = opt.mood
|
||||
? '情绪-动画映射参考(用户给出期望情绪时优先匹配,可适度发挥但勿选明显冲突类型):' +
|
||||
'开心→bounce、生气→shake、惊讶→heartbeat、难过→float、兴奋→hopspin、平静→breath。'
|
||||
: '';
|
||||
const sys = [
|
||||
'你是「画个标」的动态表情设计师。用户给出行业与图标素材,你输出「一张静态底图 + 一个主动画」的结构化设计决策。',
|
||||
'铁律:只输出 JSON,不要任何解释文字。',
|
||||
'字段与取值:',
|
||||
'- overallConcept:一句话设计概念(中文,10-20 字)',
|
||||
'- base.layoutId:从白名单选一个:' + layoutList(),
|
||||
'- base.styleId:从系统风格库选一个(如 minimal/neon/flat/artdeco/corporate 等,按行业气质匹配)',
|
||||
'- anim.type:从动画库选一个:bounce/pulse/wiggle/float/spin/fade/twirl/heartbeat/shake/breath/hopspin',
|
||||
'- anim.speed:0.6-1.5(周期数,生气/兴奋偏快,平静偏慢)',
|
||||
'- anim.amplitude:0.7-1.4(强度,活泼可大,克制宜小)',
|
||||
'- anim.layered:true/false(是否元素分层错峰动画,让表情更活;多元素图形建议 true)',
|
||||
'- anim.concept:动效概念一句话(中文)',
|
||||
moodMap,
|
||||
'示例:{"overallConcept":"咖啡的松弛感","base":{"layoutId":"seal","styleId":"neon"},"anim":{"type":"float","speed":0.8,"amplitude":0.9,"layered":true,"concept":"蒸汽缓缓漂浮"}}'
|
||||
].join('\n');
|
||||
|
||||
const user = [
|
||||
'行业:' + (ind.name || '通用') + '(' + (ind.emoji || '') + ',' + (ind.desc || '') + ')',
|
||||
'品牌/主题:' + (opt.brand || '动态表情'),
|
||||
'用户图标素材:' + iconDesc,
|
||||
mood,
|
||||
'请输出严格 JSON(不要 markdown 代码块)。'
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
return [
|
||||
{ role: 'system', content: sys },
|
||||
{ role: 'user', content: user }
|
||||
];
|
||||
}
|
||||
|
||||
/* ============ 解析与校验 ============ */
|
||||
|
||||
/** 从聊天文本提取 JSON 对象(取第一个完整 {} 块,容忍前后缀文字) */
|
||||
function extractJson(text) {
|
||||
if (!text || typeof text !== 'string') return null;
|
||||
const start = text.indexOf('{');
|
||||
if (start < 0) return null;
|
||||
let depth = 0, inStr = false, esc = false;
|
||||
for (let i = start; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (inStr) {
|
||||
if (esc) esc = false;
|
||||
else if (c === '\\') esc = true;
|
||||
else if (c === '"') inStr = false;
|
||||
} else if (c === '"') inStr = true;
|
||||
else if (c === '{') depth++;
|
||||
else if (c === '}') {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
try { return JSON.parse(text.slice(start, i + 1)); } catch (e) { return null; }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inList(v, list) { return list.indexOf(v) >= 0; }
|
||||
|
||||
/** 修正 AI 输出到合法白名单(非法项回退合理默认,返回完整决策对象) */
|
||||
function validate(d, opt) {
|
||||
const ind = opt.industry || {};
|
||||
const dec = d || {};
|
||||
const base = dec.base || {};
|
||||
const anim = dec.anim || {};
|
||||
const layouts = (typeof HGB_EmojiGen !== 'undefined') ? HGB_EmojiGen.EMOJI_LAYOUTS : [];
|
||||
const animTypes = (typeof HGB_Motion !== 'undefined') ? HGB_Motion.animTypes() : [];
|
||||
const styleIds = (typeof HGB_Styles !== 'undefined') ? HGB_Styles.map(s => s.id) : [];
|
||||
|
||||
let layoutId = inList(base.layoutId, layouts) ? base.layoutId : null;
|
||||
if (!layoutId && ind.favLayout && inList(ind.favLayout, layouts)) layoutId = ind.favLayout;
|
||||
if (!layoutId) layoutId = layouts[0] || 'orb';
|
||||
|
||||
const styleId = inList(base.styleId, styleIds) ? base.styleId : (opt.styleIds && opt.styleIds[0]);
|
||||
|
||||
const animType = inList(anim.type, animTypes.map(t => t.id)) ? anim.type : null;
|
||||
let aType = animType || 'bounce';
|
||||
|
||||
const clamp = (v, lo, hi, dft) => {
|
||||
const n = Number(v);
|
||||
return (isFinite(n) && n >= lo && n <= hi) ? n : dft;
|
||||
};
|
||||
|
||||
return {
|
||||
overallConcept: String(dec.overallConcept || '').trim().slice(0, 30) || '动态表情包',
|
||||
base: { layoutId, styleId },
|
||||
anim: {
|
||||
type: aType,
|
||||
speed: clamp(anim.speed, 0.6, 1.5, 1),
|
||||
amplitude: clamp(anim.amplitude, 0.7, 1.4, 1),
|
||||
layered: !!anim.layered,
|
||||
concept: String(anim.concept || '').trim().slice(0, 30)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* ============ 主入口 ============ */
|
||||
|
||||
/**
|
||||
* AI 表情设计决策(失败/无 Key/超时 → 返回 null,供生成器降级)
|
||||
* @param {object} opt { industry, brand, icons, mood }
|
||||
* @returns {Promise<{overallConcept, base:{layoutId,styleId}, anim:{...}}|null>}
|
||||
*/
|
||||
async function suggest(opt) {
|
||||
if (typeof HGB_AI === 'undefined') return null;
|
||||
try {
|
||||
if (typeof HGB_AI.hasKey === 'function' && !HGB_AI.hasKey()) return null;
|
||||
const messages = buildPrompt(opt);
|
||||
const content = await HGB_AI.chat(messages, { json: true, maxTokens: 2048 });
|
||||
const d = extractJson(content);
|
||||
if (!d) return null;
|
||||
return validate(d, opt);
|
||||
} catch (e) {
|
||||
return null; // AI 失败 → 生成器降级内置池
|
||||
}
|
||||
}
|
||||
|
||||
return { buildPrompt, extractJson, validate, suggest };
|
||||
})();
|
||||
|
||||
// Node 无头冒烟自检(浏览器同样执行,幂等)
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = HGB_EmojiAI;
|
||||
Reference in New Issue
Block a user