Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions dubbing/nextjs/quickstart/PROMPT.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
Before writing any code, invoke the `/text-to-speech` 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.

## 1. `app/api/dubbing/route.ts`

Secure POST endpoint that starts a dubbing job from an uploaded recording.
Secure POST endpoint that starts a dubbing project from an uploaded recording.

- Read `ELEVENLABS_API_KEY` from `process.env`. Return 500 if missing.
- Accept `audio` (File), `targetLang` (string), and optional `sourceLang` (string, default `auto`) from request `FormData`.
- Return 400 for missing or invalid audio, or a missing `targetLang`.
- Use `ElevenLabsClient` and call `client.dubbing.create({ file: audio, targetLang, sourceLang: sourceLang === "auto" ? undefined : sourceLang, name: "Browser dubbing demo" })`.
- Read the job id from the SDK response (`dubbingId`) and return JSON `{ dubbingId, expectedDurationSec }`.
- Use `ElevenLabsClient` and call `client.dubbing.project.create({ file: audio, targetLanguage: targetLang, sourceLanguage: sourceLang === "auto" ? undefined : sourceLang, reference: "Browser dubbing demo" })`. The `targetLanguage` shortcut also queues a language target that starts generating automatically once the project finishes transcribing.
- Return JSON `{ projectId, languageId }`, reading `languageId` from `project.languageIds?.[0] ?? null`.
- Wrap failures in readable JSON errors.

## 2. `app/api/dubbing/[dubbingId]/route.ts`
## 2. `app/api/dubbing/[projectId]/route.ts`

Secure GET endpoint that returns dubbing status metadata for polling.
Secure GET endpoint that returns combined project and language status for polling.

- Read and validate `dubbingId` from the route params.
- Call `client.dubbing.get(dubbingId)`.
- Return JSON with `status`, `error`, `sourceLanguage`, and `targetLanguages`.
- Read and validate `projectId` from the route params.
- Call `client.dubbing.project.get(projectId)`. Project statuses are `queued`, `preparing`, `processing`, `ready`, or `failed`.
- If the project has a language target (`project.languageIds?.[0]`), also call `client.dubbing.project.language.get(projectId, languageId)`. Language statuses are `queued`, `processing`, `completed`, `stale`, or `failed`.
- Return JSON with `projectStatus`, `languageId`, and `languageStatus`.
- Keep the response small and friendly for client polling.

## 3. `app/api/dubbing/[dubbingId]/audio/[languageCode]/route.ts`
## 3. `app/api/dubbing/[projectId]/audio/[languageId]/route.ts`

Secure GET endpoint that proxies the dubbed audio file.
Secure GET endpoint that proxies the dubbed audio output.

- Read and validate `dubbingId` and `languageCode` from the route params.
- Call `client.dubbing.audio.get(dubbingId, languageCode)`.
- Collect the returned stream into a `Buffer` and respond with `audio/mpeg`.
- Return readable JSON errors when the dub is not ready or fails.
- Read and validate `projectId` and `languageId` from the route params.
- Call `client.dubbing.project.language.get(projectId, languageId)`. Once the language is `completed`, `outputs.losslessAudio` carries a signed download URL that expires after about an hour; fetching the language again returns a fresh one.
- Return 503 with a readable JSON error if the language is not `completed` yet or has no output URL.
- Fetch the signed URL server-side and stream the body back with the upstream content type (default `audio/wav`).

## 4. `app/page.tsx`

Expand All @@ -38,8 +41,8 @@ In-browser voice recorder and dubbing page.
- After stopping, convert the recorded blob to a WAV `File` in the browser before upload. Do not send raw `audio/webm;codecs=opus` to `/api/dubbing`, because the Dubbing API rejects that content type.
- Show clear states: idle, recording, preparing, polling, ready, and error. While recording, show elapsed time and a pulsing red indicator.
- After recording, show the original audio player plus source-language and target-language selects. Prevent choosing the same explicit source and target language.
- On **Dub Recording**, `POST` `FormData` with the converted WAV file to `/api/dubbing`.
- Poll `/api/dubbing/${dubbingId}` every 5 seconds until the status is `dubbed`; stop early and show the API error if one is returned.
- When ready, fetch `/api/dubbing/${dubbingId}/audio/${targetLang}`, create an object URL, and render a dubbed `<audio>` player with controls plus a download link.
- On **Dub Recording**, `POST` `FormData` with the converted WAV file to `/api/dubbing` and keep the returned `projectId`.
- Poll `/api/dubbing/${projectId}` every 5 seconds. While polling, show "Transcribing your recording…" until `projectStatus` is `ready`, then "Generating the dubbed audio…". Stop with an error if `projectStatus` or `languageStatus` is `failed`.
- When `languageStatus` is `completed`, fetch `/api/dubbing/${projectId}/audio/${languageId}`, create an object URL, and render a dubbed `<audio>` player with controls plus a WAV download link.
- Display inline errors for microphone denial, upload failures, and dubbing failures.
- Keep the UI minimal and easy to scan.
2 changes: 1 addition & 1 deletion dubbing/nextjs/quickstart/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dubbing Recorder (Next.js)

Record your voice in the browser, dub it into another language with the ElevenLabs Dubbing API, and play or download the result.
Record your voice in the browser, dub it into another language with the ElevenLabs Dubbing API (Dubbing Projects), and play or download the result.

## Setup

Expand Down
2 changes: 1 addition & 1 deletion dubbing/nextjs/quickstart/example/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dubbing Recorder (Next.js)

Record your voice in the browser, dub it into another language with the ElevenLabs Dubbing API, and play or download the result.
Record your voice in the browser, dub it into another language with the ElevenLabs Dubbing API (Dubbing Projects), and play or download the result.

## Setup

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ElevenLabsClient, ElevenLabsError } from "@elevenlabs/elevenlabs-js";
import { NextResponse } from "next/server";

function jsonError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}

function isValidId(id: string) {
return /^[a-zA-Z0-9_-]+$/.test(id) && id.length > 0 && id.length <= 128;
}

export async function GET(
_request: Request,
{
params,
}: {
params: Promise<{ projectId: string; languageId: string }>;
}
) {
const apiKey = process.env.ELEVENLABS_API_KEY;
if (!apiKey) {
return jsonError("Server is missing ELEVENLABS_API_KEY.", 500);
}

const { projectId, languageId } = await params;
if (!projectId || !isValidId(projectId)) {
return jsonError("Invalid project id.", 400);
}
if (!languageId || !isValidId(languageId)) {
return jsonError("Invalid language id.", 400);
}

const client = new ElevenLabsClient({ apiKey });

try {
const language = await client.dubbing.project.language.get(
projectId,
languageId
);

// outputs carries a signed download URL once the dub has completed. The
// URL expires after about an hour; fetching the language returns a fresh one.
const outputUrl = language.outputs?.losslessAudio;
if (language.status !== "completed" || !outputUrl) {
return jsonError("Dubbed audio is not ready yet.", 503);
}

const upstream = await fetch(outputUrl);
if (!upstream.ok || !upstream.body) {
return jsonError("Failed to download dubbed audio.", 502);
}

return new NextResponse(upstream.body, {
status: 200,
headers: {
"Content-Type": upstream.headers.get("content-type") ?? "audio/wav",
"Cache-Control": "private, max-age=3600",
},
});
} catch (e) {
if (e instanceof ElevenLabsError) {
const status = e.statusCode ?? 502;
return jsonError(
e.message || "Failed to fetch dubbed audio.",
status >= 400 && status < 600 ? status : 502
);
}
const message =
e instanceof Error ? e.message : "Failed to fetch dubbed audio.";
return jsonError(message, 502);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,37 @@ function isValidId(id: string) {

export async function GET(
_request: Request,
{ params }: { params: Promise<{ dubbingId: string }> }
{ params }: { params: Promise<{ projectId: string }> }
) {
const apiKey = process.env.ELEVENLABS_API_KEY;
if (!apiKey) {
return jsonError("Server is missing ELEVENLABS_API_KEY.", 500);
}

const { dubbingId } = await params;
if (!dubbingId || !isValidId(dubbingId)) {
return jsonError("Invalid dubbing id.", 400);
const { projectId } = await params;
if (!projectId || !isValidId(projectId)) {
return jsonError("Invalid project id.", 400);
}

const client = new ElevenLabsClient({ apiKey });

try {
const meta = await client.dubbing.get(dubbingId);
const project = await client.dubbing.project.get(projectId);
const languageId = project.languageIds?.[0] ?? null;

let languageStatus: string | null = null;
if (languageId) {
const language = await client.dubbing.project.language.get(
projectId,
languageId
);
languageStatus = language.status;
}

return NextResponse.json({
status: meta.status,
error: meta.error ?? null,
sourceLanguage: meta.sourceLanguage ?? null,
targetLanguages: meta.targetLanguages ?? [],
projectStatus: project.status,
languageId,
languageStatus,
});
} catch (e) {
if (e instanceof ElevenLabsError) {
Expand Down
14 changes: 8 additions & 6 deletions dubbing/nextjs/quickstart/example/app/api/dubbing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,18 @@ export async function POST(request: Request) {
const client = new ElevenLabsClient({ apiKey });

try {
const result = await client.dubbing.create({
// The targetLanguage shortcut also queues a language target, which starts
// generating automatically once the project finishes transcribing.
const project = await client.dubbing.project.create({
file: audio,
targetLang,
sourceLang: sourceLang === "auto" ? undefined : sourceLang,
name: "Browser dubbing demo",
targetLanguage: targetLang,
sourceLanguage: sourceLang === "auto" ? undefined : sourceLang,
reference: "Browser dubbing demo",
});

return NextResponse.json({
dubbingId: result.dubbingId,
expectedDurationSec: result.expectedDurationSec,
projectId: project.projectId,
languageId: project.languageIds?.[0] ?? null,
});
} catch (e) {
if (e instanceof ElevenLabsError) {
Expand Down
Loading
Loading