Skip to content

Commit

Permalink
Cleanup subtitle related code and make it easier to use SubMaker
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 committed Nov 22, 2024
1 parent 93fb851 commit 614fc7a
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 614fc7a

Please sign in to comment.