UNPKG

@webgal-tools/voice

Version:
349 lines (348 loc) 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebGALScriptCompiler = void 0; const fs_1 = require("fs"); class WebGALScriptCompiler { /** * 检查是否为空行 * @param line 行内容 * @returns 是否为空行 */ static isEmptyLine(line) { return !line || line.trim() === ''; } /** * 解析WebGAL脚本内容为语句数组 * @param content 脚本内容 * @returns 语句数组及其行号信息 */ static parseStatements(content) { const lines = content.split('\n'); const statements = []; let reduce = undefined; let startLineNumber = 0; for (let i = 0; i < lines.length; i++) { let line = lines[i]; line = line.trim(); if (this.isEmptyLine(line)) { // 忽略空白行 continue; } // 本行未结束(不包含分号) if (!line.includes(';')) { if (reduce === undefined) { reduce = line + '\n'; startLineNumber = i + 1; } else { reduce += line + '\n'; } continue; } // 一行注释(以分号开头) if (line.startsWith(';')) { continue; } if (line.endsWith(';')) { // 单行语句 if (reduce === undefined) { statements.push({ statement: line, lineNumber: i + 1 }); continue; } // 多行语句 statements.push({ statement: reduce + line, lineNumber: startLineNumber }); reduce = undefined; continue; } // 语句末尾带有注释,例如 A: text ; 注释 const statementPart = line.split(';')[0]; // 单行语句 if (reduce === undefined) { statements.push({ statement: statementPart, lineNumber: i + 1 }); continue; } // 多行语句 statements.push({ statement: reduce + statementPart, lineNumber: startLineNumber }); reduce = undefined; } return statements; } /** * 解析单个语句 * @param statement 语句内容 * @param statementId 语句ID * @param lineNumber 行号 * @returns 解析结果 */ static parseStatement(statement, statementId, lineNumber) { // 清理语句,移除末尾的分号 const cleanStatement = statement.trim().replace(/;$/, ''); const firstColonIndex = cleanStatement.indexOf(':'); // 无效语句(没有冒号) if (firstColonIndex === -1) { return null; } // 旁白,不处理(以冒号开头) if (cleanStatement.startsWith(':')) { return null; } const character = cleanStatement.substring(0, firstColonIndex).trim(); const rightPart = cleanStatement.substring(firstColonIndex + 1); // 检查角色名是否有效(不能包含注释标记) if (character.includes('//') || character.includes(';')) { return null; } // 按'-'分割参数,但保留文本中的内容 const parts = rightPart.split(/(?=\s-)/); // 按照空格+减号分割 const text = parts[0].trim(); let audioFile; let volume; const otherArgs = []; // 解析参数(从第二部分开始) for (let i = 1; i < parts.length; i++) { const part = parts[i].trim(); if (!part.startsWith('-')) continue; const arg = part.substring(1); // 移除开头的减号 // 检测音频文件参数(以.wav结尾) if (arg.endsWith('.wav')) { audioFile = arg; } // 检测音量参数(volume=数字) else if (arg.startsWith('volume=')) { const volumeMatch = arg.match(/^volume=(\d+)$/); if (volumeMatch) { volume = volumeMatch[1]; } } // 其他参数 else { otherArgs.push(arg); } } return { character, text, audioFile, volume, statementId, otherArgs, originalStatement: statement, lineNumber }; } /** * 解析WebGAL脚本文件,提取对话内容 * @param filePath 脚本文件路径 * @param configuredCharacters 配置文件中定义的角色名列表 * @returns 对话块数组 */ static parseScript(filePath, configuredCharacters) { if (!fs_1.default.existsSync(filePath)) { throw new Error(`Script file not found: ${filePath}`); } const content = fs_1.default.readFileSync(filePath, 'utf-8'); const statements = this.parseStatements(content); const dialogues = []; // 创建角色名的集合,用于快速查找 const characterSet = new Set(configuredCharacters.map(name => name.trim())); for (let i = 0; i < statements.length; i++) { const { statement, lineNumber } = statements[i]; const parsed = this.parseStatement(statement, i, lineNumber); if (!parsed) { continue; } const { character, text, audioFile, volume, statementId, otherArgs, originalStatement } = parsed; // 只处理配置文件中定义的角色 if (characterSet.has(character)) { dialogues.push({ character, text, audioFile, volume, lineNumber, originalLine: originalStatement, statementId, otherArgs, id: statementId // 使用语句ID作为唯一标识 }); } else { console.error(`跳过未配置的角色: ${character}`); } } return dialogues; } /** * 应用语句修改 * @param statementId 语句ID * @param modifiedDialogue 修改后的对话信息 * @returns 应用后的语句字符串 */ static applyStatement(modifiedDialogue) { let statement = `${modifiedDialogue.character}: ${modifiedDialogue.text}`; if (modifiedDialogue.audioFile) { statement += ` -${modifiedDialogue.audioFile}`; } if (modifiedDialogue.volume) { statement += ` -volume=${modifiedDialogue.volume}`; } // 添加其他参数 if (modifiedDialogue.otherArgs && modifiedDialogue.otherArgs.length > 0) { for (const arg of modifiedDialogue.otherArgs) { statement += ` -${arg}`; } } statement += ';'; return statement; } /** * 重新构建脚本内容(按照编译规则.md的应用思路) * @param filePath 原始脚本文件路径 * @param newDialogues 新的对话块数组 * @returns 新的脚本内容 */ static rebuildScript(filePath, newDialogues) { const originalContent = fs_1.default.readFileSync(filePath, 'utf-8'); const lines = originalContent.split('\n'); // 创建语句ID到对话映射 const dialogueByStatementId = new Map(); for (const dialogue of newDialogues) { if (dialogue.statementId !== undefined) { dialogueByStatementId.set(dialogue.statementId, dialogue); } } let content = ''; let statementCount = 0; let reduce = undefined; for (let i = 0; i < lines.length; i++) { let line = lines[i]; const originalLine = line; line = line.trim(); if (this.isEmptyLine(line)) { content += originalLine + '\n'; continue; } // 本行未结束 if (!line.includes(';')) { if (reduce === undefined) { reduce = originalLine + '\n'; } else { reduce += originalLine + '\n'; } continue; } // 一行注释 if (line.startsWith(';')) { content += originalLine + '\n'; continue; } if (line.endsWith(';')) { // 单行语句 if (reduce === undefined) { const modifiedDialogue = dialogueByStatementId.get(statementCount); if (modifiedDialogue) { content += this.applyStatement(modifiedDialogue) + '\n'; } else { content += originalLine + '\n'; } statementCount++; continue; } // 多行语句 const modifiedDialogue = dialogueByStatementId.get(statementCount); if (modifiedDialogue) { content += this.applyStatement(modifiedDialogue) + '\n'; } else { content += reduce + originalLine + '\n'; } reduce = undefined; statementCount++; continue; } // 语句末尾带有注释,例如 A: text ; 注释 const statementPart = line.split(';')[0]; const commentPart = originalLine.substring(originalLine.indexOf(';') + 1); // 单行语句 if (reduce === undefined) { const modifiedDialogue = dialogueByStatementId.get(statementCount); if (modifiedDialogue) { content += this.applyStatement(modifiedDialogue) + commentPart + '\n'; } else { content += originalLine + '\n'; } statementCount++; continue; } // 多行语句 const modifiedDialogue = dialogueByStatementId.get(statementCount); if (modifiedDialogue) { content += this.applyStatement(modifiedDialogue) + commentPart + '\n'; } else { content += reduce + originalLine + '\n'; } reduce = undefined; statementCount++; } return content; } /** * 从对话块数组中生成键值对映射 * @param dialogues 对话块数组 * @returns 映射表 {角色名+对话: 音频文件名} */ static generateDialogueMap(dialogues) { const map = new Map(); for (const dialogue of dialogues) { const key = `${dialogue.character}:${dialogue.text}`; const audioFile = dialogue.audioFile || ''; map.set(key, audioFile); } return map; } /** * 获取所有唯一的角色名 * @param dialogues 对话块数组 * @returns 角色名数组 */ static getUniqueCharacters(dialogues) { const characters = new Set(); for (const dialogue of dialogues) { characters.add(dialogue.character); } return Array.from(characters); } /** * 按角色分组对话 * @param dialogues 对话块数组 * @returns 按角色分组的对话映射 */ static groupDialoguesByCharacter(dialogues) { const grouped = new Map(); for (const dialogue of dialogues) { if (!grouped.has(dialogue.character)) { grouped.set(dialogue.character, []); } grouped.get(dialogue.character).push(dialogue); } return grouped; } } exports.WebGALScriptCompiler = WebGALScriptCompiler;