2026-05-22 23:49:02 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* ═══════════════════════════════════════════════════
|
2026-05-27 18:31:48 +08:00
|
|
|
|
* 铸渊封面工作室 · 生成引擎 · v2.0
|
2026-05-22 23:49:02 +00:00
|
|
|
|
* ═══════════════════════════════════════════════════
|
|
|
|
|
|
*
|
2026-05-27 18:31:48 +08:00
|
|
|
|
* 动态模板加载。新增模板只需:
|
|
|
|
|
|
* 1. 创建 templates/xxx.js
|
|
|
|
|
|
* 2. 在 templates/registry.js 注册
|
|
|
|
|
|
* 3. 此文件零改动
|
2026-05-22 23:49:02 +00:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { renderToImage, renderCarousel } from './renderer.js'
|
2026-05-22 23:51:28 +00:00
|
|
|
|
import {
|
2026-05-27 18:31:48 +08:00
|
|
|
|
TEMPLATE_REGISTRY,
|
|
|
|
|
|
listTemplates,
|
|
|
|
|
|
getTemplate,
|
|
|
|
|
|
getDefaultTemplate,
|
|
|
|
|
|
prepareRenderData,
|
|
|
|
|
|
} from './templates/registry.js'
|
2026-05-22 23:51:28 +00:00
|
|
|
|
import {
|
2026-05-27 18:31:48 +08:00
|
|
|
|
analyzeText,
|
|
|
|
|
|
recommendScheme,
|
|
|
|
|
|
recommendLayout,
|
2026-05-22 23:51:28 +00:00
|
|
|
|
} from './config.js'
|
2026-05-22 23:49:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 23:51:28 +00:00
|
|
|
|
/* ═══════════════════════════════════════════════════
|
2026-05-27 18:31:48 +08:00
|
|
|
|
* 神笔马良 · 自动分析 + 生成
|
2026-05-22 23:51:28 +00:00
|
|
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
|
|
|
2026-05-22 23:49:02 +00:00
|
|
|
|
/**
|
2026-05-27 18:31:48 +08:00
|
|
|
|
* @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 - 输出文件名
|
2026-05-22 23:51:28 +00:00
|
|
|
|
* @returns {Promise<{files: string[], analysis: object}>}
|
2026-05-22 23:49:02 +00:00
|
|
|
|
*/
|
2026-05-27 18:31:48 +08:00
|
|
|
|
export async function fromText(text, options = {}) {
|
|
|
|
|
|
// 分析内容
|
2026-05-22 23:51:28 +00:00
|
|
|
|
const analysis = analyzeText(text)
|
|
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
// 选择模板
|
|
|
|
|
|
const template = options.templateId
|
|
|
|
|
|
? getTemplate(options.templateId)
|
|
|
|
|
|
: getDefaultTemplate()
|
2026-05-22 23:51:28 +00:00
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
if (!template) {
|
|
|
|
|
|
throw new Error('没有可用的模板')
|
|
|
|
|
|
}
|
2026-05-22 23:51:28 +00:00
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
// 提取内容
|
2026-05-22 23:51:28 +00:00
|
|
|
|
const lines = text.split('\n').filter(Boolean)
|
2026-05-27 18:31:48 +08:00
|
|
|
|
const title = options.title || lines[0] || ''
|
|
|
|
|
|
const bodyLines = options.title ? lines : lines.slice(1)
|
2026-05-22 23:51:28 +00:00
|
|
|
|
const body = bodyLines.join('\n')
|
|
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
// 自动选择布局
|
|
|
|
|
|
const layout = options.layout
|
|
|
|
|
|
|| recommendLayout(analysis)
|
|
|
|
|
|
|| '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
|
2026-05-22 23:51:28 +00:00
|
|
|
|
}
|
2026-05-27 18:31:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 准备渲染数据
|
|
|
|
|
|
const renderData = prepareRenderData(template.id, presetId, {
|
|
|
|
|
|
title,
|
|
|
|
|
|
body,
|
|
|
|
|
|
subtitle: options.subtitle || '',
|
|
|
|
|
|
tag: options.tag || '',
|
|
|
|
|
|
layout,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!renderData) {
|
|
|
|
|
|
throw new Error(`模板 "${template.id}" 不存在`)
|
2026-05-22 23:51:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
// 渲染
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
2026-05-22 23:51:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════════════════════════════════════
|
2026-05-27 18:31:48 +08:00
|
|
|
|
* 精确生成 · 指定所有参数
|
2026-05-22 23:51:28 +00:00
|
|
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
|
|
|
2026-05-22 23:49:02 +00:00
|
|
|
|
export async function generate(opts = {}) {
|
2026-05-27 18:31:48 +08:00
|
|
|
|
// 如果给的是自由文本,走 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,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:49:02 +00:00
|
|
|
|
const {
|
2026-05-27 18:31:48 +08:00
|
|
|
|
templateId = '',
|
|
|
|
|
|
presetId = '',
|
2026-05-22 23:49:02 +00:00
|
|
|
|
title = '',
|
|
|
|
|
|
body = '',
|
|
|
|
|
|
subtitle = '',
|
|
|
|
|
|
tag = '',
|
|
|
|
|
|
layout = 'default',
|
|
|
|
|
|
output = '',
|
2026-05-22 23:51:28 +00:00
|
|
|
|
width,
|
|
|
|
|
|
height,
|
2026-05-22 23:49:02 +00:00
|
|
|
|
} = opts
|
|
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
const template = templateId ? getTemplate(templateId) : getDefaultTemplate()
|
|
|
|
|
|
if (!template) {
|
|
|
|
|
|
throw new Error(`模板不存在: ${templateId || '(无默认模板)'}` )
|
2026-05-22 23:51:28 +00:00
|
|
|
|
}
|
2026-05-22 23:49:02 +00:00
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
const renderData = prepareRenderData(template.id, presetId, {
|
|
|
|
|
|
title,
|
|
|
|
|
|
body,
|
|
|
|
|
|
subtitle,
|
|
|
|
|
|
tag,
|
|
|
|
|
|
layout,
|
|
|
|
|
|
})
|
2026-05-22 23:49:02 +00:00
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
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 })
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
files: [file],
|
|
|
|
|
|
templateId: template.id,
|
|
|
|
|
|
templateName: template.name,
|
|
|
|
|
|
presetId: renderData.presetId,
|
|
|
|
|
|
layout: renderData.layout,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-22 23:49:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
|
|
|
|
* 导出模板列表(供 API 使用)
|
|
|
|
|
|
* ═══════════════════════════════════════════════════ */
|
2026-05-22 23:49:02 +00:00
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
export { listTemplates, getTemplate, getDefaultTemplate }
|
2026-05-22 23:49:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 23:51:28 +00:00
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
|
|
|
|
* CLI 入口
|
|
|
|
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
|
|
|
2026-05-22 23:49:02 +00:00
|
|
|
|
async function cli() {
|
|
|
|
|
|
const args = process.argv.slice(2)
|
|
|
|
|
|
|
|
|
|
|
|
function getArg(key) {
|
2026-05-22 23:51:28 +00:00
|
|
|
|
const idx = args.indexOf(`--${key}`)
|
2026-05-22 23:49:02 +00:00
|
|
|
|
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])
|
|
|
|
|
|
}
|
2026-05-27 18:31:48 +08:00
|
|
|
|
return values.length === 1 ? values[0] : values.join(' ')
|
2026-05-22 23:49:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:51:28 +00:00
|
|
|
|
const text = getArg('text') || getArg('t')
|
|
|
|
|
|
if (text) {
|
2026-05-27 18:31:48 +08:00
|
|
|
|
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}`)
|
2026-05-22 23:51:28 +00:00
|
|
|
|
result.files.forEach(f => console.log(` 📄 ${f}`))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
// 帮助
|
|
|
|
|
|
console.log(`
|
|
|
|
|
|
铸渊封面工作室 v2.0 · 神笔马良
|
|
|
|
|
|
|
|
|
|
|
|
🎨 自动模式(推荐):
|
|
|
|
|
|
node generate.js --text "你的内容" --template xiaohongshu --preset tech
|
2026-05-22 23:51:28 +00:00
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
📋 可用模板:
|
|
|
|
|
|
${listTemplates().map(t => `· ${t.icon} ${t.name} (${t.id}) — ${t.description}`).join('\n ')}
|
2026-05-22 23:51:28 +00:00
|
|
|
|
|
2026-05-27 18:31:48 +08:00
|
|
|
|
🎨 可用预设:
|
|
|
|
|
|
${Object.values(TEMPLATE_REGISTRY).flatMap(t =>
|
|
|
|
|
|
t.presets.map(p => `· ${p.name} (${p.id}) — 来自 ${t.name}`)
|
|
|
|
|
|
).join('\n ')}
|
2026-05-22 23:51:28 +00:00
|
|
|
|
|
|
|
|
|
|
示例:
|
2026-05-27 18:31:48 +08:00
|
|
|
|
node generate.js --text "零基础手搓AI封面生成器\n- 不需要GPU\n- 一行命令部署\n- 语言驱动排版" --template xiaohongshu --preset tech
|
2026-05-22 23:51:28 +00:00
|
|
|
|
`)
|
2026-05-22 23:49:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:51:28 +00:00
|
|
|
|
if (process.argv[1] && process.argv[1].includes('generate.js')) {
|
2026-05-22 23:49:02 +00:00
|
|
|
|
cli().catch(err => {
|
|
|
|
|
|
console.error('❌ 生成失败:', err.message)
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|