{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "realtime-transcriber-01", "description": "Scribe V2 Realtime Transcriber", "dependencies": [ "@elevenlabs/react", "framer-motion" ], "registryDependencies": [ "badge", "button", "scroll-area", "https://ui.elevenlabs.io/r/shimmering-text.json" ], "files": [ { "path": "blocks/realtime-transcriber-01/page.tsx", "content": "\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport Link from \"next/link\"\nimport { AnimatePresence, motion } from \"framer-motion\"\nimport { Copy } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useDebounce } from \"@/registry/elevenlabs-ui/hooks/use-debounce\"\nimport { usePrevious } from \"@/registry/elevenlabs-ui/hooks/use-previous\"\nimport { useScribe } from \"@/registry/elevenlabs-ui/hooks/use-scribe\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { ShimmeringText } from \"@/components/ui/shimmering-text\"\n\nimport { getScribeToken } from \"./actions/get-scribe-token\"\nimport { LanguageSelector } from \"./components/language-selector\"\n\ninterface RecordingState {\n error: string\n latenciesMs: number[]\n}\n\ntype ConnectionState = \"idle\" | \"connecting\" | \"connected\" | \"disconnecting\"\n\nconst TranscriptCharacter = React.memo(\n ({ char, delay }: { char: string; delay: number }) => {\n return (\n 0 ? \"filter, opacity\" : \"auto\" }}\n >\n {char}\n \n )\n }\n)\nTranscriptCharacter.displayName = \"TranscriptCharacter\"\n\n// Memoize background effects to prevent re-renders\nconst BackgroundAura = React.memo(\n ({ status, isConnected }: { status: string; isConnected: boolean }) => {\n const isActive = status === \"connecting\" || isConnected\n\n return (\n \n {/* Center bottom pool - main glow */}\n \n\n {/* Pulsing layer */}\n \n\n {/* Left corner bloom */}\n \n\n {/* Left rising glow - organic curve */}\n \n\n {/* Right corner bloom */}\n \n\n {/* Right rising glow - organic curve */}\n \n\n {/* Shimmer overlay */}\n \n \n )\n }\n)\nBackgroundAura.displayName = \"BackgroundAura\"\n\n// Memoize bottom controls with comparison function\nconst BottomControls = React.memo(\n ({\n isConnected,\n hasError,\n isMac,\n onStop,\n }: {\n isConnected: boolean\n hasError: boolean\n isMac: boolean\n onStop: () => void\n }) => {\n return (\n \n {isConnected && !hasError && (\n \n \n Stop\n \n {isMac ? \"⌘K\" : \"Ctrl+K\"}\n \n \n \n )}\n \n )\n },\n (prev, next) => {\n if (prev.isConnected !== next.isConnected) return false\n if (prev.hasError !== next.hasError) return false\n if (prev.isMac !== next.isMac) return false\n return true\n }\n)\nBottomControls.displayName = \"BottomControls\"\n\nexport default function RealtimeTranscriber01() {\n const [recording, setRecording] = useState({\n error: \"\",\n latenciesMs: [],\n })\n const [selectedLanguage, setSelectedLanguage] = useState(null)\n const [connectionState, setConnectionStateState] =\n useState(\"idle\")\n const [localTranscript, setLocalTranscript] = useState(\"\")\n\n const [isMac, setIsMac] = useState(true)\n useEffect(() => {\n setIsMac(/(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent))\n }, [])\n\n const segmentStartMsRef = useRef(null)\n const lastTranscriptRef = useRef(\"\")\n const finalTranscriptsRef = useRef([])\n\n const startSoundRef = useRef(null)\n const endSoundRef = useRef(null)\n const errorSoundRef = useRef(null)\n\n const errorTimeoutRef = useRef(null)\n const lastOperationTimeRef = useRef(0)\n const timerIntervalRef = useRef(null)\n const connectionStateRef = useRef(\"idle\")\n\n const updateConnectionState = useCallback(\n (next: ConnectionState) => {\n connectionStateRef.current = next\n setConnectionStateState(next)\n },\n [setConnectionStateState]\n )\n\n const clearSessionRefs = useCallback(() => {\n if (timerIntervalRef.current) {\n clearInterval(timerIntervalRef.current)\n timerIntervalRef.current = null\n }\n if (errorTimeoutRef.current) {\n clearTimeout(errorTimeoutRef.current)\n errorTimeoutRef.current = null\n }\n\n segmentStartMsRef.current = null\n lastTranscriptRef.current = \"\"\n finalTranscriptsRef.current = []\n }, [])\n\n // === Callbacks for Scribe ===\n const onPartialTranscript = useCallback((data: { text?: string }) => {\n // Only process if we're connected\n if (connectionStateRef.current !== \"connected\") return\n\n const currentText = data.text || \"\"\n\n if (currentText === lastTranscriptRef.current) return\n\n lastTranscriptRef.current = currentText\n\n // Update local transcript with partial\n const fullText = finalTranscriptsRef.current.join(\" \")\n const combined = fullText ? `${fullText} ${currentText}` : currentText\n setLocalTranscript(combined)\n\n if (currentText.length > 0 && segmentStartMsRef.current != null) {\n const latency = performance.now() - segmentStartMsRef.current\n setRecording((prev) => ({\n ...prev,\n latenciesMs: [...prev.latenciesMs.slice(-29), latency],\n }))\n segmentStartMsRef.current = null\n }\n }, [])\n\n const onFinalTranscript = useCallback((data: { text?: string }) => {\n // Only process if we're connected\n if (connectionStateRef.current !== \"connected\") return\n\n lastTranscriptRef.current = \"\"\n\n if (data.text && data.text.length > 0) {\n // Add to final transcripts\n finalTranscriptsRef.current = [...finalTranscriptsRef.current, data.text]\n\n // Update local transcript\n setLocalTranscript(finalTranscriptsRef.current.join(\" \"))\n\n if (segmentStartMsRef.current != null) {\n const latency = performance.now() - segmentStartMsRef.current\n setRecording((prev) => ({\n ...prev,\n latenciesMs: [...prev.latenciesMs.slice(-29), latency],\n }))\n }\n }\n segmentStartMsRef.current = null\n }, [])\n\n const onError = useCallback((error: Error | Event) => {\n console.error(\"[Scribe] Error:\", error)\n\n // Ignore errors if we're not supposed to be connected\n if (connectionStateRef.current !== \"connected\") {\n console.log(\"[Scribe] Ignoring error - not connected\")\n return\n }\n\n const errorMessage =\n error instanceof Error ? error.message : \"Transcription error\"\n\n if (errorTimeoutRef.current) {\n clearTimeout(errorTimeoutRef.current)\n }\n\n errorTimeoutRef.current = setTimeout(() => {\n if (connectionStateRef.current !== \"connected\") return\n\n setRecording((prev) => ({\n ...prev,\n error: errorMessage,\n }))\n errorSoundRef.current?.play().catch(() => {})\n }, 500)\n }, [])\n\n const scribeConfig = useMemo(\n () => ({\n modelId: \"scribe_realtime_v2\" as const,\n onPartialTranscript,\n onFinalTranscript,\n onError,\n }),\n [onPartialTranscript, onFinalTranscript, onError]\n )\n\n const scribe = useScribe(scribeConfig)\n\n // Clear transcript when not connected\n useEffect(() => {\n if (connectionState !== \"connected\") {\n setLocalTranscript(\"\")\n }\n }, [connectionState])\n\n // Simulate audio chunk timing for latency measurement\n useEffect(() => {\n // Clear any existing interval\n if (timerIntervalRef.current) {\n clearInterval(timerIntervalRef.current)\n timerIntervalRef.current = null\n }\n\n if (connectionState !== \"connected\") return\n\n timerIntervalRef.current = setInterval(() => {\n if (segmentStartMsRef.current === null) {\n segmentStartMsRef.current = performance.now()\n }\n }, 100)\n\n return () => {\n if (timerIntervalRef.current) {\n clearInterval(timerIntervalRef.current)\n timerIntervalRef.current = null\n }\n }\n }, [connectionState])\n\n const handleToggleRecording = useCallback(async () => {\n const now = Date.now()\n const timeSinceLastOp = now - lastOperationTimeRef.current\n\n // DISCONNECT\n if (connectionState === \"connected\" || connectionState === \"connecting\") {\n console.log(\"[Scribe] Disconnecting...\")\n\n // 1. Update UI state immediately\n updateConnectionState(\"idle\")\n setLocalTranscript(\"\")\n setRecording({ error: \"\", latenciesMs: [] })\n clearSessionRefs()\n\n // 2. Disconnect (async, don't wait)\n try {\n scribe.disconnect()\n scribe.clearTranscripts()\n } catch {\n // Ignore errors\n }\n\n // 3. Play sound\n if (endSoundRef.current) {\n endSoundRef.current.currentTime = 0\n endSoundRef.current.play().catch(() => {})\n }\n\n lastOperationTimeRef.current = now\n return\n }\n\n // Debounce rapid clicks for CONNECT\n if (timeSinceLastOp < 200) {\n console.log(\"[Scribe] Ignoring rapid click\")\n return\n }\n lastOperationTimeRef.current = now\n\n // CONNECT\n if (connectionState !== \"idle\") {\n console.log(\"[Scribe] Not in idle state, ignoring\")\n return\n }\n\n console.log(\"[Scribe] Connecting...\")\n updateConnectionState(\"connecting\")\n setLocalTranscript(\"\")\n setRecording({ error: \"\", latenciesMs: [] })\n clearSessionRefs()\n\n try {\n const result = await getScribeToken()\n\n // Check if user cancelled using ref (gets current value)\n if (connectionStateRef.current === \"idle\") {\n console.log(\"[Scribe] Cancelled during token fetch\")\n return\n }\n\n if (result.error || !result.token) {\n throw new Error(result.error || \"Failed to get token\")\n }\n\n await scribe.connect({\n token: result.token,\n languageCode: selectedLanguage || undefined,\n microphone: {\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: true,\n },\n })\n\n // Check again after connect completes\n if (connectionStateRef.current !== \"connecting\") {\n console.log(\"[Scribe] Cancelled after connection\")\n try {\n scribe.disconnect()\n } catch {\n // Ignore\n }\n return\n }\n\n console.log(\"[Scribe] Connected\")\n updateConnectionState(\"connected\")\n\n // Play start sound\n if (startSoundRef.current) {\n startSoundRef.current.currentTime = 0\n startSoundRef.current.play().catch(() => {})\n }\n } catch (error) {\n console.error(\"[Scribe] Connection error:\", error)\n updateConnectionState(\"idle\")\n setRecording((prev) => ({\n ...prev,\n error: error instanceof Error ? error.message : \"Connection failed\",\n }))\n }\n }, [\n clearSessionRefs,\n connectionState,\n scribe,\n selectedLanguage,\n updateConnectionState,\n ])\n\n // Cmd+K / Ctrl+K shortcut\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (\n e.key === \"k\" &&\n (e.metaKey || e.ctrlKey) &&\n e.target instanceof HTMLElement &&\n ![\"INPUT\", \"TEXTAREA\"].includes(e.target.tagName)\n ) {\n e.preventDefault()\n handleToggleRecording()\n }\n }\n\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown)\n }\n }, [handleToggleRecording])\n\n // Note: No unmount cleanup - React Strict Mode causes issues\n // The browser will handle websocket cleanup on page unload\n\n // Preload audio files on mount (no auto-play)\n useEffect(() => {\n const sounds = [\n {\n ref: startSoundRef,\n url: \"https://ui.elevenlabs.io/sounds/transcriber-start.mp3\",\n },\n {\n ref: endSoundRef,\n url: \"https://ui.elevenlabs.io/sounds/transcriber-end.mp3\",\n },\n {\n ref: errorSoundRef,\n url: \"https://ui.elevenlabs.io/sounds/transcriber-error.mp3\",\n },\n ]\n\n sounds.forEach(({ ref, url }) => {\n const audio = new Audio(url)\n audio.volume = 0.6\n audio.preload = \"auto\"\n audio.load()\n ref.current = audio\n })\n }, [])\n\n // Display text: prefer error, then local transcript\n const displayText = recording.error || localTranscript\n const hasContent = Boolean(displayText) && connectionState === \"connected\"\n\n // Determine if current transcript is partial (for styling)\n const isPartial = Boolean(lastTranscriptRef.current)\n\n return (\n
\n \n\n \n\n
\n {/* Main transcript area */}\n
\n {/* Transcript - shown when there's content */}\n \n {hasContent && (\n \n )}\n
\n\n {/* Status text - shown when no content */}\n \n \n \n
\n \n \n
\n \n\n {/* Language selector and button - only shown when not connected */}\n \n
\n
\n
\n

\n Realtime Speech to Text\n

\n

\n Transcribe your voice in real-time with high accuracy\n

\n
\n\n
\n \n \n
\n\n \n Start Transcribing\n \n \n\n \n \n Powered by ElevenLabs Speech to Text\n \n \n
\n
\n \n \n\n \n \n \n )\n}\n\nconst TranscriberTranscript = React.memo(\n ({\n transcript,\n error,\n isPartial,\n isConnected,\n }: {\n transcript: string\n error: string\n isPartial?: boolean\n isConnected: boolean\n }) => {\n const characters = useMemo(() => transcript.split(\"\"), [transcript])\n const previousNumChars = useDebounce(\n usePrevious(characters.length) || 0,\n 100\n )\n const scrollRef = useRef(null)\n const scrollTimeoutRef = useRef(null)\n\n // Auto-scroll to bottom when connected and text is updating\n useEffect(() => {\n if (isConnected && scrollRef.current) {\n if (scrollTimeoutRef.current) {\n clearTimeout(scrollTimeoutRef.current)\n }\n scrollTimeoutRef.current = setTimeout(() => {\n if (scrollRef.current) {\n scrollRef.current.scrollTop = scrollRef.current.scrollHeight\n }\n }, 50)\n }\n return () => {\n if (scrollTimeoutRef.current) {\n clearTimeout(scrollTimeoutRef.current)\n }\n }\n }, [transcript, isConnected])\n\n return (\n
\n
\n \n \n {characters.map((char, index) => {\n const delay =\n index >= previousNumChars\n ? (index - previousNumChars + 1) * 0.012\n : 0\n return (\n \n )\n })}\n
\n
\n \n {transcript && !error && !isPartial && (\n {\n navigator.clipboard.writeText(transcript)\n }}\n aria-label=\"Copy transcript\"\n >\n \n \n )}\n \n )\n }\n)\nTranscriberTranscript.displayName = \"TranscriberTranscript\"\n", "type": "registry:page", "target": "app/realtime-transcriber-01/page.tsx" }, { "path": "blocks/realtime-transcriber-01/actions/get-scribe-token.ts", "content": "\"use server\"\n\nexport interface ScribeTokenResult {\n token?: string\n error?: string\n}\n\nexport async function getScribeToken(): Promise {\n try {\n const apiKey = process.env.ELEVENLABS_API_KEY\n\n if (!apiKey) {\n return { error: \"Service not configured\" }\n }\n\n const response = await fetch(\n \"https://api.elevenlabs.io/v1/single-use-token/realtime_scribe\",\n {\n method: \"POST\",\n headers: {\n \"xi-api-key\": apiKey,\n },\n }\n )\n\n if (!response.ok) {\n const errorText = await response.text()\n console.error(\"Failed to get Scribe token:\", errorText)\n return { error: \"Failed to get transcription token\" }\n }\n\n const data = await response.json()\n\n if (!data.token) {\n return { error: \"Invalid token response\" }\n }\n\n return { token: data.token }\n } catch (error) {\n console.error(\"Error getting Scribe token:\", error)\n return {\n error: error instanceof Error ? error.message : \"Failed to get token\",\n }\n }\n}\n", "type": "registry:file", "target": "app/realtime-transcriber-01/actions/get-scribe-token.ts" }, { "path": "blocks/realtime-transcriber-01/components/language-selector.tsx", "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { ChevronDown, Globe } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\"\n\ninterface LanguageOption {\n code: string\n name: string\n}\n\nconst LANGUAGE_OPTIONS: LanguageOption[] = [\n { code: \"af\", name: \"Afrikaans\" },\n { code: \"ar\", name: \"Arabic\" },\n { code: \"hy\", name: \"Armenian\" },\n { code: \"az\", name: \"Azerbaijani\" },\n { code: \"be\", name: \"Belarusian\" },\n { code: \"bn\", name: \"Bengali\" },\n { code: \"bs\", name: \"Bosnian\" },\n { code: \"bg\", name: \"Bulgarian\" },\n { code: \"ca\", name: \"Catalan\" },\n { code: \"zh\", name: \"Chinese\" },\n { code: \"hr\", name: \"Croatian\" },\n { code: \"cs\", name: \"Czech\" },\n { code: \"da\", name: \"Danish\" },\n { code: \"nl\", name: \"Dutch\" },\n { code: \"en\", name: \"English\" },\n { code: \"et\", name: \"Estonian\" },\n { code: \"fi\", name: \"Finnish\" },\n { code: \"fr\", name: \"French\" },\n { code: \"gl\", name: \"Galician\" },\n { code: \"ka\", name: \"Georgian\" },\n { code: \"de\", name: \"German\" },\n { code: \"el\", name: \"Greek\" },\n { code: \"gu\", name: \"Gujarati\" },\n { code: \"he\", name: \"Hebrew\" },\n { code: \"hi\", name: \"Hindi\" },\n { code: \"hu\", name: \"Hungarian\" },\n { code: \"is\", name: \"Icelandic\" },\n { code: \"id\", name: \"Indonesian\" },\n { code: \"it\", name: \"Italian\" },\n { code: \"ja\", name: \"Japanese\" },\n { code: \"kn\", name: \"Kannada\" },\n { code: \"kk\", name: \"Kazakh\" },\n { code: \"ko\", name: \"Korean\" },\n { code: \"lv\", name: \"Latvian\" },\n { code: \"lt\", name: \"Lithuanian\" },\n { code: \"mk\", name: \"Macedonian\" },\n { code: \"ms\", name: \"Malay\" },\n { code: \"ml\", name: \"Malayalam\" },\n { code: \"mr\", name: \"Marathi\" },\n { code: \"ne\", name: \"Nepali\" },\n { code: \"no\", name: \"Norwegian\" },\n { code: \"fa\", name: \"Persian\" },\n { code: \"pl\", name: \"Polish\" },\n { code: \"pt\", name: \"Portuguese\" },\n { code: \"ro\", name: \"Romanian\" },\n { code: \"ru\", name: \"Russian\" },\n { code: \"sr\", name: \"Serbian\" },\n { code: \"sk\", name: \"Slovak\" },\n { code: \"sl\", name: \"Slovenian\" },\n { code: \"es\", name: \"Spanish\" },\n { code: \"sw\", name: \"Swahili\" },\n { code: \"sv\", name: \"Swedish\" },\n { code: \"ta\", name: \"Tamil\" },\n { code: \"te\", name: \"Telugu\" },\n { code: \"th\", name: \"Thai\" },\n { code: \"tr\", name: \"Turkish\" },\n { code: \"uk\", name: \"Ukrainian\" },\n { code: \"ur\", name: \"Urdu\" },\n { code: \"vi\", name: \"Vietnamese\" },\n]\n\ninterface LanguageSelectorProps {\n value: string | null\n onValueChange: (code: string | null) => void\n disabled?: boolean\n}\n\nexport function LanguageSelector({\n value,\n onValueChange,\n disabled = false,\n}: LanguageSelectorProps) {\n const [open, setOpen] = useState(false)\n\n const selectedName = value\n ? LANGUAGE_OPTIONS.find((l) => l.code === value)?.name || value\n : \"Auto-detect\"\n\n return (\n \n \n \n \n \n {selectedName}\n \n \n \n \n \n \n \n \n No language found.\n \n {\n onValueChange(null)\n setOpen(false)\n }}\n >\n \n Auto-detect\n \n {LANGUAGE_OPTIONS.map((language) => (\n {\n onValueChange(language.code)\n setOpen(false)\n }}\n >\n {language.name} ({language.code})\n \n ))}\n \n \n \n \n \n )\n}\n", "type": "registry:component", "target": "components/language-selector.tsx" } ], "meta": { "iframeHeight": "800px", "container": "w-full bg-surface min-h-svh flex px-4 py-12 items-center md:py-20 justify-center min-w-0", "mobile": "component" }, "categories": [ "audio" ], "type": "registry:block" }