d6ab413dcd
CI/CD Pipeline / Deploy Staging (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Failing after 47h57m37s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 47h57m37s
- 新增 text_splitter.py: 长文本智能分段(句子边界 + 短段合并) - 新增 audio_merger.py: FFmpeg concat 音频合并器 - workflow.py: 分段合成完整流程(同步合并 / 异步轮询 / 失败处理) - tts_synthesis.py: 新增 process_tts_segment_synthesis Celery 任务 - tts.py: 路由层自动识别分段任务并分发到对应 Celery task - 23 个单元测试全部通过,P0 回归测试无退化
97 lines
2.8 KiB
Python
97 lines
2.8 KiB
Python
"""FFmpeg 音频合并器 — P1 长文本分段合成。
|
||
|
||
将多个分段音频文件合并为一个完整音频文件。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import tempfile
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class AudioMergeError(Exception):
|
||
"""音频合并异常。"""
|
||
|
||
pass
|
||
|
||
|
||
class AudioMerger:
|
||
"""使用 FFmpeg 合并多个音频文件。"""
|
||
|
||
def merge(self, audio_paths: list[str], output_format: str = "mp3") -> bytes:
|
||
"""合并多个音频文件,返回合并后的音频数据。
|
||
|
||
使用 FFmpeg concat demuxer 按顺序拼接音频。
|
||
所有输入文件必须为相同格式和采样率。
|
||
|
||
Args:
|
||
audio_paths: 音频文件路径列表(按合成顺序)
|
||
output_format: 输出格式(mp3/wav/pcm)
|
||
|
||
Returns:
|
||
合并后的音频文件字节数据
|
||
|
||
Raises:
|
||
AudioMergeError: 合并失败
|
||
"""
|
||
if not audio_paths:
|
||
raise AudioMergeError("没有可合并的音频文件")
|
||
|
||
if len(audio_paths) == 1:
|
||
with open(audio_paths[0], "rb") as f:
|
||
return f.read()
|
||
|
||
temp_dir = tempfile.mkdtemp(prefix="tts_merge_")
|
||
try:
|
||
# 生成 concat demuxer 列表文件
|
||
list_path = os.path.join(temp_dir, "concat_list.txt")
|
||
with open(list_path, "w") as f:
|
||
for path in audio_paths:
|
||
# FFmpeg concat 文件需要 file: 前缀,路径中的 ' 和 \n 需转义
|
||
escaped = path.replace("'", "'\\''").replace("\n", "\\n")
|
||
f.write(f"file '{escaped}'\n")
|
||
|
||
output_path = os.path.join(temp_dir, f"merged.{output_format}")
|
||
|
||
cmd = [
|
||
"ffmpeg",
|
||
"-y",
|
||
"-f",
|
||
"concat",
|
||
"-safe",
|
||
"0",
|
||
"-i",
|
||
list_path,
|
||
"-c",
|
||
"copy",
|
||
output_path,
|
||
]
|
||
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=120,
|
||
)
|
||
|
||
if result.returncode != 0:
|
||
logger.error(f"FFmpeg 合并失败: stderr={result.stderr}")
|
||
raise AudioMergeError(f"FFmpeg 合并失败: {result.stderr[:500]}")
|
||
|
||
with open(output_path, "rb") as f:
|
||
return f.read()
|
||
|
||
except subprocess.TimeoutExpired:
|
||
raise AudioMergeError("FFmpeg 合并超时(120 秒)")
|
||
except AudioMergeError:
|
||
raise
|
||
except Exception as e:
|
||
raise AudioMergeError(f"音频合并失败: {e}")
|
||
finally:
|
||
shutil.rmtree(temp_dir, ignore_errors=True)
|