#!/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) }) }