Skip to content

Commit

Permalink
Cleanup subtitle related code and make it easier to use SubMaker (#329)
Browse files Browse the repository at this point in the history
Also don't output subtitles to STDERR by default.

Signed-off-by: rany <[email protected]>
  • Loading branch information
rany2 authored Nov 22, 2024
1 parent 93fb851 commit 1442154
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 27 deletions.
2 changes: 1 addition & 1 deletion examples/streaming_with_subtitles.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async def amain() -> None:
if chunk["type"] == "audio":
file.write(chunk["data"])
elif chunk["type"] == "WordBoundary":
submaker.add_cue((chunk["offset"], chunk["duration"]), chunk["text"])
submaker.feed(chunk)

with open(SRT_FILE, "w", encoding="utf-8") as file:
file.write(submaker.get_srt())
Expand Down
23 changes: 15 additions & 8 deletions src/edge_tts/submaker.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""SubMaker module is used to generate subtitles from WordBoundary events."""

from typing import List, Tuple
from typing import List

import srt # type: ignore

from .typing import TTSChunk


class SubMaker:
"""
Expand All @@ -13,23 +15,25 @@ class SubMaker:
def __init__(self) -> None:
self.cues: List[srt.Subtitle] = [] # type: ignore

def add_cue(self, timestamp: Tuple[float, float], text: str) -> None:
def feed(self, msg: TTSChunk) -> None:
"""
Add a cue to the SubMaker object.
Feed a WordBoundary message to the SubMaker object.
Args:
timestamp (tuple): The offset and duration of the subtitle.
text (str): The text of the subtitle.
msg (dict): The WordBoundary message.
Returns:
None
"""
if msg["type"] != "WordBoundary":
raise ValueError("Invalid message type, expected 'WordBoundary'")

self.cues.append(
srt.Subtitle(
index=len(self.cues) + 1,
start=srt.timedelta(microseconds=timestamp[0] / 10),
end=srt.timedelta(microseconds=sum(timestamp) / 10),
content=text,
start=srt.timedelta(microseconds=msg["duration"] / 10),
end=srt.timedelta(microseconds=(msg["duration"] + msg["offset"]) / 10),
content=msg["text"],
)
)

Expand All @@ -41,3 +45,6 @@ def get_srt(self) -> str:
str: The SRT formatted subtitles.
"""
return srt.compose(self.cues) # type: ignore

def __str__(self) -> str:
return self.get_srt()
46 changes: 28 additions & 18 deletions src/edge_tts/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import argparse
import asyncio
import sys
from io import TextIOWrapper
from typing import Any, TextIO, Union
from typing import Any, Optional, TextIO

from tabulate import tabulate

Expand Down Expand Up @@ -45,31 +44,42 @@ async def _run_tts(args: Any) -> None:
print("\nOperation canceled.", file=sys.stderr)
return

tts: Communicate = Communicate(
communicate = Communicate(
args.text,
args.voice,
proxy=args.proxy,
rate=args.rate,
volume=args.volume,
pitch=args.pitch,
proxy=args.proxy,
)
subs: SubMaker = SubMaker()
with (
open(args.write_media, "wb") if args.write_media else sys.stdout.buffer
) as audio_file:
async for chunk in tts.stream():
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"] == "WordBoundary":
subs.add_cue((chunk["offset"], chunk["duration"]), chunk["text"])

sub_file: Union[TextIOWrapper, TextIO] = (
open(args.write_subtitles, "w", encoding="utf-8")
if args.write_subtitles
else sys.stderr
)
with sub_file:
sub_file.write(subs.get_srt())
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:
Expand Down

0 comments on commit 1442154

Please sign in to comment.