Moyin Creator 源码深度解析
魔因漫创 · v0.2.3(2026-04-10 快照)· AGPL-3.0-or-later · Electron 桌面应用
仓库:MemeCalculate/moyin-creator · 解析 by kang · 2026-04-13
memefast.top 的"开源客户端 + 卖 API Key"商业模式。叙事骨架→5 阶段分镜校准→图像生成→视频生成的流水线是真的,但所有重计算都在外部 API 上,应用本身是个精细的调度壳。
快速导览(35 节)
- 项目定位
- 代码规模
- 技术栈 & 依赖
- 架构总览
- 主进程职责
- IPC 通道清单
- 首启动 Demo Seed
- Model Registry
- Feature Router
- Batch Processor
- Key Manager & 黑名单
- Task Poller
- callChatAPI 实现
- Episode Parser
- Script Normalizer
- Character Finder
- Viewpoint Generator
- Full Script Service
- 5 阶段总览
- Stage 1 叙事骨架
- Stage 2 视觉描述
- Stage 3 拍摄控制
- Stage 4 首帧提示词
- Stage 5 动态 + 尾帧
- Character Bible
- Storyboard 拼图
- Grid Calculator
- 52 视觉风格库
- 26 摄影档案
- 默认供应商解码
- memefast 生意链
- 灌篮少女 Demo
- 风险清单
- 决策矩阵
- 系统 Prompt 全文
1. 项目定位
魔因漫创(Moyin Creator)是一款基于 Electron 的 AI 影视分镜创作桌面应用,官方描述为"剧本到成片全流程批量化工具"。从源码看,它的本质是:
- 一个精细的 AI 调度壳:自己不做任何模型,通过 OpenAI 兼容协议调用外部 API
- 一套剧本→分镜的流水线编排:中文剧本规则解析 + 5 阶段 AI 校准 + 提示词自动生成
- 一个 memefast.top 的付费入口:默认全部功能绑定自营中转,卖 Key 变现(见 §31)
不是什么:
- 不是开源的 AI 视频生成模型
- 不是 Web SaaS,不能 headless 部署到 VPS
- 不是真的"一键成片"——成品质量完全取决于背后调用的 Seedance 2.0 / Veo / Sora 等闭源模型
- 不是视觉层面的角色锚定——README 吹的"6 层身份锚点"实际是 prompt 注入(见 §25)
2. 代码规模
核心目录 LOC 分布
| 目录 | 文件数 | 行数 | 说明 |
|---|---|---|---|
src/lib/script/ | 22 | ~13,750 | 剧本解析 & 分镜生成的主战场 |
src/lib/storyboard/ | 6 | ~2,390 | 分镜拼图 / 网格计算 / prompt 构建 |
src/lib/ai/ | 8 | ~1,400 | Model Registry / Feature Router / Batch / Runninghub |
src/lib/freedom/ | 5 | ~3,680 | Freedom 面板(自由图/视频生成模式) |
src/lib/constants/ | 多 | ~1,149 | 视觉风格 + 摄影档案常量表 |
src/packages/ai-core/ | 10 | ~909 | 类型定义层,真正的 AI 调度在 lib/ |
src/components/panels/ | 多 | 大量 | Director / SClass / Freedom / Settings 面板 UI |
electron/main.ts | 1 | ~1,700 | Electron 主进程:窗口/IPC/存储/更新 |
3. 技术栈 & 依赖
| 层 | 技术 | 版本 |
|---|---|---|
| 桌面壳 | Electron + electron-vite | 30 / 5 |
| UI 框架 | React + Radix UI | 18 |
| 样式 | Tailwind CSS v4 + shadcn 组件风格 | 4.1 |
| 状态 | Zustand + zustand/middleware persist | 5 |
| 构建 | electron-builder | 24 |
| 图像处理 | sharp (native) + mediabunny (video) | 0.34 / 1.37 |
| 表单 | react-hook-form | 7 |
| 动效 | motion (前身 framer-motion) | 12 |
| 图表 | recharts | 3 |
| UI 辅助 | cmdk / vaul / sonner / lucide-react | - |
| 工具 | nanoid / next-themes / tailwind-merge | - |
后端情况
无独立后端。src/app/api/ 只有两个 route handler,且都在 Electron renderer 内部调用:
- src/app/api/proxy-image/route.ts — 图片跨域代理
- src/app/api/ai/runninghub-test/route.ts — RunningHub 连通性测试
4. 架构总览
5. Electron 主进程职责
electron/main.ts 大约 1700 行,负责五大类职责:
5.1 窗口 & 生命周期
标准 BrowserWindow,contextIsolation: true + preload.ts,渲染进程通过 ipcRenderer.invoke 调用主进程 API。
5.2 本地文件存储
项目数据存两个根目录:
- Project Data Root:默认
~/Library/Application Support/魔因漫创/projects/(macOS),里面按_p/{projectId}/分目录,每项目有 6 个 JSON 文件(characters / scenes / script / director / sclass / media) - Media Root:图片/视频二进制文件,按类别分目录(
characters/ scenes/ shots/等)
用户可以通过 storage-link-data IPC 重新绑定到自定义目录,通过 storage-move-data 搬运数据。
5.3 图片处理
主进程直接用 Node.js fetch 下载远程 URL 到本地(save-image),支持 Base64 读回(read-image-base64),用 sharp 做缩略图,支持图床上传(image-host-upload)。
5.4 自更新
app-updater-check IPC 请求 http://68.64.176.186/moyin/version.json(raw IP + HTTP!),拉到的 version.json 里的新版本就触发"请更新"弹窗。打包版会下载 .dmg 替换,dev 模式下这个流程对开发者无效。
5.5 Demo Seed(首次启动自动植入演示)
启动时调用 seedDemoProject()(main.ts:1624),检查用户数据目录是否存在 moyin-project-store.json。不存在就从 demo-data/ 或 process.resourcesPath/demo-data/ 拷贝演示项目:
electron/main.ts:1604-1629
function getDemoDataPath(): string {
if (VITE_DEV_SERVER_URL) {
return path.join(process.env.APP_ROOT!, 'demo-data')
}
return path.join(process.resourcesPath, 'demo-data')
}
function seedDemoProject() {
const projectDataRoot = getProjectDataRoot()
const marker = path.join(projectDataRoot, 'moyin-project-store.json')
if (fs.existsSync(marker)) {
// Not first run — project store already exists
return
}
// ... 递归拷贝 demo-data/projects/* 和 demo-data/media/*
}
6. IPC 通道清单(36+ 个)
按职责分类:
图片与媒体
save-image— 下载远程图片到本地get-image-path/get-absolute-path— 路径解析delete-image— 删除本地图片read-image-base64— 二进制读为 Base64(给 API 上传图用)image-host-upload— 上传到图床
文件存储(通用 KV + 目录操作)
file-storage-get / set / remove / existsfile-storage-list/file-storage-list-dirsfile-storage-remove-dir
数据目录管理
storage-get-paths— 获取当前数据目录storage-select-directory— 打开系统目录选择器storage-validate-data-dir/storage-link-data/storage-move-datastorage-export-data/storage-import-data— 全量导入导出storage-validate-project-dir/storage-link-project-datastorage-link-media-datastorage-get-cache-size/storage-clear-cache/storage-update-config
自更新
app-updater-get-current-versionapp-updater-checkapp-updater-open-link— 外部浏览器打开 GitHub/百度网盘
系统集成
save-file-dialog— 原生"另存为"对话框
7. 首启动 Demo Seed 流程
首次运行时会自动植入演示项目,源自 demo-data/projects/_p/a4bbe260-0127-49c7-9230-e766402663c7/。这是作者从"G:\漫剧数据"(Windows 路径,见 scripts/prepare-demo-data.cjs)抽出的一份真实工作副本。
Demo 项目名:《灌篮少女》
这个 demo 不需要你花一分钱就能看到作者预生成的完整效果:剧本 / 角色参考图 / 场景图 / 分镜草稿 / 视角切换等全都在。详见 §32。
8. Model Registry(工程亮点 1/3)
src/lib/ai/model-registry.ts · 303 行
项目自称 AI 调度中心核心组件 1。职责:根据模型名称查询 contextWindow 和 maxOutput 限制,支持三层查找和错误学习。
8.1 三层查找策略
model-registry.ts:125-142
export function getModelLimits(modelName: string): ModelLimits {
const m = modelName.toLowerCase();
// Layer 1: 持久化缓存(最准确,从 API 错误中学到的真实值)
if (_getDiscoveredLimits) {
const discovered = _getDiscoveredLimits(m);
if (discovered) {
const staticFallback = lookupStatic(m);
return {
contextWindow: discovered.contextWindow ?? staticFallback.contextWindow,
maxOutput: discovered.maxOutput ?? staticFallback.maxOutput,
};
}
}
// Layer 2 + 3: 静态注册表 → _default
return lookupStatic(m);
}
8.2 静态注册表 & Prefix 匹配
model-registry.ts:50-95
const STATIC_REGISTRY: Record<string, ModelLimits> = {
'deepseek-v3.2': { contextWindow: 128000, maxOutput: 8192 },
'deepseek-r1': { contextWindow: 128000, maxOutput: 16384 },
'glm-4.7': { contextWindow: 200000, maxOutput: 128000 },
'gemini-2.5-flash': { contextWindow: 1048576, maxOutput: 65536 },
'gemini-3-pro-preview': { contextWindow: 1048576, maxOutput: 65536 },
// ... 更多精确匹配
// prefix 规则(按长度降序匹配,避免短前缀误命中)
'deepseek-': { contextWindow: 128000, maxOutput: 8192 },
'gemini-': { contextWindow: 1048576, maxOutput: 65536 },
'claude-': { contextWindow: 200000, maxOutput: 8192 },
'gpt-': { contextWindow: 128000, maxOutput: 16384 },
'_default': { contextWindow: 32000, maxOutput: 4096 },
};
// Pre-sort keys by length descending for prefix matching
const SORTED_KEYS = Object.keys(STATIC_REGISTRY)
.filter(k => k !== '_default')
.sort((a, b) => b.length - a.length);
'gemini-2.5-flash' 这种精确 key 先于 'gemini-' 匹配。换句话说,'gemini-foobar-preview' 如果不在静态表里,会 fallback 到 'gemini-' 的默认(1M ctx / 65K out),而不是落到 _default。
8.3 Error-driven Discovery(最亮眼)
当 API 返回 400 错误时,自动解析错误消息里的限制信息并持久化,下次同一个模型就知道正确值。支持解析多种主流 API 的错误格式:
model-registry.ts:178-229
export function parseModelLimitsFromError(errorText: string)
: Partial<DiscoveredModelLimits> | null {
const result: Partial<DiscoveredModelLimits> = {};
let found = false;
// Pattern 1: "valid range of max_tokens is [1, 8192]" (DeepSeek)
const rangeMatch = errorText.match(/valid\s+range.*?\[\s*\d+\s*,\s*(\d+)\s*\]/i);
if (rangeMatch) {
result.maxOutput = parseInt(rangeMatch[1], 10);
found = true;
}
// Pattern 2: "max_tokens must be less than or equal to 8192" (智谱 GLM)
if (!found) {
const lteMatch = errorText.match(
/max_tokens.*?(?:less than or equal to|<=|不超过|上限为?)\s*(\d{3,6})/i
);
if (lteMatch) { result.maxOutput = parseInt(lteMatch[1], 10); found = true; }
}
// Pattern 3: Generic fallback
if (!found) {
const genericMatch = errorText.match(/max_tokens.*?\b(\d{3,6})\b/i);
if (genericMatch) { result.maxOutput = parseInt(genericMatch[1], 10); found = true; }
}
// context length pattern: "maximum context length is 128000 tokens" (OpenAI)
const ctxMatch = errorText.match(/context.*?length.*?(\d{4,7})/i);
if (ctxMatch) { result.contextWindow = parseInt(ctxMatch[1], 10); found = true; }
// ...
return result;
}
写回缓存逻辑:
model-registry.ts:235-247
export function cacheDiscoveredLimits(
modelName: string,
limits: Partial<DiscoveredModelLimits>,
): boolean {
if (!_setDiscoveredLimits) return false;
_setDiscoveredLimits(modelName.toLowerCase(), limits);
console.log(`[ModelRegistry] 🧠 已学习 ${modelName} 的限制:`, ...);
return true;
}
这个设计特别适合中转 API(像 memefast.top),因为中转平台常常对模型参数做自定义封装,官方文档查不到准确的限制。撞一次 400 就能学到,后续自动避让。
8.4 Token 估算
model-registry.ts:260-262
export function estimateTokens(text: string): number {
return Math.ceil(text.length / 1.5);
}
刻意用保守值(字符数/1.5)。中文 1 token ≈ 0.6~1.0 汉字,除 1.5 相当于放大估算;英文/JSON 1 token ≈ 3~4 字符,除 1.5 也偏保守。设计哲学:宁可多分批也不撞限制。
8.5 智能截断
safeTruncate() 优先在换行/中英文句末截断(。!?.),避免在 JSON 中间切断导致解析失败。截断后追加 ...[后续内容已截断] 提示 AI 信息不完整,减少幻觉。
9. Feature Router(工程亮点 2/3)
src/lib/ai/feature-router.ts · 351 行
9.1 8 种 AI 功能类型
stores/api-config-store.ts:33-41
export type AIFeature =
| 'script_analysis' // 剧本分析
| 'character_generation' // 角色图片生成
| 'scene_generation' // 场景图片生成
| 'video_generation' // 视频生成
| 'image_understanding' // 图片理解/分析
| 'chat' // 通用对话
| 'freedom_image' // 自由板块-图片生成
| 'freedom_video'; // 自由板块-视频生成
9.2 默认平台映射(所有功能默认走 memefast)
feature-router.ts:41-49
const FEATURE_PLATFORM_MAP: Partial<Record<AIFeature, string>> = {
script_analysis: 'memefast',
character_generation: 'memefast',
video_generation: 'memefast',
image_understanding: 'memefast',
chat: 'memefast',
freedom_image: 'memefast',
freedom_video: 'memefast',
};
这是整个项目最诚实的一段代码——告诉你所有 AI 功能的默认去向。
9.3 多模型轮询调度
每个功能可以绑定多个 platform:model,router 内部用 Map 记录每个功能的当前索引,每次调用自动轮换:
feature-router.ts:36, 188-197
const featureRoundRobinIndex: Map<AIFeature, number> = new Map();
// ...
// 多模型轮询
const currentIndex = featureRoundRobinIndex.get(feature) || 0;
const config = configs[currentIndex % configs.length];
featureRoundRobinIndex.set(feature, currentIndex + 1);
console.log(`[FeatureRouter] 多模型轮询: ${feature} -> ${config.provider.name}:${config.model} (${currentIndex % configs.length + 1}/${configs.length})`);
return config;
使用场景:假设 script_analysis 绑定了 memefast:deepseek-v3.2 和 memefast:glm-4.7,批量跑 100 集剧本时会交替调用两个模型,分散单模型负载和 Key 限流风险。
9.4 统一调用入口
feature-router.ts:253-294
export async function callFeatureAPI(
feature: AIFeature,
systemPrompt: string,
userPrompt: string,
options?: CallFeatureAPIOptions
): Promise<string> {
const config = options?.configOverride || getFeatureConfig(feature);
if (!config) {
throw new Error(getFeatureNotConfiguredMessage(feature));
}
const model = options?.modelOverride || config.model || config.models?.[0];
const baseUrl = config.baseUrl?.replace(/\/+$/, '');
// ... 校验
// 结构化 JSON 输出任务默认关闭深度思考,避免 reasoning 耗尽 token
const disableThinking = options?.disableThinking ?? true;
return await callChatAPI(systemPrompt, userPrompt, {
apiKey: config.allApiKeys.join(','),
provider: 'openai',
baseUrl, model,
temperature: options?.temperature,
maxTokens: options?.maxTokens,
keyManager: config.keyManager,
disableThinking,
});
}
10. Adaptive Batch Processor(工程亮点 3/3)
src/lib/ai/batch-processor.ts · 327 行
项目自称 AI 调度中心核心组件 3。职责:将大量 items(比如 100 个分镜)自动分批发给 AI,同时满足 input 和 output token 双重约束。
10.1 核心常量
batch-processor.ts:24-33
/** 无论模型支持多大上下文,每批 input 最多 60K token */
const HARD_CAP_TOKENS = 60000;
/** 单批次最大重试次数 */
const MAX_BATCH_RETRIES = 2;
/** 重试基础延迟(ms),指数退避 */
const RETRY_BASE_DELAY = 3000;
代码注释说得很清楚:"防止超长上下文模型 TTFT 过高 / Lost in the middle"。即使 Gemini 2.5 Pro 能吃 1M token,真的塞进去,首 token 时间会很长,而且 LLM 在超长上下文中容易忽略中间的信息。60K 是性能和效果的折衷点。
10.2 双重约束贪心分组
batch-processor.ts:131-160
const inputBudget = Math.min(
Math.floor(limits.contextWindow * 0.6), // 只用 60% ctx
HARD_CAP_TOKENS
);
const outputBudget = Math.floor(limits.maxOutput * 0.8); // 留 20% 给 JSON 格式开销
// ...
const batches = createBatches(
items,
getItemTokens,
getItemOutputTokens,
inputBudget,
outputBudget,
systemPromptTokens,
);
分组算法(batch-processor.ts:246-285):
function createBatches<TItem>(
items: TItem[],
getItemTokens: (item: TItem) => number,
getItemOutputTokens: (item: TItem) => number,
inputBudget: number,
outputBudget: number,
systemPromptTokens: number,
): TItem[][] {
const batches: TItem[][] = [];
let currentBatch: TItem[] = [];
let currentInputTokens = systemPromptTokens; // system prompt 每批都要带
let currentOutputTokens = 0;
for (const item of items) {
const itemInput = getItemTokens(item);
const itemOutput = getItemOutputTokens(item);
const wouldExceedInput = currentInputTokens + itemInput > inputBudget;
const wouldExceedOutput = currentOutputTokens + itemOutput > outputBudget;
if (currentBatch.length > 0 && (wouldExceedInput || wouldExceedOutput)) {
batches.push(currentBatch);
currentBatch = [];
currentInputTokens = systemPromptTokens;
currentOutputTokens = 0;
}
currentBatch.push(item);
currentInputTokens += itemInput;
currentOutputTokens += itemOutput;
}
if (currentBatch.length > 0) batches.push(currentBatch);
return batches;
}
贪心策略:依次添加 item,任一约束即将超出就开始新批次。单个 item 超出预算时仍独立成批(至少每批 1 个 item,不 drop)。
10.3 并发 & 容错
- 用
runStaggered(batchTasks, concurrency, 5000)做并发控制,默认 1 并发,用户可在 api-config-store 调 - 单批失败不影响其他批(容错隔离)
- 失败批次指数退避重试:第 1 次 3s、第 2 次 6s,最多 2 次
TOKEN_BUDGET_EXCEEDED特殊错误不重试(重试也没用,输入太大)
10.4 5 阶段分镜校准就是这个的主要消费者
每个 stage 的 runStage() 内部都是 processBatched 调用。典型场景:30 个分镜,单个 item ~200 output tokens,用 DeepSeek V3.2(ctx 128K, out 8K)—— 自动分成 3-4 批并发跑。详见 §19。
11. API Key Manager(轮询 + 黑名单)
src/lib/api-key-manager.ts · 500+ 行
11.1 能力
- 多 Key 用逗号或换行分隔,
parseApiKeys()解析为数组 - 初始化时 随机起始索引 做负载均衡(避免多用户从同一位置开始打单 Key)
- 失败 Key 自动进黑名单,默认封禁
90 秒 - 按错误类型区分封禁理由:
rate_limit / auth / service_unavailable / model_incompatible - 每次
getCurrentKey()都自动清理过期黑名单
11.2 错误分类 → 轮转策略
api-key-manager.ts:372-392
handleError(statusCode: number, errorText?: string): boolean {
if (statusCode === 429) {
this.markCurrentKeyFailed('rate_limit');
return true;
}
if (statusCode === 401 || statusCode === 403) {
this.markCurrentKeyFailed('auth');
return true;
}
// 所有 5xx 服务端错误均触发 key 轮转
// memefast 等中转站 500 多为临时性故障
if (statusCode >= 500) {
this.markCurrentKeyFailed('service_unavailable');
return true;
}
// 400 且错误消息包含 "not support" 之类的模型不兼容
if (statusCode === 400 && isModelIncompatibleError(errorText)) {
this.markCurrentKeyFailed('model_incompatible', 15 * 1000);
return true;
}
return false;
}
11.3 对 memefast 的特殊兼容
memefast 中转站有个坑:上游负载饱和时它返回 HTTP 500 而不是标准的 503/529。作者专门做了关键词检测:
api-key-manager.ts:276-288
function isUpstreamOverloadError(errorText?: string): boolean {
if (!errorText) return false;
const text = errorText.toLowerCase();
return (
text.includes('上游负载') ||
text.includes('负载已饱和') ||
text.includes('负载饱和') ||
text.includes('overloaded') ||
text.includes('无可用渠道') ||
text.includes('no available channel')
);
}
11.4 scope 隔离
每个 feature:model 组合都有独立的 ApiKeyManager 实例(通过 getProviderKeyManager(providerId, apiKey, scopeKey)),避免一个功能的黑名单污染另一个功能。
12. Task Poller(异步任务轮询)
src/packages/ai-core/api/task-poller.ts · 139 行
图像和视频生成 API 通常是异步的:先 POST 返回 task_id,然后客户端轮询状态直到完成。这个类负责封装轮询逻辑。
12.1 动态超时调整(关键设计)
task-poller.ts:25-28, 75-84
private defaultInterval = 3000; // 3 seconds
private defaultTimeout = 600000; // 10 minutes
private maxTimeout = 1800000; // 30 minutes cap
// ... 在 poll 循环中:
// Dynamic timeout adjustment based on server estimate
if (result.estimatedTime && result.estimatedTime > 0) {
// Give 2x buffer + 2 minutes, capped at maxTimeout
const buffered = (result.estimatedTime * 2 + 120) * 1000;
const newTimeout = Math.min(buffered, this.maxTimeout);
if (newTimeout > effectiveTimeout) {
effectiveTimeout = newTimeout;
console.log(`[TaskPoller] Extended timeout to ${Math.floor(effectiveTimeout / 60000)} minutes based on server estimate`);
}
}
服务端如果告诉 estimatedTime(生成预估秒数),客户端就把超时放宽到 estimatedTime × 2 + 2 分钟,但上限 30 分钟。这对视频生成特别重要:Sora 生成 10 秒视频可能要几分钟,不能用死的 10 分钟。
12.2 网络错误的处理
task-poller.ts:103-115
try {
// ...
} catch (e) {
const error = e as Error;
// Re-throw user cancellation and timeout errors
if (error.message.includes('cancelled') ||
error.message.includes('timeout') ||
error.message.includes('Task failed')) {
throw error;
}
// Network errors: log and continue polling
console.warn(`[TaskPoller] Network error on poll #${pollCount}, will retry:`, error.message);
}
区分致命错误和临时网络抖动——后者打印 warning 继续轮询,前者才抛出。长轮询场景下这是必要的容错。
13. callChatAPI 的核心实现
src/lib/script/script-parser.ts:210-400+
虽然放在 script-parser.ts 里,callChatAPI 是全项目所有文本 AI 调用的底层入口。它是 OpenAI 兼容协议的实现,支持所有中转/原生 OpenAI API。
13.1 URL 规范化
script-parser.ts:242-245
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
const url = /\/v\d+$/.test(normalizedBaseUrl)
? `${normalizedBaseUrl}/chat/completions`
: `${normalizedBaseUrl}/v1/chat/completions`;
如果 baseUrl 已经带 /v1、/v2,直接拼 /chat/completions;否则补 /v1/。
13.2 Token Budget 计算 + 自动 clamp
script-parser.ts:248-284
// 从 Model Registry 查询模型限制
const modelLimits = getModelLimits(model);
const requestedMaxTokens = options.maxTokens ?? 4096;
const effectiveMaxTokens = Math.min(requestedMaxTokens, modelLimits.maxOutput);
// === Token Budget Calculator ===
const inputTokens = estimateTokens(systemPrompt + userPrompt);
const safetyMargin = Math.ceil(modelLimits.contextWindow * 0.1);
const availableForOutput = modelLimits.contextWindow - inputTokens - safetyMargin;
const utilization = Math.round((inputTokens / modelLimits.contextWindow) * 100);
// 输入已超过 context window 的 90% → 抛出错误(不发请求,省钱)
if (inputTokens > modelLimits.contextWindow * 0.9) {
const err = new Error(
`[TokenBudget] 输入 token (≈${inputTokens}) 超出 ${model} 的 context window ` +
`(${modelLimits.contextWindow}) 的 90%,请缩减输入或使用更大上下文的模型`
);
(err as any).code = 'TOKEN_BUDGET_EXCEEDED';
throw err;
}
省钱设计:超过 90% ctx 就 fail-fast,不真的发请求。避免用户在 UI 上点一次花掉一次 API 调用但被服务端退回的浪费。
13.3 智谱 GLM 的 disable thinking
script-parser.ts:317-321
// 智谱推理模型 (GLM-4.7/4.5 等) 支持通过 thinking.type 关闭深度思考
if (options.disableThinking) {
body.thinking = { type: 'disabled' };
console.log('[callChatAPI] 已关闭深度思考 (thinking: disabled)');
}
项目所有结构化 JSON 输出任务默认都关 thinking——因为 reasoning tokens 会大量占用 maxOutput 预算,真正的 JSON 还没输出就被截断。这是用过智谱推理模型的人才知道的坑。
13.4 Error-driven Discovery 自动重试
script-parser.ts:337-349
// === Error-driven Discovery: 400 错误自动发现模型限制并重试 ===
if (response.status === 400) {
const discovered = parseModelLimitsFromError(errorText);
if (discovered) {
cacheDiscoveredLimits(model, discovered);
// 如果发现了 maxOutput 限制且当前请求超出,立即用正确值重试
if (discovered.maxOutput && effectiveMaxTokens > discovered.maxOutput) {
const correctedMaxTokens = Math.min(requestedMaxTokens, discovered.maxOutput);
console.warn(
`[callChatAPI] 🧠 发现 ${model} maxOutput=${discovered.maxOutput},` +
`以 max_tokens=${correctedMaxTokens} 自动重试...`
);
// ...重试
}
}
}
这是 Model Registry Discovery 的消费端。整个反馈循环:call → 400 → parse error → cache → retry → success → next call uses cached limit。
14. Episode Parser(中文剧本规则引擎)
src/lib/script/episode-parser.ts · 1117 行
这是项目里最有"中文剧本 domain knowledge"的一块。不用 AI,纯正则规则解析标准中文电视剧剧本格式。
14.1 支持的格式
episode-parser.ts:1-17
/**
* Episode Parser - 中文剧本规则解析器
* 解析标准中文剧本格式,提取集、场景、对白、动作等结构化信息
*
* 支持的格式:
* - 集标记:第X集
* - 场景头:**1-1日 内 沪上 张家** 或 1-1 日 内 沪上 张家
* - 人物行:人物:张明、张父
* - 字幕:【字幕:2002年夏】
* - 动作描写:△窗外栀子花绽放...
* - 对白:张父:(喝酒)我们明明真是太有出息了!
* - 闪回:【闪回】...【闪回结束】
* - 旁白/VO:【VO:...】
*/
14.2 parseFullScript 的 8 个提取步骤
episode-parser.ts:49-101
export function parseFullScript(fullText: string) {
// 1. 提取标题 (从《》或「」中取)
// 2. 提取大纲 (从"大纲:"到"人物小传:"之间)
// 3. 提取人物小传 (从"人物小传:"到第一集前)
// 4. 提取时代背景和时间线设定
// 5. 提取类型 (genre)
// 6. 提取世界观/风格设定
// 7. 提取主题关键词
// 8. 解析各集内容
}
14.3 时代识别(多级 fallback)
代码里有一套精妙的时代推断逻辑(episode-parser.ts:106-203):
- 显式年份:匹配"1990-2020年"、"2002年夏天"等,如
2000 → 现代,1949-1999 → 现代(新中国),1912-1948 → 民国,1840-1911 → 清末/近代 - 显式朝代词:正则匹配"民国/清末/唐朝/汉朝/战国..."
- 古风术语推断(当没有年份也没有朝代词时):
- 古代官职(高置信度):
城主|王爷|太守|县令|丞相|皇帝|嫔妃|大将军→ 古代 - 武侠/古风术语(中置信度):
武功|内力|真气|门派|掌门|暗器 - 古代场景:
城楼|客栈|衙门|镖局|府邸|宫殿 - 文化 + 场景同时命中 → 古代;仅文化 → 古代(推断)
- 古代官职(高置信度):
14.4 类型识别(20 种 genre)
episode-parser.ts:213-234
const genrePatterns: Array<{ keywords: RegExp; genre: string }> = [
{ keywords: /武侠|江湖|门派|武功|剑|刀法|内力|武林/, genre: '武侠' },
{ keywords: /仙侠|修仙|灵气|渡劫|飞升|法宝|灵根/, genre: '仙侠' },
{ keywords: /玄幻|魔法|异世界|龙族|精灵|魔族/, genre: '玄幻' },
{ keywords: /科幻|太空|星际|机器人|AI|外星|未来世界/, genre: '科幻' },
{ keywords: /悬疑|谋杀|侦探|推理|凶手|案件|警察/, genre: '悬疑' },
{ keywords: /恐怖|鬼|灵异|诅咒|闹鬼/, genre: '恐怖' },
{ keywords: /商战|创业|公司|股权|融资|上市|企业/, genre: '商战' },
{ keywords: /宫斗|后宫|嫔妃|皇上|太后|选秀/, genre: '宫斗' },
{ keywords: /宅斗|嫡女|庶出|大宅门|内宅/, genre: '宅斗' },
{ keywords: /谍战|特工|间谍|密码|潜伏|情报/, genre: '谍战' },
// ... 军旅 / 刑侦 / 医疗 / 律政 / 校园 / 爱情 / 家庭 / 喜剧 / 历史 / 乡村
];
14.5 场景头双格式支持
episode-parser.ts:369-448
// 标准格式:**1-1日 内 沪上 张家** 或 1-1 日 内 沪上 张家
const sceneHeaderRegex = /\*{0,2}(\d+-\d+)\s*(日|夜|晨|暮|黄昏|黎明|清晨|傍晚)\s*(内|外|内\/外)\s+([^\*\n]+)\*{0,2}/g;
// ...匹配失败就 fallback 到宽松格式:
// 1-1 规则怪谈世界,集合广场,日
const looseSceneRegex = /^\*{0,2}(\d+-\d+)\s+([^\*\n]+)\*{0,2}$/gm;
// ...宽松格式再失败就用 parseAlternativeSceneFormat() 兜底
三层 fallback 保证对不同规范度的剧本都能解析。宽松格式还会智能提取末尾的时间词(日/夜/晨/暮)和内/外标记。
14.6 主题关键词(14 种)
奋斗 / 复仇 / 爱情 / 亲情 / 友情 / 权谋 / 正义 / 自由 / 救赎 / 背叛与信任 / 命运 / 战争与和平 / 传承 / 生死。最多返回 5 个主题(slice(0, 5))。
15. Script Normalizer
src/lib/script/script-normalizer.ts · 597 行
职责:处理用户粘贴进来的非规范剧本,把它规范到 Episode Parser 能吃的格式。
preprocessLineBreaks()— 单行超长文本自动插入换行normalizeScriptFormat()— 统一标点符号、全角半角转换analyzeScriptStructureWithAI()— 调用 LLM 分析剧本结构(当规则解析失败时的 fallback)applyAIAnalysis()— 把 AI 分析结果合并回规范化后的文本
16. AI Character Finder
src/lib/script/ai-character-finder.ts · 561 行
根据用户自然语言描述(如"缺第10集的王大哥这个角色")从剧本中查找角色并生成专业角色数据。
16.1 4 种 query 解析模式
ai-character-finder.ts:48-104
function parseUserQuery(query: string): { name: string | null; episodeNumber: number | null } {
// 提取集数:第X集、第X话、EP.X、EpX
const episodeMatch = query.match(/第\s*(\d+)\s*[集话]|EP\.?\s*(\d+)|episode\s*(\d+)/i);
// 模式1:X这个角色 / X这个人
let nameMatch = cleanQuery.match(/[「"']?([^「」""'\s,,。!?]+?)[」"']?\s*这个[角色人]/);
// 模式2:缺/需要/添加 + 角色名
if (!name) {
nameMatch = cleanQuery.match(/^[缺需要添加找查想请帮我的]+\s*[「"']?([^...]{2,8})[」"']?/);
}
// 模式3:角色:/角色名:
if (!name) {
nameMatch = cleanQuery.match(/角色[::名]?\s*[「"']?([^...]{2,8})[」"']?/);
}
// 模式4:直接就是角色名 (2-8 个字符的纯中英文)
if (!name) {
const pureQuery = cleanQuery.replace(/^[缺需要添加找查想请帮我的]+/g, '').trim();
if (pureQuery.length >= 2 && pureQuery.length <= 8 && /^[\u4e00-\u9fa5A-Za-z]+$/.test(pureQuery)) {
name = pureQuery;
}
}
}
找到名字后去剧本里扫描该角色出现的集数、对白、动作,把上下文塞给 LLM 生成完整角色数据。
17. Scene Viewpoint Generator(联合图核心)
src/lib/script/scene-viewpoint-generator.ts · 1389 行
17.1 核心概念:视点(Viewpoint)
一个场景(比如"大巴车内")可能需要多个视点拍分镜:过道视角、座位视角、窗外视角、司机视角。系统为每个视点单独生成一张参考图,确保同一场景下不同分镜的空间一致性。
scene-viewpoint-generator.ts:15-28
export interface SceneViewpoint {
id: string; // 视角ID,如 'dining', 'sofa', 'window'
name: string; // 中文名:餐桌区、沙发区、窗边
nameEn: string; // 英文名:Dining Area, Sofa Area, Window
shotIds: string[]; // 关联的分镜ID列表
keyProps: string[]; // 该视角需要的道具(中文)
keyPropsEn: string[]; // 该视角需要的道具(英文)
description: string; // 视角描述(中文)
descriptionEn: string;// 视角描述(英文)
gridIndex: number; // 在联合图中的位置 (0-5)
}
17.2 环境类型检测(9 种)
scene-viewpoint-generator.ts:59-68
export type SceneEnvironmentType =
| 'vehicle' // 现代交通工具(大巴、汽车、火车、飞机等)
| 'outdoor' // 现代户外(公路、街道、公园等)
| 'indoor_home' // 现代室内家居
| 'indoor_work' // 现代室内办公/商业
| 'indoor_public' // 现代室内公共(医院、学校、餐厅等)
| 'ancient_indoor' // 古代室内(宫殿、府邸、客栈、寺庙等)
| 'ancient_outdoor'// 古代户外(官道、集市、城门等)
| 'ancient_vehicle'// 古代交通(马车、轿子、船等)
| 'unknown';
每种环境类型下有几十个关键词用于识别(如 ancient_indoor 包含"宫殿/殿/皇宫/内廷/御书房/太和殿/冷宫/...")。识别准确直接决定后续生成图片的时代感。
18. Full Script Service(主流程,2568 行)
src/lib/script/full-script-service.ts · 2568 行(项目最大单文件)
18.1 职责
- 导入完整剧本(大纲 + 人物小传 + 60 集内容)
- 按集生成分镜(一次生成一集)
- 更新单集或全部分镜
- AI 校准:为缺失标题的集数生成标题
18.2 依赖关系(调用链)
full-script-service.ts:13-43
import { parseFullScript, convertToScriptData, parseScenes } from "./episode-parser";
import { normalizeScriptFormat, analyzeScriptStructureWithAI,
applyAIAnalysis, preprocessLineBreaks } from "./script-normalizer";
import { populateSeriesMetaFromImport } from "./series-meta-sync";
import { callFeatureAPI } from "@/lib/ai/feature-router";
import { processBatched } from "@/lib/ai/batch-processor";
import { useScriptStore } from "@/stores/script-store";
import { useCharacterLibraryStore } from "@/stores/character-library-store";
import { useAPIConfigStore } from "@/stores/api-config-store";
import { retryOperation } from "@/lib/utils/retry";
import { ApiKeyManager } from "@/lib/api-key-manager";
import { getStyleDescription, getMediaType } from "@/lib/constants/visual-styles";
import { buildCinematographyGuidance } from "@/lib/constants/cinematography-profiles";
import { getMediaTypeGuidance } from "@/lib/generation/media-type-tokens";
import { getVariationForEpisode } from "./character-stage-analyzer";
import { analyzeSceneViewpoints } from "./viewpoint-analyzer";
import { runStaggered } from "@/lib/utils/concurrency";
import { calibrateShotsMultiStage } from "./shot-calibration-stages";
import { buildSeriesContextSummary } from "./series-meta-sync";
它是整个项目的"指挥官"——协调规则解析(episode-parser)+ 规范化(normalizer)+ 角色识别 + 视角分析 + 批处理 + 5 阶段校准。
18.3 数据流
用户粘贴剧本 ↓ preprocessLineBreaks() ← 自动换行 ↓ parseFullScript() ← 规则提取 background + episodes ↓ normalizeScriptFormat() ← 标点/全半角规范化 ↓ analyzeScriptStructureWithAI() ← 规则失败时的 AI fallback ↓ populateSeriesMetaFromImport() ← 同步剧级元数据 ↓ 【按集循环】 ↓ parseScenes(rawContent) ← 场景头正则解析 ↓ analyzeSceneViewpoints() ← 每场景识别需要几个视点 ↓ generateShotsFromScene() ← 生成分镜草稿 ↓ calibrateShotsMultiStage() ← 5 阶段 AI 校准(最重的一步) ↓ 保存到 useScriptStore
19. 5 阶段分镜校准 — 总览
src/lib/script/shot-calibration-stages.ts · 413 行
一个分镜要产出 30+ 个字段(景别、运动、时长、视觉描述、音频、灯光、焦距、首帧 prompt、视频 prompt、尾帧 prompt 等)。如果一次性让 AI 输出全部,推理模型(DeepSeek-R1、GLM 深度思考)的 reasoning tokens 会耗尽 maxOutput 预算,JSON 还没写完就被截断。拆成 5 个独立调用,每个 stage 只要 AI 关注几个相关字段,收敛得又快又稳。
19.1 阶段划分
| Stage | 名称 | 字段数 | maxTokens | 每 item 预估 |
|---|---|---|---|---|
| 1 | 叙事骨架 | 9 | 4096 | 200 |
| 2 | 视觉描述 + 音频 | 6 | 4096 | 200 |
| 3 | 拍摄控制 | 15 | 4096 | 200 |
| 4 | 首帧提示词 | 3 | 8192 | 400 |
| 5 | 动态 + 尾帧提示词 | 4 | 8192 | 400 |
19.2 共用的上下文构建
shot-calibration-stages.ts:88-123
const contextLine = [
`《${title}》`, genre || '', era || '',
totalEpisodes ? `共${totalEpisodes}集` : '',
`第${currentEpisode}集「${episodeTitle}」`,
episodeSeason || '',
].filter(Boolean).join(' | ');
// 剧级上下文摘要:来自 SeriesMeta
const seriesCtx = globalContext.seriesContextSummary || '';
// 叙事锚点:故事核心 + 世界观 + 核心冲突(截断避免过长)
const narrativeAnchorParts = [
seriesCtx ? `【剧级知识】\n${seriesCtx}` : '',
outline ? `【故事核心】\n${outline.slice(0, 600)}` : '',
worldSetting ? `【世界观/规则】\n${worldSetting.slice(0, 400)}` : '',
themes?.length ? `【核心主题】${themes.join('、')}` : '',
characterBios ? `【主要人物】\n${characterBios.slice(0, 400)}` : '',
].filter(Boolean);
// 时代/世界观上下文:供 Stage 2/4/5 视觉生成使用
// 避免 AI 产生与时代不符的幻觉
const eraContextBlock = `\n\n【⚠️ 剧本背景 — 视觉生成必须严格遵循】\n${
[contextLine,
era ? `⚠️ 时代背景:${era}——所有人物服装、发型、道具、建筑必须严格符合「${era}」时期,禁止出现其他时代的元素(如古装剧禁止西装/T恤/手机等现代物品)` : '',
worldSetting ? `世界观设定:${worldSetting.slice(0, 300)}` : '',
characterBios ? `人物造型参考:${characterBios.slice(0, 300)}` : '',
].filter(Boolean).join('\n')
}`;
每个 stage 的 system prompt 开头都会注入这些上下文,确保跨 stage 的剧本背景一致性。
20. Stage 1 — 叙事骨架
职责:分析每个分镜的叙事功能和镜头参数。不涉及视觉细节,只关心"这个镜头在讲什么故事"。
System prompt 全文
shot-calibration-stages.ts:184-208
注意点:
specialTechnique枚举里有 "hitchcock-zoom"(希区柯克变焦)、"bullet-time"(子弹时间)、"fpv-shuttle"(FPV 穿梭)等 12 种专业特殊技法narrativeFunction强制 LLM 表达每个镜头的故事功能,避免生成"纯描述镜头"storyAlignment: needs-review用于 flag 可能违反世界观的镜头让用户审核
21. Stage 2 — 视觉描述 + 音频
职责:基于 Stage 1 的叙事分析,生成中文视觉描述、英文 prompt(可选)、情绪标签、环境声/音效。
System prompt 全文
shot-calibration-stages.ts:233-243
闪回处理的细节:
shot-calibration-stages.ts:249
const hasFlashback = /闪回|叠画|回忆|穿插/.test(s.sourceText || '');
// 如果包含闪回,在 prompt 里特别强调:
// "【主场景(不可更改)】: {sceneLocation} ⚠️含闪回,主场景不变!"
原因:闪回镜头容易让 AI "跑偏"到回忆场景的设定,导致后续分镜的地点混乱。用 "画面叠加" 描述方案固定主场景。
22. Stage 3 — 拍摄控制(15 字段)
职责:根据 DP(摄影指导)的视角确定灯光、景深、镜头、焦距等技术参数。这是最"专业"的一个 stage。
System prompt 全文
shot-calibration-stages.ts:262-281
所有参数的枚举都来自真实电影摄影术语。focalLength 列出了从 14mm 广角到 200mm 长焦的标准镜头规格。photographyTechnique 里的 long-exposure / double-exposure 这些只在静态摄影里有意义的技法也都支持——为 photo-realistic 风格留接口。
23. Stage 4 — 首帧提示词
职责:根据所有前面阶段的结果,生成用于图像模型的首帧 prompt。这是真正要喂给 gemini-3-pro-image-preview / gpt-image-1.5 的内容。
System prompt(完整版 = zh + en 双语)
shot-calibration-stages.ts:322-342
语言三态:promptLanguage 可以是 'zh' / 'en' / 'zh+en',对应的字段会动态调整——代码在 shot-calibration-stages.ts:305-321,用三元运算符选择 s4Fields / s4JsonFormat / s4LangWarning。
needsEndFrame 字段:决定后续是否需要生成尾帧图。纯对白不动画面的镜头不需要(省一次图生成),运动镜头必须要。这是成本控制的关键字段。
24. Stage 5 — 动态 + 尾帧提示词
职责:根据首帧和动作描述,生成视频动作 prompt 和尾帧 prompt。这是图→视频的最后一公里。
System prompt 全文
shot-calibration-stages.ts:382-397
视频模型的玩法:Seedance 2.0 / Sora / Veo 的 image-to-video 模式需要首帧图 + 动态描述 + 尾帧图(可选)。提供尾帧能让模型更好控制动作走向,但代价是多生成一张图。这里的 needsEndFrame 决定是否走"双帧方案"。
25. Character Bible(角色一致性)
src/lib/script/character-calibrator.ts · 1234 行
src/lib/script/character-stage-analyzer.ts · 303 行
src/packages/ai-core/services/character-bible.ts · 300 行
25.1 什么是"6 层身份锚点"?
README 吹的"6 层身份锚点技术"实际是一套纯 prompt 工程,不是视觉锚定。机制:
character-bible.ts:14-47
export interface CharacterBible {
id: string;
screenplayId: string;
// Basic info
name: string;
type: AICharacter['type'];
// Visual description (for image generation)
visualTraits: string;
// Style tokens for consistency
styleTokens: string[];
// Color palette
colorPalette: string[];
// Chinese personality description
personality: string;
// Reference images
referenceImages: ReferenceImage[];
// Generated three-view images (for consistency)
threeViewImages?: {
front?: string;
side?: string;
back?: string;
};
createdAt: number;
updatedAt: number;
}
"6 层" 大致对应:
- visualTraits:文本描述(外貌、体型、发型、服装)
- styleTokens:关键词标签数组
- colorPalette:主色调数组
- personality:性格描述(影响姿势/表情)
- referenceImages:用户上传的参考图(送给图像模型做 in-context reference)
- threeViewImages:AI 生成的三视图(正/侧/背),用于跨镜头视角一致性
25.2 生成 prompt 的实际逻辑
character-bible.ts:126-136, 233-250
buildCharacterPrompt(characterIds: string[]): string {
const characters = characterIds
.map(id => this.characters.get(id))
.filter((c): c is CharacterBible => c !== null);
if (characters.length === 0) return '';
return characters
.map(c => `[${c.name}]: ${c.visualTraits}`)
.join('; ');
}
export function generateConsistencyPrompt(character: CharacterBible): string {
const parts: string[] = [];
if (character.visualTraits) parts.push(character.visualTraits);
if (character.styleTokens.length > 0) {
parts.push(character.styleTokens.join(', '));
}
parts.push(`character: ${character.name}`);
return parts.join(', ');
}
也就是说,角色一致性的实现就是:每次生成分镜时把角色 Bible 的 visualTraits 字符串拼接到 imagePrompt 前面。比如生成第 3 集第 5 个分镜时,prompt 里会有:
[沈星晴]: 22岁的支教老师,前篮球队主力,叛逆不羁,穿着休闲但带有专业气息...
Ancient Chinese city street at dawn, ... [原分镜 prompt]
25.3 Stage Analyzer(角色随剧情变化)
character-stage-analyzer.ts · 303 行 处理的是:角色在长剧本里会经历外貌/服装/阶段变化(比如少年→青年→中年,或者从乞丐→富豪),这个模块用 AI 分析剧本为每个角色生成多个阶段变体,生成分镜时根据集数查询合适的变体。
full-script-service.ts:39
import { getVariationForEpisode } from "./character-stage-analyzer";
// ...
// 为第 N 集查找角色的第 X 个服装变体
const variation = getVariationForEpisode(character, episodeIndex);
26. Storyboard 拼图服务
src/lib/storyboard/storyboard-service.ts · 768 行
26.1 核心思路
传统分镜做法:为每个分镜单独生成一张图。但这样做:
- 成本高:30 个分镜就是 30 次调用
- 一致性差:每张图独立生成,角色/场景容易漂
魔因的方案:一次生成一张大拼图(contact sheet),里面是多格分镜网格,然后切割成单张。这样一次 API 调用搞定 N 个镜头,一致性也更好(模型会自动保持同一张图里的连贯风格)。
26.2 流程
buildStoryboardPrompt()— 组装多镜头 prompt(所有分镜描述串成一个 prompt + 网格约束)calculateGrid()— 根据镜头数、分辨率、宽高比算最优网格布局submitGridImageRequest()— 调用图像 API 生成拼图image-splitter.ts— 按网格切分成单张分镜图
26.3 支持的分辨率
storyboard/grid-calculator.ts:12-27
export const RESOLUTION_PRESETS = {
'2K': {
'16:9': { width: 1920, height: 1080 },
'9:16': { width: 1080, height: 1920 },
},
'4K': {
'16:9': { width: 3840, height: 2160 },
'9:16': { width: 2160, height: 3840 },
},
} as const;
// Scene count limits per resolution
export const SCENE_LIMITS = {
'2K': 12, // 2K 画布最多 12 个分镜
'4K': 48, // 4K 画布最多 48 个分镜
} as const;
27. Grid Calculator(最优布局搜索)
src/lib/storyboard/grid-calculator.ts · 319 行
给定 N 个分镜、宽高比、分辨率,算出 cols × rows 让单格尺寸最大化。
27.1 算法
grid-calculator.ts:65-110
function calculateLandscapeGrid(sceneCount: number, canvasWidth: number, canvasHeight: number): GridConfig {
let bestConfig: GridConfig | null = null;
let bestMinDimension = 0;
// Try different grid configurations
for (let cols = 1; cols <= sceneCount; cols++) {
const rows = Math.ceil(sceneCount / cols);
// Skip if we'd have too many empty cells (more than one row worth)
if (cols * rows - sceneCount >= cols) continue;
// Calculate cell dimensions maintaining 16:9 ratio
const cellWidth = Math.floor(canvasWidth / cols);
const cellHeight = Math.floor(cellWidth * 9 / 16);
// Check if all rows fit in canvas height
const totalHeight = cellHeight * rows;
if (totalHeight > canvasHeight) continue;
// For landscape, prefer cols >= rows
if (cols < rows && sceneCount > 1) continue;
// Calculate minimum dimension (we want to maximize this)
const minDim = Math.min(cellWidth, cellHeight);
if (minDim > bestMinDimension) {
bestMinDimension = minDim;
bestConfig = { cols, rows, cellWidth, cellHeight, ... };
}
}
return bestConfig;
}
暴力搜索所有 cols × rows 组合,过滤掉:
- 空格子超过一行的配置(8 分镜不会用 3×4 留 4 个空)
- 单格比例不符合目标宽高比
- 总高度超出画布
- 横向模式下 cols < rows 的配置(强制横向扁平)
在通过的配置里选单格最短边最大的那个。
28. 52 视觉风格库
src/lib/constants/visual-styles.ts · 683 行
28.1 分类
- 3D 风格:3D 玄幻 / 3D 美式 / 3D Q 版 / 3D 写实 / 3D 积木 / 3D 体素 / 3D 手游 / 3D 渲 2D / JP 3D 渲 2D
- 2D 动画:2D 动画 / 2D 电影 / 2D 幻想 / 2D 复古 / 2D 美式 / 2D 吉卜力 / 2D 复古女孩 / 2D 韩系 / 2D 少年 / 2D Akira / 2D 哆啦 A 梦 / 2D 藤本树 / 2D Mob Psycho / 2D JOJO / 2D 名侦探 / 2D 灌篮 / 2D 铁壁 / 2D 死神笔记 / 2D 粗线条 / 2D 橡皮管 / 2D Q 版 + 更多
- 真人风格:Real Cinematic / Real Doc / Real Vintage / Real Fashion / 更多
- 定格动画:Stop Motion / Clay / Papercraft
28.2 每个风格的数据结构
visual-styles.ts:23-37
export interface StylePreset {
id: string;
name: string;
category: StyleCategory; // '3d' | '2d' | 'real' | 'stop_motion'
mediaType: MediaType; // 'cinematic' | 'animation' | 'stop-motion' | 'graphic'
prompt: string; // 英文 prompt 注入
negativePrompt: string; // 负面 prompt
description: string; // 中文说明
thumbnail: string; // 缩略图文件名
}
28.3 示例:3D 玄幻
visual-styles.ts:44-53
{
id: '3d_xuanhuan',
name: '3D玄幻',
category: '3d',
mediaType: 'cinematic',
prompt: '(best quality, masterpiece, 8k, high detailed:1.2), (stunning stylized 3D Chinese animation character render:1.3), (Unreal Engine 5 style:1.2), (cinematic lighting, soft volumetric fog:1.1), (smooth porcelain skin texture:1.1), (intricate traditional Chinese fabric details, fine embroidery, flowing robes:1.1), ethereal atmosphere, glowing spiritual energy, beautiful facial features, (delicate body proportions), sharp focus, detailed background',
negativePrompt: '(worst quality, low quality, bad quality:1.4), (blurry, fuzzy, distorted, out of focus:1.3), (2D, flat, drawing, painting, sketch, anime, cartoon:1.2), (realistic, photo, real life, photography:1.1), (western style, modern clothing), (extra limbs, missing limbs, mutated hands, distorted body), ugly, watermark, signature, text, easynegative, bad-hands-5',
description: '中国风玄幻,仙侠,虚幻引擎渲染,光效华丽',
thumbnail: '3d_xuanhuan.png',
},
这些 prompt 都是 Stable Diffusion 风格的 weighted tags((keyword:weight)),直接送给图像 API。默认风格是 '2d_ghibli'(吉卜力),可能因为覆盖面广。
28.4 MediaType 决定摄影参数翻译策略
mediaType 字段是关键的间接层——它告诉 prompt-builder 如何把"35mm 镜头" "low-angle" 这种摄影术语翻译成 prompt:
- cinematic:完整物理摄影词汇(真人/写实 3D)— "shot on ARRI Alexa, 35mm anamorphic lens"
- animation:动画运镜适配(2D 动画/风格化 3D)— "dynamic action pose, wide angle composition"
- stop-motion:微缩实拍约束(定格动画)— "miniature scale, handcrafted feel"
- graphic:仅色彩/情绪/节奏(像素/水彩/简笔画等)— "flat colors, bold composition"
29. 26 摄影档案(Cinematography Profiles)
src/lib/constants/cinematography-profiles.ts · 466 行
视觉风格管"像什么画",摄影档案管"像哪部电影拍的"。26 种档案,分 5 类:
| 类别 | 档案 |
|---|---|
| cinematic 电影类 | classic-cinematic / intimate-drama / epic-blockbuster / romantic-film / film-noir / action-intense |
| documentary 纪实类 | documentary-raw / news-report |
| stylized 风格化 | cyberpunk-neon / music-video |
| genre 类型片 | wuxia-classic / horror-thriller / suspense-mystery / family-warmth |
| era 时代风格 | hk-retro-90s / golden-age-hollywood |
每个档案包含灯光默认、焦点默认、器材默认等,在 5 阶段校准的 Stage 3 里作为"回退基线"使用。当单镜头没有显式设置灯光/机位时,用当前档案的默认值。
cinematography-profiles.ts:36-60
export interface CinematographyProfile {
id: string;
name: string; // 中文名
nameEn: string; // 英文名
category: CinematographyCategory;
description: string;
emoji: string;
// ---- 灯光默认 (Gaffer) ----
defaultLighting: {
style: LightingStyle;
direction: LightingDirection;
colorTemperature: ColorTemperature;
};
// ---- 焦点默认 (Focus Puller) ----
defaultFocus: { ... };
// ---- 器材默认 (Camera Rig) ----
defaultRig: { ... };
}
字段命名用了真实剧组岗位(Gaffer / Focus Puller / Camera Rig),说明作者是懂影视生产流程的。
30. 默认供应商解码
src/lib/api-key-manager.ts:40-67
export const DEFAULT_PROVIDERS: Omit<IProvider, 'id' | 'apiKey'>[] = [
{
platform: 'memefast',
name: '魔因API',
baseUrl: 'https://memefast.top',
model: [
'deepseek-v3.2',
'glm-4.7',
'gemini-3-pro-preview',
'gemini-3-pro-image-preview',
'gpt-image-1.5',
'doubao-seedance-1-5-pro-251215', // ← 传说中的 Seedance 2.0
'veo3.1',
'sora-2-all',
'wan2.6-i2v',
'grok-video-3-10s',
'claude-haiku-4-5-20251001',
],
capabilities: ['text', 'vision', 'image_generation', 'video_generation'],
},
{
platform: 'runninghub',
name: 'RunningHub',
baseUrl: 'https://www.runninghub.cn/openapi/v2',
model: ['2009613632530812930'], // 一个 workflow id,专做视角切换
capabilities: ['image_generation', 'vision'],
},
];
30.1 模型对应表
| 模型名 | 真实来源 | 用途 |
|---|---|---|
deepseek-v3.2 | DeepSeek V3.2 官方 | 剧本分析(主力) |
glm-4.7 | 智谱 GLM-4.7 | 剧本分析(备选) |
gemini-3-pro-preview | Google Gemini 3 Pro | 长剧本分析(1M ctx) |
gemini-3-pro-image-preview | Gemini 3 Pro 图像版 | 角色图 / 场景图 |
gpt-image-1.5 | OpenAI GPT Image 1.5 | 角色图 / 场景图(备选) |
doubao-seedance-1-5-pro-251215 | 字节豆包 Seedance 2.0 (Pro 版, 20261215 发布) | 视频生成(主打) |
veo3.1 | Google Veo 3.1 | 视频生成 |
sora-2-all | OpenAI Sora 2 | 视频生成 |
wan2.6-i2v | 阿里通义万相 2.6 image-to-video | 视频生成 |
grok-video-3-10s | xAI Grok Video 3 (10 秒) | 视频生成 |
claude-haiku-4-5-20251001 | Anthropic Claude Haiku 4.5 | 对话 / 快速分析 |
也就是说:魔因漫创不绑任何单一视频模型,是通过 memefast 中转调用市面上主流的所有视频生成模型。你付 memefast 的钱,memefast 帮你调 字节 / Google / OpenAI / 阿里 / xAI 的 API。
31. memefast 生意链分析
31.1 商业模式拆解
┌─────────────────┐
│ 魔因漫创用户 │
│ (个人创作者/团队) │
└────────┬────────┘
│ 1. 免费下载 Moyin Creator
│ 2. 注册 memefast.top 充值
│ 3. 把 API Key 填进 Moyin Creator
│
↓ API 调用(OpenAI 兼容)
┌─────────────────┐
│ memefast.top │ ← 作者自营中转 (hotflow2024)
│ (AI 聚合网关) │ ← 加价 / 流量分成 / 差价
└────────┬────────┘
│ 真实调用
↓
┌──────────────────────────────────┐
│ 字节豆包 / Google / OpenAI / │
│ 阿里云 / xAI / Anthropic / 智谱 │
└──────────────────────────────────┘
31.2 证据
- 应用里硬编码引导到 memefast.top(SettingsPanel.tsx:712, 757, 954)——三处 UI 链接都指向 memefast.top
- 默认所有 feature 都路由到 memefast(feature-router.ts:41-49)
- UI 文案(FeatureBindingPanel.tsx:560):"建议在 memefast.top 后台为以上分组都添加 Key,Key 越多可用性越高。"——这是促进充钱的话术
- SettingsPanel.tsx:754:"推荐使用魔因API,支持 543+ 模型一站式接入"
- api-key-manager.ts 里对 memefast 的专门错误处理(上游负载饱和检测)—— 只有自己用的中转才会知道这种细节
- 自更新服务器:
68.64.176.186是一个美国东海岸的 IP,大概率是作者自己的 VPS
31.3 诚实度评估
这不是骗局,但你需要知道:
- 加分 客户端是真开源的(AGPL-3.0),代码质量也高
- 加分 支持自定义 baseUrl,理论上可以换成 OpenRouter / Poe / 直连原厂
- 减分 默认所有东西都绑死 memefast,新手很难知道可以换
- 减分 一些默认模型名是 memefast 自定义的聚合命名(如
doubao-seedance-1-5-pro-251215),换 provider 不一定有同名模型 - 减分 自更新走 HTTP + raw IP 无签名,潜在安全风险
31.4 换掉 memefast 的成本
如果你技术够,可以:
- 在 Settings 里添加自定义 Provider(填 OpenAI / OpenRouter / Poe 的 baseUrl)
- 配置对应的模型名(注意原厂模型名 vs memefast 聚合名)
- 在功能绑定里把
script_analysis / character_generation / ...全部改绑到新 provider - 单独为视频生成配置 OpenAI/Google 原生 SDK——这一步最麻烦,因为原厂的图生视频 API 格式和 memefast 聚合的 OpenAI 兼容协议不完全一样,可能要改代码
综合判断:Moyin Creator 的设计让 memefast 成为最省事的路径,但不是唯一路径。对于想研究 AI 视频流水线的人,这是一个难得的开源参考。对于想直接用的人,要么付 memefast 的钱,要么花时间折腾。
32. 《灌篮少女》Demo 项目分析
应用首次启动会自动 seed 一份 demo 项目到用户数据目录,来自 demo-data/projects/_p/a4bbe260-0127-49c7-9230-e766402663c7/。
32.1 项目结构
demo-data/
├── projects/
│ ├── moyin-project-store.json ← 全局项目列表
│ ├── moyin-character-library.json ← 角色库
│ ├── _shared/ ← 跨项目共享数据
│ │ ├── characters.json
│ │ ├── scenes.json
│ │ └── media.json
│ └── _p/{projectId}/
│ ├── script.json ← 剧本 + 大纲 + 人物小传
│ ├── characters.json ← 本项目角色
│ ├── scenes.json ← 24 个场景
│ ├── director.json ← Director 面板数据
│ ├── sclass.json ← SClass 面板数据
│ └── media.json ← 媒体资源引用
└── media/
├── characters/ ← 角色参考图 (1 张)
└── scenes/ ← 场景参考图 (4 张)
32.2 剧本信息
- 项目名:灌篮少女(演示)
- 角色:1 个(沈星晴)
- 场景:24 个(大部分是"1-1"场景按视点切分的子场景)
32.3 示例角色:沈星晴
name: "沈星晴"
visualTraits: "沈星晴 character, 【身份/背景】
22岁的支教老师,前篮球队主力,叛逆不羁,
穿着休闲但带有专业气息,充满活力。..."
views: 1 entries ← 三视图生成了 1 个视角
variations: 0 ← 没有跨阶段变体
32.4 场景视点拆分示例
一个"乡村公路/大巴车"场景被拆成 4 个视点子场景:
| 场景名 | 视点 | 关联分镜数 |
|---|---|---|
| 1-1 乡村公路/大巴车-过道与座位全景 | 过道与座位全景 | 2 |
| 1-1 乡村公路/大巴车-过道地面特写 | 过道地面特写 | 2 |
| 1-1 乡村公路/大巴车-车窗外景 | 车窗外景 | 2 |
| 1-1 乡村公路/大巴车-座位对话视角 | 座位对话视角 | 2 |
这体现了 SceneViewpoint 机制的实际效果——一个物理场景(大巴车内部)在分镜上被切分成多个独立的"视点",每个视点各自生成参考图,保证跨分镜的空间一致性。
33. 风险清单(完整版)
| 级别 | 风险 | 缓解 |
|---|---|---|
| 高 | 自更新走明文 HTTP + 裸 IP + 无签名 | 禁用自更新,用 git pull 拉源码更新 |
| 高 | 成本不可控 | 小剧本试水,注意 memefast 计费,Key 余额设限 |
| 中 | 对 memefast 强耦合 | 技术强者可换自定义 baseUrl 但要适配模型名 |
| 中 | 角色一致性是 prompt 工程,不是视觉锚定 | 接受 10-20% 漂移,长剧本用更保守的角色描述 |
| 中 | localStorage + 本地文件单点存储 | 用 storage-export-data IPC 定期导出备份 |
| 中 | Electron 依赖大(sharp / mediabunny / electron 30) | 装包时走 npmmirror 的 electron 镜像 |
| 中 | 中转站 memefast 有上游负载饱和问题 | 充 Key 时多买几个分散压力,按作者建议 |
| 低 | Windows 偏向(prebuild-cleanup.ps1) | Mac 上能跑但偶尔有路径问题 |
| 低 | 演示项目自动 seed,首次启动会占空间 | 可以通过 IPC 删除 |
34. 决策矩阵(你该不该用)
| 你的目标 | 推荐度 | 理由 |
|---|---|---|
| 个人创作者,想可视化剧本 | ★★★★☆ | 5 阶段校准的质量是真的,demo 效果不错 |
| 团队协作,多人编辑剧本 | ★☆☆☆☆ | 没有云同步,没有协作功能,本地存储 |
| 研究 AI 视频流水线编排怎么做 | ★★★★★ | 代码质量高,5 阶段 prompt 工程是 state-of-art 参考 |
| 想做商业成片 | ★★☆☆☆ | 质量上限取决于底层模型,后期修复难,不如找人做 |
| 不想付费给 memefast | ★★☆☆☆ | 理论上能换自定义 provider,但要折腾,成本高 |
| 想部署到 VPS 给多人用 | ★☆☆☆☆ | 不可行,Electron 客户端,没有 web 模式 |
| 想魔改 | ★★★★☆ | AGPL-3.0 + 代码结构清晰,容易二开 |
最终建议
- 先跑演示项目:Mac 上
npm run dev,看《灌篮少女》演示,判断质量是否达到你的预期 - 满意就充最小额度:memefast.top ¥20-50 试水真正的剧本
- 不满意就当研究项目:克隆源码,读 5 阶段校准和 model-registry 的实现学 prompt 工程
35. 系统 Prompt 全文集合
前面章节散落了各 stage 的 prompt,这里集中列出最重要的几个——你可以直接用到自己的项目里。
35.1 Script Parser — 剧本结构化
script-parser.ts:57-134
35.2 Shot Generation — 单场景分镜生成
script-parser.ts:137-180
本文档基于对 MemeCalculate/moyin-creator v0.2.3 (commit 5f45421) 的源码阅读整理。
所有代码引用均包含文件名 + 行号,可按图索骥。
解析过程真实阅读了 28+ 个核心文件,约 8000 行代码。
个人研究 · moyin-docs.kang-kang.com · 2026-04-13 · kang@kang-kang.com
本分析本身采用 MIT 许可,可自由引用/修改。
Moyin Creator 源代码采用 AGPL-3.0 许可。