{"repo": "bxxdm1/podcast-transcriber", "files": {".env.example": {"b47fdb127e62eea524ea21643ceee4f431c157a2": {"t": 1785745175.7693806, "n": 231, "h": "925e56d13d782f38", "c": "# DeepSeek API 配置\nDEEPSEEK_API_KEY=your_deepseek_api_key_here\nDEEPSEEK_BASE_URL=https://api.deepseek.com\n\n# 本地 Whisper 配置\nWHISPER_MODEL=base\n\n# 服务器配置\nPORT=3000\n\n# Python 路径 (Anaconda)\nPYTHON_PATH=D:/Study_tools/Anaconda/python.exe\n"}, "369723ad7dd34f4b2e44febf054990558cdb4826": {"t": 1785748407.6012282, "n": 231, "h": "925e56d13d782f38", "c": "# DeepSeek API 配置\nDEEPSEEK_API_KEY=your_deepseek_api_key_here\nDEEPSEEK_BASE_URL=https://api.deepseek.com\n\n# 本地 Whisper 配置\nWHISPER_MODEL=base\n\n# 服务器配置\nPORT=3000\n\n# Python 路径 (Anaconda)\nPYTHON_PATH=D:/Study_tools/Anaconda/python.exe\n"}}, "server/services/deepseekService.js": {"b47fdb127e62eea524ea21643ceee4f431c157a2": {"t": 1785746683.0711823, "n": 11180, "h": "c1849ce1199bf8f1", "c": "/**\n * DeepSeek AI 服务\n * 替代 OpenAI,用于文本优化和总结\n * DeepSeek API 兼容 OpenAI 格式\n */\nconst OpenAI = require('openai');\nconst fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst { promisify } = require('util');\n\nconst execAsync = promisify(exec);\n\n// 初始化 DeepSeek 客户端(兼容 OpenAI SDK)\nconst deepseek = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',\n timeout: 600000,\n maxRetries: 2\n});\n\nconst DEEPSEEK_MODEL = 'deepseek-chat';\n\nconsole.log(`🤖 AI服务: DeepSeek (${DEEPSEEK_MODEL})`);\n\n/**\n * 生成标准化的文件名\n */\nfunction generateFilePrefix(type, title) {\n let cleanTitle = title\n .replace(/\\s*\\|\\s*/g, '-')\n .replace(/\\s*:\\s*/g, '-')\n .replace(/[<>:\"/\\\\|?*]/g, '')\n .replace(/\\s+/g, '_')\n .replace(/[^\\w一-龥\\-_.]/g, '');\n if (cleanTitle.length > 30) cleanTitle = cleanTitle.substring(0, 30);\n const uuid = Math.random().toString(36).substr(2, 6).toUpperCase();\n return `${type}_${cleanTitle}_${uuid}`;\n}\n\n/**\n * 处理音频文件:转录 + 优化 + 总结\n */\nasync function processAudio(audioFiles, shouldSummarize = false, tempDir = null, podcastTitle = null, sessionId = null, sendProgress = null) {\n try {\n console.log(`🤖 开始音频处理`);\n const files = Array.isArray(audioFiles) ? audioFiles : [audioFiles];\n console.log(`📄 处理文件数量: ${files.length}`);\n\n let transcript = '';\n let savedFiles = [];\n\n if (files.length === 1) {\n // 单文件处理 - 调用 Python Whisper 脚本\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const filePrefix = generateFilePrefix('raw', podcastTitle || 'Untitled');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${files[0]}\" --model ${process.env.WHISPER_MODEL || 'base'} --save-transcript \"${tempDir}\" --file-prefix \"${filePrefix}\" --podcast-title \"${podcastTitle || 'Untitled'}\"`;\n\n console.log(`🎤 Whisper 转录: ${path.basename(files[0])}`);\n if (sendProgress) sendProgress(30, 'transcription', '语音转录中...');\n\n const { stdout, stderr } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 3600000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n if (stderr && stderr.trim()) console.log(`🔧 Whisper日志: ${stderr.trim()}`);\n\n const result = JSON.parse(stdout);\n if (!result.success) throw new Error(result.error || '转录失败');\n\n transcript = result.text || '';\n savedFiles = result.savedFiles || [];\n console.log(`✅ 转录完成: ${transcript.length} 字符`);\n\n // DeepSeek 智能优化转录文本\n if (sendProgress) sendProgress(50, 'optimizing', 'AI优化文本中...');\n let optimizedTranscript = transcript;\n try {\n optimizedTranscript = await optimizeTranscript(transcript);\n console.log(`✅ 文本优化完成: ${optimizedTranscript.length} 字符`);\n\n // 保存优化后的文本\n if (savedFiles.length > 0) {\n const transcriptFile = savedFiles.find(f => f.type === 'transcript');\n if (transcriptFile && fs.existsSync(transcriptFile.path)) {\n // 备份原始转录\n const backupPath = transcriptFile.path.replace('.md', '_original.md');\n if (!fs.existsSync(backupPath)) {\n fs.copyFileSync(transcriptFile.path, backupPath);\n }\n fs.writeFileSync(transcriptFile.path, optimizedTranscript, 'utf8');\n }\n }\n transcript = optimizedTranscript;\n } catch (optErr) {\n console.warn(`⚠️ 文本优化失败,使用原始转录: ${optErr.message}`);\n }\n\n // 生成总结\n let summary = null;\n if (shouldSummarize) {\n console.log(`📝 生成总结...`);\n if (sendProgress) sendProgress(70, 'summary', 'AI生成总结中...');\n try {\n summary = await generateSummary(transcript);\n const summaryPrefix = generateFilePrefix('summary', podcastTitle || 'Untitled');\n const summaryFileName = `${summaryPrefix}.md`;\n const summaryPath = path.join(tempDir, summaryFileName);\n fs.writeFileSync(summaryPath, `# 🎙️ ${podcastTitle || '播客总结'}\\n\\n${summary}`, 'utf8');\n savedFiles.push({\n type: 'summary',\n filename: summaryFileName,\n path: summaryPath,\n size: fs.statSync(summaryPath).size\n });\n console.log(`📋 总结已保存: ${summaryFileName}`);\n } catch (sumErr) {\n console.error(`❌ 总结生成失败: ${sumErr.message}`);\n }\n }\n\n return {\n transcript: transcript,\n summary: summary,\n language: 'zh',\n audioDuration: result.duration,\n savedFiles: savedFiles\n };\n }\n\n // 多文件处理\n for (let i = 0; i < files.length; i++) {\n console.log(`🎵 转录片段 ${i + 1}/${files.length}`);\n const result = await transcribeSingleFile(files[i]);\n if (result) transcript += result + '\\n\\n';\n }\n\n return { transcript: transcript.trim(), summary: null, savedFiles: [] };\n\n } catch (error) {\n console.error('❌ 音频处理失败:', error);\n throw error;\n }\n}\n\n/**\n * 调用 Whisper 转录单个文件\n */\nasync function transcribeSingleFile(audioPath) {\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${audioPath}\" --model ${process.env.WHISPER_MODEL || 'base'}`;\n\n const { stdout } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 1200000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n const result = JSON.parse(stdout);\n return result.success ? result.text : null;\n}\n\n/**\n * DeepSeek 优化转录文本\n */\nasync function optimizeTranscript(rawTranscript) {\n const maxChars = 6000;\n if (rawTranscript.length <= maxChars) {\n return await optimizeChunk(rawTranscript);\n }\n\n // 分块处理长文本\n const chunks = splitText(rawTranscript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块优化`);\n\n const optimized = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 优化第 ${i + 1}/${chunks.length} 块`);\n const chunk = await optimizeChunk(chunks[i], i > 0 ? chunks[i - 1].slice(-100) : null);\n optimized.push(chunk);\n if (i < chunks.length - 1) await sleep(1000);\n }\n return optimized.join('\\n\\n');\n}\n\n/**\n * DeepSeek 优化单个文本块\n */\nasync function optimizeChunk(text, prevContext = null) {\n let contextHint = '';\n if (prevContext) {\n contextHint = `[上文续:${prevContext}]\\n\\n`;\n }\n\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: '你是专业的音频转录文本优化助手。修正错别字、同音字错误,改善语法,添加合适标点,但必须保持原意不变。按话题合理分段,Markdown格式,段落间双换行,不加分隔线。【必须使用简体中文输出,严禁繁体中文。写\"投资\"不写\"投資\",写\"经济\"不写\"經濟\"。】'\n },\n {\n role: 'user',\n content: `${contextHint}请优化以下转录文本:\\n\\n${text}`\n }\n ],\n temperature: 0.1,\n max_tokens: 4096\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * DeepSeek 生成播客总结\n */\nasync function generateSummary(transcript) {\n const maxChars = 6000;\n if (transcript.length <= maxChars) {\n return await generateSummaryChunk(transcript);\n }\n\n // 分块总结再整合\n const chunks = splitText(transcript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块总结`);\n\n const summaries = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 总结第 ${i + 1}/${chunks.length} 块`);\n const s = await generateSummaryChunk(chunks[i]);\n summaries.push(s);\n if (i < chunks.length - 1) await sleep(1000);\n }\n\n // 整合所有分块总结\n const combined = summaries.join('\\n\\n');\n return await mergeSummaries(combined);\n}\n\n/**\n * DeepSeek 生成单块总结\n */\nasync function generateSummaryChunk(text) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `你是一个专业的播客内容分析师。请生成全面、结构化的总结。\n\n【输出语言要求】必须使用简体中文输出,严禁使用繁体中文。例如写\"投资\"而不是\"投資\",写\"周期\"而不是\"週期\",写\"经济\"而不是\"經濟\"。\n\n要求:\n1. 提取主要话题和核心观点\n2. 保持逻辑结构清晰\n3. 包含重要的讨论内容、观点和结论\n4. 使用简洁明了的简体中文\n\n【严格排除】赞助商广告、节目资助方信息、播客开头结尾语、制作团队信息。\n\n格式:Markdown,段落之间用双换行分隔,每个段落专注一个主题。`\n },\n { role: 'user', content: text }\n ],\n temperature: 0.5,\n max_tokens: 3000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 整合分块总结\n */\nasync function mergeSummaries(combinedSummaries) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `请将以下分段总结整合成一个完整、连贯的播客总结。\n【输出语言要求】必须使用简体中文,严禁繁体中文。例如写\"投资\"不写\"投資\"。\n1. 去除重复内容\n2. 按主题重新组织\n3. 段落之间必须有空行分隔\n4. Markdown格式,简体中文输出\n5. 严格排除广告等无价值内容`\n },\n { role: 'user', content: combinedSummaries }\n ],\n temperature: 0.3,\n max_tokens: 4000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 智能分块(按段落和句子边界)\n */\nfunction splitText(text, maxChars) {\n const chunks = [];\n const paragraphs = text.split(/\\n\\s*\\n/).filter(p => p.trim());\n let current = '';\n\n for (const para of paragraphs) {\n const test = current + (current ? '\\n\\n' : '') + para;\n if (test.length > maxChars && current) {\n chunks.push(current.trim());\n current = para;\n } else {\n current = test;\n }\n }\n if (current.trim()) chunks.push(current.trim());\n\n // 对仍然超长的块按句子分割\n const result = [];\n for (const chunk of chunks) {\n if (chunk.length <= maxChars) {\n result.push(chunk);\n } else {\n const sentences = chunk.split(/([。!?.!?]+\\s*)/).filter(s => s.trim());\n let sub = '';\n for (let i = 0; i < sentences.length; i += 2) {\n const s = sentences[i] + (sentences[i + 1] || '');\n if ((sub + s).length > maxChars && sub) {\n result.push(sub.trim());\n sub = s;\n } else {\n sub += s;\n }\n }\n if (sub.trim()) result.push(sub.trim());\n }\n }\n return result;\n}\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\nmodule.exports = { processAudio, generateSummary, optimizeTranscript };\n"}, "e05c42a5e3db6efd5a6f7c3443ecf3fd32d034ce": {"t": 1785748407.5047245, "n": 11180, "h": "c1849ce1199bf8f1", "c": "/**\n * DeepSeek AI 服务\n * 替代 OpenAI,用于文本优化和总结\n * DeepSeek API 兼容 OpenAI 格式\n */\nconst OpenAI = require('openai');\nconst fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst { promisify } = require('util');\n\nconst execAsync = promisify(exec);\n\n// 初始化 DeepSeek 客户端(兼容 OpenAI SDK)\nconst deepseek = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',\n timeout: 600000,\n maxRetries: 2\n});\n\nconst DEEPSEEK_MODEL = 'deepseek-chat';\n\nconsole.log(`🤖 AI服务: DeepSeek (${DEEPSEEK_MODEL})`);\n\n/**\n * 生成标准化的文件名\n */\nfunction generateFilePrefix(type, title) {\n let cleanTitle = title\n .replace(/\\s*\\|\\s*/g, '-')\n .replace(/\\s*:\\s*/g, '-')\n .replace(/[<>:\"/\\\\|?*]/g, '')\n .replace(/\\s+/g, '_')\n .replace(/[^\\w一-龥\\-_.]/g, '');\n if (cleanTitle.length > 30) cleanTitle = cleanTitle.substring(0, 30);\n const uuid = Math.random().toString(36).substr(2, 6).toUpperCase();\n return `${type}_${cleanTitle}_${uuid}`;\n}\n\n/**\n * 处理音频文件:转录 + 优化 + 总结\n */\nasync function processAudio(audioFiles, shouldSummarize = false, tempDir = null, podcastTitle = null, sessionId = null, sendProgress = null) {\n try {\n console.log(`🤖 开始音频处理`);\n const files = Array.isArray(audioFiles) ? audioFiles : [audioFiles];\n console.log(`📄 处理文件数量: ${files.length}`);\n\n let transcript = '';\n let savedFiles = [];\n\n if (files.length === 1) {\n // 单文件处理 - 调用 Python Whisper 脚本\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const filePrefix = generateFilePrefix('raw', podcastTitle || 'Untitled');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${files[0]}\" --model ${process.env.WHISPER_MODEL || 'base'} --save-transcript \"${tempDir}\" --file-prefix \"${filePrefix}\" --podcast-title \"${podcastTitle || 'Untitled'}\"`;\n\n console.log(`🎤 Whisper 转录: ${path.basename(files[0])}`);\n if (sendProgress) sendProgress(30, 'transcription', '语音转录中...');\n\n const { stdout, stderr } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 3600000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n if (stderr && stderr.trim()) console.log(`🔧 Whisper日志: ${stderr.trim()}`);\n\n const result = JSON.parse(stdout);\n if (!result.success) throw new Error(result.error || '转录失败');\n\n transcript = result.text || '';\n savedFiles = result.savedFiles || [];\n console.log(`✅ 转录完成: ${transcript.length} 字符`);\n\n // DeepSeek 智能优化转录文本\n if (sendProgress) sendProgress(50, 'optimizing', 'AI优化文本中...');\n let optimizedTranscript = transcript;\n try {\n optimizedTranscript = await optimizeTranscript(transcript);\n console.log(`✅ 文本优化完成: ${optimizedTranscript.length} 字符`);\n\n // 保存优化后的文本\n if (savedFiles.length > 0) {\n const transcriptFile = savedFiles.find(f => f.type === 'transcript');\n if (transcriptFile && fs.existsSync(transcriptFile.path)) {\n // 备份原始转录\n const backupPath = transcriptFile.path.replace('.md', '_original.md');\n if (!fs.existsSync(backupPath)) {\n fs.copyFileSync(transcriptFile.path, backupPath);\n }\n fs.writeFileSync(transcriptFile.path, optimizedTranscript, 'utf8');\n }\n }\n transcript = optimizedTranscript;\n } catch (optErr) {\n console.warn(`⚠️ 文本优化失败,使用原始转录: ${optErr.message}`);\n }\n\n // 生成总结\n let summary = null;\n if (shouldSummarize) {\n console.log(`📝 生成总结...`);\n if (sendProgress) sendProgress(70, 'summary', 'AI生成总结中...');\n try {\n summary = await generateSummary(transcript);\n const summaryPrefix = generateFilePrefix('summary', podcastTitle || 'Untitled');\n const summaryFileName = `${summaryPrefix}.md`;\n const summaryPath = path.join(tempDir, summaryFileName);\n fs.writeFileSync(summaryPath, `# 🎙️ ${podcastTitle || '播客总结'}\\n\\n${summary}`, 'utf8');\n savedFiles.push({\n type: 'summary',\n filename: summaryFileName,\n path: summaryPath,\n size: fs.statSync(summaryPath).size\n });\n console.log(`📋 总结已保存: ${summaryFileName}`);\n } catch (sumErr) {\n console.error(`❌ 总结生成失败: ${sumErr.message}`);\n }\n }\n\n return {\n transcript: transcript,\n summary: summary,\n language: 'zh',\n audioDuration: result.duration,\n savedFiles: savedFiles\n };\n }\n\n // 多文件处理\n for (let i = 0; i < files.length; i++) {\n console.log(`🎵 转录片段 ${i + 1}/${files.length}`);\n const result = await transcribeSingleFile(files[i]);\n if (result) transcript += result + '\\n\\n';\n }\n\n return { transcript: transcript.trim(), summary: null, savedFiles: [] };\n\n } catch (error) {\n console.error('❌ 音频处理失败:', error);\n throw error;\n }\n}\n\n/**\n * 调用 Whisper 转录单个文件\n */\nasync function transcribeSingleFile(audioPath) {\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${audioPath}\" --model ${process.env.WHISPER_MODEL || 'base'}`;\n\n const { stdout } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 1200000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n const result = JSON.parse(stdout);\n return result.success ? result.text : null;\n}\n\n/**\n * DeepSeek 优化转录文本\n */\nasync function optimizeTranscript(rawTranscript) {\n const maxChars = 6000;\n if (rawTranscript.length <= maxChars) {\n return await optimizeChunk(rawTranscript);\n }\n\n // 分块处理长文本\n const chunks = splitText(rawTranscript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块优化`);\n\n const optimized = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 优化第 ${i + 1}/${chunks.length} 块`);\n const chunk = await optimizeChunk(chunks[i], i > 0 ? chunks[i - 1].slice(-100) : null);\n optimized.push(chunk);\n if (i < chunks.length - 1) await sleep(1000);\n }\n return optimized.join('\\n\\n');\n}\n\n/**\n * DeepSeek 优化单个文本块\n */\nasync function optimizeChunk(text, prevContext = null) {\n let contextHint = '';\n if (prevContext) {\n contextHint = `[上文续:${prevContext}]\\n\\n`;\n }\n\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: '你是专业的音频转录文本优化助手。修正错别字、同音字错误,改善语法,添加合适标点,但必须保持原意不变。按话题合理分段,Markdown格式,段落间双换行,不加分隔线。【必须使用简体中文输出,严禁繁体中文。写\"投资\"不写\"投資\",写\"经济\"不写\"經濟\"。】'\n },\n {\n role: 'user',\n content: `${contextHint}请优化以下转录文本:\\n\\n${text}`\n }\n ],\n temperature: 0.1,\n max_tokens: 4096\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * DeepSeek 生成播客总结\n */\nasync function generateSummary(transcript) {\n const maxChars = 6000;\n if (transcript.length <= maxChars) {\n return await generateSummaryChunk(transcript);\n }\n\n // 分块总结再整合\n const chunks = splitText(transcript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块总结`);\n\n const summaries = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 总结第 ${i + 1}/${chunks.length} 块`);\n const s = await generateSummaryChunk(chunks[i]);\n summaries.push(s);\n if (i < chunks.length - 1) await sleep(1000);\n }\n\n // 整合所有分块总结\n const combined = summaries.join('\\n\\n');\n return await mergeSummaries(combined);\n}\n\n/**\n * DeepSeek 生成单块总结\n */\nasync function generateSummaryChunk(text) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `你是一个专业的播客内容分析师。请生成全面、结构化的总结。\n\n【输出语言要求】必须使用简体中文输出,严禁使用繁体中文。例如写\"投资\"而不是\"投資\",写\"周期\"而不是\"週期\",写\"经济\"而不是\"經濟\"。\n\n要求:\n1. 提取主要话题和核心观点\n2. 保持逻辑结构清晰\n3. 包含重要的讨论内容、观点和结论\n4. 使用简洁明了的简体中文\n\n【严格排除】赞助商广告、节目资助方信息、播客开头结尾语、制作团队信息。\n\n格式:Markdown,段落之间用双换行分隔,每个段落专注一个主题。`\n },\n { role: 'user', content: text }\n ],\n temperature: 0.5,\n max_tokens: 3000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 整合分块总结\n */\nasync function mergeSummaries(combinedSummaries) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `请将以下分段总结整合成一个完整、连贯的播客总结。\n【输出语言要求】必须使用简体中文,严禁繁体中文。例如写\"投资\"不写\"投資\"。\n1. 去除重复内容\n2. 按主题重新组织\n3. 段落之间必须有空行分隔\n4. Markdown格式,简体中文输出\n5. 严格排除广告等无价值内容`\n },\n { role: 'user', content: combinedSummaries }\n ],\n temperature: 0.3,\n max_tokens: 4000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 智能分块(按段落和句子边界)\n */\nfunction splitText(text, maxChars) {\n const chunks = [];\n const paragraphs = text.split(/\\n\\s*\\n/).filter(p => p.trim());\n let current = '';\n\n for (const para of paragraphs) {\n const test = current + (current ? '\\n\\n' : '') + para;\n if (test.length > maxChars && current) {\n chunks.push(current.trim());\n current = para;\n } else {\n current = test;\n }\n }\n if (current.trim()) chunks.push(current.trim());\n\n // 对仍然超长的块按句子分割\n const result = [];\n for (const chunk of chunks) {\n if (chunk.length <= maxChars) {\n result.push(chunk);\n } else {\n const sentences = chunk.split(/([。!?.!?]+\\s*)/).filter(s => s.trim());\n let sub = '';\n for (let i = 0; i < sentences.length; i += 2) {\n const s = sentences[i] + (sentences[i + 1] || '');\n if ((sub + s).length > maxChars && sub) {\n result.push(sub.trim());\n sub = s;\n } else {\n sub += s;\n }\n }\n if (sub.trim()) result.push(sub.trim());\n }\n }\n return result;\n}\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\nmodule.exports = { processAudio, generateSummary, optimizeTranscript };\n"}, "a987e2b68560378e5eb9441a8a4613ea9b2743fe": {"t": 1785748408.6151142, "n": 11092, "h": "24986af7f5970389", "c": "/**\n * DeepSeek AI 服务\n * 替代 OpenAI,用于文本优化和总结\n * DeepSeek API 兼容 OpenAI 格式\n */\nconst OpenAI = require('openai');\nconst fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst { promisify } = require('util');\n\nconst execAsync = promisify(exec);\n\n// 初始化 DeepSeek 客户端(兼容 OpenAI SDK)\nconst deepseek = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',\n timeout: 600000,\n maxRetries: 2\n});\n\nconst DEEPSEEK_MODEL = 'deepseek-chat';\n\nconsole.log(`🤖 AI服务: DeepSeek (${DEEPSEEK_MODEL})`);\n\n/**\n * 生成标准化的文件名\n */\nfunction generateFilePrefix(type, title) {\n let cleanTitle = title\n .replace(/\\s*\\|\\s*/g, '-')\n .replace(/\\s*:\\s*/g, '-')\n .replace(/[<>:\"/\\\\|?*]/g, '')\n .replace(/\\s+/g, '_')\n .replace(/[^\\w一-龥\\-_.]/g, '');\n if (cleanTitle.length > 30) cleanTitle = cleanTitle.substring(0, 30);\n const uuid = Math.random().toString(36).substr(2, 6).toUpperCase();\n return `${type}_${cleanTitle}_${uuid}`;\n}\n\n/**\n * 处理音频文件:转录 + 优化 + 总结\n */\nasync function processAudio(audioFiles, shouldSummarize = false, tempDir = null, podcastTitle = null, sessionId = null, sendProgress = null) {\n try {\n console.log(`🤖 开始音频处理`);\n const files = Array.isArray(audioFiles) ? audioFiles : [audioFiles];\n console.log(`📄 处理文件数量: ${files.length}`);\n\n let transcript = '';\n let savedFiles = [];\n\n if (files.length === 1) {\n // 单文件处理 - 调用 Python Whisper 脚本\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const filePrefix = generateFilePrefix('raw', podcastTitle || 'Untitled');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${files[0]}\" --model ${process.env.WHISPER_MODEL || 'base'} --save-transcript \"${tempDir}\" --file-prefix \"${filePrefix}\" --podcast-title \"${podcastTitle || 'Untitled'}\"`;\n\n console.log(`🎤 Whisper 转录: ${path.basename(files[0])}`);\n if (sendProgress) sendProgress(30, 'transcription', '语音转录中...');\n\n const { stdout, stderr } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 3600000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n if (stderr && stderr.trim()) console.log(`🔧 Whisper日志: ${stderr.trim()}`);\n\n const result = JSON.parse(stdout);\n if (!result.success) throw new Error(result.error || '转录失败');\n\n transcript = result.text || '';\n savedFiles = result.savedFiles || [];\n console.log(`✅ 转录完成: ${transcript.length} 字符`);\n\n // DeepSeek 智能优化转录文本\n if (sendProgress) sendProgress(50, 'optimizing', 'AI优化文本中...');\n let optimizedTranscript = transcript;\n try {\n optimizedTranscript = await optimizeTranscript(transcript);\n console.log(`✅ 文本优化完成: ${optimizedTranscript.length} 字符`);\n\n // 保存优化后的文本\n if (savedFiles.length > 0) {\n const transcriptFile = savedFiles.find(f => f.type === 'transcript');\n if (transcriptFile && fs.existsSync(transcriptFile.path)) {\n // 备份原始转录\n const backupPath = transcriptFile.path.replace('.md', '_original.md');\n if (!fs.existsSync(backupPath)) {\n fs.copyFileSync(transcriptFile.path, backupPath);\n }\n fs.writeFileSync(transcriptFile.path, optimizedTranscript, 'utf8');\n }\n }\n transcript = optimizedTranscript;\n } catch (optErr) {\n console.warn(`⚠️ 文本优化失败,使用原始转录: ${optErr.message}`);\n }\n\n // 生成总结\n let summary = null;\n if (shouldSummarize) {\n console.log(`📝 生成总结...`);\n if (sendProgress) sendProgress(70, 'summary', 'AI生成总结中...');\n try {\n summary = await generateSummary(transcript);\n const summaryPrefix = generateFilePrefix('summary', podcastTitle || 'Untitled');\n const summaryFileName = `${summaryPrefix}.md`;\n const summaryPath = path.join(tempDir, summaryFileName);\n fs.writeFileSync(summaryPath, `# 🎙️ ${podcastTitle || '播客总结'}\\n\\n${summary}`, 'utf8');\n savedFiles.push({\n type: 'summary',\n filename: summaryFileName,\n path: summaryPath,\n size: fs.statSync(summaryPath).size\n });\n console.log(`📋 总结已保存: ${summaryFileName}`);\n } catch (sumErr) {\n console.error(`❌ 总结生成失败: ${sumErr.message}`);\n }\n }\n\n return {\n transcript: transcript,\n summary: summary,\n language: 'zh',\n audioDuration: result.duration,\n savedFiles: savedFiles\n };\n }\n\n // 多文件处理\n for (let i = 0; i < files.length; i++) {\n console.log(`🎵 转录片段 ${i + 1}/${files.length}`);\n const result = await transcribeSingleFile(files[i]);\n if (result) transcript += result + '\\n\\n';\n }\n\n return { transcript: transcript.trim(), summary: null, savedFiles: [] };\n\n } catch (error) {\n console.error('❌ 音频处理失败:', error);\n throw error;\n }\n}\n\n/**\n * 调用 Whisper 转录单个文件\n */\nasync function transcribeSingleFile(audioPath) {\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${audioPath}\" --model ${process.env.WHISPER_MODEL || 'base'}`;\n\n const { stdout } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 1200000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n const result = JSON.parse(stdout);\n return result.success ? result.text : null;\n}\n\n/**\n * DeepSeek 优化转录文本\n */\nasync function optimizeTranscript(rawTranscript) {\n const maxChars = 6000;\n if (rawTranscript.length <= maxChars) {\n return await optimizeChunk(rawTranscript);\n }\n\n // 分块处理长文本\n const chunks = splitText(rawTranscript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块优化`);\n\n const optimized = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 优化第 ${i + 1}/${chunks.length} 块`);\n const chunk = await optimizeChunk(chunks[i], i > 0 ? chunks[i - 1].slice(-100) : null);\n optimized.push(chunk);\n if (i < chunks.length - 1) await sleep(1000);\n }\n return optimized.join('\\n\\n');\n}\n\n/**\n * DeepSeek 优化单个文本块\n */\nasync function optimizeChunk(text, prevContext = null) {\n let contextHint = '';\n if (prevContext) {\n contextHint = `[上文续:${prevContext}]\\n\\n`;\n }\n\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: '你是专业的音频转录文本优化助手。修正错别字、同音字错误,改善语法,添加合适标点,但必须保持原意不变。按话题合理分段,使用Markdown格式,段落间用双换行分隔。不要添加分隔线。输出必须使用简体中文。'\n },\n {\n role: 'user',\n content: `${contextHint}请优化以下转录文本:\\n\\n${text}`\n }\n ],\n temperature: 0.1,\n max_tokens: 4096\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * DeepSeek 生成播客总结\n */\nasync function generateSummary(transcript) {\n const maxChars = 6000;\n if (transcript.length <= maxChars) {\n return await generateSummaryChunk(transcript);\n }\n\n // 分块总结再整合\n const chunks = splitText(transcript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块总结`);\n\n const summaries = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 总结第 ${i + 1}/${chunks.length} 块`);\n const s = await generateSummaryChunk(chunks[i]);\n summaries.push(s);\n if (i < chunks.length - 1) await sleep(1000);\n }\n\n // 整合所有分块总结\n const combined = summaries.join('\\n\\n');\n return await mergeSummaries(combined);\n}\n\n/**\n * DeepSeek 生成单块总结\n */\nasync function generateSummaryChunk(text) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `你是一个专业的播客内容分析师。请生成全面、结构化的总结:\n\n要求:\n1. 提取主要话题和核心观点\n2. 保持逻辑结构清晰\n3. 包含重要的讨论内容、观点和结论\n4. 使用简洁明了的**简体中文**输出\n\n**严格排除以下内容:**\n- 赞助商广告和商业推广\n- 节目资助方信息\n- 播客标准开头结尾语\n- 制作团队信息\n\n格式要求:Markdown格式,段落之间用双换行分隔。每个段落专注一个主题。`\n },\n { role: 'user', content: text }\n ],\n temperature: 0.5,\n max_tokens: 3000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 整合分块总结\n */\nasync function mergeSummaries(combinedSummaries) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `请将以下分段总结整合成一个完整、连贯的播客总结:\n1. 去除重复内容\n2. 按主题重新组织\n3. 段落之间必须有空行分隔\n4. 使用Markdown格式\n5. 使用简洁明了的**简体中文**输出\n6. 严格排除广告等无价值内容`\n },\n { role: 'user', content: combinedSummaries }\n ],\n temperature: 0.3,\n max_tokens: 4000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 智能分块(按段落和句子边界)\n */\nfunction splitText(text, maxChars) {\n const chunks = [];\n const paragraphs = text.split(/\\n\\s*\\n/).filter(p => p.trim());\n let current = '';\n\n for (const para of paragraphs) {\n const test = current + (current ? '\\n\\n' : '') + para;\n if (test.length > maxChars && current) {\n chunks.push(current.trim());\n current = para;\n } else {\n current = test;\n }\n }\n if (current.trim()) chunks.push(current.trim());\n\n // 对仍然超长的块按句子分割\n const result = [];\n for (const chunk of chunks) {\n if (chunk.length <= maxChars) {\n result.push(chunk);\n } else {\n const sentences = chunk.split(/([。!?.!?]+\\s*)/).filter(s => s.trim());\n let sub = '';\n for (let i = 0; i < sentences.length; i += 2) {\n const s = sentences[i] + (sentences[i + 1] || '');\n if ((sub + s).length > maxChars && sub) {\n result.push(sub.trim());\n sub = s;\n } else {\n sub += s;\n }\n }\n if (sub.trim()) result.push(sub.trim());\n }\n }\n return result;\n}\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\nmodule.exports = { processAudio, generateSummary, optimizeTranscript };\n"}, "ca33bc9c39aaedd5f05ce9e80c65525d7dc302ee": {"t": 1785748409.7196069, "n": 11067, "h": "237f6a6d0b61a300", "c": "/**\n * DeepSeek AI 服务\n * 替代 OpenAI,用于文本优化和总结\n * DeepSeek API 兼容 OpenAI 格式\n */\nconst OpenAI = require('openai');\nconst fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst { promisify } = require('util');\n\nconst execAsync = promisify(exec);\n\n// 初始化 DeepSeek 客户端(兼容 OpenAI SDK)\nconst deepseek = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',\n timeout: 600000,\n maxRetries: 2\n});\n\nconst DEEPSEEK_MODEL = 'deepseek-chat';\n\nconsole.log(`🤖 AI服务: DeepSeek (${DEEPSEEK_MODEL})`);\n\n/**\n * 生成标准化的文件名\n */\nfunction generateFilePrefix(type, title) {\n let cleanTitle = title\n .replace(/\\s*\\|\\s*/g, '-')\n .replace(/\\s*:\\s*/g, '-')\n .replace(/[<>:\"/\\\\|?*]/g, '')\n .replace(/\\s+/g, '_')\n .replace(/[^\\w一-龥\\-_.]/g, '');\n if (cleanTitle.length > 30) cleanTitle = cleanTitle.substring(0, 30);\n const uuid = Math.random().toString(36).substr(2, 6).toUpperCase();\n return `${type}_${cleanTitle}_${uuid}`;\n}\n\n/**\n * 处理音频文件:转录 + 优化 + 总结\n */\nasync function processAudio(audioFiles, shouldSummarize = false, tempDir = null, podcastTitle = null, sessionId = null, sendProgress = null) {\n try {\n console.log(`🤖 开始音频处理`);\n const files = Array.isArray(audioFiles) ? audioFiles : [audioFiles];\n console.log(`📄 处理文件数量: ${files.length}`);\n\n let transcript = '';\n let savedFiles = [];\n\n if (files.length === 1) {\n // 单文件处理 - 调用 Python Whisper 脚本\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const filePrefix = generateFilePrefix('raw', podcastTitle || 'Untitled');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${files[0]}\" --model ${process.env.WHISPER_MODEL || 'base'} --save-transcript \"${tempDir}\" --file-prefix \"${filePrefix}\" --podcast-title \"${podcastTitle || 'Untitled'}\"`;\n\n console.log(`🎤 Whisper 转录: ${path.basename(files[0])}`);\n if (sendProgress) sendProgress(30, 'transcription', '语音转录中...');\n\n const { stdout, stderr } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 3600000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n if (stderr && stderr.trim()) console.log(`🔧 Whisper日志: ${stderr.trim()}`);\n\n const result = JSON.parse(stdout);\n if (!result.success) throw new Error(result.error || '转录失败');\n\n transcript = result.text || '';\n savedFiles = result.savedFiles || [];\n console.log(`✅ 转录完成: ${transcript.length} 字符`);\n\n // DeepSeek 智能优化转录文本\n if (sendProgress) sendProgress(50, 'optimizing', 'AI优化文本中...');\n let optimizedTranscript = transcript;\n try {\n optimizedTranscript = await optimizeTranscript(transcript);\n console.log(`✅ 文本优化完成: ${optimizedTranscript.length} 字符`);\n\n // 保存优化后的文本\n if (savedFiles.length > 0) {\n const transcriptFile = savedFiles.find(f => f.type === 'transcript');\n if (transcriptFile && fs.existsSync(transcriptFile.path)) {\n // 备份原始转录\n const backupPath = transcriptFile.path.replace('.md', '_original.md');\n if (!fs.existsSync(backupPath)) {\n fs.copyFileSync(transcriptFile.path, backupPath);\n }\n fs.writeFileSync(transcriptFile.path, optimizedTranscript, 'utf8');\n }\n }\n transcript = optimizedTranscript;\n } catch (optErr) {\n console.warn(`⚠️ 文本优化失败,使用原始转录: ${optErr.message}`);\n }\n\n // 生成总结\n let summary = null;\n if (shouldSummarize) {\n console.log(`📝 生成总结...`);\n if (sendProgress) sendProgress(70, 'summary', 'AI生成总结中...');\n try {\n summary = await generateSummary(transcript);\n const summaryPrefix = generateFilePrefix('summary', podcastTitle || 'Untitled');\n const summaryFileName = `${summaryPrefix}.md`;\n const summaryPath = path.join(tempDir, summaryFileName);\n fs.writeFileSync(summaryPath, `# 🎙️ ${podcastTitle || '播客总结'}\\n\\n${summary}`, 'utf8');\n savedFiles.push({\n type: 'summary',\n filename: summaryFileName,\n path: summaryPath,\n size: fs.statSync(summaryPath).size\n });\n console.log(`📋 总结已保存: ${summaryFileName}`);\n } catch (sumErr) {\n console.error(`❌ 总结生成失败: ${sumErr.message}`);\n }\n }\n\n return {\n transcript: transcript,\n summary: summary,\n language: 'zh',\n audioDuration: result.duration,\n savedFiles: savedFiles\n };\n }\n\n // 多文件处理\n for (let i = 0; i < files.length; i++) {\n console.log(`🎵 转录片段 ${i + 1}/${files.length}`);\n const result = await transcribeSingleFile(files[i]);\n if (result) transcript += result + '\\n\\n';\n }\n\n return { transcript: transcript.trim(), summary: null, savedFiles: [] };\n\n } catch (error) {\n console.error('❌ 音频处理失败:', error);\n throw error;\n }\n}\n\n/**\n * 调用 Whisper 转录单个文件\n */\nasync function transcribeSingleFile(audioPath) {\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${audioPath}\" --model ${process.env.WHISPER_MODEL || 'base'}`;\n\n const { stdout } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 1200000,\n env: { ...process.env, PYTHONIOENCODING: 'utf-8' }\n });\n\n const result = JSON.parse(stdout);\n return result.success ? result.text : null;\n}\n\n/**\n * DeepSeek 优化转录文本\n */\nasync function optimizeTranscript(rawTranscript) {\n const maxChars = 6000;\n if (rawTranscript.length <= maxChars) {\n return await optimizeChunk(rawTranscript);\n }\n\n // 分块处理长文本\n const chunks = splitText(rawTranscript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块优化`);\n\n const optimized = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 优化第 ${i + 1}/${chunks.length} 块`);\n const chunk = await optimizeChunk(chunks[i], i > 0 ? chunks[i - 1].slice(-100) : null);\n optimized.push(chunk);\n if (i < chunks.length - 1) await sleep(1000);\n }\n return optimized.join('\\n\\n');\n}\n\n/**\n * DeepSeek 优化单个文本块\n */\nasync function optimizeChunk(text, prevContext = null) {\n let contextHint = '';\n if (prevContext) {\n contextHint = `[上文续:${prevContext}]\\n\\n`;\n }\n\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: '你是专业的音频转录文本优化助手。修正错别字、同音字错误,改善语法,添加合适标点,但必须保持原意不变。按话题合理分段,使用Markdown格式,段落间用双换行分隔。不要添加分隔线。'\n },\n {\n role: 'user',\n content: `${contextHint}请优化以下转录文本:\\n\\n${text}`\n }\n ],\n temperature: 0.1,\n max_tokens: 4096\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * DeepSeek 生成播客总结\n */\nasync function generateSummary(transcript) {\n const maxChars = 6000;\n if (transcript.length <= maxChars) {\n return await generateSummaryChunk(transcript);\n }\n\n // 分块总结再整合\n const chunks = splitText(transcript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块总结`);\n\n const summaries = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 总结第 ${i + 1}/${chunks.length} 块`);\n const s = await generateSummaryChunk(chunks[i]);\n summaries.push(s);\n if (i < chunks.length - 1) await sleep(1000);\n }\n\n // 整合所有分块总结\n const combined = summaries.join('\\n\\n');\n return await mergeSummaries(combined);\n}\n\n/**\n * DeepSeek 生成单块总结\n */\nasync function generateSummaryChunk(text) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `你是一个专业的播客内容分析师。请生成全面、结构化的中文总结:\n\n要求:\n1. 提取主要话题和核心观点\n2. 保持逻辑结构清晰\n3. 包含重要的讨论内容、观点和结论\n4. 使用简洁明了的中文\n\n**严格排除以下内容:**\n- 赞助商广告和商业推广\n- 节目资助方信息\n- 播客标准开头结尾语\n- 制作团队信息\n\n格式要求:Markdown格式,段落之间用双换行分隔。每个段落专注一个主题。`\n },\n { role: 'user', content: text }\n ],\n temperature: 0.5,\n max_tokens: 3000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 整合分块总结\n */\nasync function mergeSummaries(combinedSummaries) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `请将以下分段总结整合成一个完整、连贯的播客总结:\n1. 去除重复内容\n2. 按主题重新组织\n3. 段落之间必须有空行分隔\n4. 使用Markdown格式\n5. 使用简洁明了的中文\n6. 严格排除广告等无价值内容`\n },\n { role: 'user', content: combinedSummaries }\n ],\n temperature: 0.3,\n max_tokens: 4000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 智能分块(按段落和句子边界)\n */\nfunction splitText(text, maxChars) {\n const chunks = [];\n const paragraphs = text.split(/\\n\\s*\\n/).filter(p => p.trim());\n let current = '';\n\n for (const para of paragraphs) {\n const test = current + (current ? '\\n\\n' : '') + para;\n if (test.length > maxChars && current) {\n chunks.push(current.trim());\n current = para;\n } else {\n current = test;\n }\n }\n if (current.trim()) chunks.push(current.trim());\n\n // 对仍然超长的块按句子分割\n const result = [];\n for (const chunk of chunks) {\n if (chunk.length <= maxChars) {\n result.push(chunk);\n } else {\n const sentences = chunk.split(/([。!?.!?]+\\s*)/).filter(s => s.trim());\n let sub = '';\n for (let i = 0; i < sentences.length; i += 2) {\n const s = sentences[i] + (sentences[i + 1] || '');\n if ((sub + s).length > maxChars && sub) {\n result.push(sub.trim());\n sub = s;\n } else {\n sub += s;\n }\n }\n if (sub.trim()) result.push(sub.trim());\n }\n }\n return result;\n}\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\nmodule.exports = { processAudio, generateSummary, optimizeTranscript };\n"}, "369723ad7dd34f4b2e44febf054990558cdb4826": {"t": 1785748411.4484403, "n": 10939, "h": "0287bd652e9e5ef9", "c": "/**\n * DeepSeek AI 服务\n * 替代 OpenAI,用于文本优化和总结\n * DeepSeek API 兼容 OpenAI 格式\n */\nconst OpenAI = require('openai');\nconst fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst { promisify } = require('util');\n\nconst execAsync = promisify(exec);\n\n// 初始化 DeepSeek 客户端(兼容 OpenAI SDK)\nconst deepseek = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',\n timeout: 600000,\n maxRetries: 2\n});\n\nconst DEEPSEEK_MODEL = 'deepseek-chat';\n\nconsole.log(`🤖 AI服务: DeepSeek (${DEEPSEEK_MODEL})`);\n\n/**\n * 生成标准化的文件名\n */\nfunction generateFilePrefix(type, title) {\n let cleanTitle = title\n .replace(/\\s*\\|\\s*/g, '-')\n .replace(/\\s*:\\s*/g, '-')\n .replace(/[<>:\"/\\\\|?*]/g, '')\n .replace(/\\s+/g, '_')\n .replace(/[^\\w一-龥\\-_.]/g, '');\n if (cleanTitle.length > 30) cleanTitle = cleanTitle.substring(0, 30);\n const uuid = Math.random().toString(36).substr(2, 6).toUpperCase();\n return `${type}_${cleanTitle}_${uuid}`;\n}\n\n/**\n * 处理音频文件:转录 + 优化 + 总结\n */\nasync function processAudio(audioFiles, shouldSummarize = false, tempDir = null, podcastTitle = null, sessionId = null, sendProgress = null) {\n try {\n console.log(`🤖 开始音频处理`);\n const files = Array.isArray(audioFiles) ? audioFiles : [audioFiles];\n console.log(`📄 处理文件数量: ${files.length}`);\n\n let transcript = '';\n let savedFiles = [];\n\n if (files.length === 1) {\n // 单文件处理 - 调用 Python Whisper 脚本\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const filePrefix = generateFilePrefix('raw', podcastTitle || 'Untitled');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${files[0]}\" --model ${process.env.WHISPER_MODEL || 'base'} --save-transcript \"${tempDir}\" --file-prefix \"${filePrefix}\" --podcast-title \"${podcastTitle || 'Untitled'}\"`;\n\n console.log(`🎤 Whisper 转录: ${path.basename(files[0])}`);\n if (sendProgress) sendProgress(30, 'transcription', '语音转录中...');\n\n const { stdout, stderr } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 3600000\n });\n\n if (stderr && stderr.trim()) console.log(`🔧 Whisper日志: ${stderr.trim()}`);\n\n const result = JSON.parse(stdout);\n if (!result.success) throw new Error(result.error || '转录失败');\n\n transcript = result.text || '';\n savedFiles = result.savedFiles || [];\n console.log(`✅ 转录完成: ${transcript.length} 字符`);\n\n // DeepSeek 智能优化转录文本\n if (sendProgress) sendProgress(50, 'optimizing', 'AI优化文本中...');\n let optimizedTranscript = transcript;\n try {\n optimizedTranscript = await optimizeTranscript(transcript);\n console.log(`✅ 文本优化完成: ${optimizedTranscript.length} 字符`);\n\n // 保存优化后的文本\n if (savedFiles.length > 0) {\n const transcriptFile = savedFiles.find(f => f.type === 'transcript');\n if (transcriptFile && fs.existsSync(transcriptFile.path)) {\n // 备份原始转录\n const backupPath = transcriptFile.path.replace('.md', '_original.md');\n if (!fs.existsSync(backupPath)) {\n fs.copyFileSync(transcriptFile.path, backupPath);\n }\n fs.writeFileSync(transcriptFile.path, optimizedTranscript, 'utf8');\n }\n }\n transcript = optimizedTranscript;\n } catch (optErr) {\n console.warn(`⚠️ 文本优化失败,使用原始转录: ${optErr.message}`);\n }\n\n // 生成总结\n let summary = null;\n if (shouldSummarize) {\n console.log(`📝 生成总结...`);\n if (sendProgress) sendProgress(70, 'summary', 'AI生成总结中...');\n try {\n summary = await generateSummary(transcript);\n const summaryPrefix = generateFilePrefix('summary', podcastTitle || 'Untitled');\n const summaryFileName = `${summaryPrefix}.md`;\n const summaryPath = path.join(tempDir, summaryFileName);\n fs.writeFileSync(summaryPath, `# 🎙️ ${podcastTitle || '播客总结'}\\n\\n${summary}`, 'utf8');\n savedFiles.push({\n type: 'summary',\n filename: summaryFileName,\n path: summaryPath,\n size: fs.statSync(summaryPath).size\n });\n console.log(`📋 总结已保存: ${summaryFileName}`);\n } catch (sumErr) {\n console.error(`❌ 总结生成失败: ${sumErr.message}`);\n }\n }\n\n return {\n transcript: transcript,\n summary: summary,\n language: 'zh',\n audioDuration: result.duration,\n savedFiles: savedFiles\n };\n }\n\n // 多文件处理\n for (let i = 0; i < files.length; i++) {\n console.log(`🎵 转录片段 ${i + 1}/${files.length}`);\n const result = await transcribeSingleFile(files[i]);\n if (result) transcript += result + '\\n\\n';\n }\n\n return { transcript: transcript.trim(), summary: null, savedFiles: [] };\n\n } catch (error) {\n console.error('❌ 音频处理失败:', error);\n throw error;\n }\n}\n\n/**\n * 调用 Whisper 转录单个文件\n */\nasync function transcribeSingleFile(audioPath) {\n const scriptPath = path.join(__dirname, '..', 'whisper_transcribe.py');\n const pythonPath = process.env.PYTHON_PATH || 'python';\n const command = `\"${pythonPath}\" \"${scriptPath}\" \"${audioPath}\" --model ${process.env.WHISPER_MODEL || 'base'}`;\n\n const { stdout } = await execAsync(command, {\n cwd: path.join(__dirname, '..'),\n maxBuffer: 1024 * 1024 * 20,\n timeout: 1200000\n });\n\n const result = JSON.parse(stdout);\n return result.success ? result.text : null;\n}\n\n/**\n * DeepSeek 优化转录文本\n */\nasync function optimizeTranscript(rawTranscript) {\n const maxChars = 6000;\n if (rawTranscript.length <= maxChars) {\n return await optimizeChunk(rawTranscript);\n }\n\n // 分块处理长文本\n const chunks = splitText(rawTranscript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块优化`);\n\n const optimized = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 优化第 ${i + 1}/${chunks.length} 块`);\n const chunk = await optimizeChunk(chunks[i], i > 0 ? chunks[i - 1].slice(-100) : null);\n optimized.push(chunk);\n if (i < chunks.length - 1) await sleep(1000);\n }\n return optimized.join('\\n\\n');\n}\n\n/**\n * DeepSeek 优化单个文本块\n */\nasync function optimizeChunk(text, prevContext = null) {\n let contextHint = '';\n if (prevContext) {\n contextHint = `[上文续:${prevContext}]\\n\\n`;\n }\n\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: '你是专业的音频转录文本优化助手。修正错别字、同音字错误,改善语法,添加合适标点,但必须保持原意不变。按话题合理分段,使用Markdown格式,段落间用双换行分隔。不要添加分隔线。'\n },\n {\n role: 'user',\n content: `${contextHint}请优化以下转录文本:\\n\\n${text}`\n }\n ],\n temperature: 0.1,\n max_tokens: 4096\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * DeepSeek 生成播客总结\n */\nasync function generateSummary(transcript) {\n const maxChars = 6000;\n if (transcript.length <= maxChars) {\n return await generateSummaryChunk(transcript);\n }\n\n // 分块总结再整合\n const chunks = splitText(transcript, maxChars);\n console.log(`📄 长文本分为 ${chunks.length} 块总结`);\n\n const summaries = [];\n for (let i = 0; i < chunks.length; i++) {\n console.log(`🔄 总结第 ${i + 1}/${chunks.length} 块`);\n const s = await generateSummaryChunk(chunks[i]);\n summaries.push(s);\n if (i < chunks.length - 1) await sleep(1000);\n }\n\n // 整合所有分块总结\n const combined = summaries.join('\\n\\n');\n return await mergeSummaries(combined);\n}\n\n/**\n * DeepSeek 生成单块总结\n */\nasync function generateSummaryChunk(text) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `你是一个专业的播客内容分析师。请生成全面、结构化的中文总结:\n\n要求:\n1. 提取主要话题和核心观点\n2. 保持逻辑结构清晰\n3. 包含重要的讨论内容、观点和结论\n4. 使用简洁明了的中文\n\n**严格排除以下内容:**\n- 赞助商广告和商业推广\n- 节目资助方信息\n- 播客标准开头结尾语\n- 制作团队信息\n\n格式要求:Markdown格式,段落之间用双换行分隔。每个段落专注一个主题。`\n },\n { role: 'user', content: text }\n ],\n temperature: 0.5,\n max_tokens: 3000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 整合分块总结\n */\nasync function mergeSummaries(combinedSummaries) {\n const response = await deepseek.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages: [\n {\n role: 'system',\n content: `请将以下分段总结整合成一个完整、连贯的播客总结:\n1. 去除重复内容\n2. 按主题重新组织\n3. 段落之间必须有空行分隔\n4. 使用Markdown格式\n5. 使用简洁明了的中文\n6. 严格排除广告等无价值内容`\n },\n { role: 'user', content: combinedSummaries }\n ],\n temperature: 0.3,\n max_tokens: 4000\n });\n\n return response.choices[0].message.content.trim();\n}\n\n/**\n * 智能分块(按段落和句子边界)\n */\nfunction splitText(text, maxChars) {\n const chunks = [];\n const paragraphs = text.split(/\\n\\s*\\n/).filter(p => p.trim());\n let current = '';\n\n for (const para of paragraphs) {\n const test = current + (current ? '\\n\\n' : '') + para;\n if (test.length > maxChars && current) {\n chunks.push(current.trim());\n current = para;\n } else {\n current = test;\n }\n }\n if (current.trim()) chunks.push(current.trim());\n\n // 对仍然超长的块按句子分割\n const result = [];\n for (const chunk of chunks) {\n if (chunk.length <= maxChars) {\n result.push(chunk);\n } else {\n const sentences = chunk.split(/([。!?.!?]+\\s*)/).filter(s => s.trim());\n let sub = '';\n for (let i = 0; i < sentences.length; i += 2) {\n const s = sentences[i] + (sentences[i + 1] || '');\n if ((sub + s).length > maxChars && sub) {\n result.push(sub.trim());\n sub = s;\n } else {\n sub += s;\n }\n }\n if (sub.trim()) result.push(sub.trim());\n }\n }\n return result;\n}\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\nmodule.exports = { processAudio, generateSummary, optimizeTranscript };\n"}}}}