Files
edge-tts/src/edge_tts/util.py
rany2 c78e49d28e Cleanup SentenceBoundary support (#396)
- Default to SentenceBoundary
- Modify boundary argument to lowercase to match other options.
- Drop merge_cues support as SentenceBoundary renders it obsolete.

Signed-off-by: rany <rany2@riseup.net>
2025-08-05 14:30:30 +03:00

148 lines
4.6 KiB
Python

"""Utility functions for the command line interface. Used by the main module."""
import argparse
import asyncio
import sys
from typing import Optional, TextIO
from tabulate import tabulate
from . import Communicate, SubMaker, list_voices
from .constants import DEFAULT_VOICE
from .data_classes import UtilArgs
async def _print_voices(*, proxy: Optional[str]) -> None:
"""Print all available voices."""
voices = await list_voices(proxy=proxy)
voices = sorted(voices, key=lambda voice: voice["ShortName"])
headers = ["Name", "Gender", "ContentCategories", "VoicePersonalities"]
table = [
[
voice["ShortName"],
voice["Gender"],
", ".join(voice["VoiceTag"]["ContentCategories"]),
", ".join(voice["VoiceTag"]["VoicePersonalities"]),
]
for voice in voices
]
print(tabulate(table, headers))
async def _run_tts(args: UtilArgs) -> None:
"""Run TTS after parsing arguments from command line."""
try:
if sys.stdin.isatty() and sys.stdout.isatty() and not args.write_media:
print(
"Warning: TTS output will be written to the terminal. "
"Use --write-media to write to a file.\n"
"Press Ctrl+C to cancel the operation. "
"Press Enter to continue.",
file=sys.stderr,
)
input()
except KeyboardInterrupt:
print("\nOperation canceled.", file=sys.stderr)
return
communicate = Communicate(
args.text,
args.voice,
rate=args.rate,
volume=args.volume,
pitch=args.pitch,
proxy=args.proxy,
)
submaker = SubMaker()
try:
audio_file = (
open(args.write_media, "wb")
if args.write_media is not None and args.write_media != "-"
else sys.stdout.buffer
)
sub_file: Optional[TextIO] = (
open(args.write_subtitles, "w", encoding="utf-8")
if args.write_subtitles is not None and args.write_subtitles != "-"
else None
)
if sub_file is None and args.write_subtitles == "-":
sub_file = sys.stderr
async for chunk in communicate.stream():
if chunk["type"] == "audio":
audio_file.write(chunk["data"])
elif chunk["type"] in ("WordBoundary", "SentenceBoundary"):
submaker.feed(chunk)
if sub_file is not None:
sub_file.write(submaker.get_srt())
finally:
if audio_file is not sys.stdout.buffer:
audio_file.close()
if sub_file is not None and sub_file is not sys.stderr:
sub_file.close()
async def amain() -> None:
"""Async main function"""
parser = argparse.ArgumentParser(
description="Text-to-speech using Microsoft Edge's online TTS service."
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-t", "--text", help="what TTS will say")
group.add_argument("-f", "--file", help="same as --text but read from file")
parser.add_argument(
"-v",
"--voice",
help=f"voice for TTS. Default: {DEFAULT_VOICE}",
default=DEFAULT_VOICE,
)
group.add_argument(
"-l",
"--list-voices",
help="lists available voices and exits",
action="store_true",
)
parser.add_argument("--rate", help="set TTS rate. Default +0%%.", default="+0%")
parser.add_argument("--volume", help="set TTS volume. Default +0%%.", default="+0%")
parser.add_argument("--pitch", help="set TTS pitch. Default +0Hz.", default="+0Hz")
parser.add_argument(
"--words-in-cue",
help="number of words in a subtitle cue. Default: 10.",
default=10,
type=int,
)
parser.add_argument(
"--write-media", help="send media output to file instead of stdout"
)
parser.add_argument(
"--write-subtitles",
help="send subtitle output to provided file instead of stderr",
)
parser.add_argument("--proxy", help="use a proxy for TTS and voice list.")
args = parser.parse_args(namespace=UtilArgs())
if args.list_voices:
await _print_voices(proxy=args.proxy)
sys.exit(0)
if args.file is not None:
if args.file in ("-", "/dev/stdin"):
args.text = sys.stdin.read()
else:
with open(args.file, encoding="utf-8") as file:
args.text = file.read()
if args.text is not None:
await _run_tts(args)
def main() -> None:
"""Run the main function using asyncio."""
asyncio.run(amain())
if __name__ == "__main__":
main()