root 651cc9e1fd D121: xiaohongshu-cover module v5 complete
MODULE-COVER-001: full package for external AI consumption
- server.js + renderer.js + generate.js + config.js
- templates/ (5 templates: xiaohongshu + dynamic + jike + poster + registry)
- package.json + README.md + MODULE.hdlp
- INDEX.hdlp + SYSTEM.hdlp (HLDP declarations)
- Fonts: Noto Sans CJK SC priority (no tofu blocks)
- No domain watermark
- Updated module-registry.json
- Copyright 2026-A-00037559
2026-06-01 16:36:00 +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) })
}