aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp/voices.py
blob: b26a0be3761377cdb099f7cdf6bd3bb9c4b766d1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""Reference-.wav transcription planning and execution."""

import argparse
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from backends.common import (PROMPT_TEXT_FILENAME, find_wav_files,
                             read_prompt_text)
from converter.clients import (transcribe_reference_audio,
                               whisper_backend_available)

def transcribe_wav_dir(wav_files: list, whisper_model: str,
                       cancel=None) -> Dict[str, str]:
    """Transcribe each wav file and return a mapping of stem -> transcript.

    CANCEL (a ``threading.Event``) is checked between files so the in-TUI
    task view can stop a long transcription early.
    """
    transcripts: Dict[str, str] = {}
    for wav_file in wav_files:
        if cancel is not None and cancel.is_set():
            print("[INFO] Transcription cancelled")
            break
        name = wav_file.stem
        print(f"[INFO] Transcribing {wav_file.name} (voice '{name}')...")
        text = transcribe_reference_audio(str(wav_file), model_name=whisper_model)
        if text:
            print(f"[OK] {name}: {text}")
        else:
            print(f"[WARNING] No transcript for '{name}'; cloning works best "
                  "with an accurate transcript — consider editing prompt_text "
                  "by hand before starting the server")
        transcripts[name] = text or ""
    return transcripts


def print_empty_transcript_warning(transcripts: Dict[str, str]) -> None:
    """Print a loud, final warning for voices whose transcript is empty."""
    empty = sorted(name for name, text in transcripts.items() if not text)
    if not empty:
        return
    bar = "=" * 70
    print()
    print(bar)
    print("[WARNING] MANUAL TRANSCRIPTION REQUIRED")
    print(bar)
    listing = "  - " + "\n  - ".join(empty) if len(empty) > 1 else f"  - {empty[0]}"
    print(f"The following voice(s) have an EMPTY transcript in prompt_text:\n"
          f"{listing}")
    print("Those voices will NOT work until you add an accurate transcript.")
    print(f"Edit {PROMPT_TEXT_FILENAME} in your voice directory and fill in the "
          "text after '|' for each voice above.")
    print(bar)


def _transcribe(args: argparse.Namespace, plan: Optional[dict],
                cancel=None) -> Tuple[Dict[str, str], bool]:
    """Transcribe the wav directory into a stem -> transcript mapping.

    Returns the mapping and a flag indicating whether it should be written to
    prompt_text (False when an existing, complete prompt_text is kept as-is).
    PLAN is always pre-collected — by the TUI setup form (mode "all",
    "missing" or "keep") or by _flag_plan for a non-interactive run — so no
    questions are asked here; a None PLAN defaults to "transcribe everything".
    CANCEL is checked between files.
    """
    wav_files = find_wav_files(args.input_dir)
    if not wav_files:
        print(f"[WARNING] No .wav files found in {args.input_dir}; writing the "
              "config without a voice_dir")
        return {}, False

    prompt_path = args.input_dir / PROMPT_TEXT_FILENAME
    existing = dict((plan or {}).get("existing") or {})
    mode = plan["mode"] if plan else "all"

    if mode == "keep":
        print(f"[INFO] Kept existing {prompt_path}; all voices were "
              "already transcribed, nothing new to transcribe")
        return existing, False

    if whisper_backend_available() is None:
        print("[WARNING] Neither faster_whisper nor whisper was found, so "
              "reference .wav files cannot be transcribed automatically and "
              "every transcript will be empty.")
        print("          Install whisper (or faster_whisper) in your "
              "audiobook environment to transcribe automatically; otherwise "
              "transcripts must be added by hand (see the warning at the end).")

    if plan["mode"] == "missing":
        new_transcripts = transcribe_wav_dir(plan["missing"], args.whisper_model,
                                             cancel=cancel)
        transcripts = dict(existing)
        transcripts.update(new_transcripts)
    else:
        transcripts = transcribe_wav_dir(wav_files, args.whisper_model,
                                         cancel=cancel)
    return transcripts, True


def _flag_plan(wav_files: list, prompt_path: Path, force: bool) -> dict:
    """Build a transcription plan for a non-interactive (flag-only) run.

    With --force everything is re-transcribed; otherwise an existing
    prompt_text is reused and only voices with an empty transcript are
    re-transcribed, mirroring what the TUI confirms interactively.
    """
    if prompt_path.exists() and not force:
        existing = read_prompt_text(prompt_path)
        missing = [wav for wav in wav_files
                   if not existing.get(wav.stem, "").strip()]
        if not missing:
            return {"mode": "keep", "missing": [], "existing": existing}
        return {"mode": "missing", "missing": missing, "existing": existing}
    return {"mode": "all", "missing": [], "existing": {}}