Skip to content

Added 60dB integration#44

Open
manishEMS47 wants to merge 1 commit into
BolajiAyodeji:mainfrom
manishEMS47:main
Open

Added 60dB integration#44
manishEMS47 wants to merge 1 commit into
BolajiAyodeji:mainfrom
manishEMS47:main

Conversation

@manishEMS47

Copy link
Copy Markdown

What I did

Added 60db as a second, interchangeable text-to-speech engine alongside ElevenLabs, wired through a single provider abstraction so
the UI, chat flow, and audio playback stay identical regardless of which engine is active.

  • New provider layer (app/lib/providers/): elevenlabs.ts + sixtydb.ts behind a shared interface (types.ts). 60db calls GET
    /myvoices and POST /tts-synthesize via fetch (Bearer auth) — no new dependency.
  • Speech Engine selector (chatProvider.tsx): switch ElevenLabs ⇄ 60db at runtime; choice saved to localStorage.
  • Provider-aware routes: getVoices(provider, apiKey) and /api/speech now dispatch per engine; 60db's base64 JSON is decoded
    server-side and served as audio/mpeg, so client playback is untouched.
  • Consistency normalization: voices mapped to one NormalizedVoice shape and keyed by voice_id; synthesis settings expressed on a
    single 0–1 scale (rescaled to 60db's 0–100 internally); selected voice auto-reconciles when the engine changes.
  • Third API key: optional 60db key field in settings + SIXTYDB_API_KEY in .env.example; dev uses env key, prod uses the user's key
    (same model as the existing keys).
  • Docs: README "Speech Engines" section (comparison table + usage) and updated file table.

Closes: #

How to test

  1. Add OPENAI_API_KEY and at least one of ELEVENLABS_API_KEY / SIXTYDB_API_KEY to .env.local.
  2. npm install && npm run dev, open http://localhost:3000/chat.
  3. In the Speech Engine dropdown, select ElevenLabs → pick a voice → ask Siri → confirm spoken reply.
  4. Switch the dropdown to 60db → confirm the voice list refreshes and a question produces 60db audio.
  5. Reload the page → confirm engine + voice selection persist.
  6. Build check: npm run build (passes — typecheck + production build clean).

@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the BA Team on Vercel.

A member of the Team first needs to authorize it.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the 60db text-to-speech engine alongside ElevenLabs, abstracting speech synthesis and voice fetching into a unified provider interface and updating the UI to allow runtime switching. Key feedback includes securing the API key inputs by using type="password" instead of a shared isFocused state, wrapping req.json() in a try block to prevent server crashes on malformed requests, splitting a useEffect hook in ChatPage to avoid stale closures, avoiding direct state mutation when sorting voices, and adding an API key fallback for ElevenLabs voice fetching in production.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +155 to +171
<label className="absolute text-sm p-5 text-gray-500" htmlFor="60db-key">
{isFocused ? "" : "60db..."}
</label>
<input
id="60db-key"
name="60db-key"
defaultValue={SIXTYDB_KEY ? JSON.parse(SIXTYDB_KEY) : ""}
onFocus={() => setIsFocused(true)}
onBlur={() => {
setIsFocused(false);
}}
className={`${
isFocused
? `text-white transition ease-in-out duration-700`
: `text-transparent transition ease-in-out`
} w-2/4 lg:w-52 p-4 border-x-2 border-b-2 border-t-0 bg-transparent focus:outline-none focus:border-blue-500`}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The shared isFocused state causes a security and UX issue: focusing on the new 60db-key input reveals the API keys for OpenAI and ElevenLabs because they all share the same isFocused state to toggle text-transparent / text-white classes.

Using type="password" is the standard, secure way to handle API keys. It natively masks the input, supports password managers, and allows using standard HTML placeholders, completely eliminating the need for the custom isFocused state, absolute labels, and complex CSS transitions. (Note: The other API key inputs should also be updated to type="password" for consistency and security).

Suggested change
<label className="absolute text-sm p-5 text-gray-500" htmlFor="60db-key">
{isFocused ? "" : "60db..."}
</label>
<input
id="60db-key"
name="60db-key"
defaultValue={SIXTYDB_KEY ? JSON.parse(SIXTYDB_KEY) : ""}
onFocus={() => setIsFocused(true)}
onBlur={() => {
setIsFocused(false);
}}
className={`${
isFocused
? `text-white transition ease-in-out duration-700`
: `text-transparent transition ease-in-out`
} w-2/4 lg:w-52 p-4 border-x-2 border-b-2 border-t-0 bg-transparent focus:outline-none focus:border-blue-500`}
/>
<input
id="60db-key"
name="60db-key"
type="password"
placeholder="60db API Key"
defaultValue={SIXTYDB_KEY ? JSON.parse(SIXTYDB_KEY) : ""}
className="w-2/4 lg:w-52 p-4 border-x-2 border-b-2 border-t-0 bg-transparent text-white focus:outline-none focus:border-blue-500"
/>

Comment thread app/api/speech/route.ts
Comment on lines 8 to 46
export async function POST(req: Request) {
const { apiKey, message, voice } = await req.json();
const {
provider = "elevenlabs",
apiKey,
message,
voice
}: { provider?: Provider; apiKey?: string; message: string; voice: string } = await req.json();

const elevenlabs = new ElevenLabsClient({
apiKey: isProduction ? apiKey : process.env.ELEVENLABS_API_KEY
});
// Pick the right key: the user's in production, the env key in development.
const key = isProduction
? apiKey
: provider === "sixtydb"
? process.env.SIXTYDB_API_KEY
: process.env.ELEVENLABS_API_KEY;

const options = {
apiKey: key,
text: message,
voice,
stability: DEFAULT_STABILITY,
similarity: DEFAULT_SIMILARITY
};

try {
const audio = await elevenlabs.generate({
voice,
model_id: "eleven_turbo_v2",
voice_settings: { similarity_boost: 0.5, stability: 0.5 },
text: message
// stream: true,
});
const audio =
provider === "sixtydb"
? await sixtyDbSynthesize(options)
: await elevenLabsSynthesize(options);

return new Response(audio as any, {
headers: { "Content-Type": "audio/mpeg" }
});
} catch (error: any) {
console.error(error);
return Response.json(error, { status: error.statusCode });
return Response.json(error?.message ?? "Speech synthesis failed.", {
status: error?.statusCode ?? 500
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Move the request body parsing (await req.json()) inside the try block. If the request is sent with an empty or malformed JSON body, calling req.json() outside the try block will throw an unhandled exception and result in a generic 500 server crash. Wrapping it inside the try block ensures any parsing errors are caught and handled gracefully.

export async function POST(req: Request) {
  try {
    const {
      provider = "elevenlabs",
      apiKey,
      message,
      voice
    }: { provider?: Provider; apiKey?: string; message: string; voice: string } = await req.json();

    // Pick the right key: the user's in production, the env key in development.
    const key = isProduction
      ? apiKey
      : provider === "sixtydb"
        ? process.env.SIXTYDB_API_KEY
        : process.env.ELEVENLABS_API_KEY;

    const options = {
      apiKey: key,
      text: message,
      voice,
      stability: DEFAULT_STABILITY,
      similarity: DEFAULT_SIMILARITY
    };

    const audio =
      provider === "sixtydb"
        ? await sixtyDbSynthesize(options)
        : await elevenLabsSynthesize(options);

    return new Response(audio as any, {
      headers: { "Content-Type": "audio/mpeg" }
    });
  } catch (error: any) {
    console.error(error);
    return Response.json(error?.message ?? "Speech synthesis failed.", {
      status: error?.statusCode ?? 500
    });
  }
}

Comment thread app/chat/page.tsx
Comment on lines 119 to +138
useEffect(() => {
getVoices()
.then((voices) => {
setVoices(voices ?? []);
setVoicesLoading(true);
getVoices(provider, ttsKey)
.then((result) => {
const list = result ?? [];
setVoices(list);
if (list.length === 0) {
setSelectedVoice("");
} else if (!list.some((voice) => voice.voice_id === selectedVoice)) {
setSelectedVoice(list[0].voice_id);
}
})
.catch((error) => {
console.error("Error fetching voices:", error);
});
}, []);
setVoices([]);
})
.finally(() => setVoicesLoading(false));
// selectedVoice is intentionally omitted to avoid a refetch loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider, elevenLabsKey, sixtyDbKey]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Split the voice fetching and selected voice reconciliation into two separate useEffect hooks. This avoids disabling the react-hooks/exhaustive-deps lint rule, prevents potential stale closure bugs, and follows React best practices by separating concerns.

Suggested change
useEffect(() => {
getVoices()
.then((voices) => {
setVoices(voices ?? []);
setVoicesLoading(true);
getVoices(provider, ttsKey)
.then((result) => {
const list = result ?? [];
setVoices(list);
if (list.length === 0) {
setSelectedVoice("");
} else if (!list.some((voice) => voice.voice_id === selectedVoice)) {
setSelectedVoice(list[0].voice_id);
}
})
.catch((error) => {
console.error("Error fetching voices:", error);
});
}, []);
setVoices([]);
})
.finally(() => setVoicesLoading(false));
// selectedVoice is intentionally omitted to avoid a refetch loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider, elevenLabsKey, sixtyDbKey]);
useEffect(() => {
setVoicesLoading(true);
getVoices(provider, ttsKey)
.then((result) => {
setVoices(result ?? []);
})
.catch((error) => {
console.error("Error fetching voices:", error);
setVoices([]);
})
.finally(() => setVoicesLoading(false));
}, [provider, elevenLabsKey, sixtyDbKey]);
useEffect(() => {
if (voices.length === 0) {
setSelectedVoice("");
} else if (!voices.some((voice) => voice.voice_id === selectedVoice)) {
setSelectedVoice(voices[0].voice_id);
}
}, [voices, selectedVoice, setSelectedVoice]);

Comment on lines 21 to +22
voices
.sort((a, b) => a.name!.localeCompare(b.name!))
.sort((a, b) => a.name.localeCompare(b.name))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid mutating React state directly. voices.sort() sorts the array in-place, which mutates the voices prop (which is a state variable in the parent component). Copy the array before sorting it (e.g., using [...voices].sort()).

Suggested change
voices
.sort((a, b) => a.name!.localeCompare(b.name!))
.sort((a, b) => a.name.localeCompare(b.name))
[...voices]
.sort((a, b) => a.name.localeCompare(b.name))

Comment thread app/utils/getVoices.ts
Comment on lines +23 to +25
// ElevenLabs voices are global presets — load them from the env key so
// they're available on first render regardless of mode.
return await getElevenLabsVoices(process.env.ELEVENLABS_API_KEY);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In production, if the developer has not set a global ELEVENLABS_API_KEY on the server, fetching ElevenLabs voices will fail or return empty if it requires authentication. Fall back to the user's provided apiKey to ensure the voice list loads correctly.

Suggested change
// ElevenLabs voices are global presets — load them from the env key so
// they're available on first render regardless of mode.
return await getElevenLabsVoices(process.env.ELEVENLABS_API_KEY);
// ElevenLabs voices are global presets — load them from the env key so
// they're available on first render regardless of mode.
return await getElevenLabsVoices(process.env.ELEVENLABS_API_KEY || apiKey);

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.

1 participant