| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- const fs = require('fs');
- const path = require('path');
- const { spawn } = require('child_process');
- function registerAudioExtractionRoutes(app, deps) {
- app.post('/api/remix/extract-audio', (req, res) => {
- const { videoId } = req.body || {};
- if (!videoId) {
- return res.status(400).json({ error: '缺少 videoId 参数' });
- }
- const manifest = deps.readManifest();
- const video = manifest.find(v => v.id === videoId);
- if (!video) {
- return res.status(404).json({ error: `未找到视频: ${videoId}` });
- }
- const videoPath = deps.resolveVideoPath(video.filename);
- if (!fs.existsSync(videoPath)) {
- return res.status(404).json({ error: `视频文件不存在: ${video.filename}` });
- }
- const safeBaseName = path.basename(video.filename, path.extname(video.filename)).replace(/[^a-zA-Z0-9._-]/g, '_') || `audio-${Date.now()}`;
- const outputFilename = `${safeBaseName}-${Date.now()}.wav`;
- const outputPath = path.join(deps.dataRemixAssetDir, outputFilename);
- const ffmpegArgs = ['-y', '-i', videoPath, '-vn', '-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2', outputPath];
- const proc = spawn('ffmpeg', ffmpegArgs, { cwd: deps.projectRoot });
- let stderr = '';
- proc.stderr.on('data', (data) => {
- stderr += data.toString();
- });
- proc.on('error', (err) => {
- deps.removeFileIfExists(outputPath);
- const message = /ENOENT/i.test(err.message)
- ? '未检测到 ffmpeg,请先安装 ffmpeg 并确保已加入系统 PATH 环境变量'
- : `无法启动 ffmpeg:${err.message}`;
- res.status(500).json({ error: message });
- });
- proc.on('close', (code) => {
- if (code !== 0 || !fs.existsSync(outputPath)) {
- deps.removeFileIfExists(outputPath);
- const lastErrorLine = stderr.split('\n').filter(Boolean).slice(-1)[0] || '';
- const normalizedMessage = /ffmpeg/i.test(stderr) && /not recognized|not found|no such file/i.test(stderr)
- ? '未检测到 ffmpeg,请先安装 ffmpeg 并确保已加入系统 PATH 环境变量'
- : `音频提取失败${lastErrorLine ? `: ${lastErrorLine}` : ''}`;
- return res.status(500).json({ error: normalizedMessage });
- }
- res.setHeader('Content-Type', 'audio/wav');
- res.setHeader('Content-Disposition', `attachment; filename="${outputFilename}"`);
- const stream = fs.createReadStream(outputPath);
- stream.on('close', () => {
- deps.removeFileIfExists(outputPath);
- });
- stream.on('error', () => {
- deps.removeFileIfExists(outputPath);
- if (!res.headersSent) {
- res.status(500).json({ error: '音频文件读取失败' });
- } else {
- res.end();
- }
- });
- stream.pipe(res);
- });
- });
- app.post('/api/extract-audio-mp3', (req, res) => {
- const { videoId } = req.body || {};
- if (!videoId) {
- return res.status(400).json({ error: '缺少 videoId 参数' });
- }
- const manifest = deps.readManifest();
- const video = manifest.find(v => v.id === videoId);
- if (!video) {
- console.warn(`⚠️ extract-audio-mp3: 未找到 videoId=${videoId}`);
- return res.status(404).json({ error: `未找到视频: ${videoId}` });
- }
- const videoPath = deps.resolveVideoPath(video.filename);
- if (!fs.existsSync(videoPath)) {
- console.warn(`⚠️ extract-audio-mp3: 视频文件不存在: ${videoPath}`);
- return res.status(404).json({ error: `视频文件不存在: ${video.filename}` });
- }
- const safeBaseName = path.basename(video.filename, path.extname(video.filename)).replace(/[^a-zA-Z0-9._-]/g, '_') || `audio-${Date.now()}`;
- const outputFilename = `${safeBaseName}-${Date.now()}.m4a`;
- const outputPath = path.join(deps.dataRemixAssetDir, outputFilename);
- const outputMimeType = 'audio/mp4';
- console.log(`🎵 开始音频提取: ${videoPath} → ${outputPath}`);
- const ffmpegArgs = ['-y', '-i', videoPath, '-vn', '-c:a', 'aac', '-b:a', '64k', '-ar', '16000', '-ac', '1', outputPath];
- const proc = spawn('ffmpeg', ffmpegArgs, { cwd: deps.projectRoot });
- let stderr = '';
- let responded = false;
- const safeRespond = (status, body) => {
- if (responded) return;
- responded = true;
- res.status(status).json(body);
- };
- proc.stderr.on('data', (data) => { stderr += data.toString(); });
- proc.on('error', (err) => {
- deps.removeFileIfExists(outputPath);
- const message = /ENOENT/i.test(err.message)
- ? '未检测到 ffmpeg,请先安装 ffmpeg 并确保已加入系统 PATH 环境变量'
- : `无法启动 ffmpeg:${err.message}`;
- console.error(`❌ extract-audio-mp3 spawn error: ${err.message}`);
- safeRespond(500, { error: message });
- });
- proc.on('close', (code) => {
- if (code !== 0 || !fs.existsSync(outputPath)) {
- const tail = stderr.split('\n').filter(Boolean).slice(-5).join(' | ');
- console.error(`❌ ffmpeg exit code=${code} 输出文件存在=${fs.existsSync(outputPath)}`);
- console.error(` stderr 末尾: ${tail}`);
- deps.removeFileIfExists(outputPath);
- const lastErrorLine = stderr.split('\n').filter(Boolean).slice(-1)[0] || '';
- return safeRespond(500, { error: `音频提取失败 (exit=${code})${lastErrorLine ? `: ${lastErrorLine}` : ''}` });
- }
- const stats = fs.statSync(outputPath);
- const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
- console.log(`🎵 音频提取完成: ${outputFilename} (${sizeMB}MB)`);
- const audioBuffer = fs.readFileSync(outputPath);
- const audioBase64 = audioBuffer.toString('base64');
- deps.removeFileIfExists(outputPath);
- safeRespond(200, {
- success: true,
- audio: {
- base64: audioBase64,
- mimeType: outputMimeType,
- sizeMB: parseFloat(sizeMB),
- filename: outputFilename,
- },
- });
- });
- });
- }
- module.exports = { registerAudioExtractionRoutes };
|