/** * 光湖视频AI系统 · 剧本解析器 * D130 · 铸渊 ICE-GL-ZY001 * * 支持格式: 动态漫/短剧标准剧本 * 输入: 腾讯文档读出的纯文本 * 输出: 结构化 JSON(剧集→场次→角色→台词→场景描述) */ /** * 解析剧本全文 * @param {string} text - 剧本纯文本 * @param {object} [opts] * @param {number} [opts.maxEpisodes] - 最多解析多少集(默认全部) * @returns {object} { title, episodes: [...], stats } */ function parseScript(text, opts = {}) { const { maxEpisodes = Infinity } = opts; // 提取标题(第一行非空) const lines = text.split('\n').map(l => l.trim()).filter(Boolean); let title = ''; let startIdx = 0; for (let i = 0; i < lines.length; i++) { if (!lines[i].startsWith('第') || !lines[i].includes('集')) { title += lines[i].replace(/[::]/g, '').trim(); } else { startIdx = i; break; } } // 按「第N集」分割 const episodes = []; let currentEpisode = null; let currentScene = null; for (let i = startIdx; i < lines.length; i++) { const line = lines[i]; // 检测新剧集 const epMatch = line.match(/^第(\d+)集[::]?/); if (epMatch) { if (episodes.length >= maxEpisodes) break; if (currentScene) { if (currentEpisode) currentEpisode.scenes.push(currentScene); currentScene = null; } currentEpisode = { episodeNumber: parseInt(epMatch[1]), title: line.replace(/^第\d+集[::]\s*/, ''), scenes: [], }; episodes.push(currentEpisode); continue; } if (!currentEpisode) continue; // 检测新场次: 「N-M 时间 场景 地点」 const sceneMatch = line.match(/^(\d+-\d+)\s+(日|夜|晨|暮|傍晚|黎明|黄昏|清晨|深夜)\s+(内|外)\s+(.+)$/); if (sceneMatch) { if (currentScene) { currentEpisode.scenes.push(currentScene); } currentScene = { id: sceneMatch[1], time: sceneMatch[2], location: sceneMatch[3], name: sceneMatch[4].trim(), characters: [], descriptions: [], dialogues: [], effects: [], }; continue; } if (!currentScene) continue; // 人物行: 「人物:A(描述),B(描述)...」 const charMatch = line.match(/^人物[::]\s*(.+)$/); if (charMatch) { const charText = charMatch[1]; // 用正则匹配每个「名字(描述)」或「名字」单元(描述中可有逗号) const charRe = /([^,,、]+?)(?:[((]([^))]+)[))])?(?=[,,、]|$)/g; let cm; while ((cm = charRe.exec(charText)) !== null) { const name = cm[1].trim(); if (name) { currentScene.characters.push({ name, description: (cm[2] || '').trim(), }); } } continue; } // 场景描述: 「△......」 if (line.startsWith('△')) { const desc = line.replace(/^△\s*/, ''); // 检测特效 if (desc.startsWith('特效:') || desc.startsWith('特效:')) { currentScene.effects.push({ type: 'effect', description: desc.replace(/^特效[::]\s*/, ''), raw: line, }); } else if (desc.startsWith('特写:') || desc.startsWith('特写:')) { currentScene.effects.push({ type: 'closeup', description: desc.replace(/^特写[::]\s*/, ''), raw: line, }); } else if (desc.startsWith('切画面') || desc.startsWith('闪切')) { currentScene.effects.push({ type: 'transition', description: desc, raw: line, }); } else { currentScene.descriptions.push(desc); } continue; } // 对话/独白: 「名字(情绪):台词」 或 「名字(情绪)OS:台词」 const dialogueMatch = line.match(/^(.+?)[((](.+?)[))](OS)?[::]\s*(.+)$/); if (dialogueMatch) { currentScene.dialogues.push({ character: dialogueMatch[1].trim(), emotion: dialogueMatch[2].trim(), type: dialogueMatch[3] ? 'inner' : 'dialogue', line: dialogueMatch[4].trim(), }); continue; } // 系统界面: 「系统:」 const systemMatch = line.match(/^系统[::]\s*(.+)$/); if (systemMatch) { currentScene.effects.push({ type: 'system_ui', description: systemMatch[1], raw: line, }); continue; } // 其他行(可能是续行或多行描述)→ 追加到当前最后一条description if (line && currentScene) { const lastDesc = currentScene.descriptions; const lastDialogues = currentScene.dialogues; if (lastDialogues.length > 0) { lastDialogues[lastDialogues.length - 1].line += ' ' + line; } else if (lastDesc.length > 0) { lastDesc[lastDesc.length - 1] += ' ' + line; } } } // 处理最后一幕 if (currentScene && currentEpisode) { currentEpisode.scenes.push(currentScene); } return { title: title || '未命名剧本', episodes, stats: { totalEpisodes: episodes.length, totalScenes: episodes.reduce((s, e) => s + e.scenes.length, 0), totalDialogues: episodes.reduce((s, e) => s + e.scenes.reduce((ss, sc) => ss + sc.dialogues.length, 0), 0), totalCharacters: countUniqueCharacters(episodes), }, }; } /** * 统计不重复角色 */ function countUniqueCharacters(episodes) { const chars = new Set(); for (const ep of episodes) { for (const sc of ep.scenes) { for (const c of sc.characters) chars.add(c.name); for (const d of sc.dialogues) chars.add(d.character); } } return chars.size; } /** * 为视频AI生成Markdown格式的分镜摘要(铸渊阅读用) * @param {object} scene - 单场场景对象 */ function sceneToMarkdown(scene) { const lines = []; lines.push(`## 第${scene.id}场 · ${scene.name}`); lines.push(`**时间**: ${scene.time} | **场景**: ${scene.location}景`); lines.push(''); if (scene.characters.length) { lines.push('**出场人物**:'); for (const c of scene.characters) { lines.push(`- ${c.name}${c.description ? `(${c.description})` : ''}`); } lines.push(''); } if (scene.descriptions.length || scene.effects.length) { lines.push('**画面**:'); for (const d of scene.descriptions) lines.push(`- ${d}`); for (const e of scene.effects) { if (e.type === 'closeup') lines.push(`- 🎥 特写: ${e.description}`); else if (e.type === 'transition') lines.push(`- ✂️ ${e.description}`); else if (e.type === 'system_ui') lines.push(`- 📟 系统UI: ${e.description}`); else lines.push(`- ✨ 特效: ${e.description}`); } lines.push(''); } if (scene.dialogues.length) { lines.push('**台词**:'); for (const d of scene.dialogues) { const prefix = d.type === 'inner' ? '💭' : '🗣️'; lines.push(`- ${prefix} **${d.character}**(${d.emotion}): ${d.line}`); } lines.push(''); } return lines.join('\n'); } /** * 提取所有角色的完整信息(跨集合并) */ function extractAllCharacters(episodes) { const map = new Map(); for (const ep of episodes) { for (const sc of ep.scenes) { for (const c of sc.characters) { if (!map.has(c.name) || (c.description && !map.get(c.name).description)) { map.set(c.name, { name: c.name, description: c.description || map.get(c.name)?.description || '', firstAppearance: ep.episodeNumber, }); } } } } return Array.from(map.values()); } module.exports = { parseScript, sceneToMarkdown, extractAllCharacters, };