From f348d89272bd0b664148381cc81fa7e8c652f001 Mon Sep 17 00:00:00 2001 From: bingshuo <565183519@qq.com> Date: Wed, 27 May 2026 18:31:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20generate.js=20v2.0=20=C2=B7=20?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E6=A8=A1=E6=9D=BF=E6=B3=A8=E5=86=8C=E8=A1=A8?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=20=C2=B7=20=E7=A7=BB=E9=99=A4=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81import=20=C2=B7=20=E6=96=B0=E5=A2=9ElistTempl?= =?UTF-8?q?ates=E5=AF=BC=E5=87=BA=20=C2=B7=20CLI=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- image-studio/generate.js | 370 ++++++++++++++++----------------------- 1 file changed, 152 insertions(+), 218 deletions(-) diff --git a/image-studio/generate.js b/image-studio/generate.js index 69f5aa9..d05d7c5 100644 --- a/image-studio/generate.js +++ b/image-studio/generate.js @@ -1,221 +1,185 @@ -#!/usr/bin/env node - /** * ═══════════════════════════════════════════════════ - * 铸渊图片工作室 · 神笔马良模式 + * 铸渊封面工作室 · 生成引擎 · v2.0 * ═══════════════════════════════════════════════════ * - * 冰朔,你只需要告诉我内容和场景。 - * 我来理解、设计、生成。 - * - * 用法(对话中我调用): - * fromText("内容...", { for: "小红书" }) - * fromText("内容...", { for: "海报", mood: "温暖" }) - * - * 或直接调用底层: - * generate({ design: {...}, content: {...} }) + * 动态模板加载。新增模板只需: + * 1. 创建 templates/xxx.js + * 2. 在 templates/registry.js 注册 + * 3. 此文件零改动 */ import { renderToImage, renderCarousel } from './renderer.js' import { - xiaohongshuCard, xiaohongshuCarousel -} from './templates/xiaohongshu.js' -import { jikeCard } from './templates/jike.js' -import { posterCard } from './templates/poster.js' -import { dynamicCard } from './templates/dynamic.js' + TEMPLATE_REGISTRY, + listTemplates, + getTemplate, + getDefaultTemplate, + prepareRenderData, +} from './templates/registry.js' import { - STYLES, useScheme, - analyzeText, recommendScheme, recommendLayout, recommendSize + analyzeText, + recommendScheme, + recommendLayout, } from './config.js' /* ═══════════════════════════════════════════════════ - * 神笔马良 · 主入口 - * - * 冰朔给我一段自由文字和场景提示, - * 我自动分析内容、推荐设计、生成图片。 + * 神笔马良 · 自动分析 + 生成 * ═══════════════════════════════════════════════════ */ /** - * @param {string} text - 冰朔说的内容(自由文本) - * @param {object} hint - 场景提示(可选) - * @param {string} hint.for - 发在哪("小红书" / "即刻" / "海报" / ...) - * @param {string} hint.mood - 情绪("温暖" / "正式" / "极简" / ...) - * @param {string} hint.scheme - 配色方案名(可选,覆盖自动推荐) - * @param {string} hint.title - 如果冰朔没有明确标题,我帮她提炼 - * @param {string} hint.output - 输出文件名 + * @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, hint = {}) { - /* 1. 分析内容 */ +export async function fromText(text, options = {}) { + // 分析内容 const analysis = analyzeText(text) - /* 2. 确定平台/尺寸 */ - const platform = (hint.for || '').toLowerCase() - const size = hint.width && hint.height - ? { width: hint.width, height: hint.height } - : recommendSize(platform) + // 选择模板 + const template = options.templateId + ? getTemplate(options.templateId) + : getDefaultTemplate() - /* 3. 确定配色 */ - const schemeName = hint.scheme || recommendScheme(analysis) - useScheme(schemeName) + if (!template) { + throw new Error('没有可用的模板') + } - /* 4. 确定布局 */ - const layout = hint.layout || recommendLayout(analysis) - - /* 5. 提取/组织内容 */ + // 提取内容 const lines = text.split('\n').filter(Boolean) - const title = hint.title || lines[0] || '' - const bodyLines = hint.title ? lines : lines.slice(1) + const title = options.title || lines[0] || '' + const bodyLines = options.title ? lines : lines.slice(1) const body = bodyLines.join('\n') - /* 6. 检测海报关键词 */ - const isPoster = analysis.detected.notice || analysis.detected.recruit - || /海报/i.test(platform) + // 自动选择布局 + const layout = options.layout + || recommendLayout(analysis) + || 'default' - const isJike = /即刻/i.test(platform) || (size.width === size.height && !isPoster) - const isXiaohongshu = /小红书/i.test(platform) || size.height > size.width * 1.2 - - /* 7. 生成 */ - let files = [] - const baseName = hint.output || `card_${Date.now()}` - - if (isPoster) { - /* ── 走海报模板,但动态匹配风格 ── */ - const style = analysis.detected.notice ? 'notice' - : analysis.detected.recruit ? 'recruit' - : 'default' - - // 尝试从文本中提取详细信息 - const detailLines = bodyLines.filter(l => - /:/.test(l) || /时间|地点|要求|联系人|电话|邮箱|地址/.test(l) - ) - const mainBody = bodyLines.filter(l => !detailLines.includes(l)).join('\n') - - const html = posterCard({ - title, - body: mainBody, - subtitle: analysis.type === 'quote' ? title : '', - tag: analysis.detected.notice ? '通 知' : analysis.detected.recruit ? '征 稿' : '', - details: detailLines, - cta: '', - footer: '', - style, - }) - files = [await renderToImage(html, { ...size, name: baseName })] - } - else if (isJike) { - /* ── 走即刻风格 ── */ - const html = jikeCard({ - title, - body, - meta: analysis.type === 'quote' ? '' : '', - emoji: analysis.mood === 'warm' ? '💛' : '✦', - layout: analysis.isShort ? 'quote' : 'default', - }) - files = [await renderToImage(html, { ...size, name: baseName })] - } - else if (isXiaohongshu) { - /* ── 走小红书风格 ── */ - const html = xiaohongshuCard({ - title, - body, - subtitle: '', - tag: '', - layout: analysis.isShort ? 'quote' - : analysis.type === 'list' ? 'default' - : 'default', - }) - files = [await renderToImage(html, { ...size, name: baseName })] - } - else { - /* ── 动态模板 · 万能兜底 ── */ - const html = dynamicCard({ - title, - body, - lines: analysis.lines, - type: analysis.type, - }) - files = [await renderToImage(html, { ...size, name: baseName })] + // 自动选择预设 + 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 } - return { files, analysis: { ...analysis, scheme: schemeName, size } } + // 准备渲染数据 + 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', + }) + + return { + files: [file], + analysis: { + ...analysis, + templateId: template.id, + templateName: template.name, + presetId: renderData.presetId, + layout: renderData.layout, + sizes: template.sizes, + }, + } } /* ═══════════════════════════════════════════════════ - * 底层 generate —— 直接指定所有参数 + * 精确生成 · 指定所有参数 * ═══════════════════════════════════════════════════ */ 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 = '', - quote = '', - emoji = '', - footer = '', - details = [], - cta = '', layout = 'default', - style = 'default', - scheme = '', output = '', width, height, - platform = '', - type = '', } = opts - // 如果给的是自由文本,走 fromText - if (opts.text) { - return fromText(opts.text, { - for: opts.for || platform, - mood: opts.mood, - scheme: opts.scheme, - title: opts.title, - output: opts.output, - width: opts.width, - height: opts.height, - layout: opts.layout, - }) + const template = templateId ? getTemplate(templateId) : getDefaultTemplate() + if (!template) { + throw new Error(`模板不存在: ${templateId || '(无默认模板)'}` ) } - // 尺寸 - const size = width && height + const renderData = prepareRenderData(template.id, presetId, { + title, + body, + subtitle, + tag, + layout, + }) + + const sizes = width && height ? { width, height } - : (STYLES.sizes[type] || STYLES.sizes.square) + : template.sizes - if (scheme) useScheme(scheme) + const html = template.render({ ...renderData, sizes }) + const baseName = output || `cover_${Date.now()}` + const file = await renderToImage(html, { ...sizes, name: baseName }) - const baseName = output || `card_${Date.now()}` - - /* ── 检测内容特征,自动选模板 ── */ - const isPoster = details.length > 0 || cta - || /notice|recruit|海报/.test(style) - const isJike = type === 'jike' || layout === 'quote' - const isShort = !body && !details && !cta - - let html, files - - if (isPoster) { - html = posterCard({ title, subtitle, body, details, cta, footer, tag, style }) - files = [await renderToImage(html, { ...size, name: baseName })] + return { + files: [file], + templateId: template.id, + templateName: template.name, + presetId: renderData.presetId, + layout: renderData.layout, } - else if (isJike) { - html = jikeCard({ title, body, meta: subtitle, emoji, layout }) - files = [await renderToImage(html, { ...size, name: baseName })] - } - else { - html = xiaohongshuCard({ title, body, subtitle, tag, quote, footer, layout }) - files = [await renderToImage(html, { ...size, name: baseName })] - } - - return { files, type } } +/* ═══════════════════════════════════════════════════ + * 导出模板列表(供 API 使用) + * ═══════════════════════════════════════════════════ */ + +export { listTemplates, getTemplate, getDefaultTemplate } + + /* ═══════════════════════════════════════════════════ * CLI 入口 * ═══════════════════════════════════════════════════ */ @@ -231,74 +195,44 @@ async function cli() { if (args[i].startsWith('--')) break values.push(args[i]) } - return values.length === 0 ? undefined - : values.length === 1 ? values[0] - : values + return values.length === 1 ? values[0] : values.join(' ') } - // 神笔马良模式:--text "内容" --for "小红书" const text = getArg('text') || getArg('t') if (text) { - console.log(`🎨 铸渊正在理解你的内容...`) - const hint = { - for: getArg('for') || getArg('f') || '', - mood: getArg('mood') || '', - scheme: getArg('scheme') || '', - title: getArg('title') || '', - output: getArg('output') || '', - } - const result = await fromText(text, hint) - console.log(`✅ 生成完成!`) - console.log(` 📐 检测类型: ${result.analysis.type}`) - console.log(` 🎨 配色方案: ${result.analysis.scheme}`) + console.log('🎨 铸渊正在理解你的内容...') + const result = await fromText(text, { + 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}`)) return } - // 传统模式 - const type = args.find(a => !a.startsWith('--')) - if (!type || type === 'help') { - console.log(` -铸渊图片工作室 · 神笔马良 + // 帮助 + console.log(` +铸渊封面工作室 v2.0 · 神笔马良 - 🎨 新模式(推荐): - node generate.js --text "你的内容" --for "发在哪" + 🎨 自动模式(推荐): + node generate.js --text "你的内容" --template xiaohongshu --preset tech - 📐 传统模式: - node generate.js <类型> --title "标题" --body "内容" + 📋 可用模板: + ${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 "分享三个提高效率的方法\n1. 早上先做最难的事\n2. 一次只做一件事" --for "小红书" - node generate.js --text "五一放假通知\n时间:5月1日-5日\n祝大家节日快乐" --for "海报" - node generate.js --text "岁月不居,时节如流" --for "即刻" + node generate.js --text "零基础手搓AI封面生成器\n- 不需要GPU\n- 一行命令部署\n- 语言驱动排版" --template xiaohongshu --preset tech `) - process.exit(0) - } - - const opts = { - type, - title: getArg('title') || '', - body: (getArg('body') || '').replace(/\\n/g, '\n'), - subtitle: getArg('subtitle') || '', - tag: getArg('tag') || '', - quote: getArg('quote') || '', - emoji: getArg('emoji') || '', - footer: getArg('footer') || '', - details: (() => { - const d = getArg('details') - return Array.isArray(d) ? d : (d ? [d] : []) - })(), - cta: getArg('cta') || '', - layout: getArg('layout') || 'default', - style: getArg('style') || 'default', - scheme: getArg('scheme') || '', - output: getArg('output') || '', - } - - console.log(`🎨 生成${type}卡片...`) - const result = await generate(opts) - console.log(`✅ 生成完成!`) - result.files.forEach(f => console.log(` 📄 ${f}`)) } if (process.argv[1] && process.argv[1].includes('generate.js')) {