Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sync versions of stream and save methods #215

Merged
merged 2 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/basic_sync_audio_streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python3

"""
Basic audio streaming example for sync interface

"""

import edge_tts

TEXT = "Hello World!"
VOICE = "en-GB-SoniaNeural"
OUTPUT_FILE = "test.mp3"


def main() -> None:
"""Main function to process audio and metadata synchronously."""
communicate = edge_tts.Communicate(TEXT, VOICE)
with open(OUTPUT_FILE, "wb") as file:
for chunk in communicate.stream_sync():
if chunk["type"] == "audio":
file.write(chunk["data"])
elif chunk["type"] == "WordBoundary":
print(f"WordBoundary: {chunk}")


if __name__ == "__main__":
main()
21 changes: 21 additions & 0 deletions examples/basic_sync_generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3

"""
Basic example of edge_tts usage in synchronous function
"""

import edge_tts

TEXT = "Hello World!"
VOICE = "en-GB-SoniaNeural"
OUTPUT_FILE = "test.mp3"


def main() -> None:
"""Main function"""
communicate = edge_tts.Communicate(TEXT, VOICE)
communicate.save_sync(OUTPUT_FILE)


if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions examples/sync_audio_generation_in_async_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3

"""
This example shows that sync version of save function also works when run from
a sync function called itself from an async function.
The simple implementation of save_sync() with only asyncio.run would fail in this scenario,
that's why ThreadPoolExecutor is used in implementation.

"""

import asyncio

import edge_tts

TEXT = "Hello World!"
VOICE = "en-GB-SoniaNeural"
OUTPUT_FILE = "test.mp3"


def sync_main() -> None:
"""Main function"""
communicate = edge_tts.Communicate(TEXT, VOICE)
communicate.save_sync(OUTPUT_FILE)


async def amain() -> None:
"""Main function"""
sync_main()


if __name__ == "__main__":
loop = asyncio.get_event_loop_policy().get_event_loop()
try:
loop.run_until_complete(amain())
finally:
loop.close()
42 changes: 42 additions & 0 deletions examples/sync_audio_stream_in_async_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env python3

"""
This example shows the sync version of stream function which also
works when run from a sync function called itself from an async function.
"""

import asyncio

import edge_tts

TEXT = "Hello World!"
VOICE = "en-GB-SoniaNeural"
OUTPUT_FILE = "test.mp3"


def main() -> None:
"""Main function to process audio and metadata synchronously."""
communicate = edge_tts.Communicate(TEXT, VOICE)
with open(OUTPUT_FILE, "wb") as file:
for chunk in communicate.stream_sync():
if chunk["type"] == "audio":
file.write(chunk["data"])
elif chunk["type"] == "WordBoundary":
print(f"WordBoundary: {chunk}")


async def amain() -> None:
""" "
Async main function to call sync main function

This demonstrates that this works even when called from an async function.
"""
main()


if __name__ == "__main__":
loop = asyncio.get_event_loop_policy().get_event_loop()
try:
loop.run_until_complete(amain())
finally:
loop.close()
40 changes: 40 additions & 0 deletions src/edge_tts/communicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
Communicate package.
"""

import asyncio
import concurrent.futures
import json
import re
import ssl
import time
import uuid
from contextlib import nullcontext
from io import TextIOWrapper
from queue import Queue
from typing import (
Any,
AsyncGenerator,
Expand Down Expand Up @@ -498,3 +501,40 @@ async def save(
):
json.dump(message, metadata)
metadata.write("\n")

def stream_sync(self) -> Generator[Dict[str, Any], None, None]:
"""Synchronous interface for async stream method"""

def fetch_async_items(queue: Queue) -> None: # type: ignore
async def get_items() -> None:
async for item in self.stream():
queue.put(item)
queue.put(None)

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(get_items())
loop.close()

queue: Queue = Queue() # type: ignore

with concurrent.futures.ThreadPoolExecutor() as executor:
executor.submit(fetch_async_items, queue)

while True:
item = queue.get()
if item is None:
break
yield item

def save_sync(
self,
audio_fname: Union[str, bytes],
metadata_fname: Optional[Union[str, bytes]] = None,
) -> None:
"""Synchronous interface for async save method."""
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(
asyncio.run, self.save(audio_fname, metadata_fname)
)
future.result()
Loading