feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+152
View File
@@ -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;