Skip to content

fix(sse): iterate async TTS generator from sync context without async io.run; return proper SSE chunks - #35

Open
DanTulovsky wants to merge 5 commits into
travisvn:mainfrom
DanTulovsky:fix/sse-async-generator
Open

fix(sse): iterate async TTS generator from sync context without async io.run; return proper SSE chunks#35
DanTulovsky wants to merge 5 commits into
travisvn:mainfrom
DanTulovsky:fix/sse-async-generator

Conversation

@DanTulovsky

Copy link
Copy Markdown

This is a fix for: #34

Full disclosure, fix provided by GPT5. It does seem to fix the error though. It said:

So the exact failure is: asyncio.run expects a coroutine object, but it was given an async generator (_generate_audio_stream(...)), producing “a coroutine was expected, got <async_generator ...>”, which your SSE wrapper then emits as a JSON error.

Status: Reviewed app/tts_handler.py and app/server.py and identified a type mismatch between an async generator and asyncio.run as the cause of the SSE error.

Cause: asyncio.run(_generate_audio_stream(...)) where _generate_audio_stream is an async generator.
Effect: SSE path emits {"type":"error","error":"a coroutine was expected, got <async_generator ...>"}.
The non-SSE path works because it runs a proper coroutine (_generate_audio) via asyncio.run.```

Before:

❯ curl -X POST http://localhost:5050/v1/audio/speech
-H "Content-Type: application/json"
-H "Authorization: Bearer your_api_key_here"
-d '{
"model": "tts-1",
"input": "This will stream as Server-Sent Events with JSON data containing base64-encoded audio chunks.",
"voice": "alloy",
"stream_format": "sse"
}'
data: {"type": "error", "error": "a coroutine was expected, got <async_generator object _generate_audio_stream at 0x2aaaae3a7450>"}


After:

❯ curl -X POST http://localhost:5050/v1/audio/speech
-H "Content-Type: application/json"
-H "Authorization: Bearer your_api_key_here"
-d '{
"model": "tts-1",
"input": "This will stream as Server-Sent Events with JSON data containing base64-encoded audio chunks.",
"voice": "alloy",
"stream_format": "sse"
}'
data: {"type": "speech.audio.delta", "audio": "//NkxAAAAANIAAAAAExBTUVVVVUJUpHz0MCZbJZCAIWIDA8LoAIg6d3phCbs8RRfXd4Bi+xNCSwAL3N4hV3PrvEU65EHFuaRHOIUQnRETriz8OBmUT3fiImiITz6l/SgZvu7nER+IhJuifXcy6f5Qk656JHghE3dyrn/C+U0LPQjQOOW504II/gAocXEqV3M//NkxHwAAANIAAAAACDBz/+mEI4INEPhE0pBB7soU7RZIzHRAZc0ngE1yFEZa0sP8fJbzKxWElxhqdvRCVHEv4W2XoTpcWQ3yq47fVlNRUelrrJm6WlaPD05bYfNkZqmTH5y9DeKxYgocHaFLXRmd2NL8Edm2FBgWGI3U7JcP/M4zxZE02rSHixQmRa2YQKX//NkxP8iG/H0FGGGbORLWJDynunjRwirAssflthdiZuzlDlhpmGNTZwuawpeu8YEvzNe4V9NjiRLxYWmW1607Kjh4tJG/eD6RrJZXQsJ2s9swbOS0neq1VCYbhp5/5mlPx4YopiS2WXIQpetsPtrjEtRnrnwbFvrIarG165epebqNNUylpOhI3RJnwtiXZ1t//NkxPk6VDn0CsPYUCykisiSDbIoygabAaCg6ZHaJJ6ZVsRyOxUyGCSxjvR1O3uYHmpN2DMVTSkB/YylWaQ5xqifJHacyWPBE0pPRHfKGFuXZcEMn6s9D3RZ+nV1lGL3sCb21vhpm5dLnUM59SsmfUkyNnxPpVYgMRkKltwwomm6YOQOR2LZ2mo2BMyntT2r//NkxJIrXCIQNHqMgZ5Voyfg/E93GaZAghLcnw1zG5EQhJrwozQR4ZVPCKo7rBsIFisTQabxhcjCBo0TCUzp+aFGgbMknJrIT8klpqR8vM+KGJHUGc7uEl2uLMj4x2tkpGZkz6YKOWRlTXJiKq2RdO0RD/jRjKwlcjIiJ6TsTMwixQaKM1cOpA4aL/GRzagP"}

data: {"type": "speech.audio.delta", "audio": "//NkxGckNBoMAMJGXTVTJYb2rSunnv65otiHDMqMYJSI6hvbHpq68bDDMeXK0sI1Rp0XUzQEVZMrYmib2pZqor+VunW8c9uDZGMoLuGqO+uRWYZssQKmHpF20DNiPkbc3omGFpUlveuTr4u8EWeSO5kRPRgbucnLCyWeRkSGmh2FgyJjgT8oTXvpWsnSZtiK//NkxFkiOmoQAMJGtbdWF5EZrAu+A93gCV2vpWNafZ2rJv00Xu69zjQ/3VWcy7ncVDqkR3b49EJpJyfWNjBYHBIEMIBoyswDp08NDEcwyEAoIAMIBwBGDgIgBGgM3JMSHWdt/jL5qvHr1er0PAcEAGgsKuAwaHJJQSA4HAlig/QFiDQ+BoDcF5ZY0O3U6DxK//NkxFMt4+IYAVxAAeQhlMKCg3HC558pYy5u5yUQx4FEqrey3tKmOd5sodUcPaTLkGUgyv7fIRK9rWhpf46bnlEMP4pYRBTnt9h3on8dW93dR9xwQnVb9zJ8vT8IiTBby98/c39W+/Zdlx70O058/WpKwZVQp/v+aISb7OCajQv/PqsDX+gtNRKkT09AYYjD//NkxB4k7DqYAZhoAJzb9zdBiMbAkAXseH/dBheEzJA8J2E4/3ZDkiThxl9FJf/6t0EDcuFpMIAwo8is3//W6akDRP5RLxqOYeI7xMBzlIlGNf////9AlyTN3OGhuXy+ShSDkDwHcMj///+yC3fb/5cNx2E0oF0tPpIoskZMxrTWkaWBSCIAT+OOoaFQigx1//NkxA0doba9k9lgAGzKV5Ekp2phhc1utJWmRTH/xp7/zVpZTrQlRXz+sL7LKLC2BA8BmXCQnLiowWP8/A4s1dEVz8z42LDR4xTr5Sm/2zOzPTefpX7z0NuMLOc4uj/qbSLqNbLnxjF1qGqcGf1UAH96KVNZstXkgN8BfPvVX9tiAIcZfugXyaImEiNERtUI"}

etugrand pushed a commit to etugrand/openai-edge-tts that referenced this pull request Jun 20, 2026
…avisvn#38) into v1

- travisvn#35: fix generate_speech_stream — drive the async generator from a
  dedicated event loop instead of asyncio.run() (which raised "a coroutine
  was expected, got async_generator" and broke the SSE path, issue travisvn#34)
- travisvn#56: add correct pcm ffmpeg maps (pcm_s16le / s16le) to the file-based
  _generate_audio path; skip bitrate for pcm like wav
- travisvn#39: introduce HOST env var (default 0.0.0.0) so non-Docker installs can
  bind localhost; wired into waitress.serve()
- travisvn#38: make /models OpenAI-compatible ({"object":"list","data":[...]} with
  object/owned_by on each model)

Skipped travisvn#45 (v1 already has /health, HEALTHCHECK, curl, EXPOSE) and travisvn#59
(opt-in 60db.ai paid backend — out of scope for v1's free edge-tts focus).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant