guanghulab/image-studio/generate.js

244 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ═══════════════════════════════════════════════════
* 铸渊封面工作室 · 生成引擎 · v2.0
* ═══════════════════════════════════════════════════
*
* 动态模板加载。新增模板只需:
* 1. 创建 templates/xxx.js
* 2. 在 templates/registry.js 注册
* 3. 此文件零改动
*/
import { renderToImage, renderCarousel } from './renderer.js'
import {
TEMPLATE_REGISTRY,
listTemplates,
getTemplate,
getDefaultTemplate,
prepareRenderData,
} from './templates/registry.js'
import {
analyzeText,
recommendScheme,
recommendLayout,
} from './config.js'
/* ═══════════════════════════════════════════════════
* 神笔马良 · 自动分析 + 生成
* ═══════════════════════════════════════════════════ */
/**
* @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('没有可用的模板')
}
// 提取内容
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'
// 自动选择预设
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',
})
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 })
return {
files: [file],
templateId: template.id,
templateName: template.name,
presetId: renderData.presetId,
layout: renderData.layout,
}
}
/* ═══════════════════════════════════════════════════
* 导出模板列表(供 API 使用)
* ═══════════════════════════════════════════════════ */
export { listTemplates, getTemplate, getDefaultTemplate }
/* ═══════════════════════════════════════════════════
* 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 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'),
})
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
}
// 帮助
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
`)
}
if (process.argv[1] && process.argv[1].includes('generate.js')) {
cli().catch(err => {
console.error('❌ 生成失败:', err.message)
process.exit(1)
})
}