guanghulab/image-studio/generate.js
铸渊 549c51dab0 🎨 神笔马良模式 - 冰朔说内容,铸渊出图
彻底重构图片工作室:
- fromText() 入口:冰朔给自由文本,系统自动分析内容特征
- 自动检测内容类型(notice/recruit/quote/list/article...)
- 自动推荐配色方案(根据情绪和类型)
- 自动推荐布局和尺寸
- 新增 dynamic.js 万能兜底模板
- 保留底层渲染引擎不变

使用方式:
  node generate.js --text "你的内容" --for "小红书/即刻/海报"
或者对话中我直接调用 fromText()
2026-05-22 23:51:28 +00:00

310 lines
10 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.

#!/usr/bin/env node
/**
* ═══════════════════════════════════════════════════
* 铸渊图片工作室 · 神笔马良模式
* ═══════════════════════════════════════════════════
*
* 冰朔,你只需要告诉我内容和场景。
* 我来理解、设计、生成。
*
* 用法(对话中我调用):
* fromText("内容...", { for: "小红书" })
* fromText("内容...", { for: "海报", mood: "温暖" })
*
* 或直接调用底层:
* generate({ design: {...}, content: {...} })
*/
import { renderToImage, renderCarousel } from './renderer.js'
import {
xiaohongshuCard, xiaohongshuCarousel
} from './templates/xiaohongshu.js'
import { jikeCard } from './templates/jike.js'
import { posterCard } from './templates/poster.js'
import { dynamicCard } from './templates/dynamic.js'
import {
STYLES, useScheme,
analyzeText, recommendScheme, recommendLayout, recommendSize
} from './config.js'
/* ═══════════════════════════════════════════════════
* 神笔马良 · 主入口
*
* 冰朔给我一段自由文字和场景提示,
* 我自动分析内容、推荐设计、生成图片。
* ═══════════════════════════════════════════════════ */
/**
* @param {string} text - 冰朔说的内容(自由文本)
* @param {object} hint - 场景提示(可选)
* @param {string} hint.for - 发在哪("小红书" / "即刻" / "海报" / ...
* @param {string} hint.mood - 情绪("温暖" / "正式" / "极简" / ...
* @param {string} hint.scheme - 配色方案名(可选,覆盖自动推荐)
* @param {string} hint.title - 如果冰朔没有明确标题,我帮她提炼
* @param {string} hint.output - 输出文件名
* @returns {Promise<{files: string[], analysis: object}>}
*/
export async function fromText(text, hint = {}) {
/* 1. 分析内容 */
const analysis = analyzeText(text)
/* 2. 确定平台/尺寸 */
const platform = (hint.for || '').toLowerCase()
const size = hint.width && hint.height
? { width: hint.width, height: hint.height }
: recommendSize(platform)
/* 3. 确定配色 */
const schemeName = hint.scheme || recommendScheme(analysis)
useScheme(schemeName)
/* 4. 确定布局 */
const layout = hint.layout || recommendLayout(analysis)
/* 5. 提取/组织内容 */
const lines = text.split('\n').filter(Boolean)
const title = hint.title || lines[0] || ''
const bodyLines = hint.title ? lines : lines.slice(1)
const body = bodyLines.join('\n')
/* 6. 检测海报关键词 */
const isPoster = analysis.detected.notice || analysis.detected.recruit
|| /海报/i.test(platform)
const isJike = /即刻/i.test(platform) || (size.width === size.height && !isPoster)
const isXiaohongshu = /小红书/i.test(platform) || size.height > size.width * 1.2
/* 7. 生成 */
let files = []
const baseName = hint.output || `card_${Date.now()}`
if (isPoster) {
/* ── 走海报模板,但动态匹配风格 ── */
const style = analysis.detected.notice ? 'notice'
: analysis.detected.recruit ? 'recruit'
: '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 } }
}
/* ═══════════════════════════════════════════════════
* 底层 generate —— 直接指定所有参数
* ═══════════════════════════════════════════════════ */
export async function generate(opts = {}) {
const {
title = '',
body = '',
subtitle = '',
tag = '',
quote = '',
emoji = '',
footer = '',
details = [],
cta = '',
layout = 'default',
style = 'default',
scheme = '',
output = '',
width,
height,
platform = '',
type = '',
} = opts
// 如果给的是自由文本,走 fromText
if (opts.text) {
return fromText(opts.text, {
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 size = width && height
? { width, height }
: (STYLES.sizes[type] || STYLES.sizes.square)
if (scheme) useScheme(scheme)
const baseName = output || `card_${Date.now()}`
/* ── 检测内容特征,自动选模板 ── */
const isPoster = details.length > 0 || cta
|| /notice|recruit|海报/.test(style)
const isJike = type === 'jike' || layout === 'quote'
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 }
}
/* ═══════════════════════════════════════════════════
* 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 === 0 ? undefined
: values.length === 1 ? values[0]
: values
}
// 神笔马良模式:--text "内容" --for "小红书"
const text = getArg('text') || getArg('t')
if (text) {
console.log(`🎨 铸渊正在理解你的内容...`)
const hint = {
for: getArg('for') || getArg('f') || '',
mood: getArg('mood') || '',
scheme: getArg('scheme') || '',
title: getArg('title') || '',
output: getArg('output') || '',
}
const result = await fromText(text, hint)
console.log(`✅ 生成完成!`)
console.log(` 📐 检测类型: ${result.analysis.type}`)
console.log(` 🎨 配色方案: ${result.analysis.scheme}`)
result.files.forEach(f => console.log(` 📄 ${f}`))
return
}
// 传统模式
const type = args.find(a => !a.startsWith('--'))
if (!type || type === 'help') {
console.log(`
铸渊图片工作室 · 神笔马良
🎨 新模式(推荐):
node generate.js --text "你的内容" --for "发在哪"
📐 传统模式:
node generate.js <类型> --title "标题" --body "内容"
示例:
node generate.js --text "分享三个提高效率的方法\n1. 早上先做最难的事\n2. 一次只做一件事" --for "小红书"
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')) {
cli().catch(err => {
console.error('❌ 生成失败:', err.message)
process.exit(1)
})
}