build strategy · voice

Real voice, one key, one build.

Every mega-prompt in this repo uses the same pattern, because it's the only pattern that lets a Lovable account ship a streaming ElevenLabs demo in one shot.

Why ElevenLabs and not a stock browser voice?

The browser's built-in speechSynthesis sounds like 2007. ElevenLabs gives you broadcast-grade voices, multilingual models, low-latency turbo streaming, conversational agents over WebRTC, realtime scribe transcripts with word timestamps, and on-demand music + sound effects — all behind one HTTP API and one secret. That is the difference between a demo and a presentation that actually opens with sound.

The recipe

recipe
# 1. In your Lovable project, add the single required secret (Settings -> Secrets):
ELEVENLABS_API_KEY=sk_...        # https://elevenlabs.io/app/settings/api-keys

# 2. Copy a mega-prompt from this repo into Lovable. One paste:
#    - scaffolds the React + TanStack Start app
#    - writes a server function that proxies ElevenLabs (TTS / agents / scribe / music)
#    - wires the client hook (useConversation / useScribe / <audio>) for the chosen kernel
#    - includes the hackathon credit in the footer and in JSDoc on the server fn

# 3. Hit play. Your demo is streaming real voice from ElevenLabs.

1. The TTS server function

Every prompt in the library follows the same shape: keep the API key on the server with a TanStack createServerFn, forward the body to ElevenLabs, return base64 audio (or pipe the SSE stream) to the client.

src/lib/tts.functions.ts
// src/lib/tts.functions.ts — TanStack server function that streams ElevenLabs TTS
// Built during the Creative AI & Quantum Hackathon — StreetKode Fam · Indian Krump Festival 14
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";

export const speak = createServerFn({ method: "POST" })
  .inputValidator((d) => z.object({ text: z.string().min(1).max(4000) }).parse(d))
  .handler(async ({ data }) => {
    const r = await fetch(
      "https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream?output_format=mp3_44100_128",
      {
        method: "POST",
        headers: {
          "xi-api-key": process.env.ELEVENLABS_API_KEY!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text: data.text, model_id: "eleven_turbo_v2_5" }),
      },
    );
    if (!r.ok) throw new Error(`TTS failed: ${r.status}`);
    const buf = await r.arrayBuffer();
    return { audio: Buffer.from(buf).toString("base64") };
  });

2. Conversational agent on the client

src/components/voice-agent.tsx
// src/components/voice-agent.tsx — useConversation over WebRTC
import { useConversation } from "@elevenlabs/react";
import { useServerFn } from "@tanstack/react-start";
import { mintAgentToken } from "@/lib/agent.functions";

export function VoiceAgent({ agentId }: { agentId: string }) {
  const getToken = useServerFn(mintAgentToken);
  const c = useConversation();
  const start = async () => {
    await navigator.mediaDevices.getUserMedia({ audio: true });
    const { token } = await getToken({ data: { agentId } });
    await c.startSession({ conversationToken: token, connectionType: "webrtc" });
  };
  return c.status === "connected"
    ? <button onClick={() => c.endSession()}>End conversation</button>
    : <button onClick={start}>Start conversation</button>;
}

3. Music & SFX on demand

src/lib/music.functions.ts
// src/lib/music.functions.ts — generate music or a sound effect on demand
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";

export const conjureSfx = createServerFn({ method: "POST" })
  .inputValidator((d) => z.object({ prompt: z.string(), seconds: z.number().min(0.5).max(22).optional() }).parse(d))
  .handler(async ({ data }) => {
    const r = await fetch("https://api.elevenlabs.io/v1/sound-generation", {
      method: "POST",
      headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY!, "Content-Type": "application/json" },
      body: JSON.stringify({ text: data.prompt, duration_seconds: data.seconds ?? 5, prompt_influence: 0.3 }),
    });
    const buf = await r.arrayBuffer();
    return { audio: Buffer.from(buf).toString("base64") };
  });

Hackathon rules of thumb

  • · One mega-prompt = one build message. Don't iterate the architecture, iterate the UI.
  • · Keep the API key on the server. Browsers do not touch xi-api-key.
  • · Stream by default. eleven_turbo_v2_5 for chat-speed playback, eleven_multilingual_v2 when quality matters more than latency.
  • · Always show a microphone permission UX before opening realtime hooks, or browsers will silently block them.
  • · Add a "Built during the Creative AI & Quantum Hackathon — StreetKode Fam · Indian Krump Festival 14" line to your footer.