refactor: generate.js v2.0 · 动态模板注册表驱动 · 移除硬编码import · 新增listTemplates导出 · CLI增强
This commit is contained in:
parent
991452b544
commit
f348d89272
@ -1,221 +1,185 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ═══════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════
|
||||||
* 铸渊图片工作室 · 神笔马良模式
|
* 铸渊封面工作室 · 生成引擎 · v2.0
|
||||||
* ═══════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════
|
||||||
*
|
*
|
||||||
* 冰朔,你只需要告诉我内容和场景。
|
* 动态模板加载。新增模板只需:
|
||||||
* 我来理解、设计、生成。
|
* 1. 创建 templates/xxx.js
|
||||||
*
|
* 2. 在 templates/registry.js 注册
|
||||||
* 用法(对话中我调用):
|
* 3. 此文件零改动
|
||||||
* fromText("内容...", { for: "小红书" })
|
|
||||||
* fromText("内容...", { for: "海报", mood: "温暖" })
|
|
||||||
*
|
|
||||||
* 或直接调用底层:
|
|
||||||
* generate({ design: {...}, content: {...} })
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { renderToImage, renderCarousel } from './renderer.js'
|
import { renderToImage, renderCarousel } from './renderer.js'
|
||||||
import {
|
import {
|
||||||
xiaohongshuCard, xiaohongshuCarousel
|
TEMPLATE_REGISTRY,
|
||||||
} from './templates/xiaohongshu.js'
|
listTemplates,
|
||||||
import { jikeCard } from './templates/jike.js'
|
getTemplate,
|
||||||
import { posterCard } from './templates/poster.js'
|
getDefaultTemplate,
|
||||||
import { dynamicCard } from './templates/dynamic.js'
|
prepareRenderData,
|
||||||
|
} from './templates/registry.js'
|
||||||
import {
|
import {
|
||||||
STYLES, useScheme,
|
analyzeText,
|
||||||
analyzeText, recommendScheme, recommendLayout, recommendSize
|
recommendScheme,
|
||||||
|
recommendLayout,
|
||||||
} from './config.js'
|
} from './config.js'
|
||||||
|
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════
|
||||||
* 神笔马良 · 主入口
|
* 神笔马良 · 自动分析 + 生成
|
||||||
*
|
|
||||||
* 冰朔给我一段自由文字和场景提示,
|
|
||||||
* 我自动分析内容、推荐设计、生成图片。
|
|
||||||
* ═══════════════════════════════════════════════════ */
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} text - 冰朔说的内容(自由文本)
|
* @param {string} text - 内容(自由文本)
|
||||||
* @param {object} hint - 场景提示(可选)
|
* @param {object} options
|
||||||
* @param {string} hint.for - 发在哪("小红书" / "即刻" / "海报" / ...)
|
* @param {string} options.templateId - 模板ID(默认自动选择)
|
||||||
* @param {string} hint.mood - 情绪("温暖" / "正式" / "极简" / ...)
|
* @param {string} options.presetId - 预设风格ID
|
||||||
* @param {string} hint.scheme - 配色方案名(可选,覆盖自动推荐)
|
* @param {string} options.title - 标题(不传则从文本提取)
|
||||||
* @param {string} hint.title - 如果冰朔没有明确标题,我帮她提炼
|
* @param {string} options.layout - 布局
|
||||||
* @param {string} hint.output - 输出文件名
|
* @param {string} options.output - 输出文件名
|
||||||
* @returns {Promise<{files: string[], analysis: object}>}
|
* @returns {Promise<{files: string[], analysis: object}>}
|
||||||
*/
|
*/
|
||||||
export async function fromText(text, hint = {}) {
|
export async function fromText(text, options = {}) {
|
||||||
/* 1. 分析内容 */
|
// 分析内容
|
||||||
const analysis = analyzeText(text)
|
const analysis = analyzeText(text)
|
||||||
|
|
||||||
/* 2. 确定平台/尺寸 */
|
// 选择模板
|
||||||
const platform = (hint.for || '').toLowerCase()
|
const template = options.templateId
|
||||||
const size = hint.width && hint.height
|
? getTemplate(options.templateId)
|
||||||
? { width: hint.width, height: hint.height }
|
: getDefaultTemplate()
|
||||||
: recommendSize(platform)
|
|
||||||
|
|
||||||
/* 3. 确定配色 */
|
if (!template) {
|
||||||
const schemeName = hint.scheme || recommendScheme(analysis)
|
throw new Error('没有可用的模板')
|
||||||
useScheme(schemeName)
|
}
|
||||||
|
|
||||||
/* 4. 确定布局 */
|
// 提取内容
|
||||||
const layout = hint.layout || recommendLayout(analysis)
|
|
||||||
|
|
||||||
/* 5. 提取/组织内容 */
|
|
||||||
const lines = text.split('\n').filter(Boolean)
|
const lines = text.split('\n').filter(Boolean)
|
||||||
const title = hint.title || lines[0] || ''
|
const title = options.title || lines[0] || ''
|
||||||
const bodyLines = hint.title ? lines : lines.slice(1)
|
const bodyLines = options.title ? lines : lines.slice(1)
|
||||||
const body = bodyLines.join('\n')
|
const body = bodyLines.join('\n')
|
||||||
|
|
||||||
/* 6. 检测海报关键词 */
|
// 自动选择布局
|
||||||
const isPoster = analysis.detected.notice || analysis.detected.recruit
|
const layout = options.layout
|
||||||
|| /海报/i.test(platform)
|
|| recommendLayout(analysis)
|
||||||
|
|| 'default'
|
||||||
|
|
||||||
const isJike = /即刻/i.test(platform) || (size.width === size.height && !isPoster)
|
// 自动选择预设
|
||||||
const isXiaohongshu = /小红书/i.test(platform) || size.height > size.width * 1.2
|
let presetId = options.presetId
|
||||||
|
if (!presetId && template.presets.length > 0) {
|
||||||
/* 7. 生成 */
|
const scheme = recommendScheme(analysis)
|
||||||
let files = []
|
const presetMap = {
|
||||||
const baseName = hint.output || `card_${Date.now()}`
|
cream: 'warm',
|
||||||
|
rose: 'rose',
|
||||||
if (isPoster) {
|
green: 'green',
|
||||||
/* ── 走海报模板,但动态匹配风格 ── */
|
night: 'dark',
|
||||||
const style = analysis.detected.notice ? 'notice'
|
}
|
||||||
: analysis.detected.recruit ? 'recruit'
|
presetId = presetMap[scheme] || template.presets[0].id
|
||||||
: '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 })]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {}) {
|
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 {
|
const {
|
||||||
|
templateId = '',
|
||||||
|
presetId = '',
|
||||||
title = '',
|
title = '',
|
||||||
body = '',
|
body = '',
|
||||||
subtitle = '',
|
subtitle = '',
|
||||||
tag = '',
|
tag = '',
|
||||||
quote = '',
|
|
||||||
emoji = '',
|
|
||||||
footer = '',
|
|
||||||
details = [],
|
|
||||||
cta = '',
|
|
||||||
layout = 'default',
|
layout = 'default',
|
||||||
style = 'default',
|
|
||||||
scheme = '',
|
|
||||||
output = '',
|
output = '',
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
platform = '',
|
|
||||||
type = '',
|
|
||||||
} = opts
|
} = opts
|
||||||
|
|
||||||
// 如果给的是自由文本,走 fromText
|
const template = templateId ? getTemplate(templateId) : getDefaultTemplate()
|
||||||
if (opts.text) {
|
if (!template) {
|
||||||
return fromText(opts.text, {
|
throw new Error(`模板不存在: ${templateId || '(无默认模板)'}` )
|
||||||
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 renderData = prepareRenderData(template.id, presetId, {
|
||||||
const size = width && height
|
title,
|
||||||
|
body,
|
||||||
|
subtitle,
|
||||||
|
tag,
|
||||||
|
layout,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sizes = width && height
|
||||||
? { 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()}`
|
return {
|
||||||
|
files: [file],
|
||||||
/* ── 检测内容特征,自动选模板 ── */
|
templateId: template.id,
|
||||||
const isPoster = details.length > 0 || cta
|
templateName: template.name,
|
||||||
|| /notice|recruit|海报/.test(style)
|
presetId: renderData.presetId,
|
||||||
const isJike = type === 'jike' || layout === 'quote'
|
layout: renderData.layout,
|
||||||
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 })]
|
|
||||||
}
|
}
|
||||||
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 入口
|
* CLI 入口
|
||||||
* ═══════════════════════════════════════════════════ */
|
* ═══════════════════════════════════════════════════ */
|
||||||
@ -231,74 +195,44 @@ async function cli() {
|
|||||||
if (args[i].startsWith('--')) break
|
if (args[i].startsWith('--')) break
|
||||||
values.push(args[i])
|
values.push(args[i])
|
||||||
}
|
}
|
||||||
return values.length === 0 ? undefined
|
return values.length === 1 ? values[0] : values.join(' ')
|
||||||
: values.length === 1 ? values[0]
|
|
||||||
: values
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 神笔马良模式:--text "内容" --for "小红书"
|
|
||||||
const text = getArg('text') || getArg('t')
|
const text = getArg('text') || getArg('t')
|
||||||
if (text) {
|
if (text) {
|
||||||
console.log(`🎨 铸渊正在理解你的内容...`)
|
console.log('🎨 铸渊正在理解你的内容...')
|
||||||
const hint = {
|
const result = await fromText(text, {
|
||||||
for: getArg('for') || getArg('f') || '',
|
templateId: getArg('template'),
|
||||||
mood: getArg('mood') || '',
|
presetId: getArg('preset'),
|
||||||
scheme: getArg('scheme') || '',
|
title: getArg('title'),
|
||||||
title: getArg('title') || '',
|
output: getArg('output'),
|
||||||
output: getArg('output') || '',
|
})
|
||||||
}
|
console.log('✅ 生成完成!')
|
||||||
const result = await fromText(text, hint)
|
console.log(` 📐 模板: ${result.analysis.templateName}`)
|
||||||
console.log(`✅ 生成完成!`)
|
console.log(` 🎨 预设: ${result.analysis.presetId}`)
|
||||||
console.log(` 📐 检测类型: ${result.analysis.type}`)
|
console.log(` 📐 布局: ${result.analysis.layout}`)
|
||||||
console.log(` 🎨 配色方案: ${result.analysis.scheme}`)
|
|
||||||
result.files.forEach(f => console.log(` 📄 ${f}`))
|
result.files.forEach(f => console.log(` 📄 ${f}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 传统模式
|
// 帮助
|
||||||
const type = args.find(a => !a.startsWith('--'))
|
console.log(`
|
||||||
if (!type || type === 'help') {
|
铸渊封面工作室 v2.0 · 神笔马良
|
||||||
console.log(`
|
|
||||||
铸渊图片工作室 · 神笔马良
|
|
||||||
|
|
||||||
🎨 新模式(推荐):
|
🎨 自动模式(推荐):
|
||||||
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 "零基础手搓AI封面生成器\n- 不需要GPU\n- 一行命令部署\n- 语言驱动排版" --template xiaohongshu --preset tech
|
||||||
node generate.js --text "五一放假通知\n时间:5月1日-5日\n祝大家节日快乐" --for "海报"
|
|
||||||
node generate.js --text "岁月不居,时节如流" --for "即刻"
|
|
||||||
`)
|
`)
|
||||||
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')) {
|
if (process.argv[1] && process.argv[1].includes('generate.js')) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user