Add sync versions of stream and save methods (#215)
* Add sync versions of stream and save methods In order to provide synchronous interface to the library * Fix save_sync() failing to use metadata_fname and fix typing issues Signed-off-by: rany <ranygh@riseup.net> --------- Signed-off-by: rany <ranygh@riseup.net> Co-authored-by: rany <ranygh@riseup.net>
This commit is contained in:
27
examples/basic_sync_audio_streaming.py
Normal file
27
examples/basic_sync_audio_streaming.py
Normal file
@@ -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
examples/basic_sync_generation.py
Normal file
21
examples/basic_sync_generation.py
Normal file
@@ -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
examples/sync_audio_generation_in_async_context.py
Normal file
36
examples/sync_audio_generation_in_async_context.py
Normal file
@@ -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
examples/sync_audio_stream_in_async_context.py
Normal file
42
examples/sync_audio_stream_in_async_context.py
Normal file
@@ -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()
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
Communicate package.
|
Communicate package.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import concurrent.futures
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
@@ -9,6 +11,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from io import TextIOWrapper
|
from io import TextIOWrapper
|
||||||
|
from queue import Queue
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
AsyncGenerator,
|
AsyncGenerator,
|
||||||
@@ -498,3 +501,40 @@ class Communicate:
|
|||||||
):
|
):
|
||||||
json.dump(message, metadata)
|
json.dump(message, metadata)
|
||||||
metadata.write("\n")
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user