UNPKG

@webgal-tools/voice

Version:
353 lines (352 loc) 16.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VoiceGenerator = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const crypto_1 = require("crypto"); const compiler_js_1 = require("./compiler.js"); const index_js_1 = require("./translate/index.js"); const request_js_1 = require("./request.js"); // 移除getEnvConfig依赖,使用当前工作目录 const config_js_1 = require("./config.js"); const backup_js_1 = require("./backup.js"); const context_js_1 = require("./context.js"); const parallel_processor_js_1 = require("./parallel-processor.js"); const logger_1 = require("@webgal-tools/logger"); class VoiceGenerator { constructor(workDir) { this.workDir = workDir; // 使用传入的工作目录,如果没有则使用当前工作目录 this.configManager = new config_js_1.VoiceConfigManager(this.workDir); this.backupManager = new backup_js_1.BackupManager(this.workDir); this.api = new request_js_1.GPTSoVITSAPI(this.configManager.getGptSovitsUrl(), this.configManager.getModelVersion()); this.audioOutputDir = path_1.default.join(this.workDir, 'vocal'); this.ensureAudioDir(); this.initializeCharacterStyles(); } /** * 初始化角色语言特色 */ initializeCharacterStyles() { try { const config = this.configManager.loadConfig(); for (const character of config.characters) { if (character.prompt) { (0, index_js_1.setCharacterStyle)(character.character_name, character.prompt); } } } catch (error) { logger_1.logger.error('加载角色语言特色失败:', error); } } ensureAudioDir() { if (!fs_1.default.existsSync(this.audioOutputDir)) { fs_1.default.mkdirSync(this.audioOutputDir, { recursive: true }); } } /** * 生成基于内容的音频文件名 * @param character 角色名 * @param text 对话内容 * @returns 音频文件名 */ generateAudioFileName(character, text) { // 使用角色名和对话内容生成哈希 const contentHash = (0, crypto_1.createHash)('md5') .update(`${character}:${text}`) .digest('hex') .substring(0, 12); // 取前12位作为文件名 return `${character}_${contentHash}.wav`; } /** * 检查音频文件是否已存在 * @param audioFileName 音频文件名 * @returns 文件是否存在 */ audioFileExists(audioFileName) { const audioPath = path_1.default.join(this.audioOutputDir, audioFileName); return fs_1.default.existsSync(audioPath); } /** * 生成内容哈希 * @param character 角色名 * @param text 对话内容 * @returns 内容哈希 */ generateContentHash(character, text) { return (0, crypto_1.createHash)('md5') .update(`${character}:${text}`) .digest('hex') .substring(0, 12); } /** * 删除音频文件 * @param audioFileName 音频文件名 */ deleteAudioFile(audioFileName) { if (!audioFileName.trim()) return; const audioPath = path_1.default.join(this.audioOutputDir, audioFileName); if (fs_1.default.existsSync(audioPath)) { try { fs_1.default.unlinkSync(audioPath); logger_1.logger.info(`删除音频文件: ${audioFileName}`); } catch (error) { logger_1.logger.error(`删除音频文件失败 ${audioFileName}:`, error); } } } /** * 创建语音生成任务(优化版本) * @param addedDialogues 新增的对话 * @returns 去重后的语音任务数组 */ createVoiceTasks(addedDialogues) { const tasks = []; const uniqueTasks = new Map(); // 用于去重的映射 logger_1.logger.info(`📋 创建语音任务,共 ${addedDialogues.length} 个对话`); for (const dialogue of addedDialogues) { const contentHash = this.generateContentHash(dialogue.character, dialogue.text); const audioFileName = this.generateAudioFileName(dialogue.character, dialogue.text); // 检查音频文件是否已存在 if (this.audioFileExists(audioFileName)) { logger_1.logger.info(`✅ 音频文件已存在,跳过任务: ${audioFileName}`); continue; } // 使用内容哈希作为去重key const taskKey = contentHash; if (!uniqueTasks.has(taskKey)) { const task = { character: dialogue.character, originalText: dialogue.text, targetText: dialogue.text, // 如果需要翻译,后面会更新 audioFileName, contentHash }; uniqueTasks.set(taskKey, task); tasks.push(task); logger_1.logger.info(`📝 创建任务: ${dialogue.character} - ${dialogue.text.substring(0, 20)}...`); } else { logger_1.logger.info(`🔄 发现重复任务,已合并: ${dialogue.character} - ${dialogue.text.substring(0, 20)}...`); } } logger_1.logger.info(`🎯 任务创建完成:原始 ${addedDialogues.length} 个对话,去重后 ${tasks.length} 个任务`); return tasks; } /** * 使用并行处理器处理翻译和语音合成任务 * @param tasks 语音任务数组 * @param allDialogues 所有对话(用于提取上下文) * @returns 成功处理的任务数组 */ async processTasksParallel(tasks, allDialogues) { if (tasks.length === 0) { return []; } // 检查翻译服务可用性 if (this.configManager.isTranslateEnabled()) { const translateConfig = this.configManager.getTranslateConfig(); logger_1.logger.info(`检查 ${translateConfig.model_type} 服务可用性...`); const isServiceAvailable = await (0, index_js_1.checkTranslatorService)(translateConfig); if (!isServiceAvailable) { logger_1.logger.warn(`${translateConfig.model_type} 服务不可用,将跳过翻译步骤`); return []; } } // 准备角色配置映射 const characterConfigs = new Map(); let hasAutoModeTask = false; let hasNormalModeTask = false; for (const task of tasks) { const config = this.configManager.getCharacterConfig(task.character); if (config) { characterConfigs.set(task.character, config); // 统计任务类型 if (config.auto === true) { hasAutoModeTask = true; } else { hasNormalModeTask = true; } } else { logger_1.logger.error(`❌ 角色 ${task.character} 未在 voice.config.json 中配置`); } } logger_1.logger.info(`📊 任务统计: 自动模式 ${hasAutoModeTask ? '有' : '无'}, 普通模式 ${hasNormalModeTask ? '有' : '无'}`); // 提取上下文信息 const contextMap = new Map(); if (allDialogues && allDialogues.length > 0 && this.configManager.isTranslateEnabled()) { logger_1.logger.info('📖 提取对话上下文以提高翻译质量...'); const translateConfig = this.configManager.getTranslateConfig(); for (const task of tasks) { const dialogueIndex = allDialogues.findIndex(d => d.character === task.character && d.text === task.originalText); if (dialogueIndex !== -1) { const contextSize = translateConfig.context_size || 2; const contextInfo = context_js_1.ContextExtractor.extractContext(allDialogues, dialogueIndex, contextSize); if (contextInfo.contextText) { const taskKey = `${task.character}:${task.originalText}`; contextMap.set(taskKey, contextInfo.contextText); } } } logger_1.logger.info(`为 ${contextMap.size} 个对话提取了上下文信息`); } // 使用统一的并行处理器处理所有任务 logger_1.logger.info(`🚀 开始统一并行处理 ${tasks.length} 个任务...`); const config = this.configManager.loadConfig(); const processor = new parallel_processor_js_1.ParallelProcessor(this.api, this.audioOutputDir, config.gpt_sovits_path // 传递 GPT-SoVITS 路径 ); // 设置进度回调函数 processor.setCallbacks({ onTranslateProgress: (completed, total, result) => { const mode = result.isAutoMode ? '自动模式' : '普通模式'; logger_1.logger.info(`📝 ${mode}翻译进度: ${completed}/${total} - ${result.character}: ${result.translatedText.substring(0, 30)}...`); }, onVoiceProgress: (completed, total, result) => { logger_1.logger.info(`🎵 语音合成进度: ${completed}/${total} - ${result.character}: ${result.audioFileName}`); }, onError: (error, task) => { logger_1.logger.error(`❌ 任务处理失败: ${task.character} - ${error.message}`); } }); try { const translateConfig = this.configManager.getTranslateConfig(); const successfulTasks = await processor.processTasksParallel(tasks, characterConfigs, translateConfig, contextMap, config.gpt_sovits_path); logger_1.logger.info(`🎉 统一并行处理完成: 成功处理 ${successfulTasks.length}/${tasks.length} 个任务`); return successfulTasks; } finally { processor.cleanup(); } } /** * 主要的语音生成函数(优化版本 - 基于文件缓存) * @param fileName 脚本文件名(相对于工作目录/scene) * @param forceMode 强制模式,清理现有音频文件并重新生成所有语音 */ async generateVoice(fileName, forceMode = false) { const filePath = path_1.default.resolve(this.workDir, 'scene', fileName); if (!fs_1.default.existsSync(filePath)) { throw new Error(`脚本文件不存在: ${filePath}`); } logger_1.logger.info(`开始处理脚本文件: ${filePath}`); if (forceMode) { logger_1.logger.info(`⚡ 强制模式:清理现有音频文件并重新生成所有语音`); } // 获取配置的角色列表 const configuredCharacters = this.configManager.getAllCharacterNames(); // 解析所有对话 const allDialogues = compiler_js_1.WebGALScriptCompiler.parseScript(filePath, configuredCharacters); logger_1.logger.info(`📖 解析到 ${allDialogues.length} 条对话`); if (allDialogues.length === 0) { logger_1.logger.info('没有找到需要处理的对话'); return; } let needVoiceDialogues = []; if (forceMode) { // 强制模式:清理所有相关的音频文件 logger_1.logger.info('🧹 强制模式:清理现有音频文件...'); for (const dialogue of allDialogues) { const audioFileName = this.generateAudioFileName(dialogue.character, dialogue.text); if (this.audioFileExists(audioFileName)) { this.deleteAudioFile(audioFileName); } } // 所有对话都需要重新生成 needVoiceDialogues = allDialogues; logger_1.logger.info(`强制模式:将重新生成 ${needVoiceDialogues.length} 条对话的语音`); } else { // 正常模式:筛选出没有音频文件的对话 logger_1.logger.info('🔍 检查音频缓存状态...'); for (const dialogue of allDialogues) { const audioFileName = this.generateAudioFileName(dialogue.character, dialogue.text); if (!this.audioFileExists(audioFileName)) { needVoiceDialogues.push(dialogue); } else { logger_1.logger.info(`✅ 音频已缓存: ${dialogue.character} - ${dialogue.text.substring(0, 20)}...`); } } logger_1.logger.info(`检查完成:${allDialogues.length} 条对话中,${needVoiceDialogues.length} 条需要生成语音`); } // 如果没有需要生成语音的对话,直接更新脚本文件引用 if (needVoiceDialogues.length === 0) { logger_1.logger.info('🎉 所有对话都已有音频缓存,只需更新脚本文件引用'); this.updateScriptFileReferences(filePath, allDialogues); return; } // 创建语音生成任务 const voiceTasks = this.createVoiceTasks(needVoiceDialogues); if (voiceTasks.length === 0) { logger_1.logger.info('没有有效的语音生成任务'); return; } // 使用并行处理器处理翻译和语音合成 const successfulTasks = await this.processTasksParallel(voiceTasks, allDialogues); // 更新脚本文件 - 包含新生成的和已缓存的音频 logger_1.logger.info('📝 更新脚本文件引用...'); this.updateScriptFileReferences(filePath, allDialogues, successfulTasks); logger_1.logger.info(`🎉 语音生成完成!新生成 ${successfulTasks.length} 条,复用缓存 ${allDialogues.length - needVoiceDialogues.length} 条`); } /** * 更新脚本文件引用(新方法) * @param filePath 脚本文件路径 * @param allDialogues 所有对话 * @param successfulTasks 成功的语音任务(可选) */ updateScriptFileReferences(filePath, allDialogues, successfulTasks) { // 创建任务映射 const taskMap = new Map(); if (successfulTasks) { for (const task of successfulTasks) { if (task.contentHash) { taskMap.set(task.contentHash, task); } } } // 更新所有对话的音频文件信息 const updatedDialogues = []; for (const dialogue of allDialogues) { const contentHash = this.generateContentHash(dialogue.character, dialogue.text); let audioFileName; // 优先使用新生成的任务结果 const task = taskMap.get(contentHash); if (task) { audioFileName = task.audioFileName; } else { // 检查是否有缓存的音频文件 const cachedAudioFileName = this.generateAudioFileName(dialogue.character, dialogue.text); if (this.audioFileExists(cachedAudioFileName)) { audioFileName = cachedAudioFileName; } } // 创建更新后的对话块 const updatedDialogue = { ...dialogue, audioFile: audioFileName, volume: audioFileName ? this.configManager.getDefaultVolume().toString() : dialogue.volume }; updatedDialogues.push(updatedDialogue); } // 使用新的重构方法生成脚本内容 const newContent = compiler_js_1.WebGALScriptCompiler.rebuildScript(filePath, updatedDialogues); // 创建备份 try { const fileName = path_1.default.basename(filePath); this.backupManager.createBackup(filePath); this.backupManager.cleanOldBackups(fileName, 5); } catch (error) { logger_1.logger.error('创建备份时出错:', error); } // 写入新内容 fs_1.default.writeFileSync(filePath, newContent); logger_1.logger.info(`✅ 更新脚本文件: ${filePath}`); } } exports.VoiceGenerator = VoiceGenerator;