diff options
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | converter/config.py | 15 | ||||
| -rw-r--r-- | converter/converter.py | 14 | ||||
| -rw-r--r-- | converter/tts.py | 4 | ||||
| -rw-r--r-- | tests/test_chunking.py | 10 |
5 files changed, 40 insertions, 7 deletions
@@ -188,6 +188,10 @@ Third-party wheels: https://mjunya.com/flash-attention-prebuild-wheels/ (hosted Transcription affects the output a lot. Whisper is okay, but does not give perfect transcription. A manual transcription passed via `--transcription` is better. +Keep `CHUNK_SIZE_WORDS` small (default 40). Every API call is a single model generation: long generations lose prosody, can degrade into garbled audio, and text past the model's token limit is never spoken. If parts of a book sound flat, monotone, or garbled, the chunk size is the first thing to check. + +`MIN_DELAY_BETWEEN_CHUNKS` only matters for hosted demos (rate limits); a local server needs no delay (default 0). + Manual transcription, imperfect whisper transcription, and `--no-transcription` each provide different results. Usually the most accurate transcription is the best, but sometimes `--no-transcription` can produce a flat tone that might be preferable for certain voices. Even tiny amounts of pause between phrases in the sample audio can have a big impact. Try increasing or decreasing them. diff --git a/converter/config.py b/converter/config.py index efec0dd..dcec145 100644 --- a/converter/config.py +++ b/converter/config.py @@ -123,8 +123,9 @@ FASTER_TTS_API_URL = "http://127.0.0.1:8000" # name is unknown, so a mismatch here is easy to miss. FASTER_TTS_VOICE = "default" FASTER_TTS_SAMPLE_RATE = 24000 # Qwen3-TTS 12Hz codec output rate -# The Gradio demo sub-chunked text server-side (~200 chars); the faster server -# takes one generation per request, so long chunks are sub-chunked client-side. +# Safety net: the faster server takes one generation per request, so any +# chunk longer than this is sub-chunked client-side (a no-op at the default +# CHUNK_SIZE_WORDS above; kept in case the chunk size is ever raised). FASTER_SUBCHUNK_WORDS = 40 # ~200 chars per request FASTER_HTTP_TIMEOUT = 300 # Seconds before a speech request times out FASTER_SUBCHUNK_RETRIES = 3 # Attempts per sub-chunk request @@ -138,8 +139,14 @@ AUDIOBOOKS_FOLDER = BASE_DIR / "output" # Output folder CHUNKS_FOLDER = BASE_DIR / "chunks" # Scratch space for per-chunk audio (cleaned per book) LOGS_FOLDER = BASE_DIR / "logs" -CHUNK_SIZE_WORDS = 1500 # Words per TTS chunk -MIN_DELAY_BETWEEN_CHUNKS = 1 # Seconds between API calls +# Words per TTS generation request. Each API call is ONE model generation: +# long generations lose prosody, can degrade into garbled audio, and text +# past the model's token limit is never spoken. ~40 words (~200 chars) is +# the per-request length the old qwen-tts demo enforced server-side. +CHUNK_SIZE_WORDS = 40 +# Pause between API calls (rate-limit protection for hosted demos; a local +# server needs no delay). +MIN_DELAY_BETWEEN_CHUNKS = 0 HEARTBEAT_INTERVAL_SECONDS = 30 # Print "still working" this often during a chunk # ============================================================================= diff --git a/converter/converter.py b/converter/converter.py index ec06bbb..3b2782f 100644 --- a/converter/converter.py +++ b/converter/converter.py @@ -340,7 +340,6 @@ class AudiobookConverter: avg_chunk_size = sum(chunk_sizes) / len(chunk_sizes) logger.info("Split into %d chunks (avg %.0f words per chunk)", total_chunks, avg_chunk_size) print(f"[INFO] Processing {total_chunks} chunks via Qwen API...") - print(f"[INFO] Estimated time: ~{total_chunks * 4} minutes (4 min per chunk)") results = self._synthesize_chunks(chunks) successful_chunks = sum(1 for path in results.values() if path) @@ -414,6 +413,7 @@ class AudiobookConverter: def run(self) -> bool: """Main conversion process. Returns True if all books converted.""" + run_start = time.time() self._print_banner() # Check for books @@ -493,4 +493,16 @@ class AudiobookConverter: if successful > 0: print(f"\n[INFO] Audiobooks saved to: {config.AUDIOBOOKS_FOLDER}/") + elapsed = int(time.time() - run_start) + hours, remainder = divmod(elapsed, 3600) + minutes, seconds = divmod(remainder, 60) + if hours: + duration = f"{hours}h {minutes}m {seconds}s" + elif minutes: + duration = f"{minutes}m {seconds}s" + else: + duration = f"{seconds}s" + print(f"\n[INFO] Generation completed in {duration}") + logger.info("Generation completed in %s", duration) + return total > 0 and successful == total diff --git a/converter/tts.py b/converter/tts.py index cb09936..1f5606f 100644 --- a/converter/tts.py +++ b/converter/tts.py @@ -114,8 +114,8 @@ class _BaseTTSClient: Returns the generated chunk file's path, or None when all attempts failed. """ - # Small delay between chunks to avoid rate limiting (only if not first chunk) - if chunk_num > 1: + # Optional pause between API calls (rate limiting on hosted demos) + if chunk_num > 1 and config.MIN_DELAY_BETWEEN_CHUNKS > 0: time.sleep(config.MIN_DELAY_BETWEEN_CHUNKS) for attempt in range(config.MAX_RETRIES): diff --git a/tests/test_chunking.py b/tests/test_chunking.py index 659a771..d1d95bd 100644 --- a/tests/test_chunking.py +++ b/tests/test_chunking.py @@ -2,9 +2,19 @@ import unittest +from converter import config from converter.chunking import split_into_chunks +class ChunkSizeDefaultTests(unittest.TestCase): + """Guard the default chunk size: each API call is one model generation, + and long single generations lose prosody, can turn garbled, and are + truncated at the model's token limit (text past it is never spoken).""" + + def test_default_chunk_size_within_single_generation_budget(self): + self.assertLessEqual(config.CHUNK_SIZE_WORDS, 60) + + class SplitIntoChunksTests(unittest.TestCase): def test_empty_input(self): self.assertEqual(split_into_chunks(""), []) |
