aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--converter/config.py15
-rw-r--r--converter/converter.py14
-rw-r--r--converter/tts.py4
-rw-r--r--tests/test_chunking.py10
5 files changed, 40 insertions, 7 deletions
diff --git a/README.md b/README.md
index fd24a8d..0913632 100644
--- a/README.md
+++ b/README.md
@@ -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(""), [])