From df57cf2733e398473a58d788cd97fea3a618f892 Mon Sep 17 00:00:00 2001 From: historia Date: Sun, 23 Aug 2026 14:01:56 -0400 Subject: feat: tui for make_audiocpp_server_json --- converter/converter.py | 54 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 13 deletions(-) (limited to 'converter/converter.py') diff --git a/converter/converter.py b/converter/converter.py index 3915fe4..b24ac09 100644 --- a/converter/converter.py +++ b/converter/converter.py @@ -138,7 +138,9 @@ class AudiobookConverter: speed: float = 1.0, single_file: bool = False, output_format: str = config.AUDIO_FORMAT, language: Optional[str] = None, backend: str = config.BACKEND, voice: Optional[str] = None, debug: bool = False, - chunk: bool = False, model_id: Optional[str] = None): + chunk: bool = False, model_id: Optional[str] = None, + instructions: Optional[str] = None, + request_options: Optional[Dict[str, str]] = None): if speed <= 0: raise ValueError(f"Speed must be a positive number, got {speed}") if output_format not in AUDIO_FORMATS: @@ -164,6 +166,11 @@ class AudiobookConverter: # defaults to one request per chapter; --chunk forces client-side # chunking on top (possible needless double-chunking). self.client_chunks = bool(chunk) or backend != BACKEND_AUDIOCPP + # Voice design / style instruction and free-form request options + # (audio.cpp only): forwarded to AudioCppTTSClient, which validates + # them against the server-hosted model at connect time. + self.instructions = instructions + self.request_options = dict(request_options or {}) self._validate_configuration() if backend == BACKEND_FASTER: # The faster backend always voice-clones using a reference voice @@ -172,10 +179,14 @@ class AudiobookConverter: elif backend == BACKEND_AUDIOCPP: # Speaker mode (no voice) uses a built-in CustomVoice speaker; # an explicit voice selects a server-side preset (cloning). - # model_id overrides AUDIOCPP_MODEL_ID for multi-model servers. + # model_id overrides AUDIOCPP_MODEL_ID for multi-model servers; + # instructions describe or style the voice, request_options pass + # per-model controls through to the server. self.tts = AudioCppTTSClient(voice=voice, language=self.language, chunk_text=self.client_chunks, - model_id=model_id) + model_id=model_id, + instructions=instructions, + request_options=self.request_options) else: self.tts = QwenTTSClient( voice_mode=voice_mode, @@ -214,18 +225,22 @@ class AudiobookConverter: def _narrator_tag(self) -> str: """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) + self.backend, self.voice, self.voice_mode, + self.voice_clone_ref_audio, self.instructions) @staticmethod def compute_narrator_tag(backend: str, voice: Optional[str], voice_mode: str, - voice_clone_ref_audio: Optional[str]) -> str: + voice_clone_ref_audio: Optional[str], + instructions: Optional[str] = None) -> 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). + the built-in speaker for the audiocpp backend's speaker mode). An + instruction without a voice (voice design, or instruction-defined + voices on families without built-in speakers) uses "designed". Spaces become underscores (e.g. "Uncle Fu" -> "Uncle_Fu"). Pure (no I/O, no server) so the pre-flight overwrite check can @@ -235,7 +250,13 @@ class AudiobookConverter: if backend == BACKEND_FASTER: narrator = voice or config.FASTER_VOICE elif backend == BACKEND_AUDIOCPP: - narrator = voice or speaker_display_name() + if voice: + narrator = voice + elif instructions: + # The voice comes from the instruction, not a speaker name. + narrator = "designed" + else: + narrator = speaker_display_name() elif voice_mode == VOICE_MODE_CLONE: narrator = Path(voice_clone_ref_audio).stem else: @@ -590,9 +611,14 @@ class AudiobookConverter: if self.voice: print("Backend: audio.cpp (voice cloning, reference configured on server)") print(f"Voice: {self.voice}") + elif self.instructions: + print("Backend: audio.cpp (voice from --instructions description)") + print(f"Instruction: {self.instructions}") else: print("Backend: audio.cpp (custom voice, built-in speaker)") print(f"Speaker: {config.SPEAKER}") + if self.request_options: + print(f"Request options: {self.request_options}") if self.client_chunks: print("Chunking: client-side (--chunk; the server also chunks " "long text itself, so this may double-chunk)") @@ -629,8 +655,9 @@ class AudiobookConverter: 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]]]: + output_format: str, + instructions: Optional[str] = None + ) -> 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 @@ -639,8 +666,8 @@ class AudiobookConverter: 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. + 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() @@ -658,7 +685,7 @@ class AudiobookConverter: # 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) + backend, voice, voice_mode, voice_clone_ref_audio, instructions) for book_file in book_files: output_name = book_file.stem if stem_counts[book_file.stem] > 1: @@ -689,7 +716,8 @@ class AudiobookConverter: else: book_files, planned = AudiobookConverter.preflight_overwrites( self.backend, self.voice, self.voice_mode, - self.voice_clone_ref_audio, self.output_format) + self.voice_clone_ref_audio, self.output_format, + self.instructions) if not book_files: print(f"[INFO] No supported files found in {BOOKS_FOLDER}") -- cgit v1.2.3