diff options
| author | historia <historiavg@proton.me> | 2026-08-20 22:38:58 +0000 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-20 23:02:09 +0000 |
| commit | e4b42be01fc031810160126013833175413ec84c (patch) | |
| tree | 1793b8d060d1272c9f95290599e45e2f1b477ff3 /converter | |
| parent | 5ca77f86b70718b4ef1a07299efbd6431268d546 (diff) | |
| download | tts-audiobook-generator-e4b42be01fc031810160126013833175413ec84c.tar.gz | |
feat: prompt for overwrites before connecting to TTS server
Diffstat (limited to 'converter')
| -rw-r--r-- | converter/converter.py | 113 |
1 files changed, 81 insertions, 32 deletions
diff --git a/converter/converter.py b/converter/converter.py index f1064f1..84ac766 100644 --- a/converter/converter.py +++ b/converter/converter.py @@ -106,8 +106,9 @@ def prompt_overwrite(existing: List[Path], output_name: str) -> bool: """Ask whether to reconvert a book whose output files already exist. All overwrite questions are asked before any conversion starts so the - rest of the run is unattended. Returns False when no interactive input - is available (stdin closed), keeping existing files safe. + rest of the run is unattended. Pressing Enter defaults to yes (so a + user can just hit Enter through the prompts), but a closed stdin + (non-interactive run) declines and keeps existing files safe. """ if len(existing) == 1: message = f"{existing[0].name} already exists. Convert anyway and overwrite it?" @@ -116,15 +117,17 @@ def prompt_overwrite(existing: List[Path], output_name: str) -> bool: f"(e.g. {existing[0].name}). Convert anyway and overwrite them?") while True: try: - answer = input(f"{message} (y/n): ").strip().lower() + answer = input(f"{message} [Y/n]: ").strip().lower() except EOFError: print("\n[WARNING] No interactive input available; keeping existing output") return False + if not answer: + return True if answer in ("y", "yes"): return True if answer in ("n", "no"): return False - print("Please answer 'y' or 'n'.") + print("Please answer 'y' or 'n' (or press Enter for yes).") class AudiobookConverter: @@ -207,23 +210,36 @@ class AudiobookConverter: return cleaned[:80] or fallback def _narrator_tag(self) -> str: - """Narrator name used in output file names. + """Narrator name used in output file names (see compute_narrator_tag).""" + return self.compute_narrator_tag( + self.backend, self.voice, self.voice_mode, self.voice_clone_ref_audio) + + @staticmethod + def compute_narrator_tag(backend: str, voice: Optional[str], + voice_mode: str, + voice_clone_ref_audio: Optional[str]) -> str: + """Narrator name used in output file names, without a server connection. Custom voice mode uses the built-in speaker's display name; voice clone mode uses the reference audio file's stem; the faster and audiocpp backends use the server-side voice name (falling back to the built-in speaker for the audiocpp backend's speaker mode). Spaces become underscores (e.g. "Uncle Fu" -> "Uncle_Fu"). + + Pure (no I/O, no server) so the pre-flight overwrite check can + compute the exact output names a run would produce before spending + time connecting to a TTS server. """ - if self.backend == BACKEND_FASTER: - narrator = self.voice or config.FASTER_VOICE - elif self.backend == BACKEND_AUDIOCPP: - narrator = self.voice or speaker_display_name() - elif self.voice_mode == VOICE_MODE_CLONE: - narrator = Path(self.voice_clone_ref_audio).stem + if backend == BACKEND_FASTER: + narrator = voice or config.FASTER_VOICE + elif backend == BACKEND_AUDIOCPP: + narrator = voice or speaker_display_name() + elif voice_mode == VOICE_MODE_CLONE: + narrator = Path(voice_clone_ref_audio).stem else: narrator = speaker_display_name() - return self._sanitize_filename(narrator, fallback="narrator").replace(" ", "_") + return AudiobookConverter._sanitize_filename( + narrator, fallback="narrator").replace(" ", "_") # ------------------------------------------------------------------ # Debug dumps (--debug) @@ -580,30 +596,33 @@ class AudiobookConverter: print(f"Debug dumps (per-chunk text + raw audio): {DEBUG_FOLDER}") print("=" * 70) - def run(self) -> bool: - """Main conversion process. Returns True if all books converted.""" - run_start = time.time() - self._print_banner() + # ------------------------------------------------------------------ + # Pre-flight: overwrite checks before connecting to a TTS server + # ------------------------------------------------------------------ + @staticmethod + def preflight_overwrites(backend: str, voice: Optional[str], + voice_mode: str, + voice_clone_ref_audio: Optional[str], + output_format: str) -> Tuple[List[Path], + List[Tuple[Path, str]]]: + """Discover books and ask every overwrite question up front. + + Pure of the TTS server: it scans the books folder, computes the + output name each book would produce (including the narrator tag + and stem-collision suffix), and asks whether to overwrite any + existing output files. Returns ``(book_files, planned)`` where + ``planned`` is the subset the user agreed to (re)convert. + + Asking before connecting means a user who declines a prompt (or + has nothing to convert) never waits on a slow server handshake. + """ book_files = sorted( f for f in BOOKS_FOLDER.iterdir() if f.is_file() and f.suffix.lower() in SUPPORTED_FORMATS ) - if not book_files: - print(f"[INFO] No supported files found in {BOOKS_FOLDER}") - print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") - - # Create sample file - sample_file = BOOKS_FOLDER / "sample.txt" - sample_file.write_text( - "This is a sample audiobook for testing the Qwen-based converter. " - "The system will send this text to the Qwen API for voice generation. " - "You can replace this file with your own books to convert.", - encoding="utf-8", - ) - print(f"[INFO] Created sample file: {sample_file}") - return True + return [], [] print(f"[INFO] Found {len(book_files)} books to convert") @@ -613,16 +632,46 @@ class AudiobookConverter: # Ask every overwrite question up front, before any conversion # starts, so the rest of the run is unattended. planned: List[Tuple[Path, str]] = [] + narrator_tag = AudiobookConverter.compute_narrator_tag( + backend, voice, voice_mode, voice_clone_ref_audio) for book_file in book_files: output_name = book_file.stem if stem_counts[book_file.stem] > 1: output_name = f"{book_file.stem}_{book_file.suffix.lstrip('.')}" - output_name = f"{output_name}_{self._narrator_tag()}" - existing = find_existing_outputs(output_name, self.output_format) + output_name = f"{output_name}_{narrator_tag}" + existing = find_existing_outputs(output_name, output_format) if existing and not prompt_overwrite(existing, output_name): print(f"[INFO] Skipping {book_file.name} (existing output kept)") continue planned.append((book_file, output_name)) + return book_files, planned + + # ------------------------------------------------------------------ + # Main conversion loop + # ------------------------------------------------------------------ + + def run(self) -> bool: + """Main conversion process. Returns True if all books converted.""" + run_start = time.time() + self._print_banner() + + # When main() has already done the pre-flight overwrite check, use + # its results so the prompts are not asked a second time; otherwise + # (e.g. a converter constructed directly) discover and ask here. + if getattr(self, "_planned", None) is not None: + book_files = self._book_files + planned = self._planned + else: + book_files, planned = AudiobookConverter.preflight_overwrites( + self.backend, self.voice, self.voice_mode, + self.voice_clone_ref_audio, self.output_format) + + if not book_files: + print(f"[INFO] No supported files found in {BOOKS_FOLDER}") + print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") + print("[INFO] Nothing to convert. Add a .txt, .pdf, or .epub file " + f"to {BOOKS_FOLDER} and run again.") + return True if not planned: print("[INFO] Nothing to convert (all books skipped)") |
