Skip to content

Commit

Permalink
Add sync versions of stream and save methods
Browse files Browse the repository at this point in the history
In order to provide synchronous interface to the library
  • Loading branch information
lzieniew committed Apr 21, 2024
1 parent bafe5d8 commit 1899609
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 0 deletions.
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()
40 changes: 40 additions & 0 deletions examples/sync_audio_stream_in_async_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python3

"""
This example shows that sync version of string function also works when run from
a sync function called itself from an async function.
The simple implementation of stream_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 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():
main()


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

import asyncio
import json
import re
import ssl
import time
import uuid
import concurrent.futures
from contextlib import nullcontext
from io import TextIOWrapper
from typing import (
Expand All @@ -21,6 +23,7 @@
Union,
)
from xml.sax.saxutils import escape
from queue import Queue

import aiohttp
import certifi
Expand Down Expand Up @@ -498,3 +501,38 @@ 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):
async def get_items():
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()

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))
future.result()

0 comments on commit 1899609

Please sign in to comment.