diff --git a/examples/dynamic_voice_selection.py b/examples/async_audio_gen_with_dynamic_voice_selection.py similarity index 79% rename from examples/dynamic_voice_selection.py rename to examples/async_audio_gen_with_dynamic_voice_selection.py index 2b05395..62c6fd1 100644 --- a/examples/dynamic_voice_selection.py +++ b/examples/async_audio_gen_with_dynamic_voice_selection.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -""" -Example of dynamic voice selection using VoicesManager. -""" +"""Simple example to generate an audio file with randomized +dynamic voice selection based on attributes such as Gender, +Language, or Locale.""" import asyncio import random diff --git a/examples/basic_generation.py b/examples/async_audio_gen_with_predefined_voice.py similarity index 81% rename from examples/basic_generation.py rename to examples/async_audio_gen_with_predefined_voice.py index 24d391a..7d36d8c 100644 --- a/examples/basic_generation.py +++ b/examples/async_audio_gen_with_predefined_voice.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -""" -Basic example of edge_tts usage. -""" +"""Simple example to generate audio with preset voice using async/await""" import asyncio diff --git a/examples/streaming_with_subtitles.py b/examples/async_audio_streaming_with_predefined_voice_and_subtitles.py similarity index 79% rename from examples/streaming_with_subtitles.py rename to examples/async_audio_streaming_with_predefined_voice_and_subtitles.py index 766ba67..3ed2579 100644 --- a/examples/streaming_with_subtitles.py +++ b/examples/async_audio_streaming_with_predefined_voice_and_subtitles.py @@ -1,11 +1,7 @@ #!/usr/bin/env python3 -""" -Streaming TTS example with subtitles. - -This example is similar to the example basic_audio_streaming.py, but it shows -WordBoundary events to create subtitles using SubMaker. -""" +"""Example showing how to use use .stream() method to get audio chunks +and feed them to SubMaker to generate subtitles""" import asyncio diff --git a/examples/basic_audio_streaming.py b/examples/basic_audio_streaming.py deleted file mode 100644 index 503d76d..0000000 --- a/examples/basic_audio_streaming.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 - -""" -Basic audio streaming example. - -This example shows how to stream the audio data from the TTS engine, -and how to get the WordBoundary events from the engine (which could -be ignored if not needed). - -The example streaming_with_subtitles.py shows how to use the -WordBoundary events to create subtitles using SubMaker. -""" - -import asyncio - -import edge_tts - -TEXT = "Hello World!" -VOICE = "en-GB-SoniaNeural" -OUTPUT_FILE = "test.mp3" - - -async def amain() -> None: - """Main function""" - communicate = edge_tts.Communicate(TEXT, VOICE) - with open(OUTPUT_FILE, "wb") as file: - async for chunk in communicate.stream(): - if chunk["type"] == "audio": - file.write(chunk["data"]) - elif chunk["type"] == "WordBoundary": - print(f"WordBoundary: {chunk}") - - -if __name__ == "__main__": - asyncio.run(amain()) diff --git a/examples/basic_sync_generation.py b/examples/sync_audio_gen_with_predefined_voice.py similarity index 78% rename from examples/basic_sync_generation.py rename to examples/sync_audio_gen_with_predefined_voice.py index e6785bd..09914dd 100644 --- a/examples/basic_sync_generation.py +++ b/examples/sync_audio_gen_with_predefined_voice.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 -""" -Basic example of edge_tts usage in synchronous function -""" +"""Sync variant of the example for generating audio with a predefined voice""" + import edge_tts diff --git a/examples/sync_audio_generation_in_async_context.py b/examples/sync_audio_generation_in_async_context.py deleted file mode 100644 index d159514..0000000 --- a/examples/sync_audio_generation_in_async_context.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/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() diff --git a/examples/sync_audio_stream_in_async_context.py b/examples/sync_audio_stream_in_async_context.py deleted file mode 100644 index 54c20a7..0000000 --- a/examples/sync_audio_stream_in_async_context.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/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() diff --git a/examples/basic_sync_audio_streaming.py b/examples/sync_audio_streaming_with_predefined_voice_subtitles.py similarity index 57% rename from examples/basic_sync_audio_streaming.py rename to examples/sync_audio_streaming_with_predefined_voice_subtitles.py index c906b85..8ef0a5a 100644 --- a/examples/basic_sync_audio_streaming.py +++ b/examples/sync_audio_streaming_with_predefined_voice_subtitles.py @@ -1,26 +1,30 @@ #!/usr/bin/env python3 -""" -Basic audio streaming example for sync interface - -""" +"""Sync variant of the async .stream() method to +get audio chunks and feed them to SubMaker to +generate subtitles""" import edge_tts TEXT = "Hello World!" VOICE = "en-GB-SoniaNeural" OUTPUT_FILE = "test.mp3" +SRT_FILE = "test.srt" def main() -> None: - """Main function to process audio and metadata synchronously.""" + """Main function""" communicate = edge_tts.Communicate(TEXT, VOICE) + submaker = edge_tts.SubMaker() 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}") + submaker.feed(chunk) + + with open(SRT_FILE, "w", encoding="utf-8") as file: + file.write(submaker.get_srt()) if __name__ == "__main__": diff --git a/src/edge_tts/communicate.py b/src/edge_tts/communicate.py index f82fbfd..c053950 100644 --- a/src/edge_tts/communicate.py +++ b/src/edge_tts/communicate.py @@ -518,7 +518,7 @@ async def save( json.dump(message, metadata) metadata.write("\n") - def stream_sync(self) -> Generator[Dict[str, Any], None, None]: + def stream_sync(self) -> Generator[TTSChunk, None, None]: """Synchronous interface for async stream method""" def fetch_async_items(queue: Queue) -> None: # type: ignore diff --git a/src/edge_tts/typing.py b/src/edge_tts/typing.py index cdd2161..68b23f4 100644 --- a/src/edge_tts/typing.py +++ b/src/edge_tts/typing.py @@ -82,3 +82,11 @@ class VoiceManagerVoice(Voice): """Voice data for VoiceManager.""" Language: str + + +class VoiceManagerFind(TypedDict): + """Voice data for VoiceManager.find().""" + + Gender: NotRequired[Literal["Female", "Male"]] + Locale: NotRequired[str] + Language: NotRequired[str] diff --git a/src/edge_tts/voices.py b/src/edge_tts/voices.py index fd544b5..2754788 100644 --- a/src/edge_tts/voices.py +++ b/src/edge_tts/voices.py @@ -7,10 +7,11 @@ import aiohttp import certifi +from typing_extensions import Unpack from .constants import SEC_MS_GEC_VERSION, VOICE_HEADERS, VOICE_LIST from .drm import DRM -from .typing import Voice, VoiceManagerVoice +from .typing import Voice, VoiceManagerFind, VoiceManagerVoice async def __list_voices( @@ -94,7 +95,9 @@ def __init__(self) -> None: self.called_create: bool = False @classmethod - async def create(cls: Any, custom_voices: Optional[List[Voice]] = None) -> Any: + async def create( + cls: Any, custom_voices: Optional[List[Voice]] = None + ) -> "VoicesManager": """ Creates a VoicesManager object and populates it with all available voices. """ @@ -106,7 +109,7 @@ async def create(cls: Any, custom_voices: Optional[List[Voice]] = None) -> Any: self.called_create = True return self - def find(self, **kwargs: Any) -> List[VoiceManagerVoice]: + def find(self, **kwargs: Unpack[VoiceManagerFind]) -> List[VoiceManagerVoice]: """ Finds all matching voices based on the provided attributes. """