Skip to content

Add streamAudio method for real-time audio streaming - #12

Open
huynhquangtoan wants to merge 1 commit into
SchneeHertz:masterfrom
huynhquangtoan:master
Open

Add streamAudio method for real-time audio streaming#12
huynhquangtoan wants to merge 1 commit into
SchneeHertz:masterfrom
huynhquangtoan:master

Conversation

@huynhquangtoan

Copy link
Copy Markdown

Add streamAudio() for SSE / real-time streaming

Summary

This PR adds a new method streamAudio(text) that streams TTS audio as it is received from the Edge WebSocket, without writing to a file. It is intended for servers that need to send audio over SSE (e.g. speech.audio.delta / speech.audio.done) or other real-time streaming use cases.

What changed

  • New API: streamAudio(text: string): AsyncGenerator<Buffer>

    • Async generator that yields each binary audio chunk when the WebSocket receives it.
    • Stream ends when: Path:turn.end is received, constructor timeout is reached, or the WebSocket errors.
    • Chunk format: raw audio Buffer (payload after Path:audio\r\n in the binary message).
  • Existing behavior: ttsPromise(text, audioPath) is unchanged and still writes to a file.

  • Docs: README updated with a "streamAudio (streaming / SSE)" section: usage, chunk format, and example with for await.

Why

  • Use case: Backends that need to stream TTS to the client in real time (e.g. OpenAI-compatible stream_format: "sse") can consume streamAudio() and forward chunks as SSE events instead of buffering and then sending a file.
  • Compatibility: No breaking changes; existing ttsPromise usage stays the same.

Example

const tts = new EdgeTTS({ voice: 'en-US-AriaNeural', lang: 'en-US' })

for await (const chunk of tts.streamAudio('Hello world')) {
  process.stdout.write(chunk)  // or send via SSE
}

@SchneeHertz

Copy link
Copy Markdown
Owner

Could you provide an example demonstrating how to play audio using a streamAudio output buffer?

@huynhquangtoan

Copy link
Copy Markdown
Author

This is how I’m using streamAudio(text), alongside the existing file‑based API:

app.post("/v1/audio/speech", async (req, res) => {
  const input = req.body?.input ?? req.body?.text;
  
...
  const tts = new EdgeTTS({
    voice: edgeVoice,
    lang: "en-US",
    saveSubtitles: false,
    rate,
    timeout: 30000,
  });

  try {
    if (streamFormat === "sse") {
      res.setHeader("Content-Type", "text/event-stream");
      res.setHeader("Cache-Control", "no-cache");
      res.setHeader("Connection", "keep-alive");
      res.flushHeaders && res.flushHeaders();

      for await (const chunk of tts.streamAudio(input)) {
        const b64 = chunk.toString("base64");
        res.write(
          `event: speech.audio.delta\ndata: ${JSON.stringify({ delta: b64 })}\n\n`,
        );
      }
      res.write(`event: speech.audio.done\ndata: {}\n\n`);
      res.end();
    } else {
      const tmpFile = path.join(
        os.tmpdir(),
        `edge-tts-${Date.now()}-${Math.random().toString(36).slice(2)}.mp3`,
      );
      try {
        await tts.ttsPromise(input, tmpFile);
        const buf = await fs.readFile(tmpFile);
        res.type("audio/mpeg").send(buf);
      } finally {
        fs.unlink(tmpFile).catch(() => {});
      }
    }
  } catch (err) {
    const message = err.message || "TTS failed";
    return sendError(res, 502, "tts_failed", message.slice(0, 200));
  }
});

Use case explanation

  • This endpoint is an OpenAI‑style /v1/audio/speech API that supports both:
    • Non‑streaming responses: when stream_format is not "sse", it calls ttsPromise(text, file) and returns a single MP3 file in the HTTP response.
    • Streaming (SSE) responses: when stream_format: "sse", it uses streamAudio(text) to get a stream of Buffer chunks from Edge TTS and sends them as server‑sent events:
      • Each chunk is base64‑encoded and sent as an event speech.audio.delta with { delta: "<base64>" }.
      • When the TTS stream finishes, it sends a final speech.audio.done event.
  • This allows a client that already speaks the OpenAI audio API / SSE protocol to:
    • Either download a full MP3 file (non‑stream),
    • Or receive chunks in real time and start playing audio immediately while the rest is still being generated.

@SchneeHertz

Copy link
Copy Markdown
Owner

Okay, actually I was hoping you could provide a test, but that's okay, I'll add it later.
I see the subtitle processing isn't finished yet. Do you want to continue adding subtitle functionality for streamAudio?

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.

2 participants