aboutsummaryrefslogtreecommitdiff
path: root/converter/converter.py
diff options
context:
space:
mode:
Diffstat (limited to 'converter/converter.py')
-rw-r--r--converter/converter.py54
1 files changed, 41 insertions, 13 deletions
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}")