Skip to content

Source code

Source code

src/scripts/presentation/edge_cli.py

This file is included in this documentation build. It is displayed as code and is not executed.

Download source

"""Audition the configured Edge voice and pronunciation spellings with clean captions.

@author Laurent Declercq l.declercq@agon-innovation.ch
@version 20260921
"""

import argparse
from pathlib import Path
import shutil
import subprocess
import sys

from cli import PYTHON, TOOLS
from pipeline import (
    ROOT,
    SOURCE,
    caption_cues,
    edge_options,
    ensure_audio,
    pronunciation_text,
    read_json,
    write_json,
)


def main():
    """Generate an isolated MP3 audition using the video pipeline's speech settings."""
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--locale", choices=["fr", "en", "de"], default="fr")
    parser.add_argument(
        "--voice", help="Override the Edge voice for this audition only."
    )
    parser.add_argument("--rate", help="Override speaking speed, for example +0%%.")
    parser.add_argument("--text-file", type=Path)
    parser.add_argument("--output", type=Path)
    parser.add_argument("--offline", action="store_true")
    parser.add_argument("--force", action="store_true")
    args = parser.parse_args()
    if args.offline and args.force:
        parser.error("--offline and --force cannot be combined.")
    if Path(sys.prefix).resolve() != TOOLS.resolve():
        if not PYTHON.is_file():
            parser.error("Run yarn presentation:setup first.")
        raise SystemExit(
            subprocess.run(
                [str(PYTHON), str(Path(__file__).resolve()), *sys.argv[1:]]
            ).returncode
        )
    settings = read_json(SOURCE / "settings.json")
    if args.voice:
        settings["voices"][args.locale] = args.voice
    if args.rate:
        settings.setdefault("rates", {})[args.locale] = args.rate
    rate, aliases = edge_options(settings, args.locale)
    source = args.text_file or SOURCE / "samples" / f"{args.locale}.txt"
    text = source.read_text(encoding="utf-8").strip()
    if not text:
        parser.error("The audition text must not be empty.")
    output = (
        args.output
        or ROOT / ".build/presentation-voice-samples-edge" / f"edge-{args.locale}.mp3"
    )
    if output.suffix.lower() != ".mp3":
        parser.error("The audition output must have a .mp3 extension.")
    audio, words = ensure_audio(
        {"id": "audition", "narration": text},
        args.locale,
        settings,
        ROOT / ".build/presentation/edge-auditions",
        args.offline,
        args.force,
    )
    output.parent.mkdir(parents=True, exist_ok=True)
    shutil.copyfile(audio, output)
    output.with_suffix(".txt").write_text(text + "\n", encoding="utf-8")
    spoken = pronunciation_text(text, aliases)
    output.with_suffix(".spoken.txt").write_text(spoken + "\n", encoding="utf-8")
    output.with_suffix(".vtt").write_text(
        "WEBVTT\n\n" + "\n".join(caption_cues(words, text, 0)), encoding="utf-8"
    )
    write_json(
        output.with_suffix(".json"),
        {
            "backend": "edge",
            "voice": settings["voices"][args.locale],
            "rate": rate,
            "pitch": "+0Hz",
            "locale": args.locale,
            "text": text,
            "spokenText": spoken,
            "pronunciations": aliases,
        },
    )
    print(f"Generated {output}.")


if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError, RuntimeError, ImportError) as error:
        print(f"Edge audition failed: {error}", file=sys.stderr)
        raise SystemExit(1) from error