diff options
| author | historia <historiavg@proton.me> | 2026-08-19 04:45:28 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-19 04:45:28 -0400 |
| commit | 9d4d7ef806c17387af9778725cd65a5e7ed10e39 (patch) | |
| tree | f43ab42b6945f031b202f8c36994ba629b131228 /converter/audio.py | |
| parent | 87e5216cd287f411b2ffab04dbc435f48c1d4aae (diff) | |
| download | tts-audiobook-generator-9d4d7ef806c17387af9778725cd65a5e7ed10e39.tar.gz | |
fix: limit chunk size to 250
Diffstat (limited to 'converter/audio.py')
| -rw-r--r-- | converter/audio.py | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/converter/audio.py b/converter/audio.py index 6e5a910..3e163be 100644 --- a/converter/audio.py +++ b/converter/audio.py @@ -5,6 +5,7 @@ import re import shutil import subprocess import traceback +import wave from pathlib import Path from typing import Dict, List, NamedTuple, Optional, Tuple @@ -50,6 +51,87 @@ def _concat_escape(path: str) -> str: return path.replace("'", "'\\''") +def _concat_wav_files(sources: List[Path], destination: Path) -> bool: + """Concatenate WAV files with matching parameters using the wave module. + + Returns False (touching nothing) when any input is not a readable WAV + or the parameters differ, so the caller can fall back to ffmpeg. + """ + opened = [] + try: + parameters = None + for source in sources: + wav_file = wave.open(str(source), "rb") + opened.append(wav_file) + current = (wav_file.getnchannels(), wav_file.getsampwidth(), + wav_file.getframerate()) + if parameters is None: + parameters = current + elif current != parameters: + return False + if parameters is None or min(parameters) < 1: + return False + with wave.open(str(destination), "wb") as output: + output.setnchannels(parameters[0]) + output.setsampwidth(parameters[1]) + output.setframerate(parameters[2]) + for wav_file in opened: + output.writeframes(wav_file.readframes(wav_file.getnframes())) + return True + except (wave.Error, EOFError, OSError): + return False + finally: + for wav_file in opened: + try: + wav_file.close() + except Exception: + pass + + +def _concat_with_ffmpeg(sources: List[Path], destination: Path) -> None: + """Concatenate audio files with ffmpeg's concat demuxer, re-encoding to + 16-bit PCM WAV (handles inputs the wave module cannot).""" + if shutil.which("ffmpeg") is None: + raise RuntimeError( + "ffmpeg is required to concatenate audio parts in non-WAV formats " + "(install ffmpeg and try again)" + ) + list_path = destination.with_name(destination.stem + "_parts.txt") + try: + with open(list_path, "w", encoding="utf-8") as list_file: + for source in sources: + list_file.write(f"file '{_concat_escape(str(source))}'\n") + command = [ + "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", + "-f", "concat", "-safe", "0", "-i", str(list_path), + "-c:a", "pcm_s16le", str(destination), + ] + proc = subprocess.run(command, capture_output=True, text=True) + if proc.returncode != 0: + raise RuntimeError( + f"ffmpeg failed to concatenate audio parts: {proc.stderr[-500:]}") + finally: + try: + list_path.unlink(missing_ok=True) + except OSError: + pass + + +def concat_audio_files(sources: List[Path], destination: Path) -> None: + """Concatenate audio files into one file at ``destination``. + + Joins the audio returned by several TTS sub-requests for a single + chunk. Uses the stdlib wave module when every input is a WAV with + matching parameters (lossless, no external tools); otherwise falls + back to ffmpeg's concat demuxer with re-encoding. + """ + if not sources: + raise ValueError("No audio files to concatenate") + if _concat_wav_files(sources, destination): + return + _concat_with_ffmpeg(sources, destination) + + def _encode_args(output_format: str) -> List[str]: """Return ffmpeg output codec/bitrate args for the requested container.""" if output_format == "m4b": |
