Files
drawlogo/js/core/emojiAi.js
T

153 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 画个标 · 核心:表情设计师 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;