/** * ═══════════════════════════════════════════════════ * 铸渊封面工作室 · 生成引擎 · v2.1 * ═══════════════════════════════════════════════════ */ import { renderToImage } from './renderer.js' import { TEMPLATE_REGISTRY, listTemplates, getTemplate, getDefaultTemplate, prepareRenderData, } from './templates/registry.js' import { analyzeText, recommendScheme, } from './config.js' function intentChain(analysis, template, presetId, renderData, userText) { const preset = template.presets.find(p => p.id === presetId) || template.presets[0] return [ { step: '你说', detail: userText || (renderData.title + (renderData.body ? '\n' + renderData.body : '')), who: '用户', icon: '💬', }, { step: '理解意图', detail: `内容类型=${analysis.type || '通用'} · 情绪=${analysis.mood || '中性'} · 行数=${analysis.lineCount || 1}`, who: '系统', icon: '🔍', why: analysis.type === 'quote' ? '检测到短句/金句模式' : analysis.type === 'list' ? '检测到列表结构,适合教程排版' : '通用内容,标准排版', }, { step: '选择模板', detail: `${template.icon} ${template.name} · ${template.sizes.width}×${template.sizes.height}`, who: '系统', icon: '📐', why: `匹配内容比例和平台特性`, }, { step: '推荐配色', detail: `${preset ? preset.name : '默认'} · ${analysis.mood === 'warm' ? '暖色调匹配温暖情绪' : analysis.mood === 'elegant' ? '文艺色调匹配优雅情绪' : '科技色调'}`, who: '系统', icon: '🎨', why: `根据内容情绪「${analysis.mood || '中性'}」自动匹配`, }, { step: '选择布局', detail: `${renderData.layout === 'hero' ? '大标题封面 · 强调视觉冲击' : renderData.layout === 'quote' ? '金句居中 · 文字为核心' : '标准排版 · 图文并茂'}`, who: '系统', icon: '📏', why: renderData.layout === 'hero' ? '标题长而正文短,适合 Hero 布局' : renderData.layout === 'quote' ? '短文本适合居中展示' : '包含列表或多段落,适合标准排版', }, { step: '渲染引擎', detail: `Puppeteer + Chrome Headless · 纯 HTML/CSS 排版 · 输出 1080p PNG`, who: '系统', icon: '⚡', why: '零 GPU · 纯排版渲染 · 非 AI 生图', }, { step: '署名', detail: '封面设计由你驱动 · 系统只是你的手', who: '用户', icon: '✨', }, ] } export async function fromText(text, options = {}) { const analysis = analyzeText(text) const template = options.templateId ? getTemplate(options.templateId) : getDefaultTemplate() if (!template) throw new Error('没有可用的模板') const lines = text.split('\n').filter(Boolean) const title = options.title || lines[0] || '' const bodyLines = options.title ? lines : lines.slice(1) const body = bodyLines.join('\n') const layout = options.layout || 'default' let presetId = options.presetId if (!presetId && template.presets.length > 0) { const scheme = recommendScheme(analysis) const presetMap = { cream: 'warm', rose: 'rose', green: 'green', night: 'dark' } presetId = presetMap[scheme] || template.presets[0].id } const renderData = prepareRenderData(template.id, presetId, { title, body, subtitle: options.subtitle || '', tag: options.tag || '', layout, }) if (!renderData) throw new Error(`模板 "${template.id}" 不存在`) const html = template.render(renderData) const baseName = options.output || `cover_${Date.now()}` const file = await renderToImage(html, { ...template.sizes, name: baseName, format: 'png' }) const userText = title + (body ? '\n' + body : '') return { files: [file], templateId: template.id, templateName: template.name, presetId: renderData.presetId, layout: renderData.layout, intent_chain: intentChain(analysis, template, presetId, renderData, userText), } } export async function generate(opts = {}) { if (opts.text) { return fromText(opts.text, { templateId: opts.templateId, presetId: opts.presetId, title: opts.title, subtitle: opts.subtitle, tag: opts.tag, layout: opts.layout, output: opts.output, }) } const { templateId = '', presetId = '', title = '', body = '', subtitle = '', tag = '', layout = 'default', output = '', width, height } = opts const template = templateId ? getTemplate(templateId) : getDefaultTemplate() if (!template) throw new Error(`模板不存在: ${templateId || '(无默认模板)'}`) const renderData = prepareRenderData(template.id, presetId, { title, body, subtitle, tag, layout, }) const sizes = width && height ? { width, height } : template.sizes const html = template.render({ ...renderData, sizes }) const baseName = output || `cover_${Date.now()}` const file = await renderToImage(html, { ...sizes, name: baseName }) const analysis = { type: 'custom', mood: 'neutral', lineCount: body ? body.split('\n').length : 1 } const userText = title + (body ? '\n' + body : '') return { files: [file], templateId: template.id, templateName: template.name, presetId: renderData.presetId, layout: renderData.layout, intent_chain: intentChain(analysis, template, renderData.presetId, renderData, userText), } } export { listTemplates, getTemplate, getDefaultTemplate } // CLI async function cli() { const args = process.argv.slice(2) function getArg(key) { const i = args.indexOf(`--${key}`) if (i === -1) return const v = [] for (let j = i + 1; j < args.length && !args[j].startsWith('--'); j++) v.push(args[j]) return v.length === 1 ? v[0] : v.join(' ') } const t = getArg('text') || getArg('t') if (t) { const r = await fromText(t, { templateId: getArg('template'), presetId: getArg('preset'), title: getArg('title'), output: getArg('output'), }) console.log('✅ 生成完成') console.log(` 📐 ${r.templateName} · 🎨 ${r.presetId} · 📏 ${r.layout}`) r.files.forEach(f => console.log(` 📄 ${f}`)) return } console.log(`\n铸渊封面工作室 v2.1 · 神笔马良\n node generate.js --text "内容" --template xiaohongshu --preset tech\n`) } if (process.argv[1] && process.argv[1].includes('generate.js')) { cli().catch(err => { console.error('❌', err.message); process.exit(1) }) }