guanghulab/image-studio/generate.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

188 lines
6.6 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.1
* ═══════════════════════════════════════════════════
*/
import { renderToImage } from './renderer.js'
import {
TEMPLATE_REGISTRY,
listTemplates,
getTemplate,
getDefaultTemplate,
prepareRenderData,
} from './templates/registry.js'
import {
analyzeText,
recommendScheme,
} 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: '✨',
},
]
}
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 || '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' })
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, 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,
})
}
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
async function cli() {
const args = process.argv.slice(2)
function getArg(key) {
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 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(` 📐 ${r.templateName} · 🎨 ${r.presetId} · 📏 ${r.layout}`)
r.files.forEach(f => console.log(` 📄 ${f}`))
return
}
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) })
}