diff --git a/agents/nextjs/quickstart/example/app/page.tsx b/agents/nextjs/quickstart/example/app/page.tsx
index 9c399555..b4debd8e 100644
--- a/agents/nextjs/quickstart/example/app/page.tsx
+++ b/agents/nextjs/quickstart/example/app/page.tsx
@@ -37,7 +37,7 @@ type VoiceAgentPageProps = {
setSessionError: (value: string | null) => void;
setStarting: (value: boolean) => void;
starting: boolean;
- nextLineId: RefObject;
+ nextLineIdRef: RefObject;
};
type ConversationMessage = {
@@ -88,7 +88,7 @@ function VoiceAgentPage({
createError,
creating,
lines,
- nextLineId,
+ nextLineIdRef,
sessionError,
setAgentIdInput,
setAgentLookupError,
@@ -167,7 +167,7 @@ function VoiceAgentPage({
}
setStarting(true);
- nextLineId.current = 0;
+ nextLineIdRef.current = 0;
setLines([]);
try {
@@ -458,7 +458,7 @@ export default function Home() {
createError={createError}
creating={creating}
lines={lines}
- nextLineId={nextLineId}
+ nextLineIdRef={nextLineId}
sessionError={sessionError}
setAgentIdInput={setAgentIdInput}
setAgentLookupError={setAgentLookupError}
diff --git a/dubbing/nextjs/quickstart/PROMPT.md b/dubbing/nextjs/quickstart/PROMPT.md
index af287078..5dd95393 100644
--- a/dubbing/nextjs/quickstart/PROMPT.md
+++ b/dubbing/nextjs/quickstart/PROMPT.md
@@ -1,4 +1,4 @@
-Before writing any code, invoke the `/text-to-speech` skill to learn the correct ElevenLabs SDK patterns.
+Before writing any code, invoke the `/dubbing` skill to learn the correct ElevenLabs SDK patterns.
This example uses the Dubbing Projects API (dubbing v2): a **project** holds the source media and its transcript, and each **language target** produces one dubbed output in a single language.
diff --git a/dubbing/nextjs/quickstart/example/app/page.tsx b/dubbing/nextjs/quickstart/example/app/page.tsx
index d3a8c116..584fa8d7 100644
--- a/dubbing/nextjs/quickstart/example/app/page.tsx
+++ b/dubbing/nextjs/quickstart/example/app/page.tsx
@@ -64,11 +64,14 @@ export default function Home() {
const recordStartRef = useRef(0);
useEffect(() => {
- return () => {
- if (originalUrl) URL.revokeObjectURL(originalUrl);
- if (dubbedUrl) URL.revokeObjectURL(dubbedUrl);
- };
- }, [originalUrl, dubbedUrl]);
+ if (!originalUrl) return;
+ return () => URL.revokeObjectURL(originalUrl);
+ }, [originalUrl]);
+
+ useEffect(() => {
+ if (!dubbedUrl) return;
+ return () => URL.revokeObjectURL(dubbedUrl);
+ }, [dubbedUrl]);
const stopTimer = () => {
if (timerRef.current) {
@@ -79,15 +82,9 @@ export default function Home() {
const startRecording = async () => {
setError(null);
- setOriginalUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return null;
- });
+ setOriginalUrl(null);
setWavFile(null);
- setDubbedUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return null;
- });
+ setDubbedUrl(null);
setProjectId(null);
try {
@@ -122,10 +119,7 @@ export default function Home() {
setPhase("preparing");
try {
const wav = await recordedBlobToWavFile(rawBlob);
- setOriginalUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return URL.createObjectURL(wav);
- });
+ setOriginalUrl(URL.createObjectURL(wav));
setWavFile(wav);
setPhase("idle");
} catch (e) {
@@ -175,13 +169,10 @@ export default function Home() {
const startDubbing = async () => {
if (!wavFile) return;
setError(null);
- setDubbedUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return null;
- });
+ setDubbedUrl(null);
setPhase("polling");
setProjectId(null);
- setPollNote(null);
+ setPollNote("Transcribing your recording…");
const body = new FormData();
body.set("audio", wavFile);
@@ -264,10 +255,7 @@ export default function Home() {
}
const blob = await audioRes.blob();
if (cancelled) return;
- setDubbedUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return URL.createObjectURL(blob);
- });
+ setDubbedUrl(URL.createObjectURL(blob));
setPhase("ready");
} catch (e) {
if (!cancelled) {
@@ -320,7 +308,7 @@ export default function Home() {
case "preparing":
return "Preparing audio…";
case "polling":
- return pollNote ?? "Dubbing in progress… checking every 5s.";
+ return pollNote ?? "Transcribing your recording…";
case "ready":
return "Dubbing complete.";
case "error":
@@ -448,7 +436,7 @@ export default function Home() {
)}
onClick={startDubbing}
>
- {phase === "polling" ? "Dubbing…" : "Dub recording"}
+ {phase === "polling" ? "Dubbing…" : "Dub Recording"}
) : null}
diff --git a/scripts/generate-examples.sh b/scripts/generate-examples.sh
index 8aa80ed3..25ff50ac 100644
--- a/scripts/generate-examples.sh
+++ b/scripts/generate-examples.sh
@@ -110,6 +110,45 @@ run_with_timeout() {
wait "${cmd_pid}"
}
+example_needs_dependencies() {
+ local example_dir="$1"
+
+ if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
+ return 0
+ fi
+ if [[ -f "${example_dir}/requirements.txt" && ! -d "${example_dir}/.venv" ]]; then
+ return 0
+ fi
+ return 1
+}
+
+# Mirrors the install step at the end of each setup.sh. Partial installs are
+# removed so example_needs_dependencies stays accurate for the prompt preamble.
+install_example_dependencies() {
+ (
+ cd "$1" || exit 1
+ if [[ -f package.json && ! -d node_modules ]]; then
+ if ! pnpm install --config.confirmModulesPurge=false; then
+ rm -rf node_modules
+ exit 1
+ fi
+ fi
+ if [[ -f requirements.txt && ! -d .venv ]]; then
+ if command -v uv >/dev/null 2>&1; then
+ uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
+ else
+ python3 -m venv .venv &&
+ .venv/bin/pip install --upgrade pip &&
+ .venv/bin/pip install -r requirements.txt
+ fi
+ if [[ $? -ne 0 ]]; then
+ rm -rf .venv
+ exit 1
+ fi
+ fi
+ )
+}
+
format_duration() {
local total_seconds="$1"
@@ -354,24 +393,45 @@ for prompt_file in "${PROMPT_FILES[@]}"; do
printf "[%s/%s] %s\n" "${CURRENT_PROMPT}" "${TOTAL_PROMPTS}" "${relative_project_dir}"
fi
- # Run setup script if present, but skip if example/ already exists
+ # Run setup script if present, but skip if example/ already exists.
+ # A skipped setup still needs dependencies installed for the agent to verify against.
if [[ -f "${project_dir}/setup.sh" ]]; then
+ setup_cmd=()
+ dependency_install_only=0
if [[ -d "${project_dir}/example" ]]; then
if (( VERBOSE_OUTPUT == 1 )); then
echo "Skipping setup (example/ already exists): ${relative_project_dir}/setup.sh"
fi
+ if example_needs_dependencies "${project_dir}/example"; then
+ if (( VERBOSE_OUTPUT == 1 )); then
+ echo "Installing dependencies: ${relative_project_dir}/example"
+ fi
+ setup_cmd=(install_example_dependencies "${project_dir}/example")
+ dependency_install_only=1
+ fi
else
if (( VERBOSE_OUTPUT == 1 )); then
echo "Running setup: ${relative_project_dir}/setup.sh"
fi
+ setup_cmd=(bash "${project_dir}/setup.sh")
+ fi
+ if (( ${#setup_cmd[@]} > 0 )); then
if (( VERBOSE_OUTPUT == 1 )); then
- bash "${project_dir}/setup.sh"
+ "${setup_cmd[@]}"
SETUP_EXIT=$?
else
- setup_output="$(bash "${project_dir}/setup.sh" 2>&1)"
+ setup_output="$("${setup_cmd[@]}" 2>&1)"
SETUP_EXIT=$?
fi
- if [[ ${SETUP_EXIT} -ne 0 ]]; then
+ if [[ ${SETUP_EXIT} -ne 0 && ${dependency_install_only} -eq 1 ]]; then
+ # The agent can still verify without dependencies, just more slowly.
+ if (( VERBOSE_OUTPUT == 1 )); then
+ echo " ! Dependency install failed; continuing without dependencies" >&2
+ else
+ echo " ! Dependency install failed; continuing without dependencies" >&2
+ printf "%s\n" "${setup_output}" | tail -n 5 >&2
+ fi
+ elif [[ ${SETUP_EXIT} -ne 0 ]]; then
FAILED_RUNS=$((FAILED_RUNS + 1))
if (( VERBOSE_OUTPUT == 1 )); then
echo " ✗ Setup failed" >&2
@@ -398,16 +458,35 @@ for prompt_file in "${PROMPT_FILES[@]}"; do
preamble+="IMPORTANT: Check the existing code in example/ first. Only make changes if the requirements below are not already met. If everything is already implemented correctly, confirm that no changes are needed."$'\n\n'
fi
if [[ -f "setup.sh" ]]; then
- setup_detail="Prerequisite: \`setup.sh\` has already been run. \`example/\` is ready with dependencies installed"
+ dependencies_ready=1
+ if [[ -d "example" ]] && example_needs_dependencies "example"; then
+ dependencies_ready=0
+ fi
+ setup_items=()
+ if (( dependencies_ready == 1 )); then
+ setup_items+=("dependencies installed")
+ fi
if [[ -d "assets" ]]; then
asset_list="$(ls -1 assets/ 2>/dev/null | paste -sd ', ' -)"
if [[ -n "${asset_list}" ]]; then
- setup_detail+=" and sample assets (${asset_list})"
+ setup_items+=("sample assets (${asset_list})")
fi
fi
- preamble+="${setup_detail}."$'\n\n'
+ setup_detail="Prerequisite: \`setup.sh\` has already been run. \`example/\` is ready"
+ if (( ${#setup_items[@]} == 2 )); then
+ setup_detail+=" with ${setup_items[0]} and ${setup_items[1]}"
+ elif (( ${#setup_items[@]} == 1 )); then
+ setup_detail+=" with ${setup_items[0]}"
+ fi
+ preamble+="${setup_detail}."
+ if (( dependencies_ready == 1 )); then
+ preamble+=" Check SDK signatures against the packages installed in \`example/\` instead of searching the web or other directories."
+ else
+ preamble+=" Its dependencies are not installed."
+ fi
+ preamble+=$'\n\n'
fi
- preamble+="Implement in \`example/\` only."
+ preamble+="Implement in \`example/\` only. Verify with typecheck and lint only; do not start dev servers, run browsers, or install extra tooling, because this run has a fixed time limit."
if [[ -f "${REPO_ROOT}/DESIGN.md" ]]; then
preamble+=$'\n\n'"Read \`DESIGN.md\` at the repo root for styling rules. Preserve any existing template header and layout container."
fi
diff --git a/sound-effects/typescript/quickstart/example/index.ts b/sound-effects/typescript/quickstart/example/index.ts
index 46614eb8..fc089c21 100644
--- a/sound-effects/typescript/quickstart/example/index.ts
+++ b/sound-effects/typescript/quickstart/example/index.ts
@@ -3,6 +3,7 @@ import { ElevenLabsClient, ElevenLabsError } from "@elevenlabs/elevenlabs-js";
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
+import type { ReadableStream as NodeReadableStream } from "node:stream/web";
const DEFAULT_PROMPT = "Cinematic Braam, Horror";
const OUTPUT_FILE = "output.mp3";
@@ -25,7 +26,10 @@ async function main(): Promise {
const client = new ElevenLabsClient();
const audio = await client.textToSoundEffects.convert({ text });
- const source = Readable.fromWeb(audio);
+ // The SDK returns the DOM ReadableStream type, which Node's typings reject.
+ const source = Readable.fromWeb(
+ audio as unknown as NodeReadableStream
+ );
await pipeline(source, createWriteStream(OUTPUT_FILE));
console.log(`Sound effect saved to ${OUTPUT_FILE}`);
diff --git a/speech-to-text/nextjs/realtime/example/app/page.tsx b/speech-to-text/nextjs/realtime/example/app/page.tsx
index 992acccb..f55689e3 100644
--- a/speech-to-text/nextjs/realtime/example/app/page.tsx
+++ b/speech-to-text/nextjs/realtime/example/app/page.tsx
@@ -1,13 +1,25 @@
"use client";
-import { useState, useCallback, useEffect } from "react";
+import { useState, useCallback, useRef } from "react";
import { useScribe, CommitStrategy } from "@elevenlabs/react";
import { LiveWaveform } from "@/components/ui/live-waveform";
+type CommittedEntry = {
+ id: number;
+ text: string;
+};
+
+function errorMessage(err: unknown, fallback: string): string {
+ return err instanceof Error && err.message ? err.message : fallback;
+}
+
export default function Home() {
const [error, setError] = useState(null);
const [partialTranscript, setPartialTranscript] = useState("");
- const [committedHistory, setCommittedHistory] = useState([]);
+ const [committedHistory, setCommittedHistory] = useState(
+ []
+ );
+ const nextEntryIdRef = useRef(0);
const scribe = useScribe({
modelId: "scribe_v2_realtime",
@@ -18,43 +30,58 @@ export default function Home() {
setPartialTranscript(data.text || "");
},
onCommittedTranscript: data => {
- if (data.text && data.text.trim()) {
- setCommittedHistory(prev => [data.text, ...prev]);
+ const text = data.text?.trim();
+ if (text) {
+ const id = nextEntryIdRef.current++;
+ setCommittedHistory(prev => [{ id, text }, ...prev]);
}
setPartialTranscript("");
},
+ onDisconnect: () => {
+ setPartialTranscript("");
+ },
onError: err => {
console.error("Scribe error:", err);
- setError("Connection error occurred. Please try again.");
+ setPartialTranscript("");
+ setError(
+ errorMessage(err, "Connection error occurred. Please try again.")
+ );
},
});
- useEffect(() => {
- if (scribe.status === "disconnected" || scribe.status === "error") {
- setPartialTranscript("");
- }
- }, [scribe.status]);
-
- // Check both connected and transcribing states to properly show active status
+ // Status moves to "transcribing" while speech is processed, so both states mean the session is live.
const isActive =
scribe.status === "connected" || scribe.status === "transcribing";
const isConnecting = scribe.status === "connecting";
+ const statusLabel =
+ scribe.status === "connecting"
+ ? "Connecting"
+ : scribe.status === "transcribing"
+ ? "Transcribing"
+ : scribe.status === "connected"
+ ? "Connected"
+ : scribe.status === "error"
+ ? "Error"
+ : "Disconnected";
+
const handleStart = useCallback(async () => {
try {
setError(null);
setPartialTranscript("");
- // Fetch a fresh single-use token from our API
+ // Tokens are single-use, so fetch a fresh one for every session.
const response = await fetch("/api/scribe-token");
- if (!response.ok) {
- throw new Error("Failed to get transcription token");
+ const body = (await response.json().catch(() => null)) as {
+ token?: string;
+ error?: string;
+ } | null;
+ if (!response.ok || !body?.token) {
+ throw new Error(body?.error || "Failed to get transcription token");
}
- const { token } = await response.json();
- // Connect with microphone access
await scribe.connect({
- token,
+ token: body.token,
microphone: {
echoCancellation: true,
noiseSuppression: true,
@@ -64,7 +91,10 @@ export default function Home() {
} catch (err) {
console.error("Failed to start transcription:", err);
setError(
- "Failed to start transcription. Please check your permissions and try again."
+ errorMessage(
+ err,
+ "Failed to start transcription. Please check your permissions and try again."
+ )
);
}
}, [scribe]);
@@ -98,106 +128,76 @@ export default function Home() {
-
- {/* Controls */}
-
-
+
+
+
+ {committedHistory.length > 0 ? (
- {committedHistory.length > 0 && (
-
- )}
-
-
- {isActive ? (
-
-
- {scribe.status === "transcribing"
- ? "Transcribing"
- : "Connected"}
-
- ) : (
- Disconnected
- )}
-
+ ) : null}
+
{statusLabel}
- {/* Error message */}
- {error && (
-
- )}
-
- {/* Live waveform */}
-
-
-
-
- {/* Partial transcript */}
- {(isActive || partialTranscript) && (
+
+ ) : null}
+
+
+
+ {isActive || partialTranscript ? (
-
- Live Transcript
-
-
-
- {partialTranscript || (
- Listening...
- )}
-
-
+
Live
+
+ {partialTranscript || "Listening…"}
+
- )}
-
- {/* Committed transcript history */}
- {committedHistory.length > 0 && (
-
-
- History
-
-
- {committedHistory.map((text, index) => (
-
- {text}
-
+ ) : null}
+
+ {committedHistory.length > 0 ? (
+
+
History
+
+ {committedHistory.map(entry => (
+
+ {entry.text}
+
))}
- )}
+ ) : null}
- {/* Instructions when not started */}
- {!isActive && !isConnecting && committedHistory.length === 0 && (
-
- Click "Start" to begin transcribing audio from your microphone.
-
- )}
+ {!isActive && !isConnecting && committedHistory.length === 0 ? (
+
+ Click Start to transcribe audio from your microphone.
+
+ ) : null}
diff --git a/text-to-speech/typescript/quickstart/example/index.ts b/text-to-speech/typescript/quickstart/example/index.ts
index b618f584..4f716584 100644
--- a/text-to-speech/typescript/quickstart/example/index.ts
+++ b/text-to-speech/typescript/quickstart/example/index.ts
@@ -16,7 +16,7 @@ try {
});
// Collect all chunks into a buffer
- const chunks: Buffer[] = [];
+ const chunks: Uint8Array[] = [];
for await (const chunk of audio) {
chunks.push(chunk);
}
diff --git a/voice-changer/nextjs/quickstart/PROMPT.md b/voice-changer/nextjs/quickstart/PROMPT.md
index f1ee591e..73b3cfa0 100644
--- a/voice-changer/nextjs/quickstart/PROMPT.md
+++ b/voice-changer/nextjs/quickstart/PROMPT.md
@@ -1,4 +1,4 @@
-Before writing any code, invoke the `/text-to-speech` skill to learn the correct ElevenLabs SDK patterns.
+Before writing any code, invoke the `/voice-changer` skill to learn the correct ElevenLabs SDK patterns.
## 1. `app/api/voice-changer/route.ts`
diff --git a/voice-changer/nextjs/quickstart/example/app/page.tsx b/voice-changer/nextjs/quickstart/example/app/page.tsx
index bed49d39..93bad713 100644
--- a/voice-changer/nextjs/quickstart/example/app/page.tsx
+++ b/voice-changer/nextjs/quickstart/example/app/page.tsx
@@ -29,14 +29,14 @@ export default function Home() {
const [originalUrl, setOriginalUrl] = useState
(null);
const [isConverting, setIsConverting] = useState(false);
- const [convertedBlob, setConvertedBlob] = useState(null);
const [convertedUrl, setConvertedUrl] = useState(null);
const [convertError, setConvertError] = useState(null);
const [micError, setMicError] = useState(null);
const recorderRef = useRef(null);
const streamRef = useRef(null);
- const prevVoiceIdRef = useRef(voiceId);
+ // Bumped whenever a conversion result would be stale, so in-flight requests are ignored.
+ const conversionIdRef = useRef(0);
useEffect(() => {
let cancelled = false;
@@ -65,53 +65,33 @@ export default function Home() {
}, []);
useEffect(() => {
- if (!recordingBlob) {
- setOriginalUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return null;
- });
- return;
- }
- const url = URL.createObjectURL(recordingBlob);
- setOriginalUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return url;
- });
- return () => {
- URL.revokeObjectURL(url);
- };
- }, [recordingBlob]);
+ if (!originalUrl) return;
+ return () => URL.revokeObjectURL(originalUrl);
+ }, [originalUrl]);
useEffect(() => {
- if (!convertedBlob) {
- setConvertedUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return null;
- });
- return;
- }
- const url = URL.createObjectURL(convertedBlob);
- setConvertedUrl(prev => {
- if (prev) URL.revokeObjectURL(prev);
- return url;
- });
- return () => {
- URL.revokeObjectURL(url);
- };
- }, [convertedBlob]);
+ if (!convertedUrl) return;
+ return () => URL.revokeObjectURL(convertedUrl);
+ }, [convertedUrl]);
- useEffect(() => {
- if (prevVoiceIdRef.current !== voiceId) {
- prevVoiceIdRef.current = voiceId;
- setConvertedBlob(null);
- setConvertError(null);
- }
- }, [voiceId]);
+ const discardConversion = useCallback(() => {
+ conversionIdRef.current += 1;
+ setConvertedUrl(null);
+ setConvertError(null);
+ setIsConverting(false);
+ }, []);
+
+ const changeVoice = useCallback(
+ (nextVoiceId: string) => {
+ setVoiceId(nextVoiceId);
+ discardConversion();
+ },
+ [discardConversion]
+ );
useEffect(() => {
if (!isRecording) return;
const start = Date.now();
- setRecordingElapsed(0);
const id = window.setInterval(() => {
setRecordingElapsed(Math.floor((Date.now() - start) / 1000));
}, 250);
@@ -147,11 +127,13 @@ export default function Home() {
type: recorder.mimeType || mimeType || "audio/webm",
});
setRecordingBlob(blob);
+ setOriginalUrl(URL.createObjectURL(blob));
setIsRecording(false);
};
recorderRef.current = recorder;
recorder.start();
+ setRecordingElapsed(0);
setIsRecording(true);
} catch (e) {
const denied = e instanceof DOMException && e.name === "NotAllowedError";
@@ -173,16 +155,18 @@ export default function Home() {
}, []);
const recordAgain = useCallback(() => {
+ discardConversion();
setRecordingBlob(null);
- setConvertedBlob(null);
- setConvertError(null);
+ setOriginalUrl(null);
setRecordingElapsed(0);
setMicError(null);
- }, []);
+ }, [discardConversion]);
const convertVoice = useCallback(async () => {
if (!recordingBlob) return;
+ const conversionId = ++conversionIdRef.current;
setIsConverting(true);
+ setConvertedUrl(null);
setConvertError(null);
try {
const file = new File([recordingBlob], "recording.webm", {
@@ -211,12 +195,15 @@ export default function Home() {
}
const blob = await res.blob();
- setConvertedBlob(blob);
+ if (conversionId !== conversionIdRef.current) return;
+ setConvertedUrl(URL.createObjectURL(blob));
} catch (e) {
- setConvertedBlob(null);
+ if (conversionId !== conversionIdRef.current) return;
setConvertError(e instanceof Error ? e.message : "Conversion failed.");
} finally {
- setIsConverting(false);
+ if (conversionId === conversionIdRef.current) {
+ setIsConverting(false);
+ }
}
}, [recordingBlob, voiceId]);
@@ -267,7 +254,7 @@ export default function Home() {
id="voice-select"
disabled={voiceSelectDisabled}
value={voiceId}
- onChange={e => setVoiceId(e.target.value)}
+ onChange={e => changeVoice(e.target.value)}
className="w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm font-medium text-neutral-900 disabled:cursor-not-allowed disabled:opacity-50"
>
{voices.length === 0 ? (
@@ -361,7 +348,7 @@ export default function Home() {
{convertedUrl && !isConverting && (
-
Converted audio
+
Converted Audio