-
Notifications
You must be signed in to change notification settings - Fork 34
chore: add elevenlabs example #373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
""" WalkthroughThe changes include updating the Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Program
participant DeepgramAgentClient
participant ElevenLabs
participant AudioFile
User->>Program: Start application
Program->>DeepgramAgentClient: Initialize and connect with settings
DeepgramAgentClient->>Program: Emit event subscriptions
Program->>AudioFile: Fetch audio from URL
loop For each audio chunk
Program->>DeepgramAgentClient: Send audio chunk
end
DeepgramAgentClient->>Program: Emit AudioResponse (with audio data)
Program->>FileSystem: Write/append audio data to .wav file
User->>Program: Press key to stop
Program->>DeepgramAgentClient: Stop connection
Program->>Deepgram: Terminate library
Suggested reviewers
Tip ⚡️ Free AI Code Reviews for VS Code, Cursor, Windsurf
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
.gitignore (1)
252-254
: Redundant ignore pattern –log.txt
is already covered by*.log
Line 66 already ignores any file with a
.log
suffix. Adding a dedicatedlog.txt
pattern does not hurt, but it is unnecessary noise that may lead future contributors to wonder iflog.txt
needs special treatment.If you want to keep it explicit, consider adding a short comment; otherwise feel free to remove the line.
examples/agent/websocket/11_labs/no_mic.csproj (1)
4-8
: Consider enabling warnings-as-errors for example projectsEnabling
TreatWarningsAsErrors
helps catch nullable-reference violations and other issues early, even in sample code.<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup>
This is optional for examples, but it reinforces good habits and prevents demos from drifting into problematic territory.
examples/agent/websocket/11_labs/Program.cs (1)
67-77
: Subscription boiler-plate can be reducedEvery subscription follows the same pattern:
subscribeResult = await agentClient.Subscribe(new EventHandler<OpenResponse>((s,e)=>{ … })); if (!subscribeResult) { Console.WriteLine("Failed …"); return; }Wrapping this into a helper drastically shortens the example and highlights the event handlers themselves:
async Task SubscribeAsync<T>(EventHandler<T> handler, string name) { if (!await agentClient.Subscribe(handler)) throw new InvalidOperationException($"Failed to subscribe to {name}"); } await SubscribeAsync<OpenResponse>((s,e)=>Console.WriteLine($"-->{e.Type}"), nameof(OpenResponse));This keeps the sample readable while still demonstrating the API.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
output_0.wav
is excluded by!**/*.wav
,!**/*.wav
📒 Files selected for processing (4)
.gitignore
(1 hunks)Deepgram/Models/Agent/v2/WebSocket/Header.cs
(0 hunks)examples/agent/websocket/11_labs/Program.cs
(1 hunks)examples/agent/websocket/11_labs/no_mic.csproj
(1 hunks)
💤 Files with no reviewable changes (1)
- Deepgram/Models/Agent/v2/WebSocket/Header.cs
🧰 Additional context used
🧠 Learnings (2)
examples/agent/websocket/11_labs/no_mic.csproj (1)
Learnt from: OoLunar
PR: deepgram/deepgram-dotnet-sdk#352
File: examples/Deepgram.Examples.Manage.Members/Deepgram.Examples.Manage.Members.csproj:4-4
Timestamp: 2024-11-05T22:01:40.632Z
Learning: In the Deepgram .NET SDK, when the main library targets stable .NET versions, it's acceptable for example projects to target newer .NET versions (e.g., .NET 9.0), as newer .NET applications can consume dependencies from older versions.
examples/agent/websocket/11_labs/Program.cs (1)
Learnt from: dvonthenen
PR: deepgram/deepgram-dotnet-sdk#343
File: examples/text-to-speech/websocket/simple/Program.cs:47-108
Timestamp: 2024-10-23T17:23:51.386Z
Learning: Example code in `examples/text-to-speech/websocket/simple/Program.cs` is intended to be simple and does not need to be fully optimized or perfect.
🔇 Additional comments (1)
examples/agent/websocket/11_labs/Program.cs (1)
318-324
: No throttling when streaming audio to the websocket
SendBinaryImmediately
is awaited, but if the server’s receive window is smaller than the 8 KB buffer or network latency spikes, the tight loop can still overwhelm the socket and increase memory usage.Consider inserting a minimal delay or reusing the
DeepgramWsClientOptions.SendDelay
(if available) to pace the upload, or reduce the buffer size to the SDK’s recommended chunk duration (e.g. 20 ms of audio).
subscribeResult = await agentClient.Subscribe(new EventHandler<AudioResponse>((sender, e) => | ||
{ | ||
Console.WriteLine($"----> {e.Type} received"); | ||
|
||
// if the last audio response is more than 5 seconds ago, add a wav header | ||
if (DateTime.Now.Subtract(lastAudioTime).TotalSeconds > 7) | ||
{ | ||
audioFileCount = audioFileCount + 1; // increment the audio file count | ||
|
||
// delete the file if it exists | ||
if (File.Exists($"output_{audioFileCount}.wav")) | ||
{ | ||
File.Delete($"output_{audioFileCount}.wav"); | ||
} | ||
|
||
using (BinaryWriter writer = new BinaryWriter(File.Open($"output_{audioFileCount}.wav", FileMode.Append))) | ||
{ | ||
Console.WriteLine("Adding WAV header to output.wav"); | ||
byte[] wavHeader = new byte[44]; | ||
int sampleRate = 24000; | ||
short bitsPerSample = 16; | ||
short channels = 1; | ||
int byteRate = sampleRate * channels * (bitsPerSample / 8); | ||
short blockAlign = (short)(channels * (bitsPerSample / 8)); | ||
|
||
wavHeader[0] = 0x52; // R | ||
wavHeader[1] = 0x49; // I | ||
wavHeader[2] = 0x46; // F | ||
wavHeader[3] = 0x46; // F | ||
wavHeader[4] = 0x00; // Placeholder for file size (will be updated later) | ||
wavHeader[5] = 0x00; // Placeholder for file size (will be updated later) | ||
wavHeader[6] = 0x00; // Placeholder for file size (will be updated later) | ||
wavHeader[7] = 0x00; // Placeholder for file size (will be updated later) | ||
wavHeader[8] = 0x57; // W | ||
wavHeader[9] = 0x41; // A | ||
wavHeader[10] = 0x56; // V | ||
wavHeader[11] = 0x45; // E | ||
wavHeader[12] = 0x66; // f | ||
wavHeader[13] = 0x6D; // m | ||
wavHeader[14] = 0x74; // t | ||
wavHeader[15] = 0x20; // Space | ||
wavHeader[16] = 0x10; // Subchunk1Size (16 for PCM) | ||
wavHeader[17] = 0x00; // Subchunk1Size | ||
wavHeader[18] = 0x00; // Subchunk1Size | ||
wavHeader[19] = 0x00; // Subchunk1Size | ||
wavHeader[20] = 0x01; // AudioFormat (1 for PCM) | ||
wavHeader[21] = 0x00; // AudioFormat | ||
wavHeader[22] = (byte)channels; // NumChannels | ||
wavHeader[23] = 0x00; // NumChannels | ||
wavHeader[24] = (byte)(sampleRate & 0xFF); // SampleRate | ||
wavHeader[25] = (byte)((sampleRate >> 8) & 0xFF); // SampleRate | ||
wavHeader[26] = (byte)((sampleRate >> 16) & 0xFF); // SampleRate | ||
wavHeader[27] = (byte)((sampleRate >> 24) & 0xFF); // SampleRate | ||
wavHeader[28] = (byte)(byteRate & 0xFF); // ByteRate | ||
wavHeader[29] = (byte)((byteRate >> 8) & 0xFF); // ByteRate | ||
wavHeader[30] = (byte)((byteRate >> 16) & 0xFF); // ByteRate | ||
wavHeader[31] = (byte)((byteRate >> 24) & 0xFF); // ByteRate | ||
wavHeader[32] = (byte)blockAlign; // BlockAlign | ||
wavHeader[33] = 0x00; // BlockAlign | ||
wavHeader[34] = (byte)bitsPerSample; // BitsPerSample | ||
wavHeader[35] = 0x00; // BitsPerSample | ||
wavHeader[36] = 0x64; // d | ||
wavHeader[37] = 0x61; // t | ||
wavHeader[38] = 0x74; // t | ||
wavHeader[39] = 0x61; // a | ||
wavHeader[40] = 0x00; // Placeholder for data chunk size (will be updated later) | ||
wavHeader[41] = 0x00; // Placeholder for data chunk size (will be updated later) | ||
wavHeader[42] = 0x00; // Placeholder for data chunk size (will be updated later) | ||
wavHeader[43] = 0x00; // Placeholder for data chunk size (will be updated later) | ||
|
||
writer.Write(wavHeader); | ||
} | ||
} | ||
|
||
if (e.Stream != null) | ||
{ | ||
using (BinaryWriter writer = new BinaryWriter(File.Open($"output_{audioFileCount}.wav", FileMode.Append))) | ||
{ | ||
writer.Write(e.Stream.ToArray()); | ||
} | ||
} | ||
|
||
// record the last audio time | ||
lastAudioTime = DateTime.Now; | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
First audio chunk is written without a WAV header
audioFileCount
starts at 0
, and a header is only added when more than seven seconds have elapsed since the previous chunk.
If the very first chunk arrives within seven seconds of lastAudioTime
(which is virtually guaranteed), output_0.wav
will begin with raw PCM data and be unplayable.
A minimal fix is to create a new file (and therefore write the header) whenever the target file does not yet exist:
- // if the last audio response is more than 5 seconds ago, add a wav header
- if (DateTime.Now.Subtract(lastAudioTime).TotalSeconds > 7)
+ // Start a new file if it's the first chunk or if >7 s have passed since the
+ // previous chunk. This guarantees that every file begins with a valid header.
+ var needNewFile = !File.Exists($"output_{audioFileCount}.wav") ||
+ DateTime.Now.Subtract(lastAudioTime).TotalSeconds > 7;
+ if (needNewFile)
{
- audioFileCount = audioFileCount + 1; // increment the audio file count
+ audioFileCount++; // move to the next file
You may also want to update the RIFF/Data chunk sizes once the file is closed, or use a helper such as WaveFileWriter
from NAudio to avoid manual header management altogether.
🤖 Prompt for AI Agents (early access)
In examples/agent/websocket/11_labs/Program.cs between lines 78 and 162, the
first audio chunk may be written to output_0.wav without a WAV header because
the header is only added if more than seven seconds have passed since the last
audio time. To fix this, modify the condition to also create a new file and
write the WAV header if the target file does not exist yet, ensuring the header
is always present at the start of a new file. Additionally, consider updating
the RIFF and data chunk sizes after writing is complete or use a library like
NAudio's WaveFileWriter to handle WAV headers automatically.
🛠️ Refactor suggestion
Possible race condition when multiple audio events arrive concurrently
The lastAudioTime
and audioFileCount
variables are shared across event invocations, but no synchronisation is applied.
If two AudioResponse
callbacks overlap (e.g. on a thread pool), they can:
- Attempt to create/delete the same file simultaneously.
- Increment
audioFileCount
twice, skipping a number. - Corrupt the header / interleave writes.
Synchronise access with a simple lock
object:
+private static readonly object _fileLock = new();
...
subscribeResult = await agentClient.Subscribe(new EventHandler<AudioResponse>((sender, e) =>
{
- // existing code …
+ lock (_fileLock)
+ {
+ // existing file-handling code …
+ }
}));
🤖 Prompt for AI Agents (early access)
In examples/agent/websocket/11_labs/Program.cs between lines 78 and 162, the
shared variables lastAudioTime and audioFileCount are accessed concurrently in
the AudioResponse event handler without synchronization, causing potential race
conditions such as simultaneous file access, incorrect increments, and corrupted
writes. To fix this, introduce a private lock object and wrap all accesses and
modifications to lastAudioTime, audioFileCount, and file operations inside a
lock statement using this object to ensure thread-safe execution.
Proposed changes
Types of changes
What types of changes does your code introduce to the community .NET SDK?
Put an
x
in the boxes that applyChecklist
Put an
x
in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.Further comments
Summary by CodeRabbit
New Features
Chores
.gitignore
to exclude.wav
files from version control.Refactor
Header
record previously used for WebSocket headers.