diff --git a/image-studio/generate.js b/image-studio/generate.js index d05d7c5..d64f375 100644 --- a/image-studio/generate.js +++ b/image-studio/generate.js @@ -1,15 +1,10 @@ /** * ═══════════════════════════════════════════════════ - * 铸渊封面工作室 · 生成引擎 · v2.0 + * 铸渊封面工作室 · 生成引擎 · v2.1 * ═══════════════════════════════════════════════════ - * - * 动态模板加载。新增模板只需: - * 1. 创建 templates/xxx.js - * 2. 在 templates/registry.js 注册 - * 3. 此文件零改动 */ -import { renderToImage, renderCarousel } from './renderer.js' +import { renderToImage } from './renderer.js' import { TEMPLATE_REGISTRY, listTemplates, @@ -20,148 +15,99 @@ import { import { analyzeText, recommendScheme, - recommendLayout, } 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: '✨', + }, + ] +} -/* ═══════════════════════════════════════════════════ - * 神笔马良 · 自动分析 + 生成 - * ═══════════════════════════════════════════════════ */ - -/** - * @param {string} text - 内容(自由文本) - * @param {object} options - * @param {string} options.templateId - 模板ID(默认自动选择) - * @param {string} options.presetId - 预设风格ID - * @param {string} options.title - 标题(不传则从文本提取) - * @param {string} options.layout - 布局 - * @param {string} options.output - 输出文件名 - * @returns {Promise<{files: string[], analysis: object}>} - */ export async function fromText(text, options = {}) { - // 分析内容 const analysis = analyzeText(text) - // 选择模板 const template = options.templateId ? getTemplate(options.templateId) : getDefaultTemplate() - if (!template) { - throw new Error('没有可用的模板') - } + 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 - || recommendLayout(analysis) - || 'default' + 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', - } + 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, + title, body, subtitle: options.subtitle || '', tag: options.tag || '', layout, }) - if (!renderData) { - throw new Error(`模板 "${template.id}" 不存在`) - } + 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 file = await renderToImage(html, { ...template.sizes, name: baseName, format: 'png' }) - return { - files: [file], - analysis: { - ...analysis, - templateId: template.id, - templateName: template.name, - presetId: renderData.presetId, - layout: renderData.layout, - sizes: template.sizes, - }, - } -} - - -/* ═══════════════════════════════════════════════════ - * 精确生成 · 指定所有参数 - * ═══════════════════════════════════════════════════ */ - -export async function generate(opts = {}) { - // 如果给的是自由文本,走 fromText - 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 userText = title + (body ? '\n' + body : '') return { files: [file], @@ -169,75 +115,73 @@ export async function generate(opts = {}) { 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, + }) + } -/* ═══════════════════════════════════════════════════ - * 导出模板列表(供 API 使用) - * ═══════════════════════════════════════════════════ */ + 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 入口 - * ═══════════════════════════════════════════════════ */ - +// CLI async function cli() { const args = process.argv.slice(2) - function getArg(key) { - const idx = args.indexOf(`--${key}`) - if (idx === -1) return undefined - const values = [] - for (let i = idx + 1; i < args.length; i++) { - if (args[i].startsWith('--')) break - values.push(args[i]) - } - return values.length === 1 ? values[0] : values.join(' ') + 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 text = getArg('text') || getArg('t') - if (text) { - console.log('🎨 铸渊正在理解你的内容...') - const result = await fromText(text, { - templateId: getArg('template'), - presetId: getArg('preset'), - title: getArg('title'), - output: getArg('output'), + 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(` 📐 模板: ${result.analysis.templateName}`) - console.log(` 🎨 预设: ${result.analysis.presetId}`) - console.log(` 📐 布局: ${result.analysis.layout}`) - result.files.forEach(f => console.log(` 📄 ${f}`)) + console.log('✅ 生成完成') + console.log(` 📐 ${r.templateName} · 🎨 ${r.presetId} · 📏 ${r.layout}`) + r.files.forEach(f => console.log(` 📄 ${f}`)) return } - - // 帮助 - console.log(` -铸渊封面工作室 v2.0 · 神笔马良 - - 🎨 自动模式(推荐): - node generate.js --text "你的内容" --template xiaohongshu --preset tech - - 📋 可用模板: - ${listTemplates().map(t => `· ${t.icon} ${t.name} (${t.id}) — ${t.description}`).join('\n ')} - - 🎨 可用预设: - ${Object.values(TEMPLATE_REGISTRY).flatMap(t => - t.presets.map(p => `· ${p.name} (${p.id}) — 来自 ${t.name}`) - ).join('\n ')} - -示例: - node generate.js --text "零基础手搓AI封面生成器\n- 不需要GPU\n- 一行命令部署\n- 语言驱动排版" --template xiaohongshu --preset tech -`) + 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) - }) + cli().catch(err => { console.error('❌', err.message); process.exit(1) }) }