From 4bc848fee9ee3533dfc118acd45809dafb7cdee4 Mon Sep 17 00:00:00 2001 From: MrWetsnow <509887+DanTulovsky@users.noreply.github.com> Date: Sun, 10 Aug 2025 10:58:05 -0400 Subject: [PATCH 1/5] fix(sse): iterate async TTS generator from sync context without asyncio.run; return proper SSE chunks --- app/tts_handler.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/app/tts_handler.py b/app/tts_handler.py index f9acf39..fecd81e 100644 --- a/app/tts_handler.py +++ b/app/tts_handler.py @@ -46,17 +46,17 @@ async def _generate_audio_stream(text, voice, speed): """Generate streaming TTS audio using edge-tts.""" # Determine if the voice is an OpenAI-compatible voice or a direct edge-tts voice edge_tts_voice = voice_mapping.get(voice, voice) # Use mapping if in OpenAI names, otherwise use as-is - + # Convert speed to SSML rate format try: speed_rate = speed_to_rate(speed) # Convert speed value to "+X%" or "-X%" except Exception as e: print(f"Error converting speed: {e}. Defaulting to +0%.") speed_rate = "+0%" - + # Create the communicator for streaming communicator = edge_tts.Communicate(text=text, voice=edge_tts_voice, rate=speed_rate) - + # Stream the audio data async for chunk in communicator.stream(): if chunk["type"] == "audio": @@ -64,7 +64,25 @@ async def _generate_audio_stream(text, voice, speed): def generate_speech_stream(text, voice, speed=1.0): """Generate streaming speech audio (synchronous wrapper).""" - return asyncio.run(_generate_audio_stream(text, voice, speed)) + # Drive the async generator from a dedicated event loop and yield chunks synchronously + async_generator = _generate_audio_stream(text, voice, speed) + loop = asyncio.new_event_loop() + try: + asyncio.set_event_loop(loop) + while True: + try: + next_chunk = loop.run_until_complete(async_generator.__anext__()) + except StopAsyncIteration: + break + yield next_chunk + finally: + # Best-effort cleanup of async generators and loop + try: + loop.run_until_complete(loop.shutdown_asyncgens()) + except Exception: + pass + asyncio.set_event_loop(None) + loop.close() async def _generate_audio(text, voice, response_format, speed): """Generate TTS audio and optionally convert to a different format.""" @@ -137,7 +155,7 @@ async def _generate_audio(text, voice, response_format, speed): Path(converted_path).unlink(missing_ok=True) # Clean up the original mp3 file as well, since conversion failed Path(temp_mp3_path).unlink(missing_ok=True) - + if DETAILED_ERROR_LOGGING: error_message = f"FFmpeg error during audio conversion. Command: '{' '.join(e.cmd)}'. Stderr: {e.stderr.decode('utf-8', 'ignore')}" print(error_message) # Log for server-side diagnosis @@ -179,10 +197,10 @@ def get_voices(language=None): def speed_to_rate(speed: float) -> str: """ Converts a multiplicative speed value to the edge-tts "rate" format. - + Args: speed (float): The multiplicative speed value (e.g., 1.5 for +50%, 0.5 for -50%). - + Returns: str: The formatted "rate" string (e.g., "+50%" or "-50%"). """ From 637305a767d270df15ee2de320a55276dc254162 Mon Sep 17 00:00:00 2001 From: MrWetsnow <509887+DanTulovsky@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:22:17 -0400 Subject: [PATCH 2/5] chore(task): add Taskfile for docker build and push (mrwetsnow/openai-edge-tts) on 2025-08-12 --- Taskfile.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Taskfile.yml diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..cef17b5 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,27 @@ +version: '3' + +# Task runner configuration for building and pushing Docker images +# Requires: https://taskfile.dev + +vars: + # Docker image repository (override with: IMAGE=yourname/yourrepo task ...) + IMAGE: + sh: 'echo ${IMAGE:-mrwetsnow/openai-edge-tts}' + # Image tag (override with: TAG=v1 task ...) + TAG: + sh: 'echo ${TAG:-latest}' + # Whether to include ffmpeg in the image build + INSTALL_FFMPEG: + sh: 'echo ${INSTALL_FFMPEG:-false}' + +tasks: + build-push: + desc: Build Docker image and push to Docker Hub + env: + DOCKER_BUILDKIT: '1' + cmds: + - docker build --pull --no-cache -t {{.IMAGE}}:{{.TAG}} --build-arg INSTALL_FFMPEG={{.INSTALL_FFMPEG}} . + - docker push {{.IMAGE}}:{{.TAG}} + silent: false + + From d106ab38b0beb2e72129150459088249915dea85 Mon Sep 17 00:00:00 2001 From: MrWetsnow <509887+DanTulovsky@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:26:29 -0400 Subject: [PATCH 3/5] chore(task): auto-generate date tag; accept TAGS/TAG; always tag + push latest --- Taskfile.yml | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index cef17b5..3399272 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,9 +7,24 @@ vars: # Docker image repository (override with: IMAGE=yourname/yourrepo task ...) IMAGE: sh: 'echo ${IMAGE:-mrwetsnow/openai-edge-tts}' - # Image tag (override with: TAG=v1 task ...) - TAG: - sh: 'echo ${TAG:-latest}' + # Date-based tag used when no tag is provided + DATE_TAG: + sh: 'date +%Y-%m-%d' + # All tags to apply: use TAGS (space/comma-separated) or TAG if provided; otherwise fallback to DATE_TAG. Always include 'latest'. + ALL_TAGS: + sh: | + set -e + # Prefer TAGS, then TAG; support comma or space separated values + if [ -n "${TAGS:-}" ]; then + INPUT_TAGS="${TAGS}" + elif [ -n "${TAG:-}" ]; then + INPUT_TAGS="${TAG}" + else + INPUT_TAGS="{{.DATE_TAG}}" + fi + # Normalize commas to spaces + INPUT_TAGS=$(echo "$INPUT_TAGS" | tr ',' ' ') + echo "$INPUT_TAGS latest" # Whether to include ffmpeg in the image build INSTALL_FFMPEG: sh: 'echo ${INSTALL_FFMPEG:-false}' @@ -20,8 +35,17 @@ tasks: env: DOCKER_BUILDKIT: '1' cmds: - - docker build --pull --no-cache -t {{.IMAGE}}:{{.TAG}} --build-arg INSTALL_FFMPEG={{.INSTALL_FFMPEG}} . - - docker push {{.IMAGE}}:{{.TAG}} + - | + bash -ce ' + tags="{{.ALL_TAGS}}" + tag_args="" + for t in $tags; do + tag_args="$tag_args -t {{.IMAGE}}:$t" + done + docker build --pull --no-cache $tag_args --build-arg INSTALL_FFMPEG={{.INSTALL_FFMPEG}} . + for t in $tags; do + docker push {{.IMAGE}}:$t + done' silent: false From dbd2199a764844ee71d4d4cf22c237d53f881955 Mon Sep 17 00:00:00 2001 From: MrWetsnow <509887+DanTulovsky@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:28:09 -0400 Subject: [PATCH 4/5] chore(task): use UTC datetime (YYYY-MM-DD-HHMMSS) for default tag to allow multiple pushes per day --- Taskfile.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 3399272..7d8b9a9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,9 +7,9 @@ vars: # Docker image repository (override with: IMAGE=yourname/yourrepo task ...) IMAGE: sh: 'echo ${IMAGE:-mrwetsnow/openai-edge-tts}' - # Date-based tag used when no tag is provided + # Date-time tag (UTC) used when no tag is provided, supports multiple pushes per day DATE_TAG: - sh: 'date +%Y-%m-%d' + sh: 'date -u +%Y-%m-%d-%H%M%S' # All tags to apply: use TAGS (space/comma-separated) or TAG if provided; otherwise fallback to DATE_TAG. Always include 'latest'. ALL_TAGS: sh: | From c861c9b2276b5981d5e2bad71a6442466b9c215d Mon Sep 17 00:00:00 2001 From: MrWetsnow <509887+DanTulovsky@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:28:38 -0400 Subject: [PATCH 5/5] chore(task): default tag minute-level UTC (YYYY-MM-DD-HHMM) --- Taskfile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index 7d8b9a9..f195ac9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -9,7 +9,7 @@ vars: sh: 'echo ${IMAGE:-mrwetsnow/openai-edge-tts}' # Date-time tag (UTC) used when no tag is provided, supports multiple pushes per day DATE_TAG: - sh: 'date -u +%Y-%m-%d-%H%M%S' + sh: 'date -u +%Y-%m-%d-%H%M' # All tags to apply: use TAGS (space/comma-separated) or TAG if provided; otherwise fallback to DATE_TAG. Always include 'latest'. ALL_TAGS: sh: |