Disciplines · Training

Video Tutorial 2: Streaming and Real-Time AI

1.

3sections3 minread

On this page

Duration: 12-15 minutes Format: Screen recording with voiceover Target Audience: Developers building real-time AI applications


Video Script#

Opening (0:00 - 0:30)#

[Screen: Title card]

Welcome back to the Oshun AI Integrations series. In this video, we'll explore streaming - a technique that dramatically improves user experience by delivering AI outputs in real-time instead of waiting for complete results.

[Screen: Before/After comparison]

Here's the difference: without streaming, users wait several seconds staring at a loading spinner. With streaming, they start receiving output immediately.


Part 1: Why Streaming? (0:30 - 2:00)#

[Screen: Diagram showing traditional vs streaming]

text
Traditional:
[Request] ──────────────── [Wait 5s] ────────────────▶ [Complete Response]

Streaming:
[Request] ─▶ [Chunk] ─▶ [Chunk] ─▶ [Chunk] ─▶ [Chunk] ─▶ [Complete]
            50ms      100ms      150ms      200ms       ...

In traditional synthesis, the entire audio is generated on the server before any data is sent. With streaming, audio chunks are sent as soon as they're ready.

This matters especially for:

  • Voice assistants that need to feel responsive
  • Long-form content where users shouldn't wait
  • Interactive applications like chatbots

Part 2: Basic Streaming Setup (2:00 - 5:00)#

[Screen: Code editor]

Let's implement streaming audio synthesis. First, we need to start a streaming session.

typescript
const session = await elevenlabs.startStreamingSession({
  voiceId: 'EXAVITQu4vr4xnSDxMaL',
  modelId: 'eleven_flash_v2_5', // Optimized for streaming
  optimizeStreamingLatency: 4,   // Maximum optimization
});

console.log(`Session started: ${session.sessionId}`);

Notice we're using eleven_flash_v2_5 - this model is specifically optimized for low-latency streaming.

[Screen: Add event handlers]

Now we set up event handlers to receive audio chunks.

typescript
const chunks: Buffer[] = [];

session.on('audio', (chunk: Buffer) => {
  chunks.push(chunk);
  console.log(`Received chunk: ${chunk.length} bytes`);
});

session.on('end', () => {
  const totalAudio = Buffer.concat(chunks);
  console.log(`Total: ${totalAudio.length} bytes`);
});

session.on('error', (error: Error) => {
  console.error('Stream error:', error.message);
});

[Screen: Send text]

Finally, we send our text and wait for the stream to complete.

typescript
await session.sendText('Hello! This is streaming audio synthesis.');
await session.flush();
await session.close();

[Screen: Run and show output]

bash
npx ts-node streaming-demo.ts
# Session started: sess_abc123
# Received chunk: 4096 bytes
# Received chunk: 4096 bytes
# Received chunk: 4096 bytes
# ...
# Total: 45632 bytes

Notice how chunks arrive continuously instead of all at once!


Part 3: Real-Time Playback (5:00 - 8:00)#

[Screen: Audio buffer concept]

In a real application, you'd play audio chunks as they arrive. Let's build a simple audio buffer to manage this.

typescript
class AudioBuffer {
  private chunks: Buffer[] = [];
  private totalBytes = 0;

  addChunk(chunk: Buffer): void {
    this.chunks.push(chunk);
    this.totalBytes += chunk.length;
  }

  // Estimate duration (44100Hz, 16-bit, mono)
  getEstimatedDuration(): number {
    return this.totalBytes / (44100 * 2);
  }

  getBuffer(): Buffer {
    return Buffer.concat(this.chunks);
  }
}

[Screen: Use with streaming]

typescript
const buffer = new AudioBuffer();

session.on('audio', (chunk) => {
  buffer.addChunk(chunk);
  console.log(`Duration so far: ${buffer.getEstimatedDuration().toFixed(2)}s`);

  // In a real app, you'd send this chunk to an audio player
  playAudioChunk(chunk);
});

[Screen: Timeline visualization]

Here's what happens over time:

text
Time:  0ms   100ms  200ms  300ms  400ms  500ms ...
       │      │      │      │      │      │
Audio: ■──────■──────■──────■──────■──────■──────
       │      └──────┴──────┴──────┴──────┘
       │              Playing as received
       └── First chunk starts playing almost immediately

Part 4: Interactive Conversations (8:00 - 11:00)#

[Screen: Conversation concept]

Streaming really shines in interactive applications. Let's build a simple voice assistant that responds to multiple messages.

typescript
async function interactiveAssistant(responses: string[]): Promise<void> {
  const session = await elevenlabs.startStreamingSession({
    voiceId: 'EXAVITQu4vr4xnSDxMaL',
    modelId: 'eleven_flash_v2_5',
    optimizeStreamingLatency: 4,
  });

  for (const response of responses) {
    console.log(`Speaking: "${response}"`);

    await session.sendText(response);
    await session.flush(); // Wait for this response to complete

    // Simulate thinking time between responses
    await new Promise(r => setTimeout(r, 500));
  }

  await session.close();
}

[Screen: Demo the assistant]

typescript
await interactiveAssistant([
  'Hello! Welcome to our AI assistant.',
  'How can I help you today?',
  'I can answer questions about many topics.',
]);

[Screen: Run and show]

Watch how each response starts playing almost immediately, creating a natural conversational flow.

[Screen: Integration with LLM]

In practice, you'd combine this with a language model. As the LLM generates tokens, you stream them to the TTS:

typescript
// Pseudo-code for LLM integration
llm.on('token', (token) => {
  textBuffer += token;

  // Send complete sentences to TTS
  if (token.match(/[.!?]/)) {
    session.sendText(textBuffer);
    textBuffer = '';
  }
});

Part 5: Best Practices (11:00 - 13:00)#

[Screen: Tips slide]

Here are some best practices for streaming:

1. Use the right model eleven_flash_v2_5 is optimized for streaming. Use it unless you need features from other models.

2. Set optimization level The optimizeStreamingLatency option ranges from 0 to 4. Higher values reduce latency but may affect quality slightly.

3. Handle errors gracefully Streams can disconnect. Always have error handlers:

typescript
session.on('error', (error) => {
  console.error('Stream error:', error);
  // Attempt reconnection or fallback
});

4. Clean up resources Always close sessions when done:

typescript
try {
  // Use the session...
} finally {
  await session.close();
}

5. Consider buffering Buffer a few chunks before playing to handle network jitter:

typescript
const MIN_BUFFER = 3; // chunks
let playbackStarted = false;

session.on('audio', (chunk) => {
  buffer.addChunk(chunk);

  if (!playbackStarted && buffer.chunkCount >= MIN_BUFFER) {
    startPlayback();
    playbackStarted = true;
  }
});

Closing (13:00 - 14:00)#

[Screen: Summary]

To recap, streaming enables:

  • Near-instant response times
  • Better user experience
  • More efficient memory usage
  • Natural conversation flows

The key points are:

  • Use streaming-optimized models
  • Handle chunks as they arrive
  • Implement proper error handling
  • Clean up resources

[Screen: Next steps]

In the next video, we'll cover batch processing and how to efficiently handle multiple AI requests. See you there!


B-Roll Suggestions#

  • Side-by-side comparison of loading vs streaming
  • Timeline animation showing chunks arriving
  • Real-time console output as chunks arrive
  • Audio waveform building up progressively
  • Chat interface with instant responses

Notes for Recording#

  1. Emphasize the responsiveness difference
  2. Show real latency numbers when possible
  3. Demonstrate actual audio playing
  4. Keep explanations practical and actionable