Files

1 line
64 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}