🎨 神笔马良模式 - 冰朔说内容,铸渊出图
彻底重构图片工作室: - fromText() 入口:冰朔给自由文本,系统自动分析内容特征 - 自动检测内容类型(notice/recruit/quote/list/article...) - 自动推荐配色方案(根据情绪和类型) - 自动推荐布局和尺寸 - 新增 dynamic.js 万能兜底模板 - 保留底层渲染引擎不变 使用方式: node generate.js --text "你的内容" --for "小红书/即刻/海报" 或者对话中我直接调用 fromText()
This commit is contained in:
parent
c1b3c02ef6
commit
549c51dab0
@ -36,11 +36,14 @@ export const STYLES = {
|
|||||||
mono: '"JetBrains Mono", "Fira Code", monospace',
|
mono: '"JetBrains Mono", "Fira Code", monospace',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 卡片尺寸 */
|
/* 卡片尺寸 —— 这些只是参考,可以随时改 */
|
||||||
sizes: {
|
sizes: {
|
||||||
xiaohongshu: { width: 1080, height: 1440 }, // 3:4
|
xiaohongshu: { width: 1080, height: 1440 }, // 3:4
|
||||||
jike: { width: 1080, height: 1080 }, // 1:1
|
jike: { width: 1080, height: 1080 }, // 1:1
|
||||||
poster: { width: 1080, height: 1920 }, // 9:16 海报
|
poster: { width: 1080, height: 1920 }, // 9:16 海报
|
||||||
|
wechat: { width: 1080, height: 1350 }, // 4:5 朋友圈
|
||||||
|
square: { width: 1080, height: 1080 }, // 1:1 通用方图
|
||||||
|
wide: { width: 1920, height: 1080 }, // 16:9 宽图
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 排版 */
|
/* 排版 */
|
||||||
@ -70,55 +73,170 @@ export const STYLES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── 预先定义几套配色方案 ── */
|
/* ── 配色方案 ── */
|
||||||
|
|
||||||
export const COLOR_SCHEMES = {
|
export const COLOR_SCHEMES = {
|
||||||
|
|
||||||
/* 光湖极简(默认) */
|
|
||||||
guanghu: {
|
guanghu: {
|
||||||
name: '光湖极简',
|
name: '光湖极简',
|
||||||
colors: {
|
primary: '#1a1a2e', bg: '#faf8f5', accent: '#0f3460',
|
||||||
primary: '#1a1a2e', bg: '#faf8f5', accent: '#0f3460',
|
highlight: '#e94560', gold: '#c9a96e', warm: '#f5e6c8',
|
||||||
highlight: '#e94560', gold: '#c9a96e', warm: '#f5e6c8',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 奶油暖调 */
|
|
||||||
cream: {
|
cream: {
|
||||||
name: '奶油暖调',
|
name: '奶油暖调',
|
||||||
colors: {
|
primary: '#5d4037', bg: '#fef7f0', accent: '#8d6e63',
|
||||||
primary: '#5d4037', bg: '#fef7f0', accent: '#8d6e63',
|
highlight: '#e07a5f', gold: '#d4a373', warm: '#fae1dd',
|
||||||
highlight: '#e07a5f', gold: '#d4a373', warm: '#fae1dd',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 暗夜深蓝 */
|
|
||||||
night: {
|
night: {
|
||||||
name: '暗夜深蓝',
|
name: '暗夜深蓝',
|
||||||
colors: {
|
primary: '#e0e0e0', bg: '#0d1117', accent: '#58a6ff',
|
||||||
primary: '#e0e0e0', bg: '#0d1117', accent: '#58a6ff',
|
highlight: '#f78166', gold: '#d4a373', warm: '#21262d',
|
||||||
highlight: '#f78166', gold: '#d4a373', warm: '#21262d',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 文艺绿植 */
|
|
||||||
green: {
|
green: {
|
||||||
name: '文艺绿植',
|
name: '文艺绿植',
|
||||||
colors: {
|
primary: '#2d3e2f', bg: '#f5f9f2', accent: '#5a8f5a',
|
||||||
primary: '#2d3e2f', bg: '#f5f9f2', accent: '#5a8f5a',
|
highlight: '#c78b5c', gold: '#b8a06e', warm: '#e8f0e0',
|
||||||
highlight: '#c78b5c', gold: '#b8a06e', warm: '#e8f0e0',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
rose: {
|
||||||
|
name: '玫瑰粉调',
|
||||||
|
primary: '#4a1942', bg: '#fdf2f8', accent: '#9d4e8d',
|
||||||
|
highlight: '#e8435e', gold: '#c9a96e', warm: '#fce4ec',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useScheme(name) {
|
||||||
|
const scheme = COLOR_SCHEMES[name]
|
||||||
|
if (!scheme) return false
|
||||||
|
const c = STYLES.colors
|
||||||
|
c.primary = scheme.primary; c.bg = scheme.bg
|
||||||
|
c.accent = scheme.accent; c.highlight = scheme.highlight
|
||||||
|
c.gold = scheme.gold; c.warm = scheme.warm
|
||||||
|
STYLES.name = scheme.name
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* 内容感知 · 自动检测函数
|
||||||
|
* ═══════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 冰朔给我一段文字,我分析它是什么类型的、什么情绪、
|
||||||
|
* 适合什么布局。不需要她告诉我。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析文本特征,返回检测结果
|
||||||
|
*/
|
||||||
|
export function analyzeText(text) {
|
||||||
|
if (!text) return { type: 'empty' }
|
||||||
|
|
||||||
|
const t = text.trim()
|
||||||
|
|
||||||
|
/* ── 特征检测 ── */
|
||||||
|
const hasList = /^[-•·*]\s|^\d+[\.\)]\s/m.test(t) // 列表项
|
||||||
|
const isShort = t.length < 30 // 短句(金句)
|
||||||
|
const isMedium = t.length < 100 // 中等长度
|
||||||
|
const hasTitle = t.includes('标题') || t.includes(':') // 可能有标题
|
||||||
|
const lines = t.split('\n').filter(Boolean)
|
||||||
|
const lineCount = lines.length
|
||||||
|
|
||||||
|
const keywords = {
|
||||||
|
notice: /通知|公告|放假|放假安排|节假日|放假通知/i,
|
||||||
|
recruit: /征稿|收稿|投稿|征集|诚征|招聘|招募|稿酬/i,
|
||||||
|
share: /干货|教程|指南|技巧|方法|如何|步骤|经验|分享/i,
|
||||||
|
quote: /说|说过|句话|名言|语录|金句|记得|曾经/i,
|
||||||
|
product: /新品|上线|发布|推出|产品|功能|更新/i,
|
||||||
|
event: /活动|沙龙|讲座|直播|分享会|聚会/i,
|
||||||
|
}
|
||||||
|
|
||||||
|
const detected = {}
|
||||||
|
let type = 'general'
|
||||||
|
|
||||||
|
for (const [key, re] of Object.entries(keywords)) {
|
||||||
|
if (re.test(t)) detected[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 类型推断 ── */
|
||||||
|
if (detected.notice) type = 'notice'
|
||||||
|
else if (detected.recruit) type = 'recruit'
|
||||||
|
else if (detected.quote || isShort) type = 'quote'
|
||||||
|
else if (detected.share || hasList) type = 'list'
|
||||||
|
else if (detected.product) type = 'product'
|
||||||
|
else if (detected.event) type = 'event'
|
||||||
|
else if (lineCount >= 4) type = 'article'
|
||||||
|
else if (isMedium) type = 'paragraph'
|
||||||
|
|
||||||
|
/* ── 情绪推断(基于关键词) ── */
|
||||||
|
let mood = 'neutral'
|
||||||
|
const warmWords = /感谢|温暖|爱|喜欢|开心|快乐|幸福|美好|谢谢/i
|
||||||
|
const urgentWords = /紧急|重要|注意|必读|截止|最后/i
|
||||||
|
const elegantWords = /岁月|时光|静好|诗意|远方|温柔/i
|
||||||
|
|
||||||
|
if (elegantWords.test(t)) mood = 'elegant'
|
||||||
|
else if (warmWords.test(t)) mood = 'warm'
|
||||||
|
else if (urgentWords.test(t)) mood = 'urgent'
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
mood,
|
||||||
|
lineCount,
|
||||||
|
isShort,
|
||||||
|
hasList,
|
||||||
|
lines,
|
||||||
|
detected,
|
||||||
|
text: t,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换到指定配色方案
|
* 根据内容分析结果推荐配色方案
|
||||||
*/
|
*/
|
||||||
export function useScheme(name) {
|
export function recommendScheme(analysis) {
|
||||||
const scheme = COLOR_SCHEMES[name]
|
const { mood, type } = analysis
|
||||||
if (!scheme) return false
|
|
||||||
Object.assign(STYLES.colors, scheme.colors)
|
if (mood === 'warm') return 'cream'
|
||||||
STYLES.name = scheme.name
|
if (mood === 'elegant') return 'green'
|
||||||
return true
|
if (mood === 'urgent') return 'guanghu'
|
||||||
|
|
||||||
|
if (type === 'recruit') return 'cream'
|
||||||
|
if (type === 'notice') return 'guanghu'
|
||||||
|
if (type === 'quote') return 'rose'
|
||||||
|
|
||||||
|
return 'guanghu'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据内容分析推荐布局
|
||||||
|
*/
|
||||||
|
export function recommendLayout(analysis) {
|
||||||
|
const { type, hasList, isShort, lineCount } = analysis
|
||||||
|
|
||||||
|
if (isShort) return 'quote'
|
||||||
|
if (type === 'notice') return 'notice'
|
||||||
|
if (type === 'recruit') return 'recruit'
|
||||||
|
if (hasList) return 'list'
|
||||||
|
if (lineCount <= 3) return 'minimal'
|
||||||
|
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据内容分析推荐尺寸
|
||||||
|
* @param {'xiaohongshu'|'jike'|'poster'|'wechat'|'wide'|string} platform
|
||||||
|
*/
|
||||||
|
export function recommendSize(platform) {
|
||||||
|
const sizes = STYLES.sizes
|
||||||
|
if (sizes[platform]) return sizes[platform]
|
||||||
|
|
||||||
|
// 关键词匹配
|
||||||
|
if (/小红书/i.test(platform)) return sizes.xiaohongshu
|
||||||
|
if (/即刻/i.test(platform)) return sizes.jike
|
||||||
|
if (/海报/i.test(platform) || /通知/i.test(platform) || /收稿/i.test(platform)) return sizes.poster
|
||||||
|
if (/朋友圈/i.test(platform)) return sizes.wechat
|
||||||
|
if (/宽/i.test(platform) || /横/i.test(platform)) return sizes.wide
|
||||||
|
|
||||||
|
// 默认识别
|
||||||
|
return sizes.square
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,52 +2,153 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ═══════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════
|
||||||
* 铸渊图片工作室 · 主入口
|
* 铸渊图片工作室 · 神笔马良模式
|
||||||
* ═══════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════
|
||||||
*
|
*
|
||||||
* 冰朔,你告诉我内容和想要的类型,我帮你生成。
|
* 冰朔,你只需要告诉我内容和场景。
|
||||||
|
* 我来理解、设计、生成。
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法(对话中我调用):
|
||||||
* node generate.js xiaohongshu --title "标题" --body "正文"
|
* fromText("内容...", { for: "小红书" })
|
||||||
* node generate.js jike --title "观点" --body "内容"
|
* fromText("内容...", { for: "海报", mood: "温暖" })
|
||||||
* node generate.js poster --title "海报标题" --details "时间:5月1日" "地点:广州"
|
|
||||||
*
|
*
|
||||||
* 也可以直接在代码里调用 API:
|
* 或直接调用底层:
|
||||||
* import { generate } from './generate.js'
|
* generate({ design: {...}, content: {...} })
|
||||||
* await generate({ type: 'xiaohongshu', title: '...', body: '...' })
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { renderToImage, renderCarousel } from './renderer.js'
|
import { renderToImage, renderCarousel } from './renderer.js'
|
||||||
import { xiaohongshuCard, xiaohongshuCarousel } from './templates/xiaohongshu.js'
|
import {
|
||||||
|
xiaohongshuCard, xiaohongshuCarousel
|
||||||
|
} from './templates/xiaohongshu.js'
|
||||||
import { jikeCard } from './templates/jike.js'
|
import { jikeCard } from './templates/jike.js'
|
||||||
import { posterCard } from './templates/poster.js'
|
import { posterCard } from './templates/poster.js'
|
||||||
import { STYLES, useScheme } from './config.js'
|
import { dynamicCard } from './templates/dynamic.js'
|
||||||
|
import {
|
||||||
|
STYLES, useScheme,
|
||||||
|
analyzeText, recommendScheme, recommendLayout, recommendSize
|
||||||
|
} from './config.js'
|
||||||
|
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* 神笔马良 · 主入口
|
||||||
|
*
|
||||||
|
* 冰朔给我一段自由文字和场景提示,
|
||||||
|
* 我自动分析内容、推荐设计、生成图片。
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一生成入口
|
* @param {string} text - 冰朔说的内容(自由文本)
|
||||||
* @param {object} opts
|
* @param {object} hint - 场景提示(可选)
|
||||||
* @param {'xiaohongshu'|'jike'|'poster'} opts.type - 卡片类型
|
* @param {string} hint.for - 发在哪("小红书" / "即刻" / "海报" / ...)
|
||||||
* @param {string} opts.title - 标题
|
* @param {string} hint.mood - 情绪("温暖" / "正式" / "极简" / ...)
|
||||||
* @param {string} opts.body - 正文
|
* @param {string} hint.scheme - 配色方案名(可选,覆盖自动推荐)
|
||||||
* @param {string} opts.subtitle - 副标题
|
* @param {string} hint.title - 如果冰朔没有明确标题,我帮她提炼
|
||||||
* @param {string} opts.tag - 标签
|
* @param {string} hint.output - 输出文件名
|
||||||
* @param {string} opts.quote - 金句(小红书 quote 布局用)
|
* @returns {Promise<{files: string[], analysis: object}>}
|
||||||
* @param {string} opts.emoji - Emoji(即刻用)
|
|
||||||
* @param {string} opts.footer - 底部文字
|
|
||||||
* @param {string[]} opts.details - 详情列表(海报用)
|
|
||||||
* @param {string} opts.cta - 行动号召(海报用)
|
|
||||||
* @param {'default'|'quote'|'list'|'minimal'} opts.layout - 布局
|
|
||||||
* @param {'default'|'notice'|'recruit'} opts.style - 海报风格
|
|
||||||
* @param {string} opts.scheme - 配色方案名
|
|
||||||
* @param {string} opts.output - 输出文件名(不含扩展名)
|
|
||||||
* @param {boolean} opts.carousel - 是否拆为多页轮播
|
|
||||||
* @param {string[]} opts.pages - 多页正文(轮播用)
|
|
||||||
* @returns {Promise<{files: string[], type: string}>}
|
|
||||||
*/
|
*/
|
||||||
|
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 = {}) {
|
export async function generate(opts = {}) {
|
||||||
const {
|
const {
|
||||||
type = 'xiaohongshu',
|
|
||||||
title = '',
|
title = '',
|
||||||
body = '',
|
body = '',
|
||||||
subtitle = '',
|
subtitle = '',
|
||||||
@ -61,93 +162,70 @@ export async function generate(opts = {}) {
|
|||||||
style = 'default',
|
style = 'default',
|
||||||
scheme = '',
|
scheme = '',
|
||||||
output = '',
|
output = '',
|
||||||
carousel = false,
|
width,
|
||||||
pages = [],
|
height,
|
||||||
|
platform = '',
|
||||||
|
type = '',
|
||||||
} = opts
|
} = 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)
|
if (scheme) useScheme(scheme)
|
||||||
|
|
||||||
const baseName = output || `${type}_${Date.now()}`
|
const baseName = output || `card_${Date.now()}`
|
||||||
const size = STYLES.sizes[type] || STYLES.sizes.xiaohongshu
|
|
||||||
|
|
||||||
let files = []
|
/* ── 检测内容特征,自动选模板 ── */
|
||||||
|
const isPoster = details.length > 0 || cta
|
||||||
|
|| /notice|recruit|海报/.test(style)
|
||||||
|
const isJike = type === 'jike' || layout === 'quote'
|
||||||
|
const isShort = !body && !details && !cta
|
||||||
|
|
||||||
switch (type) {
|
let html, files
|
||||||
case 'xiaohongshu': {
|
|
||||||
if (carousel && pages.length > 0) {
|
|
||||||
const htmls = xiaohongshuCarousel({ title, subtitle, tag, quote, layout }, pages)
|
|
||||||
files = await renderCarousel(htmls, { ...size, baseName })
|
|
||||||
} else {
|
|
||||||
const html = xiaohongshuCard({ title, body, subtitle, tag, quote, footer, layout })
|
|
||||||
files = [await renderToImage(html, { ...size, name: baseName })]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'jike': {
|
if (isPoster) {
|
||||||
const html = jikeCard({ title, body, meta: subtitle, emoji, layout })
|
html = posterCard({ title, subtitle, body, details, cta, footer, tag, style })
|
||||||
files = [await renderToImage(html, { ...size, name: baseName })]
|
files = [await renderToImage(html, { ...size, name: baseName })]
|
||||||
break
|
}
|
||||||
}
|
else if (isJike) {
|
||||||
|
html = jikeCard({ title, body, meta: subtitle, emoji, layout })
|
||||||
case 'poster': {
|
files = [await renderToImage(html, { ...size, name: baseName })]
|
||||||
const html = posterCard({ title, subtitle, body, details, cta, footer, tag, style })
|
}
|
||||||
files = [await renderToImage(html, { ...size, name: baseName })]
|
else {
|
||||||
break
|
html = xiaohongshuCard({ title, body, subtitle, tag, quote, footer, layout })
|
||||||
}
|
files = [await renderToImage(html, { ...size, name: baseName })]
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`未知卡片类型: ${type}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { files, type }
|
return { files, type }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── CLI 入口 ── */
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* CLI 入口
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
async function cli() {
|
async function cli() {
|
||||||
const args = process.argv.slice(2)
|
const args = process.argv.slice(2)
|
||||||
const type = args.find(a => !a.startsWith('--'))
|
|
||||||
|
|
||||||
if (!type || type === 'help') {
|
|
||||||
console.log(`
|
|
||||||
铸渊图片工作室 · 用法
|
|
||||||
|
|
||||||
node generate.js <类型> [选项]
|
|
||||||
|
|
||||||
类型:
|
|
||||||
xiaohongshu 小红书竖版卡片 (1080×1440)
|
|
||||||
jike 即刻方卡 (1080×1080)
|
|
||||||
poster 海报 (1080×1920)
|
|
||||||
|
|
||||||
选项:
|
|
||||||
--title 标题
|
|
||||||
--body 正文
|
|
||||||
--subtitle 副标题/元信息
|
|
||||||
--tag 标签
|
|
||||||
--quote 金句(小红书 quote 布局)
|
|
||||||
--emoji Emoji(即刻)
|
|
||||||
--details 详情(海报,可多个)
|
|
||||||
--cta 行动号召(海报)
|
|
||||||
--footer 底部文字
|
|
||||||
--layout 布局: default|quote|list|minimal
|
|
||||||
--style 海报风格: default|notice|recruit
|
|
||||||
--scheme 配色: guanghu|cream|night|green
|
|
||||||
--output 输出文件名
|
|
||||||
|
|
||||||
示例:
|
|
||||||
node generate.js xiaohongshu --title "如何高效学习" --body "第一点\\n第二点\\n第三点"
|
|
||||||
node generate.js jike --title "一个思考" --body "今天想到的..." --emoji "💡"
|
|
||||||
node generate.js poster --title "放假通知" --style notice --details "时间:5月1日" "地点:广州"
|
|
||||||
`)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArg(key) {
|
function getArg(key) {
|
||||||
const idx = args.indexOf(key)
|
const idx = args.indexOf(`--${key}`)
|
||||||
if (idx === -1) return undefined
|
if (idx === -1) return undefined
|
||||||
// 收集后续所有非 -- 开头的参数,直到遇到下一个 --
|
|
||||||
const values = []
|
const values = []
|
||||||
for (let i = idx + 1; i < args.length; i++) {
|
for (let i = idx + 1; i < args.length; i++) {
|
||||||
if (args[i].startsWith('--')) break
|
if (args[i].startsWith('--')) break
|
||||||
@ -158,29 +236,63 @@ async function cli() {
|
|||||||
: values
|
: 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 = {
|
const opts = {
|
||||||
type,
|
type,
|
||||||
title: getArg('--title') || '',
|
title: getArg('title') || '',
|
||||||
body: (() => {
|
body: (getArg('body') || '').replace(/\\n/g, '\n'),
|
||||||
const b = getArg('--body')
|
subtitle: getArg('subtitle') || '',
|
||||||
const text = Array.isArray(b) ? b.join('\n') : (b || '')
|
tag: getArg('tag') || '',
|
||||||
// 将 \n 转义符转为真实换行
|
quote: getArg('quote') || '',
|
||||||
return text.replace(/\\n/g, '\n')
|
emoji: getArg('emoji') || '',
|
||||||
})(),
|
footer: getArg('footer') || '',
|
||||||
subtitle: getArg('--subtitle') || '',
|
|
||||||
tag: getArg('--tag') || '',
|
|
||||||
quote: getArg('--quote') || '',
|
|
||||||
emoji: getArg('--emoji') || '',
|
|
||||||
footer: getArg('--footer') || '',
|
|
||||||
details: (() => {
|
details: (() => {
|
||||||
const d = getArg('--details')
|
const d = getArg('details')
|
||||||
return Array.isArray(d) ? d : (d ? [d] : [])
|
return Array.isArray(d) ? d : (d ? [d] : [])
|
||||||
})(),
|
})(),
|
||||||
cta: getArg('--cta') || '',
|
cta: getArg('cta') || '',
|
||||||
layout: getArg('--layout') || 'default',
|
layout: getArg('layout') || 'default',
|
||||||
style: getArg('--style') || 'default',
|
style: getArg('style') || 'default',
|
||||||
scheme: getArg('--scheme') || '',
|
scheme: getArg('scheme') || '',
|
||||||
output: getArg('--output') || '',
|
output: getArg('output') || '',
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🎨 生成${type}卡片...`)
|
console.log(`🎨 生成${type}卡片...`)
|
||||||
@ -189,8 +301,7 @@ async function cli() {
|
|||||||
result.files.forEach(f => console.log(` 📄 ${f}`))
|
result.files.forEach(f => console.log(` 📄 ${f}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果直接运行
|
if (process.argv[1] && process.argv[1].includes('generate.js')) {
|
||||||
if (process.argv[1] && (process.argv[1].includes('generate.js'))) {
|
|
||||||
cli().catch(err => {
|
cli().catch(err => {
|
||||||
console.error('❌ 生成失败:', err.message)
|
console.error('❌ 生成失败:', err.message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
|||||||
BIN
image-studio/output/test_ma,Liang_1.png
Normal file
BIN
image-studio/output/test_ma,Liang_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
image-studio/output/test_maliang_poster.png
Normal file
BIN
image-studio/output/test_maliang_poster.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
image-studio/output/test_maliang_quote.png
Normal file
BIN
image-studio/output/test_maliang_quote.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
192
image-studio/templates/dynamic.js
Normal file
192
image-studio/templates/dynamic.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* ═══════════════════════════════════════════════════
|
||||||
|
* 万能动态模板 · 自适应任何内容
|
||||||
|
* ═══════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 不是固定类型。是根据冰朔的文字内容,现场设计。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { STYLES } from '../config.js'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 万能动态卡片
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.title - 提炼的标题
|
||||||
|
* @param {string} data.body - 正文
|
||||||
|
* @param {string[]} data.lines - 原文行数组
|
||||||
|
* @param {string} data.type - 内容类型(analyzeText 的结果)
|
||||||
|
*/
|
||||||
|
export function dynamicCard(data) {
|
||||||
|
const { title, body, lines = [], type = 'general' } = data
|
||||||
|
const C = STYLES.colors
|
||||||
|
const F = STYLES.fonts
|
||||||
|
const T = STYLES.typography
|
||||||
|
const W = 1080
|
||||||
|
const H = 1080
|
||||||
|
|
||||||
|
// 动态计算字号:内容越多字越小
|
||||||
|
const totalChars = (title + body).length
|
||||||
|
const titleSize = totalChars < 20 ? 52 : totalChars < 50 ? 44 : 36
|
||||||
|
const bodySize = totalChars < 50 ? 32 : totalChars < 150 ? 26 : 22
|
||||||
|
|
||||||
|
// 检测有没有列表
|
||||||
|
const hasList = lines.some(l => /^[-•·*]\s/.test(l.trim()))
|
||||||
|
// 检测是不是一句话
|
||||||
|
const isSingleLine = lines.length <= 2 && totalChars < 40
|
||||||
|
|
||||||
|
/* ── 生成内容 HTML ── */
|
||||||
|
let contentHtml = ''
|
||||||
|
|
||||||
|
if (isSingleLine) {
|
||||||
|
/* 一句话 · 大字居中 */
|
||||||
|
contentHtml = `
|
||||||
|
<div class="single-line">${escapeHtml(title)}</div>
|
||||||
|
`
|
||||||
|
} else if (hasList) {
|
||||||
|
/* 列表内容 */
|
||||||
|
const items = lines
|
||||||
|
.filter(l => /^[-•·*]\s/.test(l.trim()))
|
||||||
|
.map(l => `<li>${escapeHtml(l.replace(/^[-•·*]\s*/, ''))}</li>`)
|
||||||
|
.join('\n')
|
||||||
|
contentHtml = `
|
||||||
|
<div class="title" style="font-size:${titleSize}px">${escapeHtml(title)}</div>
|
||||||
|
<ul class="dynamic-list">${items}</ul>
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
/* 一般段落 */
|
||||||
|
const paragraphs = lines
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(l => `<p>${escapeHtml(l)}</p>`)
|
||||||
|
.join('\n')
|
||||||
|
contentHtml = `
|
||||||
|
<div class="title" style="font-size:${titleSize}px">${escapeHtml(title)}</div>
|
||||||
|
<div class="body-text" style="font-size:${bodySize}px">${paragraphs}</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: ${W}px;
|
||||||
|
height: ${H}px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: ${C.bg};
|
||||||
|
font-family: ${F.body};
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动态背景装饰 */
|
||||||
|
.bg-circle {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.04;
|
||||||
|
}
|
||||||
|
.bg-c1 {
|
||||||
|
width: 400px; height: 400px;
|
||||||
|
background: ${C.accent};
|
||||||
|
top: -100px; right: -100px;
|
||||||
|
}
|
||||||
|
.bg-c2 {
|
||||||
|
width: 250px; height: 250px;
|
||||||
|
background: ${C.highlight};
|
||||||
|
bottom: -60px; left: -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 56px 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line {
|
||||||
|
font-family: ${F.title};
|
||||||
|
font-size: ${titleSize}px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${C.primary};
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: ${F.title};
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: ${C.primary};
|
||||||
|
margin-bottom: 24px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-text {
|
||||||
|
line-height: 1.7;
|
||||||
|
color: ${C.text};
|
||||||
|
}
|
||||||
|
.body-text p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.dynamic-list li {
|
||||||
|
font-size: ${bodySize}px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${C.text};
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: ${C.warm}40;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid ${C.accent};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部品牌 */
|
||||||
|
.brand-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${C.textMuted};
|
||||||
|
opacity: ${STYLES.brand.opacity};
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="bg-circle bg-c1"></div>
|
||||||
|
<div class="bg-circle bg-c2"></div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
${contentHtml}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brand-bar">${STYLES.brand.text}</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user