@webgal-tools/voice
Version:
WebGAL GPT-SoVITS语音合成应用
353 lines (352 loc) • 16.4 kB
JavaScript
;
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;