{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-scribe", "dependencies": [ "@elevenlabs/client" ], "files": [ { "path": "hooks/use-scribe.ts", "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { RealtimeEvents, Scribe } from \"@elevenlabs/client\"\nimport type {\n AudioFormat,\n AudioOptions,\n CommitStrategy,\n CommittedTranscriptMessage,\n CommittedTranscriptWithTimestampsMessage,\n MicrophoneOptions,\n PartialTranscriptMessage,\n RealtimeConnection,\n ScribeAuthErrorMessage,\n ScribeChunkSizeExceededErrorMessage,\n ScribeCommitThrottledErrorMessage,\n ScribeErrorMessage,\n ScribeInputErrorMessage,\n ScribeInsufficientAudioActivityErrorMessage,\n ScribeQueueOverflowErrorMessage,\n ScribeQuotaExceededErrorMessage,\n ScribeRateLimitedErrorMessage,\n ScribeResourceExhaustedErrorMessage,\n ScribeSessionTimeLimitExceededErrorMessage,\n ScribeTranscriberErrorMessage,\n ScribeUnacceptedTermsErrorMessage,\n} from \"@elevenlabs/client\"\n\n// ============= Types =============\n\nexport type ScribeStatus =\n | \"disconnected\"\n | \"connecting\"\n | \"connected\"\n | \"transcribing\"\n | \"error\"\n\nexport interface TranscriptSegment {\n id: string\n text: string\n timestamp: number\n isFinal: boolean\n}\n\nexport interface ScribeCallbacks {\n onSessionStarted?: () => void\n onPartialTranscript?: (data: { text: string }) => void\n onCommittedTranscript?: (data: { text: string }) => void\n onCommittedTranscriptWithTimestamps?: (data: {\n text: string\n timestamps?: { start: number; end: number }[]\n }) => void\n /** Called for any error (also called when specific error callbacks fire) */\n onError?: (error: Error | Event) => void\n onAuthError?: (data: { error: string }) => void\n onQuotaExceededError?: (data: { error: string }) => void\n onCommitThrottledError?: (data: { error: string }) => void\n onTranscriberError?: (data: { error: string }) => void\n onUnacceptedTermsError?: (data: { error: string }) => void\n onRateLimitedError?: (data: { error: string }) => void\n onInputError?: (data: { error: string }) => void\n onQueueOverflowError?: (data: { error: string }) => void\n onResourceExhaustedError?: (data: { error: string }) => void\n onSessionTimeLimitExceededError?: (data: { error: string }) => void\n onChunkSizeExceededError?: (data: { error: string }) => void\n onInsufficientAudioActivityError?: (data: { error: string }) => void\n\n onConnect?: () => void\n onDisconnect?: () => void\n}\n\nexport interface ScribeHookOptions extends ScribeCallbacks {\n // Connection options\n token?: string\n modelId?: string\n baseUri?: string\n\n // VAD options\n commitStrategy?: CommitStrategy\n vadSilenceThresholdSecs?: number\n vadThreshold?: number\n minSpeechDurationMs?: number\n minSilenceDurationMs?: number\n languageCode?: string\n\n // Microphone options (for automatic microphone mode)\n microphone?: {\n deviceId?: string\n echoCancellation?: boolean\n noiseSuppression?: boolean\n autoGainControl?: boolean\n channelCount?: number\n }\n\n // Manual audio options\n audioFormat?: AudioFormat\n sampleRate?: number\n\n // Auto-connect on mount\n autoConnect?: boolean\n\n // Include timestamps\n includeTimestamps?: boolean\n}\n\nexport interface UseScribeReturn {\n // State\n status: ScribeStatus\n isConnected: boolean\n isTranscribing: boolean\n partialTranscript: string\n committedTranscripts: TranscriptSegment[]\n error: string | null\n\n // Connection methods\n connect: (options?: Partial) => Promise\n disconnect: () => void\n\n // Audio methods (for manual mode)\n sendAudio: (\n audioBase64: string,\n options?: { commit?: boolean; sampleRate?: number; previousText?: string }\n ) => void\n commit: () => void\n\n // Utility methods\n clearTranscripts: () => void\n getConnection: () => RealtimeConnection | null\n}\n\n// ============= Hook Implementation =============\n\nexport function useScribe(options: ScribeHookOptions = {}): UseScribeReturn {\n const {\n // Callbacks\n onSessionStarted,\n onPartialTranscript,\n onCommittedTranscript,\n onCommittedTranscriptWithTimestamps,\n onError,\n onAuthError,\n onQuotaExceededError,\n onCommitThrottledError,\n onTranscriberError,\n onUnacceptedTermsError,\n onRateLimitedError,\n onInputError,\n onQueueOverflowError,\n onResourceExhaustedError,\n onSessionTimeLimitExceededError,\n onChunkSizeExceededError,\n onInsufficientAudioActivityError,\n onConnect,\n onDisconnect,\n\n // Connection options\n token: defaultToken,\n modelId: defaultModelId,\n baseUri: defaultBaseUri,\n commitStrategy: defaultCommitStrategy,\n vadSilenceThresholdSecs: defaultVadSilenceThresholdSecs,\n vadThreshold: defaultVadThreshold,\n minSpeechDurationMs: defaultMinSpeechDurationMs,\n minSilenceDurationMs: defaultMinSilenceDurationMs,\n languageCode: defaultLanguageCode,\n\n // Mode options\n microphone: defaultMicrophone,\n audioFormat: defaultAudioFormat,\n sampleRate: defaultSampleRate,\n\n // Auto-connect\n autoConnect = false,\n } = options\n\n const connectionRef = useRef(null)\n const connectionIdCounterRef = useRef(0)\n const activeConnectionIdRef = useRef(null)\n\n const [status, setStatus] = useState(\"disconnected\")\n const [partialTranscript, setPartialTranscript] = useState(\"\")\n const [committedTranscripts, setCommittedTranscripts] = useState<\n TranscriptSegment[]\n >([])\n const [error, setError] = useState(null)\n\n const disconnect = useCallback(() => {\n const connection = connectionRef.current\n if (!connection) {\n setStatus(\"disconnected\")\n activeConnectionIdRef.current = null\n return\n }\n\n activeConnectionIdRef.current = null\n connectionRef.current = null\n\n try {\n const result = connection.close()\n if (\n typeof result === \"object\" &&\n result !== null &&\n \"catch\" in result &&\n typeof (result as Promise).catch === \"function\"\n ) {\n const promise = result as Promise\n promise.catch(() => {\n /* noop */\n })\n }\n } catch (err) {\n console.warn(\"[useScribe] Failed to close connection\", err)\n } finally {\n setStatus(\"disconnected\")\n onDisconnect?.()\n }\n }, [onDisconnect])\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n disconnect()\n }\n }, [disconnect])\n\n const connect = useCallback(\n async (runtimeOptions: Partial = {}) => {\n if (connectionRef.current) {\n console.warn(\"Already connected\")\n return\n }\n\n const connectionId = connectionIdCounterRef.current + 1\n connectionIdCounterRef.current = connectionId\n\n try {\n setStatus(\"connecting\")\n setError(null)\n\n // Merge default options with runtime options\n const token = runtimeOptions.token || defaultToken\n const modelId = runtimeOptions.modelId || defaultModelId\n\n if (!token) {\n throw new Error(\"Token is required\")\n }\n if (!modelId) {\n throw new Error(\"Model ID is required\")\n }\n\n // Determine mode: microphone or manual\n const microphone = runtimeOptions.microphone || defaultMicrophone\n const audioFormat = runtimeOptions.audioFormat || defaultAudioFormat\n const sampleRate = runtimeOptions.sampleRate || defaultSampleRate\n\n let connection: RealtimeConnection\n\n const includeTimestamps =\n runtimeOptions.includeTimestamps ??\n !!(\n runtimeOptions.onCommittedTranscriptWithTimestamps ||\n onCommittedTranscriptWithTimestamps\n )\n\n if (microphone) {\n // Microphone mode\n connection = Scribe.connect({\n token,\n modelId,\n baseUri: runtimeOptions.baseUri || defaultBaseUri,\n commitStrategy:\n runtimeOptions.commitStrategy || defaultCommitStrategy,\n vadSilenceThresholdSecs:\n runtimeOptions.vadSilenceThresholdSecs ||\n defaultVadSilenceThresholdSecs,\n vadThreshold: runtimeOptions.vadThreshold || defaultVadThreshold,\n minSpeechDurationMs:\n runtimeOptions.minSpeechDurationMs || defaultMinSpeechDurationMs,\n minSilenceDurationMs:\n runtimeOptions.minSilenceDurationMs ||\n defaultMinSilenceDurationMs,\n languageCode: runtimeOptions.languageCode || defaultLanguageCode,\n microphone,\n includeTimestamps,\n } as MicrophoneOptions)\n } else if (audioFormat && sampleRate) {\n // Manual audio mode\n connection = Scribe.connect({\n token,\n modelId,\n baseUri: runtimeOptions.baseUri || defaultBaseUri,\n commitStrategy:\n runtimeOptions.commitStrategy || defaultCommitStrategy,\n vadSilenceThresholdSecs:\n runtimeOptions.vadSilenceThresholdSecs ||\n defaultVadSilenceThresholdSecs,\n vadThreshold: runtimeOptions.vadThreshold || defaultVadThreshold,\n minSpeechDurationMs:\n runtimeOptions.minSpeechDurationMs || defaultMinSpeechDurationMs,\n minSilenceDurationMs:\n runtimeOptions.minSilenceDurationMs ||\n defaultMinSilenceDurationMs,\n languageCode: runtimeOptions.languageCode || defaultLanguageCode,\n includeTimestamps,\n audioFormat,\n sampleRate,\n } as AudioOptions)\n } else {\n throw new Error(\n \"Either microphone options or (audioFormat + sampleRate) must be provided\"\n )\n }\n\n connectionRef.current = connection\n activeConnectionIdRef.current = connectionId\n\n const runIfCurrent =\n (handler: (...args: Args) => void) =>\n (...args: Args) => {\n if (activeConnectionIdRef.current !== connectionId) {\n return\n }\n handler(...args)\n }\n\n // Set up event listeners\n connection.on(\n RealtimeEvents.SESSION_STARTED,\n runIfCurrent(() => {\n setStatus(\"connected\")\n onSessionStarted?.()\n })\n )\n\n connection.on(\n RealtimeEvents.PARTIAL_TRANSCRIPT,\n runIfCurrent((data: unknown) => {\n const message = data as PartialTranscriptMessage\n setPartialTranscript(message.text)\n setStatus(\"transcribing\")\n onPartialTranscript?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.COMMITTED_TRANSCRIPT,\n runIfCurrent((data: unknown) => {\n const message = data as CommittedTranscriptMessage\n const segment: TranscriptSegment = {\n id: `${Date.now()}-${Math.random()}`,\n text: message.text,\n timestamp: Date.now(),\n isFinal: true,\n }\n setCommittedTranscripts((prev) => [...prev, segment])\n setPartialTranscript(\"\")\n onCommittedTranscript?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS,\n runIfCurrent((data: unknown) => {\n const message = data as CommittedTranscriptWithTimestampsMessage\n const segment: TranscriptSegment = {\n id: `${Date.now()}-${Math.random()}`,\n text: message.text,\n timestamp: Date.now(),\n isFinal: true,\n }\n setCommittedTranscripts((prev) => [...prev, segment])\n setPartialTranscript(\"\")\n onCommittedTranscriptWithTimestamps?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.ERROR,\n runIfCurrent((err: unknown) => {\n const message = err as ScribeErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onError?.(new Error(message.error))\n })\n )\n\n connection.on(\n RealtimeEvents.AUTH_ERROR,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeAuthErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onAuthError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.QUOTA_EXCEEDED,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeQuotaExceededErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onQuotaExceededError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.COMMIT_THROTTLED,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeCommitThrottledErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onCommitThrottledError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.TRANSCRIBER_ERROR,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeTranscriberErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onTranscriberError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.UNACCEPTED_TERMS,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeUnacceptedTermsErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onUnacceptedTermsError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.RATE_LIMITED,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeRateLimitedErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onRateLimitedError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.INPUT_ERROR,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeInputErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onInputError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.QUEUE_OVERFLOW,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeQueueOverflowErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onQueueOverflowError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.RESOURCE_EXHAUSTED,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeResourceExhaustedErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onResourceExhaustedError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.SESSION_TIME_LIMIT_EXCEEDED,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeSessionTimeLimitExceededErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onSessionTimeLimitExceededError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.CHUNK_SIZE_EXCEEDED,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeChunkSizeExceededErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onChunkSizeExceededError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.INSUFFICIENT_AUDIO_ACTIVITY,\n runIfCurrent((data: unknown) => {\n const message = data as ScribeInsufficientAudioActivityErrorMessage\n setError(message.error)\n setStatus(\"error\")\n onInsufficientAudioActivityError?.(message)\n })\n )\n\n connection.on(\n RealtimeEvents.OPEN,\n runIfCurrent(() => {\n onConnect?.()\n })\n )\n\n connection.on(\n RealtimeEvents.CLOSE,\n runIfCurrent(() => {\n activeConnectionIdRef.current = null\n connectionRef.current = null\n setStatus(\"disconnected\")\n onDisconnect?.()\n })\n )\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : \"Failed to connect\"\n setError(errorMessage)\n setStatus(\"error\")\n throw err\n }\n },\n [\n defaultToken,\n defaultModelId,\n defaultBaseUri,\n defaultCommitStrategy,\n defaultVadSilenceThresholdSecs,\n defaultVadThreshold,\n defaultMinSpeechDurationMs,\n defaultMinSilenceDurationMs,\n defaultLanguageCode,\n defaultMicrophone,\n defaultAudioFormat,\n defaultSampleRate,\n onSessionStarted,\n onPartialTranscript,\n onCommittedTranscript,\n onCommittedTranscriptWithTimestamps,\n onError,\n onAuthError,\n onQuotaExceededError,\n onCommitThrottledError,\n onTranscriberError,\n onUnacceptedTermsError,\n onRateLimitedError,\n onInputError,\n onQueueOverflowError,\n onResourceExhaustedError,\n onSessionTimeLimitExceededError,\n onChunkSizeExceededError,\n onInsufficientAudioActivityError,\n onConnect,\n onDisconnect,\n ]\n )\n\n const sendAudio = useCallback(\n (\n audioBase64: string,\n options?: { commit?: boolean; sampleRate?: number; previousText?: string }\n ) => {\n if (!connectionRef.current) {\n throw new Error(\"Not connected to Scribe\")\n }\n connectionRef.current.send({ audioBase64, ...options })\n },\n []\n )\n\n const commit = useCallback(() => {\n if (!connectionRef.current) {\n throw new Error(\"Not connected to Scribe\")\n }\n connectionRef.current.commit()\n }, [])\n\n const clearTranscripts = useCallback(() => {\n setCommittedTranscripts([])\n setPartialTranscript(\"\")\n }, [])\n\n const getConnection = useCallback(() => {\n return connectionRef.current\n }, [])\n\n // Auto-connect if enabled\n useEffect(() => {\n if (autoConnect) {\n void connect()\n }\n }, [autoConnect, connect])\n\n return {\n // State\n status,\n isConnected: status === \"connected\" || status === \"transcribing\",\n isTranscribing: status === \"transcribing\",\n partialTranscript,\n committedTranscripts,\n error,\n\n // Methods\n connect,\n disconnect,\n sendAudio,\n commit,\n clearTranscripts,\n getConnection,\n }\n}\n\n// Export types and enums from client for convenience\nexport { AudioFormat, CommitStrategy, RealtimeEvents } from \"@elevenlabs/client\"\nexport type { RealtimeConnection } from \"@elevenlabs/client\"\n", "type": "registry:hook" } ], "type": "registry:hook" }