import { ipcMain } from 'electron'; import * as aliyunApi from './index'; import { uploadToOss, getOssConfigFromProvider, OssConfig } from './oss'; import { recognizeRecordedAudio } from './transcription'; import logger from '../log/main'; import { preCacheSystemVoices, getPreCachedVoicePath, isAllVoicesCached, clearPreCache } from './voicePreCache'; import { detectAudioSegments, allocateTextToSegments } from '../audio/volumeDetect'; export function registerAliyunHandlers() { // CosyVoice TTS 合成 ipcMain.handle('aliyun:cosyvoice:synthesize', async (event, params) => { try { // 如果指定了 saveToPreCache,自动设置输出路径到预缓存目录 if (params.config.saveToPreCache) { const voiceId = params.config.saveToPreCache; const { getPreCacheDir } = await import('./voicePreCache'); const path = await import('path'); const cacheDir = getPreCacheDir(); const outputPath = path.default.join(cacheDir, `${voiceId}.wav`); logger.info('[Aliyun:IPC] 保存到预缓存目录', { voiceId, outputPath }); // 设置输出路径 params.config.outputPath = outputPath; // 移除 saveToPreCache,避免传递给 API delete params.config.saveToPreCache; } // 🆕 如果 provided language but not instruction,自动生成 instruction if (params.config.language && !params.config.instruction) { const instruction = aliyunApi.cosyvoice.getInstructionForLanguage(params.config.language); if (instruction) { params.config.instruction = instruction; logger.info('[Aliyun:IPC] 已为语言生成指令', { language: params.config.language, instruction }); } } return await aliyunApi.cosyvoice.synthesizeSpeech(params.text, params.config); } catch (error: any) { logger.error('[Aliyun:IPC] 合成失败', error); return { success: false, error: error.message }; } }); // CosyVoice 声音复刻 ipcMain.handle('aliyun:cosyvoice:enroll', async (event, params) => { try { // params: { apiKey, audioPath, name } return await aliyunApi.enrollVoice(params); } catch (error: any) { logger.error('[Aliyun:IPC] 复刻失败', error); return { success: false, error: error.message }; } }); // FunASR 语音识别(仅使用录音文件识别模式) ipcMain.handle('aliyun:funasr:recognize', async (event, params) => { try { // params: { audioPath, config: { apiKey, ossConfig } } logger.info('[Aliyun:IPC] 开始语音识别(录音文件识别模式)', { audioPath: params.audioPath }); const { audioPath, config } = params; // 检查OSS配置(必需) logger.info('[Aliyun:IPC] 接收到的配置', { hasOssConfig: !!config.ossConfig, ossConfigKeys: config.ossConfig ? Object.keys(config.ossConfig) : [], ossConfig: config.ossConfig }); const ossConfig: OssConfig | null = config.ossConfig ? { accessKeyId: config.ossConfig.accessKeyId, accessKeySecret: config.ossConfig.accessKeySecret, bucket: config.ossConfig.bucket, region: config.ossConfig.region, endpoint: config.ossConfig.endpoint } : null; // 必须配置OSS才能使用录音识别 if (!ossConfig || !ossConfig.accessKeyId || !ossConfig.bucket) { logger.error('[Aliyun:IPC] OSS未配置,无法使用录音识别', { ossConfig }); return { success: false, error: '请先在「模型」→「阿里云百炼」中配置OSS(用于快速语音识别)' }; } if (!ossConfig.region) { logger.error('[Aliyun:IPC] OSS Region 未配置', { ossConfig }); return { success: false, error: '请在「模型」→「阿里云百炼」中配置 OSS Region(地域)' }; } logger.info('[Aliyun:IPC] 使用录音文件识别模式', { bucket: ossConfig.bucket, region: ossConfig.region, hasEndpoint: !!ossConfig.endpoint }); // 检查文件类型,如果是视频文件需要提取音频 const path = await import('path'); const ext = path.extname(audioPath).toLowerCase(); let finalAudioPath = audioPath; if (['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm'].includes(ext)) { logger.info('[Aliyun:IPC] 检测到视频文件,需要提取音频', { ext, audioPath }); try { const { extractAudioFromVideo } = await import('../ipAgent/funasr'); const extractedAudioPath = await extractAudioFromVideo(audioPath); finalAudioPath = extractedAudioPath; logger.info('[Aliyun:IPC] 音频提取成功', { extractedAudioPath }); } catch (extractError: any) { logger.warn('[Aliyun:IPC] 音频提取失败,继续尝试上传原文件', { error: extractError.message }); } } // 1. 上传到OSS const uploadResult = await uploadToOss(finalAudioPath, ossConfig); if (!uploadResult.success || !uploadResult.url) { logger.error('[Aliyun:IPC] OSS上传失败', uploadResult.error); return { success: false, error: `OSS上传失败: ${uploadResult.error}` }; } logger.info('[Aliyun:IPC] OSS上传成功', { url: uploadResult.url }); // 2. 调用录音识别API const result = await recognizeRecordedAudio(uploadResult.url, { apiKey: config.apiKey, model: config.model || 'qwen3-asr-flash-filetrans' // 🔧 修复:默认使用 qwen3-asr-flash-filetrans 模型 }); // 转换结果格式以保持兼容性 return { success: result.success, fullText: result.text, sentences: result.sentences?.map(s => ({ text: s.text, startTime: s.beginTime, endTime: s.endTime, words: s.words?.map(w => ({ text: w.text, startTime: w.beginTime, endTime: w.endTime })) })), error: result.error, requestId: result.taskId }; } catch (error: any) { // 确保错误消息可序列化(避免循环引用) let errorMessage = '未知错误'; if (error && typeof error === 'object') { errorMessage = error.message || error.toString(); } else if (typeof error === 'string') { errorMessage = error; } logger.error('[Aliyun:IPC] 语音识别失败', { errorMessage }); return { success: false, error: errorMessage }; } }); // FunASR 使用 URL 直接识别(无需上传OSS) ipcMain.handle('aliyun:funasr:recognizeFromUrl', async (event, params) => { try { // params: { audioUrl, apiKey } logger.info('[Aliyun:IPC] 开始语音识别(URL模式)', { audioUrl: params.audioUrl }); const { audioUrl, apiKey } = params; if (!audioUrl || !apiKey) { return { success: false, error: '缺少必要参数:audioUrl 或 apiKey' }; } // 直接使用 URL 调用识别 API const result = await recognizeRecordedAudio(audioUrl, { apiKey, model: params.model || 'fun-asr' // 使用传递的模型,默认 fun-asr }); // 转换结果格式以保持兼容性 return { success: result.success, fullText: result.text, sentences: result.sentences?.map(s => ({ text: s.text, startTime: s.beginTime, endTime: s.endTime, words: s.words?.map(w => ({ text: w.text, startTime: w.beginTime, endTime: w.endTime })) })), error: result.error, requestId: result.taskId }; } catch (error: any) { let errorMessage = '未知错误'; if (error && typeof error === 'object') { errorMessage = error.message || error.toString(); } else if (typeof error === 'string') { errorMessage = error; } logger.error('[Aliyun:IPC] URL识别失败', { errorMessage }); return { success: false, error: errorMessage }; } }); // 预缓存系统默认音色 ipcMain.handle('aliyun:cosyvoice:preCacheSystemVoices', async (event, params) => { try { const { apiKey, force } = params; await preCacheSystemVoices(apiKey, force); return { success: true }; } catch (error: any) { logger.error('[Aliyun:IPC] 预缓存失败', error); return { success: false, error: error.message }; } }); // 获取预缓存的音色文件路径 ipcMain.handle('aliyun:cosyvoice:getPreCachedVoicePath', async (event, voiceId: string) => { try { const cachedPath = getPreCachedVoicePath(voiceId); return { success: true, path: cachedPath }; } catch (error: any) { logger.error('[Aliyun:IPC] 获取预缓存路径失败', error); return { success: false, error: error.message }; } }); // 检查所有音色是否已缓存 ipcMain.handle('aliyun:cosyvoice:isAllVoicesCached', async (event) => { try { const allCached = isAllVoicesCached(); return { success: true, allCached }; } catch (error: any) { logger.error('[Aliyun:IPC] 检查缓存状态失败', error); return { success: false, error: error.message }; } }); // 清除预缓存 ipcMain.handle('aliyun:cosyvoice:clearPreCache', async (event) => { try { clearPreCache(); return { success: true }; } catch (error: any) { logger.error('[Aliyun:IPC] 清除预缓存失败', error); return { success: false, error: error.message }; } }); // 音量检测 - 检测音频中的声音片段 ipcMain.handle('audio:detectVolume', async (event, params) => { try { // params: { audioPath, config?: { silenceThreshold, minSilenceDuration, minSoundDuration } } logger.info('[Audio:IPC] 开始音量检测', { audioPath: params.audioPath }); const result = await detectAudioSegments(params.audioPath, params.config || {}); return result; } catch (error: any) { logger.error('[Audio:IPC] 音量检测失败', error); return { success: false, segments: [], totalDuration: 0, error: error.message }; } }); // 音量检测 - 根据音频片段分配文本时间 ipcMain.handle('audio:allocateText', async (event, params) => { try { // params: { text, segments } logger.info('[Audio:IPC] 开始文本时间分配', { textLength: params.text?.length, segmentCount: params.segments?.length }); // 🔧 验证segments数据完整性 if (!params.segments || params.segments.length === 0) { logger.warn('[Audio:IPC] 警告:segments为空'); return { success: false, error: 'segments数组为空' }; } // 检查第一个和最后一个segment的结构 const firstSegment = params.segments[0]; const lastSegment = params.segments[params.segments.length - 1]; if (!firstSegment || typeof firstSegment.startTime !== 'number' || typeof firstSegment.endTime !== 'number') { logger.error('[Audio:IPC] 第一个segment数据异常:', firstSegment); return { success: false, error: `第一个segment数据异常: ${JSON.stringify(firstSegment)}` }; } if (!lastSegment || typeof lastSegment.startTime !== 'number' || typeof lastSegment.endTime !== 'number') { logger.error('[Audio:IPC] 最后一个segment数据异常:', lastSegment); return { success: false, error: `最后一个segment数据异常: ${JSON.stringify(lastSegment)}` }; } logger.info('[Audio:IPC] segments验证通过,准备分配文本'); const result = allocateTextToSegments(params.text, params.segments); return { success: true, subtitles: result }; } catch (error: any) { logger.error('[Audio:IPC] 文本时间分配失败', error); logger.error('[Audio:IPC] 完整错误堆栈:', error.stack); return { success: false, error: error.message || error.toString() }; } }); }