aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-21 00:15:57 -0400
committerhistoria <historiavg@proton.me>2026-08-21 00:15:57 -0400
commitfea9222740da007f1d7befcd7dee035265c0e5d1 (patch)
tree8ae6a1305940ec94f6f495aa59c5cdc95cd803dd
parent0017f6b0421e549e9a2cdfadc104be64433724d3 (diff)
downloadtts-audiobook-generator-fea9222740da007f1d7befcd7dee035265c0e5d1.tar.gz
feat: exit script if any chunk fails
-rw-r--r--converter/converter.py36
-rw-r--r--tests/test_converter.py24
2 files changed, 46 insertions, 14 deletions
diff --git a/converter/converter.py b/converter/converter.py
index 0ae1506..c5f1788 100644
--- a/converter/converter.py
+++ b/converter/converter.py
@@ -381,9 +381,9 @@ class AudiobookConverter:
speed=1.0, output_format="wav",
chapter=(index, total_chapters),
debug_dir=self._chapter_debug_dir(debug_dir, index, title)):
- logger.warning("Skipping chapter %d (%s) due to conversion failure",
- index, title)
- continue
+ logger.error("Chapter %d (%s) failed; aborting the conversion",
+ index, title)
+ return False
chapter_files.append(chapter_path)
titles.append(title)
@@ -405,9 +405,11 @@ class AudiobookConverter:
"""Synthesize chunks sequentially, preserving order and naming.
Returns a mapping of chunk number to the generated audio path, with
- None for chunks that failed after retries. When ``debug_dir`` is
- given (--debug), each chunk's request text and returned audio are
- also dumped there, and every request/response is logged.
+ None for chunks that failed. Generation stops at the first failed
+ chunk: a partial audiobook is never assembled, so the remaining
+ chunks are not requested. When ``debug_dir`` is given (--debug),
+ each chunk's request text and returned audio are also dumped there,
+ and every request/response is logged.
"""
total_chunks = len(chunks)
if self.client_chunks:
@@ -438,11 +440,15 @@ class AudiobookConverter:
print(f"[OK] Chunk {chunk_num:3d}/{total_chunks} completed")
logger.info("+ Chunk %d/%d completed", chunk_num, total_chunks)
else:
- logger.error("Chunk %d/%d failed", chunk_num, total_chunks)
+ logger.error("Chunk %d/%d failed; aborting the remaining chunks",
+ chunk_num, total_chunks)
+ break
except Exception as exc:
results[chunk_num] = None
- logger.error("Chunk %d/%d error: %s", chunk_num, total_chunks, exc)
+ logger.error("Chunk %d/%d error: %s; aborting the remaining chunks",
+ chunk_num, total_chunks, exc)
+ break
successful_chunks = sum(1 for path in results.values() if path)
if self.client_chunks:
@@ -528,13 +534,11 @@ class AudiobookConverter:
results = self._synthesize_chunks(chunks, debug_dir=debug_dir)
successful_chunks = sum(1 for path in results.values() if path)
- if successful_chunks == 0:
- logger.error("No chunks were successfully processed")
- return False
-
if successful_chunks < total_chunks:
- logger.warning("Only %d/%d chunks succeeded. Proceeding with partial audiobook.",
- successful_chunks, total_chunks)
+ logger.error("Chunk processing incomplete (%d/%d chunks); "
+ "aborting without producing an audiobook",
+ successful_chunks, total_chunks)
+ return False
success = audio.combine_chunks(total_chunks, output_path, chunk_results=results,
speed=speed, output_format=output_format,
@@ -710,6 +714,10 @@ class AudiobookConverter:
except Exception as exc:
logger.error("Unexpected error: %s", exc)
results[book_file.name] = False
+ if not results[book_file.name]:
+ logger.error("Conversion of %s failed; aborting the remaining books",
+ book_file.name)
+ break
successful = sum(results.values())
total = len(results)
diff --git a/tests/test_converter.py b/tests/test_converter.py
index d525c18..e7c0776 100644
--- a/tests/test_converter.py
+++ b/tests/test_converter.py
@@ -258,6 +258,20 @@ class DebugDumpTests(unittest.TestCase):
results = self.converter._synthesize_chunks(["Hello."], debug_dir=blocker / "book")
self.assertEqual(results, {1: audio})
+ def test_failed_chunk_stops_remaining_chunks(self):
+ audio = self._chunk_source("chunk_0001.wav")
+ self.converter.tts.process_chunk_with_retry.side_effect = [audio, None, audio]
+ results = self.converter._synthesize_chunks(["One.", "Two.", "Three."])
+ self.assertEqual(results, {1: audio, 2: None})
+ self.assertEqual(self.converter.tts.process_chunk_with_retry.call_count, 2)
+
+ def test_raising_chunk_stops_remaining_chunks(self):
+ audio = self._chunk_source("chunk_0001.wav")
+ self.converter.tts.process_chunk_with_retry.side_effect = [audio, RuntimeError("boom")]
+ results = self.converter._synthesize_chunks(["One.", "Two.", "Three."])
+ self.assertEqual(results, {1: audio, 2: None})
+ self.assertEqual(self.converter.tts.process_chunk_with_retry.call_count, 2)
+
def test_debug_flag_wiring(self):
with patch("converter.converter.QwenTTSClient"):
self.assertFalse(AudiobookConverter().debug)
@@ -411,6 +425,16 @@ class ServerSideChunkingOutputTests(unittest.TestCase):
self.assertNotIn("single request", out)
self.assertIn("Chapter 2/5 converted (1/1 chunks)", out)
+ def test_partial_chunks_abort_without_assembling(self):
+ converter = self._converter(client_chunks=True)
+ converter.tts.process_chunk_with_retry.side_effect = ["chunk_0001.wav", None]
+ text = " ".join(f"word{i}" for i in range(8))
+ with patch.object(config, "CHUNK_SIZE", 5), \
+ patch.object(converter_mod.audio, "combine_chunks") as mock_combine:
+ ok = converter._convert_text(text, Path("out.mp3"), time.time())
+ self.assertFalse(ok)
+ mock_combine.assert_not_called()
+
class PromptOverwriteTests(unittest.TestCase):
def test_single_file_yes(self):