gemi 0.132.0 → 0.134.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/ai/index.d.ts +2 -2
  2. package/dist/ai/index.d.ts.map +1 -1
  3. package/dist/ai/index.js +9 -9
  4. package/dist/ai/index.js.map +4 -4
  5. package/dist/ai/providers/realtime.d.ts +75 -4
  6. package/dist/ai/providers/realtime.d.ts.map +1 -1
  7. package/dist/ai/providers/sideband.d.ts +7 -2
  8. package/dist/ai/providers/sideband.d.ts.map +1 -1
  9. package/dist/ai/voice/energyVad.d.ts +42 -0
  10. package/dist/ai/voice/energyVad.d.ts.map +1 -0
  11. package/dist/ai/voice/index.d.ts +7 -0
  12. package/dist/ai/voice/index.d.ts.map +1 -1
  13. package/dist/ai/voice/index.js +601 -1
  14. package/dist/ai/voice/index.js.map +1 -1
  15. package/dist/ai/voice/liveTranscription.d.ts +99 -0
  16. package/dist/ai/voice/liveTranscription.d.ts.map +1 -0
  17. package/dist/ai/voice/transcriptionState.d.ts +52 -0
  18. package/dist/ai/voice/transcriptionState.d.ts.map +1 -0
  19. package/dist/ai/voice/useLiveTranscription.d.ts +86 -0
  20. package/dist/ai/voice/useLiveTranscription.d.ts.map +1 -0
  21. package/dist/{chunk-2vteyrjj.js → chunk-3xm7ysef.js} +1 -1
  22. package/dist/{chunk-t79n15f8.js → chunk-778kv5wf.js} +1 -1
  23. package/dist/{chunk-mr66zxsz.js → chunk-c7g1jb3f.js} +2 -2
  24. package/dist/{chunk-mr66zxsz.js.map → chunk-c7g1jb3f.js.map} +1 -1
  25. package/dist/orm/defaults.d.ts.map +1 -1
  26. package/dist/orm/index.js +3 -3
  27. package/dist/orm/index.js.map +3 -3
  28. package/dist/server/index.js +1 -1
  29. package/package.json +1 -1
  30. /package/dist/{chunk-2vteyrjj.js.map → chunk-3xm7ysef.js.map} +0 -0
  31. /package/dist/{chunk-t79n15f8.js.map → chunk-778kv5wf.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../ai/voice/state.ts","../../../ai/voice/voiceCall.ts","../../../ai/voice/useVoiceSession.ts"],"sourcesContent":["import type { VoiceCaption, VoiceStatus } from \"./wire\";\n\n/**\n * What `useVoiceSession` shows, as a pure reducer over the call's data-channel\n * events (#921). Separate from the hook so it is tested without a DOM, a\n * microphone or a peer connection.\n *\n * The events are the vendor's, as the data channel delivers them. Which ones\n * arrive there — in particular the `function_call_output` items the server's\n * sideband adds, which end `working` — follows the documented protocol and has\n * not been measured against Azure.\n */\nexport type VoiceState = {\n phase: \"idle\" | \"connecting\" | \"live\" | \"ended\";\n /** The user is speaking (VAD) or holding push-to-talk. */\n userSpeaking: boolean;\n /** The user stopped and no answer has started playing. */\n thinking: boolean;\n /** The voice's audio is playing. */\n speaking: boolean;\n /** Delegate calls with no output yet. */\n working: string[];\n captions: VoiceCaption[];\n};\n\nexport const MAX_CAPTIONS = 20;\n\nexport function initialVoiceState(): VoiceState {\n return {\n phase: \"idle\",\n userSpeaking: false,\n thinking: false,\n speaking: false,\n working: [],\n captions: [],\n };\n}\n\nexport function voiceStatus(state: VoiceState): VoiceStatus {\n if (state.phase === \"idle\") return \"idle\";\n if (state.phase === \"connecting\") return \"connecting\";\n if (state.phase === \"ended\") return \"ended\";\n if (state.speaking) return \"speaking\";\n if (state.userSpeaking) return \"listening\";\n if (state.thinking) return \"thinking\";\n if (state.working.length) return \"working\";\n return \"listening\";\n}\n\ntype Event = { type: string; [key: string]: any };\n\n/** One data-channel event onto the state. `delegate` is the delegate tool's name. */\nexport function applyVoiceEvent(state: VoiceState, event: Event, delegate: string): VoiceState {\n switch (event.type) {\n case \"input_audio_buffer.speech_started\":\n return { ...state, userSpeaking: true, thinking: false };\n case \"input_audio_buffer.speech_stopped\":\n case \"input_audio_buffer.committed\":\n return { ...state, userSpeaking: false, thinking: true };\n case \"output_audio_buffer.started\":\n return { ...state, speaking: true, thinking: false };\n case \"output_audio_buffer.stopped\":\n case \"output_audio_buffer.cleared\":\n return { ...state, speaking: false };\n case \"response.done\": {\n let next = { ...state, thinking: false };\n for (const item of event.response?.output ?? []) {\n if (item?.type === \"function_call\" && item.name === delegate && item.call_id) {\n next = addWorking(next, item.call_id);\n }\n }\n return next;\n }\n case \"response.function_call_arguments.done\":\n return event.name === delegate && event.call_id ? addWorking(state, event.call_id) : state;\n case \"conversation.item.added\":\n case \"conversation.item.created\":\n case \"conversation.item.done\": {\n const item = event.item;\n if (item?.type === \"function_call_output\" && state.working.includes(item.call_id)) {\n return { ...state, working: state.working.filter((id) => id !== item.call_id) };\n }\n return state;\n }\n case \"conversation.item.input_audio_transcription.delta\":\n return caption(state, event.item_id, \"user\", event.delta ?? \"\", false);\n case \"conversation.item.input_audio_transcription.completed\":\n return caption(state, event.item_id, \"user\", event.transcript ?? \"\", true);\n case \"response.output_audio_transcript.delta\":\n case \"response.audio_transcript.delta\":\n return caption(state, event.item_id, \"assistant\", event.delta ?? \"\", false);\n case \"response.output_audio_transcript.done\":\n case \"response.audio_transcript.done\":\n return caption(state, event.item_id, \"assistant\", event.transcript ?? \"\", true);\n default:\n return state;\n }\n}\n\nfunction addWorking(state: VoiceState, callId: string): VoiceState {\n return state.working.includes(callId) ? state : { ...state, working: [...state.working, callId] };\n}\n\n/** A delta appends to its line; a final text replaces it. */\nfunction caption(\n state: VoiceState,\n id: string | undefined,\n role: VoiceCaption[\"role\"],\n text: string,\n final: boolean,\n): VoiceState {\n if (!id) return state;\n const index = state.captions.findIndex((line) => line.id === id);\n const captions = [...state.captions];\n if (index === -1) {\n if (!text) return state;\n captions.push({ id, role, text, final });\n } else {\n const line = captions[index]!;\n captions[index] = { ...line, text: final ? text || line.text : line.text + text, final };\n }\n return { ...state, captions: captions.slice(-MAX_CAPTIONS) };\n}\n","import type { VoiceStartResponse } from \"./wire\";\n\n/**\n * The browser's half of a voice call (#921): the microphone, the peer\n * connection, the vendor's data channel and the app's three routes.\n *\n * Only `gemi/ai/voice` reaches it, so it is in an app's bundle only where the\n * app uses that entry. The packaging test checks it stays out of the entry\n * chunk.\n *\n * No vendor SDK: a raw `RTCPeerConnection`, with the audio going straight to\n * the vendor and the SDP offer going to the app's route, which creates the\n * call with the server's key. The browser never holds a key or a secret.\n */\n\nexport type VoiceCallOptions = {\n /** `/api/<route>`. */\n url: string;\n threadId?: string;\n body?: unknown;\n pushToTalk?: boolean;\n headers?: Record<string, string>;\n /** Every vendor event from the data channel. */\n onEvent: (event: { type: string; [key: string]: unknown }) => void;\n /** The connection closed or failed. */\n onClose: (error?: Error) => void;\n /** What push-to-talk turns detection back on to. Default semantic VAD. */\n turnDetection?: Record<string, unknown>;\n};\n\nexport type VoiceCall = {\n sessionId: string;\n threadId: string;\n delegate: string;\n endsAt?: number;\n mute(muted: boolean): void;\n setPushToTalk(on: boolean): void;\n /** Push-to-talk: start speaking. */\n press(): void;\n /** Push-to-talk: done speaking; the turn is committed and answered. */\n release(): void;\n setBody(body: unknown): Promise<void>;\n /** Hangs up through the app's route and closes everything. */\n end(): Promise<void>;\n /** Closes everything without telling the server (it learns from the vendor). */\n close(): void;\n};\n\nexport class VoiceCallError extends Error {\n readonly status?: number;\n readonly code?: string;\n\n constructor(message: string, options: { status?: number; code?: string; cause?: unknown } = {}) {\n super(message, { cause: options.cause });\n this.name = \"VoiceCallError\";\n this.status = options.status;\n this.code = options.code;\n }\n}\n\nexport async function startVoiceCall(options: VoiceCallOptions): Promise<VoiceCall> {\n if (typeof RTCPeerConnection === \"undefined\" || !navigator.mediaDevices?.getUserMedia) {\n throw new VoiceCallError(\"This browser can't make voice calls.\", { code: \"unsupported\" });\n }\n\n let stream: MediaStream;\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },\n });\n } catch (error) {\n throw new VoiceCallError(\"The microphone is not available.\", {\n code: \"microphone_denied\",\n cause: error,\n });\n }\n const track = stream.getAudioTracks()[0]!;\n let pushToTalk = options.pushToTalk === true;\n let muted = false;\n track.enabled = !pushToTalk;\n\n const pc = new RTCPeerConnection();\n const audio = document.createElement(\"audio\");\n audio.autoplay = true;\n pc.ontrack = (event) => {\n audio.srcObject = event.streams[0] ?? new MediaStream([event.track]);\n };\n pc.addTrack(track, stream);\n const channel = pc.createDataChannel(\"oai-events\");\n\n let closed = false;\n const close = (error?: Error) => {\n if (closed) return;\n closed = true;\n try {\n channel.close();\n } catch {\n // already closed\n }\n pc.close();\n for (const t of stream.getTracks()) t.stop();\n audio.srcObject = null;\n options.onClose(error);\n };\n\n channel.addEventListener(\"message\", (message) => {\n let event: unknown;\n try {\n event = JSON.parse(String(message.data));\n } catch {\n return;\n }\n if (\n event &&\n typeof event === \"object\" &&\n typeof (event as { type?: unknown }).type === \"string\"\n ) {\n options.onEvent(event as { type: string });\n }\n });\n channel.addEventListener(\"close\", () => close());\n pc.addEventListener(\"connectionstatechange\", () => {\n if (pc.connectionState === \"failed\") close(new VoiceCallError(\"The voice connection failed.\"));\n if (pc.connectionState === \"closed\") close();\n });\n\n // Events sent before the data channel opens (push-to-talk pressed at once)\n // wait for it rather than being dropped.\n const outbox: string[] = [];\n channel.addEventListener(\"open\", () => {\n for (const data of outbox.splice(0)) channel.send(data);\n });\n const send = (event: Record<string, unknown>) => {\n const data = JSON.stringify(event);\n if (channel.readyState === \"open\") channel.send(data);\n else if (channel.readyState === \"connecting\") outbox.push(data);\n };\n const request = (method: string, url: string, payload?: unknown, init: RequestInit = {}) =>\n fetch(url, {\n method,\n headers: { \"Content-Type\": \"application/json\", ...options.headers },\n ...(payload === undefined ? {} : { body: JSON.stringify(payload) }),\n ...init,\n });\n\n let started: VoiceStartResponse;\n try {\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n const response = await request(\"POST\", options.url, {\n ...(options.body && typeof options.body === \"object\" ? options.body : {}),\n sdp: offer.sdp,\n ...(options.threadId ? { threadId: options.threadId } : {}),\n ...(pushToTalk ? { pushToTalk: true } : {}),\n });\n if (!response.ok) {\n const error = await response.json().catch(() => null);\n throw new VoiceCallError(\n error?.error?.message ?? `The voice call could not be started (${response.status}).`,\n { status: response.status, code: error?.error?.code },\n );\n }\n started = await response.json();\n await pc.setRemoteDescription({ type: \"answer\", sdp: started.sdp });\n } catch (error) {\n close();\n throw error instanceof VoiceCallError\n ? error\n : new VoiceCallError(\"The voice call could not be started.\", { cause: error });\n }\n\n const sessionUrl = `${options.url}/${encodeURIComponent(started.sessionId)}`;\n const applyTrack = () => {\n track.enabled = !muted && !pushToTalk;\n };\n\n return {\n sessionId: started.sessionId,\n threadId: started.threadId,\n delegate: started.delegate,\n endsAt: started.endsAt,\n mute(on) {\n muted = on;\n applyTrack();\n },\n setPushToTalk(on) {\n pushToTalk = on;\n applyTrack();\n send({\n type: \"session.update\",\n session: {\n type: \"realtime\",\n audio: {\n input: {\n turn_detection: on ? null : (options.turnDetection ?? { type: \"semantic_vad\" }),\n },\n },\n },\n });\n },\n press() {\n if (!pushToTalk || muted) return;\n send({ type: \"input_audio_buffer.clear\" });\n track.enabled = true;\n },\n release() {\n if (!pushToTalk) return;\n track.enabled = false;\n send({ type: \"input_audio_buffer.commit\" });\n send({ type: \"response.create\" });\n },\n async setBody(body) {\n const response = await request(\"PATCH\", sessionUrl, body ?? {});\n if (!response.ok && response.status !== 409) {\n throw new VoiceCallError(`The voice session could not be updated (${response.status}).`, {\n status: response.status,\n });\n }\n },\n async end() {\n try {\n // `keepalive`, so a hang-up sent as the tab closes still reaches the\n // route: minutes are billed until it does.\n await request(\"DELETE\", sessionUrl, undefined, { keepalive: true });\n } finally {\n close();\n }\n },\n close: () => close(),\n };\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { RPC } from \"../../client/rpc\";\nimport { applyVoiceEvent, initialVoiceState, voiceStatus, type VoiceState } from \"./state\";\nimport { startVoiceCall, type VoiceCall } from \"./voiceCall\";\nimport type { VoiceCaption, VoiceStatus } from \"./wire\";\n\n/**\n * The voice routes, picked out of `RPC` the way `useChat` picks its agent\n * routes: `this.voice(Controller)` mounted at `\"/chat/voice\"` is the key\n * `\"/chat/voice\"`, carrying the controller's `Body`.\n */\ntype VoiceRoutes = {\n [K in keyof RPC as RPC[K] extends { __voice: true } ? K : never]: RPC[K];\n};\n\ntype BodyOf<P extends keyof VoiceRoutes> = VoiceRoutes[P] extends { body: infer B }\n ? B\n : Record<string, unknown>;\n\nexport type UseVoiceSessionParams<P extends keyof VoiceRoutes> = {\n /** The chat thread delegated turns are stored on, as `useChat` names it. */\n threadId?: string;\n /** The controller's `Body`, sent when the call starts. Change it during the\n * call with `setBody`. */\n body?: BodyOf<P>;\n /** Start with push-to-talk instead of voice activity detection. */\n pushToTalk?: boolean;\n /** Values for the route path's `:params`, e.g. `{ siteId }` for\n * `\"/sites/:siteId/voice\"`. */\n params?: Record<string, string>;\n headers?: Record<string, string>;\n /** The call could not start, or failed. */\n onError?: (error: Error) => void;\n /** The call is over, however it ended. */\n onEnd?: () => void;\n};\n\nexport type UseVoiceSessionResult<P extends keyof VoiceRoutes> = {\n status: VoiceStatus;\n /** The last lines said, the user's and the voice's, oldest first. */\n captions: VoiceCaption[];\n /** Why the call could not start or ended early. `code` is\n * `microphone_denied`, `unsupported`, or the route's error code. */\n error: (Error & { code?: string; status?: number }) | null;\n muted: boolean;\n pushToTalk: boolean;\n /** When the server will end the call, in epoch ms. */\n endsAt?: number;\n sessionId?: string;\n /** Asks for the microphone and starts the call. Call it from a user gesture\n * (a click): browsers allow the microphone and audio playback only then. */\n start(): Promise<void>;\n /** Hangs up. */\n end(): Promise<void>;\n mute(muted?: boolean): void;\n setPushToTalk(on: boolean): void;\n /** Push-to-talk: hold to speak (pointer down / key down). */\n press(): void;\n /** Push-to-talk: release to send (pointer up / key up). */\n release(): void;\n /** Sends a new `body` (the selection, the open page) for the next\n * delegated request. */\n setBody(body: BodyOf<P>): Promise<void>;\n};\n\n/**\n * A voice call to an agent, for a route mounted with `this.voice(Controller)`\n * (#921).\n *\n * ```tsx\n * const voice = useVoiceSession(\"/page-builder/voice\", { threadId, body: { pageId } });\n * <button onClick={() => (voice.status === \"idle\" ? voice.start() : voice.end())}>\n * {voice.status}\n * </button>\n * ```\n *\n * Import it from `gemi/ai/voice`, an entry of its own, so an app that never\n * uses voice never bundles it, and use it from a view or a dynamically imported\n * component, so its code is not in the entry chunk.\n *\n * @experimental Not yet verified against a live call.\n */\nexport function useVoiceSession<P extends keyof VoiceRoutes>(\n path: P,\n params: UseVoiceSessionParams<P> = {},\n): UseVoiceSessionResult<P> {\n // Not `useParams()`, which `useChat` uses: it is in a chunk `gemi/client`\n // shares, and reaching it from here made the app's bundler move it out of\n // the entry chunk into one of its own that every route then imports. A path\n // with parameters names them in `params`.\n const routeParams = params.params;\n const url = useMemo(() => {\n let out = String(path).replace(/^[A-Z]+:/, \"\");\n for (const [key, value] of Object.entries(routeParams ?? {})) {\n out = out.replace(`:${key}?`, value).replace(`:${key}`, value);\n }\n return `/api${out}`;\n }, [path, routeParams && JSON.stringify(routeParams)]);\n\n const [state, setState] = useState<VoiceState>(initialVoiceState);\n const [error, setError] = useState<UseVoiceSessionResult<P>[\"error\"]>(null);\n const [muted, setMuted] = useState(false);\n const [pushToTalk, setPushToTalkState] = useState(params.pushToTalk === true);\n const [call, setCall] = useState<VoiceCall | null>(null);\n const callRef = useRef<VoiceCall | null>(null);\n /** A `start()` in flight: a second click must not start a second call. */\n const startingRef = useRef(false);\n const latest = useRef(params);\n latest.current = params;\n const delegateRef = useRef(\"ask_agent\");\n const mounted = useRef(true);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n // Leaving the page ends the call: minutes are billed while it is up.\n void callRef.current?.end().catch(() => undefined);\n callRef.current = null;\n };\n }, []);\n\n const start = useCallback(async () => {\n if (callRef.current || startingRef.current) return;\n startingRef.current = true;\n setError(null);\n setState({ ...initialVoiceState(), phase: \"connecting\" });\n try {\n const { threadId, body, headers } = latest.current;\n const started = await startVoiceCall({\n url,\n threadId,\n body,\n headers,\n pushToTalk,\n onEvent: (event) => {\n if (!mounted.current) return;\n setState((current) =>\n current.phase === \"connecting\"\n ? applyVoiceEvent({ ...current, phase: \"live\" }, event, delegateRef.current)\n : applyVoiceEvent(current, event, delegateRef.current),\n );\n },\n onClose: (closeError) => {\n callRef.current = null;\n if (!mounted.current) return;\n setCall(null);\n setState((current) => ({ ...current, phase: \"ended\", speaking: false, thinking: false }));\n if (closeError) {\n setError(closeError);\n latest.current.onError?.(closeError);\n }\n latest.current.onEnd?.();\n },\n });\n delegateRef.current = started.delegate;\n if (!mounted.current) {\n await started.end().catch(() => undefined);\n return;\n }\n callRef.current = started;\n setCall(started);\n setState((current) =>\n current.phase === \"connecting\" ? { ...current, phase: \"live\" } : current,\n );\n } catch (failure) {\n const reported = failure instanceof Error ? failure : new Error(String(failure));\n if (!mounted.current) return;\n setState((current) => ({ ...current, phase: \"ended\" }));\n setError(reported);\n latest.current.onError?.(reported);\n } finally {\n startingRef.current = false;\n }\n }, [url, pushToTalk]);\n\n const end = useCallback(async () => {\n const current = callRef.current;\n callRef.current = null;\n await current?.end();\n }, []);\n\n const mute = useCallback((on?: boolean) => {\n setMuted((was) => {\n const next = on ?? !was;\n callRef.current?.mute(next);\n return next;\n });\n }, []);\n\n const setPushToTalk = useCallback((on: boolean) => {\n setPushToTalkState(on);\n callRef.current?.setPushToTalk(on);\n }, []);\n\n const press = useCallback(() => {\n callRef.current?.press();\n setState((current) => ({ ...current, userSpeaking: true }));\n }, []);\n\n const release = useCallback(() => {\n callRef.current?.release();\n setState((current) => ({ ...current, userSpeaking: false, thinking: true }));\n }, []);\n\n const setBody = useCallback(async (body: BodyOf<P>) => {\n latest.current = { ...latest.current, body };\n await callRef.current?.setBody(body);\n }, []);\n\n return {\n status: voiceStatus(state),\n captions: state.captions,\n error,\n muted,\n pushToTalk,\n endsAt: call?.endsAt,\n sessionId: call?.sessionId,\n start,\n end,\n mute,\n setPushToTalk,\n press,\n release,\n setBody,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../ai/voice/state.ts","../../../ai/voice/voiceCall.ts","../../../ai/voice/useVoiceSession.ts","../../../ai/voice/energyVad.ts","../../../ai/voice/liveTranscription.ts","../../../ai/voice/transcriptionState.ts","../../../ai/voice/useLiveTranscription.ts"],"sourcesContent":["import type { VoiceCaption, VoiceStatus } from \"./wire\";\n\n/**\n * What `useVoiceSession` shows, as a pure reducer over the call's data-channel\n * events (#921). Separate from the hook so it is tested without a DOM, a\n * microphone or a peer connection.\n *\n * The events are the vendor's, as the data channel delivers them. Which ones\n * arrive there — in particular the `function_call_output` items the server's\n * sideband adds, which end `working` — follows the documented protocol and has\n * not been measured against Azure.\n */\nexport type VoiceState = {\n phase: \"idle\" | \"connecting\" | \"live\" | \"ended\";\n /** The user is speaking (VAD) or holding push-to-talk. */\n userSpeaking: boolean;\n /** The user stopped and no answer has started playing. */\n thinking: boolean;\n /** The voice's audio is playing. */\n speaking: boolean;\n /** Delegate calls with no output yet. */\n working: string[];\n captions: VoiceCaption[];\n};\n\nexport const MAX_CAPTIONS = 20;\n\nexport function initialVoiceState(): VoiceState {\n return {\n phase: \"idle\",\n userSpeaking: false,\n thinking: false,\n speaking: false,\n working: [],\n captions: [],\n };\n}\n\nexport function voiceStatus(state: VoiceState): VoiceStatus {\n if (state.phase === \"idle\") return \"idle\";\n if (state.phase === \"connecting\") return \"connecting\";\n if (state.phase === \"ended\") return \"ended\";\n if (state.speaking) return \"speaking\";\n if (state.userSpeaking) return \"listening\";\n if (state.thinking) return \"thinking\";\n if (state.working.length) return \"working\";\n return \"listening\";\n}\n\ntype Event = { type: string; [key: string]: any };\n\n/** One data-channel event onto the state. `delegate` is the delegate tool's name. */\nexport function applyVoiceEvent(state: VoiceState, event: Event, delegate: string): VoiceState {\n switch (event.type) {\n case \"input_audio_buffer.speech_started\":\n return { ...state, userSpeaking: true, thinking: false };\n case \"input_audio_buffer.speech_stopped\":\n case \"input_audio_buffer.committed\":\n return { ...state, userSpeaking: false, thinking: true };\n case \"output_audio_buffer.started\":\n return { ...state, speaking: true, thinking: false };\n case \"output_audio_buffer.stopped\":\n case \"output_audio_buffer.cleared\":\n return { ...state, speaking: false };\n case \"response.done\": {\n let next = { ...state, thinking: false };\n for (const item of event.response?.output ?? []) {\n if (item?.type === \"function_call\" && item.name === delegate && item.call_id) {\n next = addWorking(next, item.call_id);\n }\n }\n return next;\n }\n case \"response.function_call_arguments.done\":\n return event.name === delegate && event.call_id ? addWorking(state, event.call_id) : state;\n case \"conversation.item.added\":\n case \"conversation.item.created\":\n case \"conversation.item.done\": {\n const item = event.item;\n if (item?.type === \"function_call_output\" && state.working.includes(item.call_id)) {\n return { ...state, working: state.working.filter((id) => id !== item.call_id) };\n }\n return state;\n }\n case \"conversation.item.input_audio_transcription.delta\":\n return caption(state, event.item_id, \"user\", event.delta ?? \"\", false);\n case \"conversation.item.input_audio_transcription.completed\":\n return caption(state, event.item_id, \"user\", event.transcript ?? \"\", true);\n case \"response.output_audio_transcript.delta\":\n case \"response.audio_transcript.delta\":\n return caption(state, event.item_id, \"assistant\", event.delta ?? \"\", false);\n case \"response.output_audio_transcript.done\":\n case \"response.audio_transcript.done\":\n return caption(state, event.item_id, \"assistant\", event.transcript ?? \"\", true);\n default:\n return state;\n }\n}\n\nfunction addWorking(state: VoiceState, callId: string): VoiceState {\n return state.working.includes(callId) ? state : { ...state, working: [...state.working, callId] };\n}\n\n/** A delta appends to its line; a final text replaces it. */\nfunction caption(\n state: VoiceState,\n id: string | undefined,\n role: VoiceCaption[\"role\"],\n text: string,\n final: boolean,\n): VoiceState {\n if (!id) return state;\n const index = state.captions.findIndex((line) => line.id === id);\n const captions = [...state.captions];\n if (index === -1) {\n if (!text) return state;\n captions.push({ id, role, text, final });\n } else {\n const line = captions[index]!;\n captions[index] = { ...line, text: final ? text || line.text : line.text + text, final };\n }\n return { ...state, captions: captions.slice(-MAX_CAPTIONS) };\n}\n","import type { VoiceStartResponse } from \"./wire\";\n\n/**\n * The browser's half of a voice call (#921): the microphone, the peer\n * connection, the vendor's data channel and the app's three routes.\n *\n * Only `gemi/ai/voice` reaches it, so it is in an app's bundle only where the\n * app uses that entry. The packaging test checks it stays out of the entry\n * chunk.\n *\n * No vendor SDK: a raw `RTCPeerConnection`, with the audio going straight to\n * the vendor and the SDP offer going to the app's route, which creates the\n * call with the server's key. The browser never holds a key or a secret.\n */\n\nexport type VoiceCallOptions = {\n /** `/api/<route>`. */\n url: string;\n threadId?: string;\n body?: unknown;\n pushToTalk?: boolean;\n headers?: Record<string, string>;\n /** Every vendor event from the data channel. */\n onEvent: (event: { type: string; [key: string]: unknown }) => void;\n /** The connection closed or failed. */\n onClose: (error?: Error) => void;\n /** What push-to-talk turns detection back on to. Default semantic VAD. */\n turnDetection?: Record<string, unknown>;\n};\n\nexport type VoiceCall = {\n sessionId: string;\n threadId: string;\n delegate: string;\n endsAt?: number;\n mute(muted: boolean): void;\n setPushToTalk(on: boolean): void;\n /** Push-to-talk: start speaking. */\n press(): void;\n /** Push-to-talk: done speaking; the turn is committed and answered. */\n release(): void;\n setBody(body: unknown): Promise<void>;\n /** Hangs up through the app's route and closes everything. */\n end(): Promise<void>;\n /** Closes everything without telling the server (it learns from the vendor). */\n close(): void;\n};\n\nexport class VoiceCallError extends Error {\n readonly status?: number;\n readonly code?: string;\n\n constructor(message: string, options: { status?: number; code?: string; cause?: unknown } = {}) {\n super(message, { cause: options.cause });\n this.name = \"VoiceCallError\";\n this.status = options.status;\n this.code = options.code;\n }\n}\n\nexport async function startVoiceCall(options: VoiceCallOptions): Promise<VoiceCall> {\n if (typeof RTCPeerConnection === \"undefined\" || !navigator.mediaDevices?.getUserMedia) {\n throw new VoiceCallError(\"This browser can't make voice calls.\", { code: \"unsupported\" });\n }\n\n let stream: MediaStream;\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },\n });\n } catch (error) {\n throw new VoiceCallError(\"The microphone is not available.\", {\n code: \"microphone_denied\",\n cause: error,\n });\n }\n const track = stream.getAudioTracks()[0]!;\n let pushToTalk = options.pushToTalk === true;\n let muted = false;\n track.enabled = !pushToTalk;\n\n const pc = new RTCPeerConnection();\n const audio = document.createElement(\"audio\");\n audio.autoplay = true;\n pc.ontrack = (event) => {\n audio.srcObject = event.streams[0] ?? new MediaStream([event.track]);\n };\n pc.addTrack(track, stream);\n const channel = pc.createDataChannel(\"oai-events\");\n\n let closed = false;\n const close = (error?: Error) => {\n if (closed) return;\n closed = true;\n try {\n channel.close();\n } catch {\n // already closed\n }\n pc.close();\n for (const t of stream.getTracks()) t.stop();\n audio.srcObject = null;\n options.onClose(error);\n };\n\n channel.addEventListener(\"message\", (message) => {\n let event: unknown;\n try {\n event = JSON.parse(String(message.data));\n } catch {\n return;\n }\n if (\n event &&\n typeof event === \"object\" &&\n typeof (event as { type?: unknown }).type === \"string\"\n ) {\n options.onEvent(event as { type: string });\n }\n });\n channel.addEventListener(\"close\", () => close());\n pc.addEventListener(\"connectionstatechange\", () => {\n if (pc.connectionState === \"failed\") close(new VoiceCallError(\"The voice connection failed.\"));\n if (pc.connectionState === \"closed\") close();\n });\n\n // Events sent before the data channel opens (push-to-talk pressed at once)\n // wait for it rather than being dropped.\n const outbox: string[] = [];\n channel.addEventListener(\"open\", () => {\n for (const data of outbox.splice(0)) channel.send(data);\n });\n const send = (event: Record<string, unknown>) => {\n const data = JSON.stringify(event);\n if (channel.readyState === \"open\") channel.send(data);\n else if (channel.readyState === \"connecting\") outbox.push(data);\n };\n const request = (method: string, url: string, payload?: unknown, init: RequestInit = {}) =>\n fetch(url, {\n method,\n headers: { \"Content-Type\": \"application/json\", ...options.headers },\n ...(payload === undefined ? {} : { body: JSON.stringify(payload) }),\n ...init,\n });\n\n let started: VoiceStartResponse;\n try {\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n const response = await request(\"POST\", options.url, {\n ...(options.body && typeof options.body === \"object\" ? options.body : {}),\n sdp: offer.sdp,\n ...(options.threadId ? { threadId: options.threadId } : {}),\n ...(pushToTalk ? { pushToTalk: true } : {}),\n });\n if (!response.ok) {\n const error = await response.json().catch(() => null);\n throw new VoiceCallError(\n error?.error?.message ?? `The voice call could not be started (${response.status}).`,\n { status: response.status, code: error?.error?.code },\n );\n }\n started = await response.json();\n await pc.setRemoteDescription({ type: \"answer\", sdp: started.sdp });\n } catch (error) {\n close();\n throw error instanceof VoiceCallError\n ? error\n : new VoiceCallError(\"The voice call could not be started.\", { cause: error });\n }\n\n const sessionUrl = `${options.url}/${encodeURIComponent(started.sessionId)}`;\n const applyTrack = () => {\n track.enabled = !muted && !pushToTalk;\n };\n\n return {\n sessionId: started.sessionId,\n threadId: started.threadId,\n delegate: started.delegate,\n endsAt: started.endsAt,\n mute(on) {\n muted = on;\n applyTrack();\n },\n setPushToTalk(on) {\n pushToTalk = on;\n applyTrack();\n send({\n type: \"session.update\",\n session: {\n type: \"realtime\",\n audio: {\n input: {\n turn_detection: on ? null : (options.turnDetection ?? { type: \"semantic_vad\" }),\n },\n },\n },\n });\n },\n press() {\n if (!pushToTalk || muted) return;\n send({ type: \"input_audio_buffer.clear\" });\n track.enabled = true;\n },\n release() {\n if (!pushToTalk) return;\n track.enabled = false;\n send({ type: \"input_audio_buffer.commit\" });\n send({ type: \"response.create\" });\n },\n async setBody(body) {\n const response = await request(\"PATCH\", sessionUrl, body ?? {});\n if (!response.ok && response.status !== 409) {\n throw new VoiceCallError(`The voice session could not be updated (${response.status}).`, {\n status: response.status,\n });\n }\n },\n async end() {\n try {\n // `keepalive`, so a hang-up sent as the tab closes still reaches the\n // route: minutes are billed until it does.\n await request(\"DELETE\", sessionUrl, undefined, { keepalive: true });\n } finally {\n close();\n }\n },\n close: () => close(),\n };\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { RPC } from \"../../client/rpc\";\nimport { applyVoiceEvent, initialVoiceState, voiceStatus, type VoiceState } from \"./state\";\nimport { startVoiceCall, type VoiceCall } from \"./voiceCall\";\nimport type { VoiceCaption, VoiceStatus } from \"./wire\";\n\n/**\n * The voice routes, picked out of `RPC` the way `useChat` picks its agent\n * routes: `this.voice(Controller)` mounted at `\"/chat/voice\"` is the key\n * `\"/chat/voice\"`, carrying the controller's `Body`.\n */\ntype VoiceRoutes = {\n [K in keyof RPC as RPC[K] extends { __voice: true } ? K : never]: RPC[K];\n};\n\ntype BodyOf<P extends keyof VoiceRoutes> = VoiceRoutes[P] extends { body: infer B }\n ? B\n : Record<string, unknown>;\n\nexport type UseVoiceSessionParams<P extends keyof VoiceRoutes> = {\n /** The chat thread delegated turns are stored on, as `useChat` names it. */\n threadId?: string;\n /** The controller's `Body`, sent when the call starts. Change it during the\n * call with `setBody`. */\n body?: BodyOf<P>;\n /** Start with push-to-talk instead of voice activity detection. */\n pushToTalk?: boolean;\n /** Values for the route path's `:params`, e.g. `{ siteId }` for\n * `\"/sites/:siteId/voice\"`. */\n params?: Record<string, string>;\n headers?: Record<string, string>;\n /** The call could not start, or failed. */\n onError?: (error: Error) => void;\n /** The call is over, however it ended. */\n onEnd?: () => void;\n};\n\nexport type UseVoiceSessionResult<P extends keyof VoiceRoutes> = {\n status: VoiceStatus;\n /** The last lines said, the user's and the voice's, oldest first. */\n captions: VoiceCaption[];\n /** Why the call could not start or ended early. `code` is\n * `microphone_denied`, `unsupported`, or the route's error code. */\n error: (Error & { code?: string; status?: number }) | null;\n muted: boolean;\n pushToTalk: boolean;\n /** When the server will end the call, in epoch ms. */\n endsAt?: number;\n sessionId?: string;\n /** Asks for the microphone and starts the call. Call it from a user gesture\n * (a click): browsers allow the microphone and audio playback only then. */\n start(): Promise<void>;\n /** Hangs up. */\n end(): Promise<void>;\n mute(muted?: boolean): void;\n setPushToTalk(on: boolean): void;\n /** Push-to-talk: hold to speak (pointer down / key down). */\n press(): void;\n /** Push-to-talk: release to send (pointer up / key up). */\n release(): void;\n /** Sends a new `body` (the selection, the open page) for the next\n * delegated request. */\n setBody(body: BodyOf<P>): Promise<void>;\n};\n\n/**\n * A voice call to an agent, for a route mounted with `this.voice(Controller)`\n * (#921).\n *\n * ```tsx\n * const voice = useVoiceSession(\"/page-builder/voice\", { threadId, body: { pageId } });\n * <button onClick={() => (voice.status === \"idle\" ? voice.start() : voice.end())}>\n * {voice.status}\n * </button>\n * ```\n *\n * Import it from `gemi/ai/voice`, an entry of its own, so an app that never\n * uses voice never bundles it, and use it from a view or a dynamically imported\n * component, so its code is not in the entry chunk.\n *\n * @experimental Not yet verified against a live call.\n */\nexport function useVoiceSession<P extends keyof VoiceRoutes>(\n path: P,\n params: UseVoiceSessionParams<P> = {},\n): UseVoiceSessionResult<P> {\n // Not `useParams()`, which `useChat` uses: it is in a chunk `gemi/client`\n // shares, and reaching it from here made the app's bundler move it out of\n // the entry chunk into one of its own that every route then imports. A path\n // with parameters names them in `params`.\n const routeParams = params.params;\n const url = useMemo(() => {\n let out = String(path).replace(/^[A-Z]+:/, \"\");\n for (const [key, value] of Object.entries(routeParams ?? {})) {\n out = out.replace(`:${key}?`, value).replace(`:${key}`, value);\n }\n return `/api${out}`;\n }, [path, routeParams && JSON.stringify(routeParams)]);\n\n const [state, setState] = useState<VoiceState>(initialVoiceState);\n const [error, setError] = useState<UseVoiceSessionResult<P>[\"error\"]>(null);\n const [muted, setMuted] = useState(false);\n const [pushToTalk, setPushToTalkState] = useState(params.pushToTalk === true);\n const [call, setCall] = useState<VoiceCall | null>(null);\n const callRef = useRef<VoiceCall | null>(null);\n /** A `start()` in flight: a second click must not start a second call. */\n const startingRef = useRef(false);\n const latest = useRef(params);\n latest.current = params;\n const delegateRef = useRef(\"ask_agent\");\n const mounted = useRef(true);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n // Leaving the page ends the call: minutes are billed while it is up.\n void callRef.current?.end().catch(() => undefined);\n callRef.current = null;\n };\n }, []);\n\n const start = useCallback(async () => {\n if (callRef.current || startingRef.current) return;\n startingRef.current = true;\n setError(null);\n setState({ ...initialVoiceState(), phase: \"connecting\" });\n try {\n const { threadId, body, headers } = latest.current;\n const started = await startVoiceCall({\n url,\n threadId,\n body,\n headers,\n pushToTalk,\n onEvent: (event) => {\n if (!mounted.current) return;\n setState((current) =>\n current.phase === \"connecting\"\n ? applyVoiceEvent({ ...current, phase: \"live\" }, event, delegateRef.current)\n : applyVoiceEvent(current, event, delegateRef.current),\n );\n },\n onClose: (closeError) => {\n callRef.current = null;\n if (!mounted.current) return;\n setCall(null);\n setState((current) => ({ ...current, phase: \"ended\", speaking: false, thinking: false }));\n if (closeError) {\n setError(closeError);\n latest.current.onError?.(closeError);\n }\n latest.current.onEnd?.();\n },\n });\n delegateRef.current = started.delegate;\n if (!mounted.current) {\n await started.end().catch(() => undefined);\n return;\n }\n callRef.current = started;\n setCall(started);\n setState((current) =>\n current.phase === \"connecting\" ? { ...current, phase: \"live\" } : current,\n );\n } catch (failure) {\n const reported = failure instanceof Error ? failure : new Error(String(failure));\n if (!mounted.current) return;\n setState((current) => ({ ...current, phase: \"ended\" }));\n setError(reported);\n latest.current.onError?.(reported);\n } finally {\n startingRef.current = false;\n }\n }, [url, pushToTalk]);\n\n const end = useCallback(async () => {\n const current = callRef.current;\n callRef.current = null;\n await current?.end();\n }, []);\n\n const mute = useCallback((on?: boolean) => {\n setMuted((was) => {\n const next = on ?? !was;\n callRef.current?.mute(next);\n return next;\n });\n }, []);\n\n const setPushToTalk = useCallback((on: boolean) => {\n setPushToTalkState(on);\n callRef.current?.setPushToTalk(on);\n }, []);\n\n const press = useCallback(() => {\n callRef.current?.press();\n setState((current) => ({ ...current, userSpeaking: true }));\n }, []);\n\n const release = useCallback(() => {\n callRef.current?.release();\n setState((current) => ({ ...current, userSpeaking: false, thinking: true }));\n }, []);\n\n const setBody = useCallback(async (body: BodyOf<P>) => {\n latest.current = { ...latest.current, body };\n await callRef.current?.setBody(body);\n }, []);\n\n return {\n status: voiceStatus(state),\n captions: state.captions,\n error,\n muted,\n pushToTalk,\n endsAt: call?.endsAt,\n sessionId: call?.sessionId,\n start,\n end,\n mute,\n setPushToTalk,\n press,\n release,\n setBody,\n };\n}\n","/**\n * The built-in end-of-speech detector for `useLiveTranscription` (#929): a\n * small energy (RMS) detector with an adaptive noise floor and hysteresis.\n *\n * WHY ENERGY AND NOT A MODEL. `gpt-live-transcribe` has no server-side turn\n * detection, so the browser decides when the user stopped and commits the\n * turn. A neural VAD (Silero through `@ricky0123/vad-web`) is better in\n * noise, but it ships an ONNX runtime and a model, megabytes against the few\n * hundred bytes here, and needs its wasm/model files served from the app's\n * origin. What this detector has to do is narrow: the user is close to the\n * mic, the browser's own noise suppression and echo cancellation are on, and\n * the question is only \"did the speech stop for `silenceMs`\". An adaptive\n * floor handles a fan or a room; a false end in a long pause costs one early\n * commit, after which the rest of the sentence is the next turn. Apps that\n * need more plug a model in through `vad` (see `VoiceActivityDetector`).\n *\n * Pure: frames in (RMS and a timestamp), `\"start\"` / `\"end\"` out, so it is\n * tested without audio.\n */\n\nexport type EnergyVadOptions = {\n /** Silence after speech before the turn ends. Default 350 ms. */\n silenceMs?: number;\n /** Speech must last this long to count, so a click or a cough doesn't open a\n * turn. Default 120 ms. */\n minSpeechMs?: number;\n /** The lowest RMS (0–1 float samples) that can count as speech, whatever\n * the noise floor. Default 0.015. */\n threshold?: number;\n /** Speech is RMS above the noise floor times this. Default 3. */\n floorRatio?: number;\n};\n\nexport const DEFAULT_SILENCE_MS = 350;\n\nexport type EnergyVad = {\n /** One analysis frame: its RMS and when it was taken (ms). */\n push(rms: number, at: number): \"start\" | \"end\" | null;\n readonly speaking: boolean;\n reset(): void;\n};\n\nexport function createEnergyVad(options: EnergyVadOptions = {}): EnergyVad {\n const silenceMs = options.silenceMs ?? DEFAULT_SILENCE_MS;\n const minSpeechMs = options.minSpeechMs ?? 120;\n const minThreshold = options.threshold ?? 0.015;\n const floorRatio = options.floorRatio ?? 3;\n\n let floor = minThreshold / floorRatio;\n let speaking = false;\n /** When the current run of loud frames began, before it counts as speech. */\n let loudSince: number | null = null;\n /** When the current run of quiet frames began, while speaking. */\n let quietSince: number | null = null;\n\n return {\n get speaking() {\n return speaking;\n },\n reset() {\n speaking = false;\n loudSince = null;\n quietSince = null;\n },\n push(rms, at) {\n const threshold = Math.max(minThreshold, floor * floorRatio);\n if (!speaking) {\n if (rms > threshold) {\n loudSince ??= at;\n if (at - loudSince >= minSpeechMs) {\n speaking = true;\n quietSince = null;\n return \"start\";\n }\n } else {\n loudSince = null;\n // The floor follows the room only while nobody speaks: fast down,\n // slow up, so a rising voice is not learned as noise.\n floor = rms < floor ? floor * 0.9 + rms * 0.1 : floor * 0.995 + rms * 0.005;\n }\n return null;\n }\n // Hysteresis: speech ends below a lower bar than it starts above, so a\n // soft syllable doesn't end the turn.\n if (rms > threshold * 0.7) {\n quietSince = null;\n return null;\n }\n quietSince ??= at;\n if (at - quietSince >= silenceMs) {\n speaking = false;\n loudSince = null;\n quietSince = null;\n return \"end\";\n }\n return null;\n },\n };\n}\n\n/** RMS of float time-domain samples. */\nexport function rmsOf(samples: Float32Array): number {\n let sum = 0;\n for (let i = 0; i < samples.length; i++) sum += samples[i]! * samples[i]!;\n return samples.length ? Math.sqrt(sum / samples.length) : 0;\n}\n","import { createEnergyVad, rmsOf, type EnergyVadOptions } from \"./energyVad\";\n\n/**\n * The browser's half of a live transcription session (#929): the microphone,\n * a raw `RTCPeerConnection` to the vendor, the `oai-events` data channel, a\n * level meter and end-of-speech detection that commits each turn.\n *\n * No vendor SDK, and nothing from `gemi/ai`'s server half: the secret (or the\n * SDP answer) comes from the app's own route, which holds the key.\n *\n * @experimental Follows OpenAI's realtime transcription and WebRTC guides; not\n * yet run against a live session.\n */\n\n/** OpenAI's WebRTC endpoint, for a secret whose route doesn't say otherwise.\n * `clientSecret()` on the server returns `callsUrl` (Azure's host included). */\nexport const OPENAI_REALTIME_CALLS_URL = \"https://api.openai.com/v1/realtime/calls\";\n\n/** What the app's token route answers: `realtime.clientSecret(...)`'s result,\n * or at least its `value`. */\nexport type LiveTranscriptionToken = {\n value: string;\n /** Where to post the SDP offer. Default OpenAI's. */\n callsUrl?: string;\n};\n\n/** A pluggable end-of-speech detector, such as `@ricky0123/vad-web`'s\n * `MicVAD` on the same stream. Call `speechStart` / `speechEnd` as speech\n * begins and ends; `speechEnd` commits the turn. */\nexport type VoiceActivityDetector = (\n stream: MediaStream,\n events: { speechStart(): void; speechEnd(): void },\n) => { stop(): void } | Promise<{ stop(): void }>;\n\nexport type LiveTranscriptionError = Error & { code?: string; status?: number };\n\nexport class LiveTranscriptionFailure extends Error {\n readonly code?: string;\n readonly status?: number;\n\n constructor(message: string, options: { code?: string; status?: number; cause?: unknown } = {}) {\n super(message, { cause: options.cause });\n this.name = \"LiveTranscriptionError\";\n this.code = options.code;\n this.status = options.status;\n }\n}\n\nexport type LiveTranscriptionConnectOptions = {\n /**\n * `\"token\"` (default): `source` gives a client secret and the browser posts\n * its SDP to the vendor itself. `\"sdp\"`: the browser posts `{ sdp, ...body }`\n * to `source` and the route answers `{ sdp }` — the route then creates the\n * call (`realtime.createCall`) and can meter it on the sideband.\n */\n connect?: \"token\" | \"sdp\";\n /** The app's route (POSTed `body` as JSON), or a function giving the token. */\n source: string | (() => Promise<LiveTranscriptionToken>);\n body?: Record<string, unknown>;\n headers?: Record<string, string>;\n /** `\"energy\"` (default): the built-in detector. A function: your own. `false`:\n * no detection; turns end only on `commit()`. */\n vad?: \"energy\" | VoiceActivityDetector | false;\n /** The built-in detector's options; `silenceMs` defaults to 350. */\n energy?: EnergyVadOptions;\n /** The vendor's events from the data channel. */\n onEvent(event: { type: string; [key: string]: unknown }): void;\n /** The channel opened: the session is live. */\n onOpen(): void;\n onSpeechStart(): void;\n /** The detector heard the user stop. `committed`: the turn was committed\n * (there was speech in it and the channel was open). */\n onSpeechEnd(committed: boolean): void;\n /** Microphone level, 0–1, about every 50 ms. */\n onLevel(level: number): void;\n /** Called once when the connection closed or failed. */\n onClose(error?: LiveTranscriptionError): void;\n};\n\nexport type LiveTranscriptionConnection = {\n /** Sends `input_audio_buffer.commit`. False when the channel isn't open. */\n commit(): boolean;\n /** Commits only if speech was heard since the last commit (always, with no\n * detector). For a graceful stop. */\n flush(): boolean;\n /** Any client event, e.g. a `session.update` with new keywords. */\n send(event: Record<string, unknown>): boolean;\n /** Pauses the microphone track (nothing is sent while muted). */\n mute(on: boolean): void;\n close(): void;\n};\n\n/** How often the level meter (and the built-in detector) reads the mic. */\nconst FRAME_MS = 30;\n\nfunction microphoneError(error: unknown): LiveTranscriptionFailure {\n const name = (error as { name?: string } | null)?.name;\n return name === \"NotAllowedError\" || name === \"SecurityError\"\n ? new LiveTranscriptionFailure(\"Microphone access was denied.\", {\n code: \"microphone_denied\",\n cause: error,\n })\n : new LiveTranscriptionFailure(\"No microphone is available.\", {\n code: \"microphone_unavailable\",\n cause: error,\n });\n}\n\nasync function routeError(response: Response, what: string): Promise<LiveTranscriptionFailure> {\n const body: any = await response.json().catch(() => null);\n return new LiveTranscriptionFailure(body?.error?.message ?? `${what} (${response.status}).`, {\n status: response.status,\n code: body?.error?.code,\n });\n}\n\n/**\n * Opens a live transcription session. Resolves once the SDP answer is applied;\n * `onOpen` follows when the data channel opens. `signal` aborts a start in\n * flight (unmount, `stop()` while connecting): everything opened so far is\n * closed and the promise rejects with an `AbortError`.\n */\nexport async function startLiveTranscription(\n options: LiveTranscriptionConnectOptions,\n signal?: AbortSignal,\n): Promise<LiveTranscriptionConnection> {\n if (typeof RTCPeerConnection === \"undefined\" || !navigator.mediaDevices?.getUserMedia) {\n throw new LiveTranscriptionFailure(\"This browser can't stream audio.\", { code: \"unsupported\" });\n }\n const connect = options.connect ?? \"token\";\n const post = (url: string, payload: unknown) =>\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...options.headers },\n body: JSON.stringify(payload),\n signal,\n });\n\n // The token and the microphone in parallel: both are on the critical path.\n const tokenPromise: Promise<LiveTranscriptionToken | null> =\n connect === \"sdp\"\n ? Promise.resolve(null)\n : typeof options.source === \"function\"\n ? options.source()\n : post(options.source, options.body ?? {}).then(async (response) => {\n if (!response.ok) throw await routeError(response, \"Live transcription is unavailable\");\n return response.json();\n });\n tokenPromise.catch(() => undefined);\n\n let stream: MediaStream;\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },\n });\n } catch (error) {\n throw microphoneError(error);\n }\n\n const track = stream.getAudioTracks()[0];\n let pc: RTCPeerConnection | undefined;\n let channel: RTCDataChannel | undefined;\n let audioContext: AudioContext | undefined;\n let meter: ReturnType<typeof setInterval> | undefined;\n let detector: { stop(): void } | undefined;\n let closed = false;\n /** `onClose` is for a session that started; a failed start throws instead. */\n let started = false;\n /** Speech heard since the last commit: a commit with none is pointless. */\n let heardSpeech = false;\n\n const close = (error?: LiveTranscriptionFailure) => {\n if (closed) return;\n closed = true;\n if (meter) clearInterval(meter);\n try {\n detector?.stop();\n } catch {\n // a third-party detector failing to stop must not keep the mic open\n }\n try {\n channel?.close();\n } catch {\n // already closed\n }\n pc?.close();\n for (const t of stream.getTracks()) t.stop();\n void audioContext?.close().catch(() => undefined);\n if (started) options.onClose(error);\n };\n\n const send = (event: Record<string, unknown>) => {\n if (closed || channel?.readyState !== \"open\") return false;\n channel.send(JSON.stringify(event));\n return true;\n };\n const commit = () => {\n const sent = send({ type: \"input_audio_buffer.commit\" });\n if (sent) heardSpeech = false;\n return sent;\n };\n const speech = {\n speechStart() {\n if (closed) return;\n heardSpeech = true;\n options.onSpeechStart();\n },\n speechEnd() {\n if (closed) return;\n // Only a turn with speech in it is committed: the detector's \"end\" after\n // a muted stretch or a reset would commit silence.\n options.onSpeechEnd(heardSpeech ? commit() : false);\n },\n };\n\n try {\n if (!track)\n throw new LiveTranscriptionFailure(\"No microphone is available.\", {\n code: \"microphone_unavailable\",\n });\n pc = new RTCPeerConnection();\n pc.addTrack(track, stream);\n channel = pc.createDataChannel(\"oai-events\");\n channel.addEventListener(\"open\", () => {\n if (!closed) options.onOpen();\n });\n channel.addEventListener(\"message\", (message) => {\n let event: unknown;\n try {\n event = JSON.parse(String(message.data));\n } catch {\n return;\n }\n if (\n event &&\n typeof event === \"object\" &&\n typeof (event as { type?: unknown }).type === \"string\"\n ) {\n options.onEvent(event as { type: string });\n }\n });\n channel.addEventListener(\"close\", () => close());\n const peer = pc;\n peer.addEventListener(\"connectionstatechange\", () => {\n if (peer.connectionState === \"failed\") {\n close(\n new LiveTranscriptionFailure(\"The transcription connection failed.\", {\n code: \"connection_failed\",\n }),\n );\n }\n if (peer.connectionState === \"closed\") close();\n });\n\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n let answer: string;\n if (connect === \"sdp\") {\n if (typeof options.source !== \"string\") {\n throw new TypeError('connect: \"sdp\" needs the route\\'s URL as the source.');\n }\n const response = await post(options.source, { ...options.body, sdp: offer.sdp });\n if (!response.ok) throw await routeError(response, \"Live transcription is unavailable\");\n answer = String(((await response.json()) as { sdp?: unknown }).sdp ?? \"\");\n } else {\n const token = await tokenPromise;\n if (!token || typeof token.value !== \"string\") {\n throw new LiveTranscriptionFailure(\"The token route answered no secret.\", {\n code: \"no_token\",\n });\n }\n const response = await fetch(token.callsUrl ?? OPENAI_REALTIME_CALLS_URL, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token.value}`, \"Content-Type\": \"application/sdp\" },\n body: offer.sdp,\n signal,\n });\n if (!response.ok)\n throw await routeError(response, \"The transcription session could not start\");\n answer = await response.text();\n }\n if (!answer) throw new LiveTranscriptionFailure(\"No SDP answer.\", { code: \"provider_error\" });\n if (signal?.aborted) throw signal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n await pc.setRemoteDescription({ type: \"answer\", sdp: answer });\n\n // The level meter, which also feeds the built-in detector.\n const energy =\n options.vad === undefined || options.vad === \"energy\"\n ? createEnergyVad(options.energy)\n : null;\n if (typeof AudioContext !== \"undefined\") {\n audioContext = new AudioContext();\n void audioContext.resume?.().catch(() => undefined);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 1024;\n audioContext.createMediaStreamSource(stream).connect(analyser);\n const samples = new Float32Array(analyser.fftSize);\n let lastLevel = -1;\n meter = setInterval(() => {\n analyser.getFloatTimeDomainData(samples);\n // A muted track reads silence, so it ends a turn in progress.\n const rms = track.enabled ? rmsOf(samples) : 0;\n const level = Math.min(1, rms / 0.2);\n if (Math.abs(level - lastLevel) >= 0.02) {\n lastLevel = level;\n options.onLevel(level);\n }\n const change = energy?.push(rms, Date.now());\n if (change === \"start\") speech.speechStart();\n else if (change === \"end\") speech.speechEnd();\n }, FRAME_MS);\n } else if (energy) {\n throw new LiveTranscriptionFailure(\"This browser can't analyse audio.\", {\n code: \"unsupported\",\n });\n }\n if (typeof options.vad === \"function\") detector = await options.vad(stream, speech);\n } catch (error) {\n close();\n if (signal?.aborted || (error as { name?: string })?.name === \"AbortError\") throw error;\n throw error instanceof LiveTranscriptionFailure\n ? error\n : new LiveTranscriptionFailure(\"Live transcription could not start.\", { cause: error });\n }\n\n if (closed) {\n throw new LiveTranscriptionFailure(\"The transcription connection closed while starting.\", {\n code: \"connection_failed\",\n });\n }\n started = true;\n return {\n commit,\n flush: () => (heardSpeech || options.vad === false ? commit() : false),\n send,\n mute(on) {\n if (track) track.enabled = !on;\n },\n close: () => close(),\n };\n}\n","/**\n * What `useLiveTranscription` shows, as a pure reducer over a transcription\n * session's data-channel events (#929). Tested without a DOM or a peer\n * connection, like `state.ts` for `useVoiceSession`.\n *\n * The events are OpenAI's (realtime transcription guide):\n * `conversation.item.input_audio_transcription.delta` (`item_id`, `delta`)\n * while the user speaks, and `.completed` (`item_id`, `transcript`, `usage`)\n * once the turn is committed. Completions of different turns may arrive out of\n * order, so turns are keyed and ordered by `item_id`, by when each item was\n * first seen.\n */\n\n/** One finished turn: what the user said between two commits. */\nexport type TranscriptTurn = {\n /** The vendor's item id. */\n id: string;\n text: string;\n /** Audio seconds the vendor billed for it, when it said (duration-billed\n * models such as `gpt-live-transcribe`). */\n seconds?: number;\n};\n\nexport type TranscriptionState = {\n /** Item ids in the order they were first seen. */\n order: string[];\n /** Text so far of items not yet completed. */\n partials: Record<string, string>;\n /** Completed turns, oldest first, at most `MAX_TURNS`. */\n turns: TranscriptTurn[];\n /** Commits sent whose transcript hasn't arrived. */\n pendingCommits: number;\n};\n\nexport const MAX_TURNS = 50;\n\nexport function initialTranscriptionState(): TranscriptionState {\n return { order: [], partials: {}, turns: [], pendingCommits: 0 };\n}\n\n/** The live text: every unfinished item's text, in order. */\nexport function partialText(state: TranscriptionState): string {\n return state.order\n .map((id) => state.partials[id])\n .filter((text): text is string => !!text)\n .join(\" \")\n .trim();\n}\n\nexport type TranscriptionEvent = { type: string; [key: string]: unknown };\n\nexport type TranscriptionEffect =\n | { type: \"final\"; turn: TranscriptTurn }\n | { type: \"error\"; code?: string; message: string };\n\n/** Vendor error codes that are part of normal operation, not failures: a\n * commit with nothing in the buffer (a manual `commit()` in silence). */\nconst BENIGN_ERRORS = new Set([\"input_audio_buffer_commit_empty\"]);\n\nexport function applyTranscriptionEvent(\n state: TranscriptionState,\n event: TranscriptionEvent,\n): { state: TranscriptionState; effects: TranscriptionEffect[] } {\n const itemId = typeof event.item_id === \"string\" ? event.item_id : undefined;\n const seen = (id: string) => (state.order.includes(id) ? state.order : [...state.order, id]);\n\n switch (event.type) {\n case \"conversation.item.input_audio_transcription.delta\": {\n if (!itemId || typeof event.delta !== \"string\") break;\n if (state.turns.some((turn) => turn.id === itemId)) break;\n return {\n state: {\n ...state,\n order: seen(itemId),\n partials: { ...state.partials, [itemId]: (state.partials[itemId] ?? \"\") + event.delta },\n },\n effects: [],\n };\n }\n case \"input_audio_buffer.committed\": {\n if (!itemId) break;\n return { state: { ...state, order: seen(itemId) }, effects: [] };\n }\n case \"conversation.item.input_audio_transcription.completed\": {\n if (!itemId || state.turns.some((turn) => turn.id === itemId)) break;\n const usage = event.usage as { type?: string; seconds?: unknown } | undefined;\n const seconds = usage?.type === \"duration\" ? Number(usage.seconds) : NaN;\n const turn: TranscriptTurn = {\n id: itemId,\n text: typeof event.transcript === \"string\" ? event.transcript.trim() : \"\",\n ...(Number.isFinite(seconds) ? { seconds } : {}),\n };\n const order = seen(itemId);\n const { [itemId]: _done, ...partials } = state.partials;\n const all = [...state.turns, turn].sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));\n const turns = all.slice(-MAX_TURNS);\n // Turns that fell off the end leave `order` too, so a long session\n // doesn't grow it without bound. Items still pending stay.\n const dropped = new Set(all.slice(0, -MAX_TURNS).map((t) => t.id));\n return {\n state: {\n ...state,\n order: dropped.size ? order.filter((id) => !dropped.has(id)) : order,\n partials,\n turns,\n pendingCommits: Math.max(0, state.pendingCommits - 1),\n },\n effects: [{ type: \"final\", turn }],\n };\n }\n case \"conversation.item.input_audio_transcription.failed\": {\n const { [itemId ?? \"\"]: _dropped, ...partials } = state.partials;\n const error = (event.error ?? {}) as { code?: string; message?: string };\n return {\n state: { ...state, partials, pendingCommits: Math.max(0, state.pendingCommits - 1) },\n effects: [\n { type: \"error\", code: error.code, message: error.message ?? \"Transcription failed.\" },\n ],\n };\n }\n case \"error\": {\n const error = (event.error ?? {}) as { code?: string; message?: string };\n if (error.code && BENIGN_ERRORS.has(error.code)) {\n return {\n state: { ...state, pendingCommits: Math.max(0, state.pendingCommits - 1) },\n effects: [],\n };\n }\n return {\n state,\n effects: [{ type: \"error\", code: error.code, message: error.message ?? \"Vendor error.\" }],\n };\n }\n }\n return { state, effects: [] };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { EnergyVadOptions } from \"./energyVad\";\nimport {\n startLiveTranscription,\n type LiveTranscriptionConnection,\n type LiveTranscriptionError,\n type LiveTranscriptionToken,\n type VoiceActivityDetector,\n} from \"./liveTranscription\";\nimport {\n applyTranscriptionEvent,\n initialTranscriptionState,\n partialText,\n type TranscriptionState,\n type TranscriptTurn,\n} from \"./transcriptionState\";\n\nexport type LiveTranscriptionStatus = \"idle\" | \"connecting\" | \"listening\";\n\nexport type UseLiveTranscriptionOptions = {\n /** `\"token\"` (default) or `\"sdp\"`. See the docs: `\"sdp\"` lets the route\n * create the call and meter it on the sideband. */\n connect?: \"token\" | \"sdp\";\n /** Sent to the route as JSON with each start. */\n body?: Record<string, unknown>;\n headers?: Record<string, string>;\n /** Silence after speech that ends a turn, for the built-in detector.\n * Default 350 ms. */\n silenceMs?: number;\n /** More options for the built-in detector. */\n energy?: Omit<EnergyVadOptions, \"silenceMs\">;\n /** `\"energy\"` (default), your own detector (e.g. `@ricky0123/vad-web`), or\n * `false` to end turns only with `commit()`. */\n vad?: \"energy\" | VoiceActivityDetector | false;\n /** The live text changed (a delta arrived, or a turn completed). */\n onPartial?: (text: string) => void;\n /** A turn's final transcript. */\n onFinal?: (turn: TranscriptTurn) => void;\n onSpeechStart?: () => void;\n /** The user stopped; the turn was committed and its final is on the way. */\n onSpeechEnd?: () => void;\n onError?: (error: LiveTranscriptionError) => void;\n};\n\nexport type UseLiveTranscriptionResult = {\n status: LiveTranscriptionStatus;\n /** What the user is saying now, before the turn is committed. */\n partial: string;\n /** Finished turns, oldest first (the last 50). */\n turns: TranscriptTurn[];\n /** The user is speaking, by the end-of-speech detector. */\n speaking: boolean;\n /** Microphone level, 0–1. */\n level: number;\n muted: boolean;\n /** The last failure. `code` is `microphone_denied`,\n * `microphone_unavailable`, `unsupported`, `connection_failed`, the\n * route's error code, or the vendor's. */\n error: LiveTranscriptionError | null;\n /** Asks for the microphone and opens the session. Call it from a user\n * gesture. Does nothing while a session is starting or live. */\n start(): Promise<void>;\n /** Closes the session and releases the microphone. `{ flush: true }`\n * commits speech in progress first and waits (up to `timeoutMs`, default\n * 1500) for its final transcript. */\n stop(options?: { flush?: boolean; timeoutMs?: number }): Promise<void>;\n /** Ends the turn now (`input_audio_buffer.commit`), e.g. for push-to-talk or\n * a \"send\" button. */\n commit(): void;\n mute(on?: boolean): void;\n /** Clears `turns` and `partial` (not the session). */\n reset(): void;\n};\n\n/**\n * Live speech-to-text (#929): the microphone streams to a transcription\n * session (`gpt-live-transcribe` by default) over WebRTC. `partial` grows as\n * the user speaks; when they stop for `silenceMs`, the turn is committed and\n * its final transcript arrives in `turns` and `onFinal`. Nothing is spoken\n * back.\n *\n * `source` is the app's route that answers a client secret (from\n * `realtime.clientSecret({ session: { type: \"transcription\", … } })`), or a\n * function returning one. With `connect: \"sdp\"`, it is the route that takes\n * the SDP offer and answers `{ sdp }`.\n *\n * ```tsx\n * const live = useLiveTranscription(\"/api/voice/token\", {\n * onFinal: (turn) => route(turn.text),\n * });\n * <button onClick={() => (live.status === \"idle\" ? live.start() : live.stop())}>Voice</button>\n * <p>{live.partial}</p>\n * ```\n *\n * From `gemi/ai/voice`, so only the views that use it load it.\n *\n * @experimental Tested against faked browser APIs only.\n */\nexport function useLiveTranscription(\n source: string | (() => Promise<LiveTranscriptionToken>),\n options: UseLiveTranscriptionOptions = {},\n): UseLiveTranscriptionResult {\n const [status, setStatus] = useState<LiveTranscriptionStatus>(\"idle\");\n const [transcript, setTranscript] = useState<TranscriptionState>(initialTranscriptionState);\n const [speaking, setSpeaking] = useState(false);\n const [level, setLevel] = useState(0);\n const [muted, setMuted] = useState(false);\n const [error, setError] = useState<LiveTranscriptionError | null>(null);\n\n const latest = useRef({ source, options });\n latest.current = { source, options };\n const stateRef = useRef(transcript);\n const connectionRef = useRef<LiveTranscriptionConnection | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n /** Resolves `stop({ flush })`'s wait when the last pending final arrives. */\n const drainedRef = useRef<(() => void) | null>(null);\n const mounted = useRef(true);\n\n const update = useCallback((next: TranscriptionState) => {\n const before = partialText(stateRef.current);\n stateRef.current = next;\n if (!mounted.current) return;\n setTranscript(next);\n const after = partialText(next);\n if (after !== before) latest.current.options.onPartial?.(after);\n }, []);\n\n const fail = useCallback((failure: LiveTranscriptionError) => {\n if (!mounted.current) return;\n setError(failure);\n latest.current.options.onError?.(failure);\n }, []);\n\n const teardown = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n const connection = connectionRef.current;\n connectionRef.current = null;\n connection?.close();\n }, []);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n // Leaving the page releases the mic and ends the session (it is billed\n // while open).\n teardown();\n };\n }, [teardown]);\n\n const start = useCallback(async () => {\n if (connectionRef.current || abortRef.current) return;\n const abort = new AbortController();\n abortRef.current = abort;\n setError(null);\n setStatus(\"connecting\");\n const { source: src, options: opts } = latest.current;\n let opened = false;\n try {\n const connection = await startLiveTranscription(\n {\n connect: opts.connect,\n source: src,\n body: opts.body,\n headers: opts.headers,\n vad: opts.vad,\n energy: { ...opts.energy, silenceMs: opts.silenceMs },\n onOpen: () => {\n opened = true;\n if (mounted.current && connectionRef.current) setStatus(\"listening\");\n },\n onEvent: (event) => {\n const { state, effects } = applyTranscriptionEvent(stateRef.current, event);\n if (state !== stateRef.current) update(state);\n for (const effect of effects) {\n if (effect.type === \"final\") {\n if (mounted.current) latest.current.options.onFinal?.(effect.turn);\n } else {\n fail(Object.assign(new Error(effect.message), { code: effect.code }));\n }\n }\n if (stateRef.current.pendingCommits === 0) drainedRef.current?.();\n },\n onSpeechStart: () => {\n if (!mounted.current) return;\n setSpeaking(true);\n latest.current.options.onSpeechStart?.();\n },\n onSpeechEnd: (committed) => {\n if (committed) {\n update({ ...stateRef.current, pendingCommits: stateRef.current.pendingCommits + 1 });\n }\n if (!mounted.current) return;\n setSpeaking(false);\n latest.current.options.onSpeechEnd?.();\n },\n onLevel: (value) => {\n if (mounted.current) setLevel(value);\n },\n onClose: (closeError) => {\n connectionRef.current = null;\n drainedRef.current?.();\n if (!mounted.current) return;\n setStatus(\"idle\");\n setSpeaking(false);\n setLevel(0);\n if (closeError) fail(closeError);\n },\n },\n abort.signal,\n );\n if (abort.signal.aborted) {\n connection.close();\n return;\n }\n connectionRef.current = connection;\n connection.mute(muted);\n if (opened && mounted.current) setStatus(\"listening\");\n } catch (failure) {\n if (abort.signal.aborted) return;\n if (mounted.current) setStatus(\"idle\");\n fail(failure instanceof Error ? failure : new Error(String(failure)));\n } finally {\n if (abortRef.current === abort) abortRef.current = null;\n }\n }, [fail, update, muted]);\n\n const commit = useCallback(() => {\n if (connectionRef.current?.commit()) {\n update({ ...stateRef.current, pendingCommits: stateRef.current.pendingCommits + 1 });\n }\n }, [update]);\n\n const stop = useCallback(\n async ({ flush = false, timeoutMs = 1500 }: { flush?: boolean; timeoutMs?: number } = {}) => {\n const connection = connectionRef.current;\n if (flush && connection) {\n if (connection.flush()) {\n update({ ...stateRef.current, pendingCommits: stateRef.current.pendingCommits + 1 });\n }\n if (stateRef.current.pendingCommits > 0) {\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, timeoutMs);\n drainedRef.current = () => {\n clearTimeout(timer);\n resolve();\n };\n });\n drainedRef.current = null;\n }\n }\n teardown();\n if (mounted.current) {\n setStatus(\"idle\");\n setSpeaking(false);\n setLevel(0);\n }\n },\n [teardown, update],\n );\n\n const mute = useCallback((on?: boolean) => {\n setMuted((was) => {\n const next = on ?? !was;\n connectionRef.current?.mute(next);\n return next;\n });\n }, []);\n\n const reset = useCallback(\n () =>\n update({ ...initialTranscriptionState(), pendingCommits: stateRef.current.pendingCommits }),\n [update],\n );\n\n return {\n status,\n partial: partialText(transcript),\n turns: transcript.turns,\n speaking,\n level,\n muted,\n error,\n start,\n stop,\n commit,\n mute,\n reset,\n };\n}\n"],"mappings":";;AA2BA,SAAgB,oBAAgC;CAC9C,OAAO;EACL,OAAO;EACP,cAAc;EACd,UAAU;EACV,UAAU;EACV,SAAS,CAAC;EACV,UAAU,CAAC;CACb;AACF;AAEA,SAAgB,YAAY,OAAgC;CAC1D,IAAI,MAAM,UAAU,QAAQ,OAAO;CACnC,IAAI,MAAM,UAAU,cAAc,OAAO;CACzC,IAAI,MAAM,UAAU,SAAS,OAAO;CACpC,IAAI,MAAM,UAAU,OAAO;CAC3B,IAAI,MAAM,cAAc,OAAO;CAC/B,IAAI,MAAM,UAAU,OAAO;CAC3B,IAAI,MAAM,QAAQ,QAAQ,OAAO;CACjC,OAAO;AACT;;AAKA,SAAgB,gBAAgB,OAAmB,OAAc,UAA8B;CAC7F,QAAQ,MAAM,MAAd;EACE,KAAK,qCACH,OAAO;GAAE,GAAG;GAAO,cAAc;GAAM,UAAU;EAAM;EACzD,KAAK;EACL,KAAK,gCACH,OAAO;GAAE,GAAG;GAAO,cAAc;GAAO,UAAU;EAAK;EACzD,KAAK,+BACH,OAAO;GAAE,GAAG;GAAO,UAAU;GAAM,UAAU;EAAM;EACrD,KAAK;EACL,KAAK,+BACH,OAAO;GAAE,GAAG;GAAO,UAAU;EAAM;EACrC,KAAK,iBAAiB;GACpB,IAAI,OAAO;IAAE,GAAG;IAAO,UAAU;GAAM;GACvC,KAAK,MAAM,QAAQ,MAAM,UAAU,UAAU,CAAC,GAC5C,IAAI,MAAM,SAAS,mBAAmB,KAAK,SAAS,YAAY,KAAK,SACnE,OAAO,WAAW,MAAM,KAAK,OAAO;GAGxC,OAAO;EACT;EACA,KAAK,yCACH,OAAO,MAAM,SAAS,YAAY,MAAM,UAAU,WAAW,OAAO,MAAM,OAAO,IAAI;EACvF,KAAK;EACL,KAAK;EACL,KAAK,0BAA0B;GAC7B,MAAM,OAAO,MAAM;GACnB,IAAI,MAAM,SAAS,0BAA0B,MAAM,QAAQ,SAAS,KAAK,OAAO,GAC9E,OAAO;IAAE,GAAG;IAAO,SAAS,MAAM,QAAQ,QAAQ,OAAO,OAAO,KAAK,OAAO;GAAE;GAEhF,OAAO;EACT;EACA,KAAK,qDACH,OAAO,QAAQ,OAAO,MAAM,SAAS,QAAQ,MAAM,SAAS,IAAI,KAAK;EACvE,KAAK,yDACH,OAAO,QAAQ,OAAO,MAAM,SAAS,QAAQ,MAAM,cAAc,IAAI,IAAI;EAC3E,KAAK;EACL,KAAK,mCACH,OAAO,QAAQ,OAAO,MAAM,SAAS,aAAa,MAAM,SAAS,IAAI,KAAK;EAC5E,KAAK;EACL,KAAK,kCACH,OAAO,QAAQ,OAAO,MAAM,SAAS,aAAa,MAAM,cAAc,IAAI,IAAI;EAChF,SACE,OAAO;CACX;AACF;AAEA,SAAS,WAAW,OAAmB,QAA4B;CACjE,OAAO,MAAM,QAAQ,SAAS,MAAM,IAAI,QAAQ;EAAE,GAAG;EAAO,SAAS,CAAC,GAAG,MAAM,SAAS,MAAM;CAAE;AAClG;;AAGA,SAAS,QACP,OACA,IACA,MACA,MACA,OACY;CACZ,IAAI,CAAC,IAAI,OAAO;CAChB,MAAM,QAAQ,MAAM,SAAS,WAAW,SAAS,KAAK,OAAO,EAAE;CAC/D,MAAM,WAAW,CAAC,GAAG,MAAM,QAAQ;CACnC,IAAI,UAAU,IAAI;EAChB,IAAI,CAAC,MAAM,OAAO;EAClB,SAAS,KAAK;GAAE;GAAI;GAAM;GAAM;EAAM,CAAC;CACzC,OAAO;EACL,MAAM,OAAO,SAAS;EACtB,SAAS,SAAS;GAAE,GAAG;GAAM,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK,OAAO;GAAM;EAAM;CACzF;CACA,OAAO;EAAE,GAAG;EAAO,UAAU,SAAS,MAAM,GAAa;CAAE;AAC7D;;;AC1EA,IAAa,iBAAb,cAAoC,MAAM;CACxC;CACA;CAEA,YAAY,SAAiB,UAA+D,CAAC,GAAG;EAC9F,MAAM,SAAS,EAAE,OAAO,QAAQ,MAAM,CAAC;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS,QAAQ;EACtB,KAAK,OAAO,QAAQ;CACtB;AACF;AAEA,eAAsB,eAAe,SAA+C;CAClF,IAAI,OAAO,sBAAsB,eAAe,CAAC,UAAU,cAAc,cACvE,MAAM,IAAI,eAAe,wCAAwC,EAAE,MAAM,cAAc,CAAC;CAG1F,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,UAAU,aAAa,aAAa,EACjD,OAAO;GAAE,kBAAkB;GAAM,kBAAkB;GAAM,iBAAiB;EAAK,EACjF,CAAC;CACH,SAAS,OAAO;EACd,MAAM,IAAI,eAAe,oCAAoC;GAC3D,MAAM;GACN,OAAO;EACT,CAAC;CACH;CACA,MAAM,QAAQ,OAAO,eAAe,CAAC,CAAC;CACtC,IAAI,aAAa,QAAQ,eAAe;CACxC,IAAI,QAAQ;CACZ,MAAM,UAAU,CAAC;CAEjB,MAAM,KAAK,IAAI,kBAAkB;CACjC,MAAM,QAAQ,SAAS,cAAc,OAAO;CAC5C,MAAM,WAAW;CACjB,GAAG,WAAW,UAAU;EACtB,MAAM,YAAY,MAAM,QAAQ,MAAM,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;CACrE;CACA,GAAG,SAAS,OAAO,MAAM;CACzB,MAAM,UAAU,GAAG,kBAAkB,YAAY;CAEjD,IAAI,SAAS;CACb,MAAM,SAAS,UAAkB;EAC/B,IAAI,QAAQ;EACZ,SAAS;EACT,IAAI;GACF,QAAQ,MAAM;EAChB,QAAQ,CAER;EACA,GAAG,MAAM;EACT,KAAK,MAAM,KAAK,OAAO,UAAU,GAAG,EAAE,KAAK;EAC3C,MAAM,YAAY;EAClB,QAAQ,QAAQ,KAAK;CACvB;CAEA,QAAQ,iBAAiB,YAAY,YAAY;EAC/C,IAAI;EACJ,IAAI;GACF,QAAQ,KAAK,MAAM,OAAO,QAAQ,IAAI,CAAC;EACzC,QAAQ;GACN;EACF;EACA,IACE,SACA,OAAO,UAAU,YACjB,OAAQ,MAA6B,SAAS,UAE9C,QAAQ,QAAQ,KAAyB;CAE7C,CAAC;CACD,QAAQ,iBAAiB,eAAe,MAAM,CAAC;CAC/C,GAAG,iBAAiB,+BAA+B;EACjD,IAAI,GAAG,oBAAoB,UAAU,MAAM,IAAI,eAAe,8BAA8B,CAAC;EAC7F,IAAI,GAAG,oBAAoB,UAAU,MAAM;CAC7C,CAAC;CAID,MAAM,SAAmB,CAAC;CAC1B,QAAQ,iBAAiB,cAAc;EACrC,KAAK,MAAM,QAAQ,OAAO,OAAO,CAAC,GAAG,QAAQ,KAAK,IAAI;CACxD,CAAC;CACD,MAAM,QAAQ,UAAmC;EAC/C,MAAM,OAAO,KAAK,UAAU,KAAK;EACjC,IAAI,QAAQ,eAAe,QAAQ,QAAQ,KAAK,IAAI;OAC/C,IAAI,QAAQ,eAAe,cAAc,OAAO,KAAK,IAAI;CAChE;CACA,MAAM,WAAW,QAAgB,KAAa,SAAmB,OAAoB,CAAC,MACpF,MAAM,KAAK;EACT;EACA,SAAS;GAAE,gBAAgB;GAAoB,GAAG,QAAQ;EAAQ;EAClE,GAAI,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,OAAO,EAAE;EACjE,GAAG;CACL,CAAC;CAEH,IAAI;CACJ,IAAI;EACF,MAAM,QAAQ,MAAM,GAAG,YAAY;EACnC,MAAM,GAAG,oBAAoB,KAAK;EAClC,MAAM,WAAW,MAAM,QAAQ,QAAQ,QAAQ,KAAK;GAClD,GAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO,CAAC;GACvE,KAAK,MAAM;GACX,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;GACzD,GAAI,aAAa,EAAE,YAAY,KAAK,IAAI,CAAC;EAC3C,CAAC;EACD,IAAI,CAAC,SAAS,IAAI;GAChB,MAAM,QAAQ,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;GACpD,MAAM,IAAI,eACR,OAAO,OAAO,WAAW,wCAAwC,SAAS,OAAO,KACjF;IAAE,QAAQ,SAAS;IAAQ,MAAM,OAAO,OAAO;GAAK,CACtD;EACF;EACA,UAAU,MAAM,SAAS,KAAK;EAC9B,MAAM,GAAG,qBAAqB;GAAE,MAAM;GAAU,KAAK,QAAQ;EAAI,CAAC;CACpE,SAAS,OAAO;EACd,MAAM;EACN,MAAM,iBAAiB,iBACnB,QACA,IAAI,eAAe,wCAAwC,EAAE,OAAO,MAAM,CAAC;CACjF;CAEA,MAAM,aAAa,GAAG,QAAQ,IAAI,GAAG,mBAAmB,QAAQ,SAAS;CACzE,MAAM,mBAAmB;EACvB,MAAM,UAAU,CAAC,SAAS,CAAC;CAC7B;CAEA,OAAO;EACL,WAAW,QAAQ;EACnB,UAAU,QAAQ;EAClB,UAAU,QAAQ;EAClB,QAAQ,QAAQ;EAChB,KAAK,IAAI;GACP,QAAQ;GACR,WAAW;EACb;EACA,cAAc,IAAI;GAChB,aAAa;GACb,WAAW;GACX,KAAK;IACH,MAAM;IACN,SAAS;KACP,MAAM;KACN,OAAO,EACL,OAAO,EACL,gBAAgB,KAAK,OAAQ,QAAQ,iBAAiB,EAAE,MAAM,eAAe,EAC/E,EACF;IACF;GACF,CAAC;EACH;EACA,QAAQ;GACN,IAAI,CAAC,cAAc,OAAO;GAC1B,KAAK,EAAE,MAAM,2BAA2B,CAAC;GACzC,MAAM,UAAU;EAClB;EACA,UAAU;GACR,IAAI,CAAC,YAAY;GACjB,MAAM,UAAU;GAChB,KAAK,EAAE,MAAM,4BAA4B,CAAC;GAC1C,KAAK,EAAE,MAAM,kBAAkB,CAAC;EAClC;EACA,MAAM,QAAQ,MAAM;GAClB,MAAM,WAAW,MAAM,QAAQ,SAAS,YAAY,QAAQ,CAAC,CAAC;GAC9D,IAAI,CAAC,SAAS,MAAM,SAAS,WAAW,KACtC,MAAM,IAAI,eAAe,2CAA2C,SAAS,OAAO,KAAK,EACvF,QAAQ,SAAS,OACnB,CAAC;EAEL;EACA,MAAM,MAAM;GACV,IAAI;IAGF,MAAM,QAAQ,UAAU,YAAY,KAAA,GAAW,EAAE,WAAW,KAAK,CAAC;GACpE,UAAU;IACR,MAAM;GACR;EACF;EACA,aAAa,MAAM;CACrB;AACF;;;;;;;;;;;;;;;;;;;;ACpJA,SAAgB,gBACd,MACA,SAAmC,CAAC,GACV;CAK1B,MAAM,cAAc,OAAO;CAC3B,MAAM,MAAM,cAAc;EACxB,IAAI,MAAM,OAAO,IAAI,CAAC,CAAC,QAAQ,YAAY,EAAE;EAC7C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,CAAC,CAAC,GACzD,MAAM,IAAI,QAAQ,IAAI,IAAI,IAAI,KAAK,CAAC,CAAC,QAAQ,IAAI,OAAO,KAAK;EAE/D,OAAO,OAAO;CAChB,GAAG,CAAC,MAAM,eAAe,KAAK,UAAU,WAAW,CAAC,CAAC;CAErD,MAAM,CAAC,OAAO,YAAY,SAAqB,iBAAiB;CAChE,MAAM,CAAC,OAAO,YAAY,SAA4C,IAAI;CAC1E,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,YAAY,sBAAsB,SAAS,OAAO,eAAe,IAAI;CAC5E,MAAM,CAAC,MAAM,WAAW,SAA2B,IAAI;CACvD,MAAM,UAAU,OAAyB,IAAI;;CAE7C,MAAM,cAAc,OAAO,KAAK;CAChC,MAAM,SAAS,OAAO,MAAM;CAC5B,OAAO,UAAU;CACjB,MAAM,cAAc,OAAO,WAAW;CACtC,MAAM,UAAU,OAAO,IAAI;CAE3B,gBAAgB;EACd,QAAQ,UAAU;EAClB,aAAa;GACX,QAAQ,UAAU;GAElB,QAAa,SAAS,IAAI,CAAC,CAAC,YAAY,KAAA,CAAS;GACjD,QAAQ,UAAU;EACpB;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,QAAQ,YAAY,YAAY;EACpC,IAAI,QAAQ,WAAW,YAAY,SAAS;EAC5C,YAAY,UAAU;EACtB,SAAS,IAAI;EACb,SAAS;GAAE,GAAG,kBAAkB;GAAG,OAAO;EAAa,CAAC;EACxD,IAAI;GACF,MAAM,EAAE,UAAU,MAAM,YAAY,OAAO;GAC3C,MAAM,UAAU,MAAM,eAAe;IACnC;IACA;IACA;IACA;IACA;IACA,UAAU,UAAU;KAClB,IAAI,CAAC,QAAQ,SAAS;KACtB,UAAU,YACR,QAAQ,UAAU,eACd,gBAAgB;MAAE,GAAG;MAAS,OAAO;KAAO,GAAG,OAAO,YAAY,OAAO,IACzE,gBAAgB,SAAS,OAAO,YAAY,OAAO,CACzD;IACF;IACA,UAAU,eAAe;KACvB,QAAQ,UAAU;KAClB,IAAI,CAAC,QAAQ,SAAS;KACtB,QAAQ,IAAI;KACZ,UAAU,aAAa;MAAE,GAAG;MAAS,OAAO;MAAS,UAAU;MAAO,UAAU;KAAM,EAAE;KACxF,IAAI,YAAY;MACd,SAAS,UAAU;MACnB,OAAO,QAAQ,UAAU,UAAU;KACrC;KACA,OAAO,QAAQ,QAAQ;IACzB;GACF,CAAC;GACD,YAAY,UAAU,QAAQ;GAC9B,IAAI,CAAC,QAAQ,SAAS;IACpB,MAAM,QAAQ,IAAI,CAAC,CAAC,YAAY,KAAA,CAAS;IACzC;GACF;GACA,QAAQ,UAAU;GAClB,QAAQ,OAAO;GACf,UAAU,YACR,QAAQ,UAAU,eAAe;IAAE,GAAG;IAAS,OAAO;GAAO,IAAI,OACnE;EACF,SAAS,SAAS;GAChB,MAAM,WAAW,mBAAmB,QAAQ,UAAU,IAAI,MAAM,OAAO,OAAO,CAAC;GAC/E,IAAI,CAAC,QAAQ,SAAS;GACtB,UAAU,aAAa;IAAE,GAAG;IAAS,OAAO;GAAQ,EAAE;GACtD,SAAS,QAAQ;GACjB,OAAO,QAAQ,UAAU,QAAQ;EACnC,UAAU;GACR,YAAY,UAAU;EACxB;CACF,GAAG,CAAC,KAAK,UAAU,CAAC;CAEpB,MAAM,MAAM,YAAY,YAAY;EAClC,MAAM,UAAU,QAAQ;EACxB,QAAQ,UAAU;EAClB,MAAM,SAAS,IAAI;CACrB,GAAG,CAAC,CAAC;CAEL,MAAM,OAAO,aAAa,OAAiB;EACzC,UAAU,QAAQ;GAChB,MAAM,OAAO,MAAM,CAAC;GACpB,QAAQ,SAAS,KAAK,IAAI;GAC1B,OAAO;EACT,CAAC;CACH,GAAG,CAAC,CAAC;CAEL,MAAM,gBAAgB,aAAa,OAAgB;EACjD,mBAAmB,EAAE;EACrB,QAAQ,SAAS,cAAc,EAAE;CACnC,GAAG,CAAC,CAAC;CAEL,MAAM,QAAQ,kBAAkB;EAC9B,QAAQ,SAAS,MAAM;EACvB,UAAU,aAAa;GAAE,GAAG;GAAS,cAAc;EAAK,EAAE;CAC5D,GAAG,CAAC,CAAC;CAEL,MAAM,UAAU,kBAAkB;EAChC,QAAQ,SAAS,QAAQ;EACzB,UAAU,aAAa;GAAE,GAAG;GAAS,cAAc;GAAO,UAAU;EAAK,EAAE;CAC7E,GAAG,CAAC,CAAC;CAEL,MAAM,UAAU,YAAY,OAAO,SAAoB;EACrD,OAAO,UAAU;GAAE,GAAG,OAAO;GAAS;EAAK;EAC3C,MAAM,QAAQ,SAAS,QAAQ,IAAI;CACrC,GAAG,CAAC,CAAC;CAEL,OAAO;EACL,QAAQ,YAAY,KAAK;EACzB,UAAU,MAAM;EAChB;EACA;EACA;EACA,QAAQ,MAAM;EACd,WAAW,MAAM;EACjB;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;ACjMA,IAAa,qBAAqB;AASlC,SAAgB,gBAAgB,UAA4B,CAAC,GAAc;CACzE,MAAM,YAAY,QAAQ,aAAA;CAC1B,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,eAAe,QAAQ,aAAa;CAC1C,MAAM,aAAa,QAAQ,cAAc;CAEzC,IAAI,QAAQ,eAAe;CAC3B,IAAI,WAAW;;CAEf,IAAI,YAA2B;;CAE/B,IAAI,aAA4B;CAEhC,OAAO;EACL,IAAI,WAAW;GACb,OAAO;EACT;EACA,QAAQ;GACN,WAAW;GACX,YAAY;GACZ,aAAa;EACf;EACA,KAAK,KAAK,IAAI;GACZ,MAAM,YAAY,KAAK,IAAI,cAAc,QAAQ,UAAU;GAC3D,IAAI,CAAC,UAAU;IACb,IAAI,MAAM,WAAW;KACnB,cAAc;KACd,IAAI,KAAK,aAAa,aAAa;MACjC,WAAW;MACX,aAAa;MACb,OAAO;KACT;IACF,OAAO;KACL,YAAY;KAGZ,QAAQ,MAAM,QAAQ,QAAQ,KAAM,MAAM,KAAM,QAAQ,OAAQ,MAAM;IACxE;IACA,OAAO;GACT;GAGA,IAAI,MAAM,YAAY,IAAK;IACzB,aAAa;IACb,OAAO;GACT;GACA,eAAe;GACf,IAAI,KAAK,cAAc,WAAW;IAChC,WAAW;IACX,YAAY;IACZ,aAAa;IACb,OAAO;GACT;GACA,OAAO;EACT;CACF;AACF;;AAGA,SAAgB,MAAM,SAA+B;CACnD,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,OAAO,QAAQ,KAAM,QAAQ;CACtE,OAAO,QAAQ,SAAS,KAAK,KAAK,MAAM,QAAQ,MAAM,IAAI;AAC5D;;;;;;;;;;;;;;;;ACzFA,IAAa,4BAA4B;AAoBzC,IAAa,2BAAb,cAA8C,MAAM;CAClD;CACA;CAEA,YAAY,SAAiB,UAA+D,CAAC,GAAG;EAC9F,MAAM,SAAS,EAAE,OAAO,QAAQ,MAAM,CAAC;EACvC,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,SAAS,QAAQ;CACxB;AACF;;AA+CA,IAAM,WAAW;AAEjB,SAAS,gBAAgB,OAA0C;CACjE,MAAM,OAAQ,OAAoC;CAClD,OAAO,SAAS,qBAAqB,SAAS,kBAC1C,IAAI,yBAAyB,iCAAiC;EAC5D,MAAM;EACN,OAAO;CACT,CAAC,IACD,IAAI,yBAAyB,+BAA+B;EAC1D,MAAM;EACN,OAAO;CACT,CAAC;AACP;AAEA,eAAe,WAAW,UAAoB,MAAiD;CAC7F,MAAM,OAAY,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;CACxD,OAAO,IAAI,yBAAyB,MAAM,OAAO,WAAW,GAAG,KAAK,IAAI,SAAS,OAAO,KAAK;EAC3F,QAAQ,SAAS;EACjB,MAAM,MAAM,OAAO;CACrB,CAAC;AACH;;;;;;;AAQA,eAAsB,uBACpB,SACA,QACsC;CACtC,IAAI,OAAO,sBAAsB,eAAe,CAAC,UAAU,cAAc,cACvE,MAAM,IAAI,yBAAyB,oCAAoC,EAAE,MAAM,cAAc,CAAC;CAEhG,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,QAAQ,KAAa,YACzB,MAAM,KAAK;EACT,QAAQ;EACR,SAAS;GAAE,gBAAgB;GAAoB,GAAG,QAAQ;EAAQ;EAClE,MAAM,KAAK,UAAU,OAAO;EAC5B;CACF,CAAC;CAGH,MAAM,eACJ,YAAY,QACR,QAAQ,QAAQ,IAAI,IACpB,OAAO,QAAQ,WAAW,aACxB,QAAQ,OAAO,IACf,KAAK,QAAQ,QAAQ,QAAQ,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,aAAa;EAChE,IAAI,CAAC,SAAS,IAAI,MAAM,MAAM,WAAW,UAAU,mCAAmC;EACtF,OAAO,SAAS,KAAK;CACvB,CAAC;CACT,aAAa,YAAY,KAAA,CAAS;CAElC,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,UAAU,aAAa,aAAa,EACjD,OAAO;GAAE,kBAAkB;GAAM,kBAAkB;GAAM,iBAAiB;EAAK,EACjF,CAAC;CACH,SAAS,OAAO;EACd,MAAM,gBAAgB,KAAK;CAC7B;CAEA,MAAM,QAAQ,OAAO,eAAe,CAAC,CAAC;CACtC,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI,SAAS;;CAEb,IAAI,UAAU;;CAEd,IAAI,cAAc;CAElB,MAAM,SAAS,UAAqC;EAClD,IAAI,QAAQ;EACZ,SAAS;EACT,IAAI,OAAO,cAAc,KAAK;EAC9B,IAAI;GACF,UAAU,KAAK;EACjB,QAAQ,CAER;EACA,IAAI;GACF,SAAS,MAAM;EACjB,QAAQ,CAER;EACA,IAAI,MAAM;EACV,KAAK,MAAM,KAAK,OAAO,UAAU,GAAG,EAAE,KAAK;EAC3C,cAAmB,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS;EAChD,IAAI,SAAS,QAAQ,QAAQ,KAAK;CACpC;CAEA,MAAM,QAAQ,UAAmC;EAC/C,IAAI,UAAU,SAAS,eAAe,QAAQ,OAAO;EACrD,QAAQ,KAAK,KAAK,UAAU,KAAK,CAAC;EAClC,OAAO;CACT;CACA,MAAM,eAAe;EACnB,MAAM,OAAO,KAAK,EAAE,MAAM,4BAA4B,CAAC;EACvD,IAAI,MAAM,cAAc;EACxB,OAAO;CACT;CACA,MAAM,SAAS;EACb,cAAc;GACZ,IAAI,QAAQ;GACZ,cAAc;GACd,QAAQ,cAAc;EACxB;EACA,YAAY;GACV,IAAI,QAAQ;GAGZ,QAAQ,YAAY,cAAc,OAAO,IAAI,KAAK;EACpD;CACF;CAEA,IAAI;EACF,IAAI,CAAC,OACH,MAAM,IAAI,yBAAyB,+BAA+B,EAChE,MAAM,yBACR,CAAC;EACH,KAAK,IAAI,kBAAkB;EAC3B,GAAG,SAAS,OAAO,MAAM;EACzB,UAAU,GAAG,kBAAkB,YAAY;EAC3C,QAAQ,iBAAiB,cAAc;GACrC,IAAI,CAAC,QAAQ,QAAQ,OAAO;EAC9B,CAAC;EACD,QAAQ,iBAAiB,YAAY,YAAY;GAC/C,IAAI;GACJ,IAAI;IACF,QAAQ,KAAK,MAAM,OAAO,QAAQ,IAAI,CAAC;GACzC,QAAQ;IACN;GACF;GACA,IACE,SACA,OAAO,UAAU,YACjB,OAAQ,MAA6B,SAAS,UAE9C,QAAQ,QAAQ,KAAyB;EAE7C,CAAC;EACD,QAAQ,iBAAiB,eAAe,MAAM,CAAC;EAC/C,MAAM,OAAO;EACb,KAAK,iBAAiB,+BAA+B;GACnD,IAAI,KAAK,oBAAoB,UAC3B,MACE,IAAI,yBAAyB,wCAAwC,EACnE,MAAM,oBACR,CAAC,CACH;GAEF,IAAI,KAAK,oBAAoB,UAAU,MAAM;EAC/C,CAAC;EAED,MAAM,QAAQ,MAAM,GAAG,YAAY;EACnC,MAAM,GAAG,oBAAoB,KAAK;EAClC,IAAI;EACJ,IAAI,YAAY,OAAO;GACrB,IAAI,OAAO,QAAQ,WAAW,UAC5B,MAAM,IAAI,UAAU,uDAAsD;GAE5E,MAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ;IAAE,GAAG,QAAQ;IAAM,KAAK,MAAM;GAAI,CAAC;GAC/E,IAAI,CAAC,SAAS,IAAI,MAAM,MAAM,WAAW,UAAU,mCAAmC;GACtF,SAAS,QAAS,MAAM,SAAS,KAAK,EAAA,CAAyB,OAAO,EAAE;EAC1E,OAAO;GACL,MAAM,QAAQ,MAAM;GACpB,IAAI,CAAC,SAAS,OAAO,MAAM,UAAU,UACnC,MAAM,IAAI,yBAAyB,uCAAuC,EACxE,MAAM,WACR,CAAC;GAEH,MAAM,WAAW,MAAM,MAAM,MAAM,YAAA,4CAAuC;IACxE,QAAQ;IACR,SAAS;KAAE,eAAe,UAAU,MAAM;KAAS,gBAAgB;IAAkB;IACrF,MAAM,MAAM;IACZ;GACF,CAAC;GACD,IAAI,CAAC,SAAS,IACZ,MAAM,MAAM,WAAW,UAAU,2CAA2C;GAC9E,SAAS,MAAM,SAAS,KAAK;EAC/B;EACA,IAAI,CAAC,QAAQ,MAAM,IAAI,yBAAyB,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;EAC5F,IAAI,QAAQ,SAAS,MAAM,OAAO,UAAU,IAAI,aAAa,WAAW,YAAY;EACpF,MAAM,GAAG,qBAAqB;GAAE,MAAM;GAAU,KAAK;EAAO,CAAC;EAG7D,MAAM,SACJ,QAAQ,QAAQ,KAAA,KAAa,QAAQ,QAAQ,WACzC,gBAAgB,QAAQ,MAAM,IAC9B;EACN,IAAI,OAAO,iBAAiB,aAAa;GACvC,eAAe,IAAI,aAAa;GAChC,aAAkB,SAAS,CAAC,CAAC,YAAY,KAAA,CAAS;GAClD,MAAM,WAAW,aAAa,eAAe;GAC7C,SAAS,UAAU;GACnB,aAAa,wBAAwB,MAAM,CAAC,CAAC,QAAQ,QAAQ;GAC7D,MAAM,UAAU,IAAI,aAAa,SAAS,OAAO;GACjD,IAAI,YAAY;GAChB,QAAQ,kBAAkB;IACxB,SAAS,uBAAuB,OAAO;IAEvC,MAAM,MAAM,MAAM,UAAU,MAAM,OAAO,IAAI;IAC7C,MAAM,QAAQ,KAAK,IAAI,GAAG,MAAM,EAAG;IACnC,IAAI,KAAK,IAAI,QAAQ,SAAS,KAAK,KAAM;KACvC,YAAY;KACZ,QAAQ,QAAQ,KAAK;IACvB;IACA,MAAM,SAAS,QAAQ,KAAK,KAAK,KAAK,IAAI,CAAC;IAC3C,IAAI,WAAW,SAAS,OAAO,YAAY;SACtC,IAAI,WAAW,OAAO,OAAO,UAAU;GAC9C,GAAG,QAAQ;EACb,OAAO,IAAI,QACT,MAAM,IAAI,yBAAyB,qCAAqC,EACtE,MAAM,cACR,CAAC;EAEH,IAAI,OAAO,QAAQ,QAAQ,YAAY,WAAW,MAAM,QAAQ,IAAI,QAAQ,MAAM;CACpF,SAAS,OAAO;EACd,MAAM;EACN,IAAI,QAAQ,WAAY,OAA6B,SAAS,cAAc,MAAM;EAClF,MAAM,iBAAiB,2BACnB,QACA,IAAI,yBAAyB,uCAAuC,EAAE,OAAO,MAAM,CAAC;CAC1F;CAEA,IAAI,QACF,MAAM,IAAI,yBAAyB,uDAAuD,EACxF,MAAM,oBACR,CAAC;CAEH,UAAU;CACV,OAAO;EACL;EACA,aAAc,eAAe,QAAQ,QAAQ,QAAQ,OAAO,IAAI;EAChE;EACA,KAAK,IAAI;GACP,IAAI,OAAO,MAAM,UAAU,CAAC;EAC9B;EACA,aAAa,MAAM;CACrB;AACF;;;AChTA,SAAgB,4BAAgD;CAC9D,OAAO;EAAE,OAAO,CAAC;EAAG,UAAU,CAAC;EAAG,OAAO,CAAC;EAAG,gBAAgB;CAAE;AACjE;;AAGA,SAAgB,YAAY,OAAmC;CAC7D,OAAO,MAAM,MACV,KAAK,OAAO,MAAM,SAAS,GAAG,CAAC,CAC/B,QAAQ,SAAyB,CAAC,CAAC,IAAI,CAAC,CACxC,KAAK,GAAG,CAAC,CACT,KAAK;AACV;;;AAUA,IAAM,gCAAgB,IAAI,IAAI,CAAC,iCAAiC,CAAC;AAEjE,SAAgB,wBACd,OACA,OAC+D;CAC/D,MAAM,SAAS,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU,KAAA;CACnE,MAAM,QAAQ,OAAgB,MAAM,MAAM,SAAS,EAAE,IAAI,MAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,EAAE;CAE1F,QAAQ,MAAM,MAAd;EACE,KAAK;GACH,IAAI,CAAC,UAAU,OAAO,MAAM,UAAU,UAAU;GAChD,IAAI,MAAM,MAAM,MAAM,SAAS,KAAK,OAAO,MAAM,GAAG;GACpD,OAAO;IACL,OAAO;KACL,GAAG;KACH,OAAO,KAAK,MAAM;KAClB,UAAU;MAAE,GAAG,MAAM;OAAW,UAAU,MAAM,SAAS,WAAW,MAAM,MAAM;KAAM;IACxF;IACA,SAAS,CAAC;GACZ;EAEF,KAAK;GACH,IAAI,CAAC,QAAQ;GACb,OAAO;IAAE,OAAO;KAAE,GAAG;KAAO,OAAO,KAAK,MAAM;IAAE;IAAG,SAAS,CAAC;GAAE;EAEjE,KAAK,yDAAyD;GAC5D,IAAI,CAAC,UAAU,MAAM,MAAM,MAAM,SAAS,KAAK,OAAO,MAAM,GAAG;GAC/D,MAAM,QAAQ,MAAM;GACpB,MAAM,UAAU,OAAO,SAAS,aAAa,OAAO,MAAM,OAAO,IAAI;GACrE,MAAM,OAAuB;IAC3B,IAAI;IACJ,MAAM,OAAO,MAAM,eAAe,WAAW,MAAM,WAAW,KAAK,IAAI;IACvE,GAAI,OAAO,SAAS,OAAO,IAAI,EAAE,QAAQ,IAAI,CAAC;GAChD;GACA,MAAM,QAAQ,KAAK,MAAM;GACzB,MAAM,GAAG,SAAS,OAAO,GAAG,aAAa,MAAM;GAC/C,MAAM,MAAM,CAAC,GAAG,MAAM,OAAO,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM,MAAM,QAAQ,EAAE,EAAE,IAAI,MAAM,QAAQ,EAAE,EAAE,CAAC;GAC3F,MAAM,QAAQ,IAAI,MAAM,GAAU;GAGlC,MAAM,UAAU,IAAI,IAAI,IAAI,MAAM,GAAG,GAAU,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC;GACjE,OAAO;IACL,OAAO;KACL,GAAG;KACH,OAAO,QAAQ,OAAO,MAAM,QAAQ,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,IAAI;KAC/D;KACA;KACA,gBAAgB,KAAK,IAAI,GAAG,MAAM,iBAAiB,CAAC;IACtD;IACA,SAAS,CAAC;KAAE,MAAM;KAAS;IAAK,CAAC;GACnC;EACF;EACA,KAAK,sDAAsD;GACzD,MAAM,GAAG,UAAU,KAAK,UAAU,GAAG,aAAa,MAAM;GACxD,MAAM,QAAS,MAAM,SAAS,CAAC;GAC/B,OAAO;IACL,OAAO;KAAE,GAAG;KAAO;KAAU,gBAAgB,KAAK,IAAI,GAAG,MAAM,iBAAiB,CAAC;IAAE;IACnF,SAAS,CACP;KAAE,MAAM;KAAS,MAAM,MAAM;KAAM,SAAS,MAAM,WAAW;IAAwB,CACvF;GACF;EACF;EACA,KAAK,SAAS;GACZ,MAAM,QAAS,MAAM,SAAS,CAAC;GAC/B,IAAI,MAAM,QAAQ,cAAc,IAAI,MAAM,IAAI,GAC5C,OAAO;IACL,OAAO;KAAE,GAAG;KAAO,gBAAgB,KAAK,IAAI,GAAG,MAAM,iBAAiB,CAAC;IAAE;IACzE,SAAS,CAAC;GACZ;GAEF,OAAO;IACL;IACA,SAAS,CAAC;KAAE,MAAM;KAAS,MAAM,MAAM;KAAM,SAAS,MAAM,WAAW;IAAgB,CAAC;GAC1F;EACF;CACF;CACA,OAAO;EAAE;EAAO,SAAS,CAAC;CAAE;AAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrCA,SAAgB,qBACd,QACA,UAAuC,CAAC,GACZ;CAC5B,MAAM,CAAC,QAAQ,aAAa,SAAkC,MAAM;CACpE,MAAM,CAAC,YAAY,iBAAiB,SAA6B,yBAAyB;CAC1F,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,CAAC,OAAO,YAAY,SAAS,CAAC;CACpC,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,OAAO,YAAY,SAAwC,IAAI;CAEtE,MAAM,SAAS,OAAO;EAAE;EAAQ;CAAQ,CAAC;CACzC,OAAO,UAAU;EAAE;EAAQ;CAAQ;CACnC,MAAM,WAAW,OAAO,UAAU;CAClC,MAAM,gBAAgB,OAA2C,IAAI;CACrE,MAAM,WAAW,OAA+B,IAAI;;CAEpD,MAAM,aAAa,OAA4B,IAAI;CACnD,MAAM,UAAU,OAAO,IAAI;CAE3B,MAAM,SAAS,aAAa,SAA6B;EACvD,MAAM,SAAS,YAAY,SAAS,OAAO;EAC3C,SAAS,UAAU;EACnB,IAAI,CAAC,QAAQ,SAAS;EACtB,cAAc,IAAI;EAClB,MAAM,QAAQ,YAAY,IAAI;EAC9B,IAAI,UAAU,QAAQ,OAAO,QAAQ,QAAQ,YAAY,KAAK;CAChE,GAAG,CAAC,CAAC;CAEL,MAAM,OAAO,aAAa,YAAoC;EAC5D,IAAI,CAAC,QAAQ,SAAS;EACtB,SAAS,OAAO;EAChB,OAAO,QAAQ,QAAQ,UAAU,OAAO;CAC1C,GAAG,CAAC,CAAC;CAEL,MAAM,WAAW,kBAAkB;EACjC,SAAS,SAAS,MAAM;EACxB,SAAS,UAAU;EACnB,MAAM,aAAa,cAAc;EACjC,cAAc,UAAU;EACxB,YAAY,MAAM;CACpB,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,QAAQ,UAAU;EAClB,aAAa;GACX,QAAQ,UAAU;GAGlB,SAAS;EACX;CACF,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,QAAQ,YAAY,YAAY;EACpC,IAAI,cAAc,WAAW,SAAS,SAAS;EAC/C,MAAM,QAAQ,IAAI,gBAAgB;EAClC,SAAS,UAAU;EACnB,SAAS,IAAI;EACb,UAAU,YAAY;EACtB,MAAM,EAAE,QAAQ,KAAK,SAAS,SAAS,OAAO;EAC9C,IAAI,SAAS;EACb,IAAI;GACF,MAAM,aAAa,MAAM,uBACvB;IACE,SAAS,KAAK;IACd,QAAQ;IACR,MAAM,KAAK;IACX,SAAS,KAAK;IACd,KAAK,KAAK;IACV,QAAQ;KAAE,GAAG,KAAK;KAAQ,WAAW,KAAK;IAAU;IACpD,cAAc;KACZ,SAAS;KACT,IAAI,QAAQ,WAAW,cAAc,SAAS,UAAU,WAAW;IACrE;IACA,UAAU,UAAU;KAClB,MAAM,EAAE,OAAO,YAAY,wBAAwB,SAAS,SAAS,KAAK;KAC1E,IAAI,UAAU,SAAS,SAAS,OAAO,KAAK;KAC5C,KAAK,MAAM,UAAU,SACnB,IAAI,OAAO,SAAS;UACd,QAAQ,SAAS,OAAO,QAAQ,QAAQ,UAAU,OAAO,IAAI;KAAA,OAEjE,KAAK,OAAO,OAAO,IAAI,MAAM,OAAO,OAAO,GAAG,EAAE,MAAM,OAAO,KAAK,CAAC,CAAC;KAGxE,IAAI,SAAS,QAAQ,mBAAmB,GAAG,WAAW,UAAU;IAClE;IACA,qBAAqB;KACnB,IAAI,CAAC,QAAQ,SAAS;KACtB,YAAY,IAAI;KAChB,OAAO,QAAQ,QAAQ,gBAAgB;IACzC;IACA,cAAc,cAAc;KAC1B,IAAI,WACF,OAAO;MAAE,GAAG,SAAS;MAAS,gBAAgB,SAAS,QAAQ,iBAAiB;KAAE,CAAC;KAErF,IAAI,CAAC,QAAQ,SAAS;KACtB,YAAY,KAAK;KACjB,OAAO,QAAQ,QAAQ,cAAc;IACvC;IACA,UAAU,UAAU;KAClB,IAAI,QAAQ,SAAS,SAAS,KAAK;IACrC;IACA,UAAU,eAAe;KACvB,cAAc,UAAU;KACxB,WAAW,UAAU;KACrB,IAAI,CAAC,QAAQ,SAAS;KACtB,UAAU,MAAM;KAChB,YAAY,KAAK;KACjB,SAAS,CAAC;KACV,IAAI,YAAY,KAAK,UAAU;IACjC;GACF,GACA,MAAM,MACR;GACA,IAAI,MAAM,OAAO,SAAS;IACxB,WAAW,MAAM;IACjB;GACF;GACA,cAAc,UAAU;GACxB,WAAW,KAAK,KAAK;GACrB,IAAI,UAAU,QAAQ,SAAS,UAAU,WAAW;EACtD,SAAS,SAAS;GAChB,IAAI,MAAM,OAAO,SAAS;GAC1B,IAAI,QAAQ,SAAS,UAAU,MAAM;GACrC,KAAK,mBAAmB,QAAQ,UAAU,IAAI,MAAM,OAAO,OAAO,CAAC,CAAC;EACtE,UAAU;GACR,IAAI,SAAS,YAAY,OAAO,SAAS,UAAU;EACrD;CACF,GAAG;EAAC;EAAM;EAAQ;CAAK,CAAC;CAExB,MAAM,SAAS,kBAAkB;EAC/B,IAAI,cAAc,SAAS,OAAO,GAChC,OAAO;GAAE,GAAG,SAAS;GAAS,gBAAgB,SAAS,QAAQ,iBAAiB;EAAE,CAAC;CAEvF,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,OAAO,YACX,OAAO,EAAE,QAAQ,OAAO,YAAY,SAAkD,CAAC,MAAM;EAC3F,MAAM,aAAa,cAAc;EACjC,IAAI,SAAS,YAAY;GACvB,IAAI,WAAW,MAAM,GACnB,OAAO;IAAE,GAAG,SAAS;IAAS,gBAAgB,SAAS,QAAQ,iBAAiB;GAAE,CAAC;GAErF,IAAI,SAAS,QAAQ,iBAAiB,GAAG;IACvC,MAAM,IAAI,SAAe,YAAY;KACnC,MAAM,QAAQ,WAAW,SAAS,SAAS;KAC3C,WAAW,gBAAgB;MACzB,aAAa,KAAK;MAClB,QAAQ;KACV;IACF,CAAC;IACD,WAAW,UAAU;GACvB;EACF;EACA,SAAS;EACT,IAAI,QAAQ,SAAS;GACnB,UAAU,MAAM;GAChB,YAAY,KAAK;GACjB,SAAS,CAAC;EACZ;CACF,GACA,CAAC,UAAU,MAAM,CACnB;CAEA,MAAM,OAAO,aAAa,OAAiB;EACzC,UAAU,QAAQ;GAChB,MAAM,OAAO,MAAM,CAAC;GACpB,cAAc,SAAS,KAAK,IAAI;GAChC,OAAO;EACT,CAAC;CACH,GAAG,CAAC,CAAC;CAEL,MAAM,QAAQ,kBAEV,OAAO;EAAE,GAAG,0BAA0B;EAAG,gBAAgB,SAAS,QAAQ;CAAe,CAAC,GAC5F,CAAC,MAAM,CACT;CAEA,OAAO;EACL;EACA,SAAS,YAAY,UAAU;EAC/B,OAAO,WAAW;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -0,0 +1,99 @@
1
+ import { type EnergyVadOptions } from "./energyVad";
2
+ /**
3
+ * The browser's half of a live transcription session (#929): the microphone,
4
+ * a raw `RTCPeerConnection` to the vendor, the `oai-events` data channel, a
5
+ * level meter and end-of-speech detection that commits each turn.
6
+ *
7
+ * No vendor SDK, and nothing from `gemi/ai`'s server half: the secret (or the
8
+ * SDP answer) comes from the app's own route, which holds the key.
9
+ *
10
+ * @experimental Follows OpenAI's realtime transcription and WebRTC guides; not
11
+ * yet run against a live session.
12
+ */
13
+ /** OpenAI's WebRTC endpoint, for a secret whose route doesn't say otherwise.
14
+ * `clientSecret()` on the server returns `callsUrl` (Azure's host included). */
15
+ export declare const OPENAI_REALTIME_CALLS_URL = "https://api.openai.com/v1/realtime/calls";
16
+ /** What the app's token route answers: `realtime.clientSecret(...)`'s result,
17
+ * or at least its `value`. */
18
+ export type LiveTranscriptionToken = {
19
+ value: string;
20
+ /** Where to post the SDP offer. Default OpenAI's. */
21
+ callsUrl?: string;
22
+ };
23
+ /** A pluggable end-of-speech detector, such as `@ricky0123/vad-web`'s
24
+ * `MicVAD` on the same stream. Call `speechStart` / `speechEnd` as speech
25
+ * begins and ends; `speechEnd` commits the turn. */
26
+ export type VoiceActivityDetector = (stream: MediaStream, events: {
27
+ speechStart(): void;
28
+ speechEnd(): void;
29
+ }) => {
30
+ stop(): void;
31
+ } | Promise<{
32
+ stop(): void;
33
+ }>;
34
+ export type LiveTranscriptionError = Error & {
35
+ code?: string;
36
+ status?: number;
37
+ };
38
+ export declare class LiveTranscriptionFailure extends Error {
39
+ readonly code?: string;
40
+ readonly status?: number;
41
+ constructor(message: string, options?: {
42
+ code?: string;
43
+ status?: number;
44
+ cause?: unknown;
45
+ });
46
+ }
47
+ export type LiveTranscriptionConnectOptions = {
48
+ /**
49
+ * `"token"` (default): `source` gives a client secret and the browser posts
50
+ * its SDP to the vendor itself. `"sdp"`: the browser posts `{ sdp, ...body }`
51
+ * to `source` and the route answers `{ sdp }` — the route then creates the
52
+ * call (`realtime.createCall`) and can meter it on the sideband.
53
+ */
54
+ connect?: "token" | "sdp";
55
+ /** The app's route (POSTed `body` as JSON), or a function giving the token. */
56
+ source: string | (() => Promise<LiveTranscriptionToken>);
57
+ body?: Record<string, unknown>;
58
+ headers?: Record<string, string>;
59
+ /** `"energy"` (default): the built-in detector. A function: your own. `false`:
60
+ * no detection; turns end only on `commit()`. */
61
+ vad?: "energy" | VoiceActivityDetector | false;
62
+ /** The built-in detector's options; `silenceMs` defaults to 350. */
63
+ energy?: EnergyVadOptions;
64
+ /** The vendor's events from the data channel. */
65
+ onEvent(event: {
66
+ type: string;
67
+ [key: string]: unknown;
68
+ }): void;
69
+ /** The channel opened: the session is live. */
70
+ onOpen(): void;
71
+ onSpeechStart(): void;
72
+ /** The detector heard the user stop. `committed`: the turn was committed
73
+ * (there was speech in it and the channel was open). */
74
+ onSpeechEnd(committed: boolean): void;
75
+ /** Microphone level, 0–1, about every 50 ms. */
76
+ onLevel(level: number): void;
77
+ /** Called once when the connection closed or failed. */
78
+ onClose(error?: LiveTranscriptionError): void;
79
+ };
80
+ export type LiveTranscriptionConnection = {
81
+ /** Sends `input_audio_buffer.commit`. False when the channel isn't open. */
82
+ commit(): boolean;
83
+ /** Commits only if speech was heard since the last commit (always, with no
84
+ * detector). For a graceful stop. */
85
+ flush(): boolean;
86
+ /** Any client event, e.g. a `session.update` with new keywords. */
87
+ send(event: Record<string, unknown>): boolean;
88
+ /** Pauses the microphone track (nothing is sent while muted). */
89
+ mute(on: boolean): void;
90
+ close(): void;
91
+ };
92
+ /**
93
+ * Opens a live transcription session. Resolves once the SDP answer is applied;
94
+ * `onOpen` follows when the data channel opens. `signal` aborts a start in
95
+ * flight (unmount, `stop()` while connecting): everything opened so far is
96
+ * closed and the promise rejects with an `AbortError`.
97
+ */
98
+ export declare function startLiveTranscription(options: LiveTranscriptionConnectOptions, signal?: AbortSignal): Promise<LiveTranscriptionConnection>;
99
+ //# sourceMappingURL=liveTranscription.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveTranscription.d.ts","sourceRoot":"","sources":["../../../ai/voice/liveTranscription.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE5E;;;;;;;;;;GAUG;AAEH;iFACiF;AACjF,eAAO,MAAM,yBAAyB,6CAA6C,CAAC;AAEpF;+BAC+B;AAC/B,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;qDAEqD;AACrD,MAAM,MAAM,qBAAqB,GAAG,CAClC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE;IAAE,WAAW,IAAI,IAAI,CAAC;IAAC,SAAS,IAAI,IAAI,CAAA;CAAE,KAC/C;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,CAAC,CAAC;AAElD,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAM/F;AAED,MAAM,MAAM,+BAA+B,GAAG;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC1B,+EAA+E;IAC/E,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;sDACkD;IAClD,GAAG,CAAC,EAAE,QAAQ,GAAG,qBAAqB,GAAG,KAAK,CAAC;IAC/C,oEAAoE;IACpE,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,iDAAiD;IACjD,OAAO,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/D,+CAA+C;IAC/C,MAAM,IAAI,IAAI,CAAC;IACf,aAAa,IAAI,IAAI,CAAC;IACtB;6DACyD;IACzD,WAAW,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IACtC,gDAAgD;IAChD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,wDAAwD;IACxD,OAAO,CAAC,KAAK,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,4EAA4E;IAC5E,MAAM,IAAI,OAAO,CAAC;IAClB;0CACsC;IACtC,KAAK,IAAI,OAAO,CAAC;IACjB,mEAAmE;IACnE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IAC9C,iEAAiE;IACjE,IAAI,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AA0BF;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,+BAA+B,EACxC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,2BAA2B,CAAC,CAuNtC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What `useLiveTranscription` shows, as a pure reducer over a transcription
3
+ * session's data-channel events (#929). Tested without a DOM or a peer
4
+ * connection, like `state.ts` for `useVoiceSession`.
5
+ *
6
+ * The events are OpenAI's (realtime transcription guide):
7
+ * `conversation.item.input_audio_transcription.delta` (`item_id`, `delta`)
8
+ * while the user speaks, and `.completed` (`item_id`, `transcript`, `usage`)
9
+ * once the turn is committed. Completions of different turns may arrive out of
10
+ * order, so turns are keyed and ordered by `item_id`, by when each item was
11
+ * first seen.
12
+ */
13
+ /** One finished turn: what the user said between two commits. */
14
+ export type TranscriptTurn = {
15
+ /** The vendor's item id. */
16
+ id: string;
17
+ text: string;
18
+ /** Audio seconds the vendor billed for it, when it said (duration-billed
19
+ * models such as `gpt-live-transcribe`). */
20
+ seconds?: number;
21
+ };
22
+ export type TranscriptionState = {
23
+ /** Item ids in the order they were first seen. */
24
+ order: string[];
25
+ /** Text so far of items not yet completed. */
26
+ partials: Record<string, string>;
27
+ /** Completed turns, oldest first, at most `MAX_TURNS`. */
28
+ turns: TranscriptTurn[];
29
+ /** Commits sent whose transcript hasn't arrived. */
30
+ pendingCommits: number;
31
+ };
32
+ export declare const MAX_TURNS = 50;
33
+ export declare function initialTranscriptionState(): TranscriptionState;
34
+ /** The live text: every unfinished item's text, in order. */
35
+ export declare function partialText(state: TranscriptionState): string;
36
+ export type TranscriptionEvent = {
37
+ type: string;
38
+ [key: string]: unknown;
39
+ };
40
+ export type TranscriptionEffect = {
41
+ type: "final";
42
+ turn: TranscriptTurn;
43
+ } | {
44
+ type: "error";
45
+ code?: string;
46
+ message: string;
47
+ };
48
+ export declare function applyTranscriptionEvent(state: TranscriptionState, event: TranscriptionEvent): {
49
+ state: TranscriptionState;
50
+ effects: TranscriptionEffect[];
51
+ };
52
+ //# sourceMappingURL=transcriptionState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcriptionState.d.ts","sourceRoot":"","sources":["../../../ai/voice/transcriptionState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,iEAAiE;AACjE,MAAM,MAAM,cAAc,GAAG;IAC3B,4BAA4B;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb;iDAC6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,kDAAkD;IAClD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,0DAA0D;IAC1D,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,wBAAgB,yBAAyB,IAAI,kBAAkB,CAE9D;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAM7D;AAED,MAAM,MAAM,kBAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE1E,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAMtD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,kBAAkB,EACzB,KAAK,EAAE,kBAAkB,GACxB;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAyE/D"}
@@ -0,0 +1,86 @@
1
+ import type { EnergyVadOptions } from "./energyVad";
2
+ import { type LiveTranscriptionError, type LiveTranscriptionToken, type VoiceActivityDetector } from "./liveTranscription";
3
+ import { type TranscriptTurn } from "./transcriptionState";
4
+ export type LiveTranscriptionStatus = "idle" | "connecting" | "listening";
5
+ export type UseLiveTranscriptionOptions = {
6
+ /** `"token"` (default) or `"sdp"`. See the docs: `"sdp"` lets the route
7
+ * create the call and meter it on the sideband. */
8
+ connect?: "token" | "sdp";
9
+ /** Sent to the route as JSON with each start. */
10
+ body?: Record<string, unknown>;
11
+ headers?: Record<string, string>;
12
+ /** Silence after speech that ends a turn, for the built-in detector.
13
+ * Default 350 ms. */
14
+ silenceMs?: number;
15
+ /** More options for the built-in detector. */
16
+ energy?: Omit<EnergyVadOptions, "silenceMs">;
17
+ /** `"energy"` (default), your own detector (e.g. `@ricky0123/vad-web`), or
18
+ * `false` to end turns only with `commit()`. */
19
+ vad?: "energy" | VoiceActivityDetector | false;
20
+ /** The live text changed (a delta arrived, or a turn completed). */
21
+ onPartial?: (text: string) => void;
22
+ /** A turn's final transcript. */
23
+ onFinal?: (turn: TranscriptTurn) => void;
24
+ onSpeechStart?: () => void;
25
+ /** The user stopped; the turn was committed and its final is on the way. */
26
+ onSpeechEnd?: () => void;
27
+ onError?: (error: LiveTranscriptionError) => void;
28
+ };
29
+ export type UseLiveTranscriptionResult = {
30
+ status: LiveTranscriptionStatus;
31
+ /** What the user is saying now, before the turn is committed. */
32
+ partial: string;
33
+ /** Finished turns, oldest first (the last 50). */
34
+ turns: TranscriptTurn[];
35
+ /** The user is speaking, by the end-of-speech detector. */
36
+ speaking: boolean;
37
+ /** Microphone level, 0–1. */
38
+ level: number;
39
+ muted: boolean;
40
+ /** The last failure. `code` is `microphone_denied`,
41
+ * `microphone_unavailable`, `unsupported`, `connection_failed`, the
42
+ * route's error code, or the vendor's. */
43
+ error: LiveTranscriptionError | null;
44
+ /** Asks for the microphone and opens the session. Call it from a user
45
+ * gesture. Does nothing while a session is starting or live. */
46
+ start(): Promise<void>;
47
+ /** Closes the session and releases the microphone. `{ flush: true }`
48
+ * commits speech in progress first and waits (up to `timeoutMs`, default
49
+ * 1500) for its final transcript. */
50
+ stop(options?: {
51
+ flush?: boolean;
52
+ timeoutMs?: number;
53
+ }): Promise<void>;
54
+ /** Ends the turn now (`input_audio_buffer.commit`), e.g. for push-to-talk or
55
+ * a "send" button. */
56
+ commit(): void;
57
+ mute(on?: boolean): void;
58
+ /** Clears `turns` and `partial` (not the session). */
59
+ reset(): void;
60
+ };
61
+ /**
62
+ * Live speech-to-text (#929): the microphone streams to a transcription
63
+ * session (`gpt-live-transcribe` by default) over WebRTC. `partial` grows as
64
+ * the user speaks; when they stop for `silenceMs`, the turn is committed and
65
+ * its final transcript arrives in `turns` and `onFinal`. Nothing is spoken
66
+ * back.
67
+ *
68
+ * `source` is the app's route that answers a client secret (from
69
+ * `realtime.clientSecret({ session: { type: "transcription", … } })`), or a
70
+ * function returning one. With `connect: "sdp"`, it is the route that takes
71
+ * the SDP offer and answers `{ sdp }`.
72
+ *
73
+ * ```tsx
74
+ * const live = useLiveTranscription("/api/voice/token", {
75
+ * onFinal: (turn) => route(turn.text),
76
+ * });
77
+ * <button onClick={() => (live.status === "idle" ? live.start() : live.stop())}>Voice</button>
78
+ * <p>{live.partial}</p>
79
+ * ```
80
+ *
81
+ * From `gemi/ai/voice`, so only the views that use it load it.
82
+ *
83
+ * @experimental Tested against faked browser APIs only.
84
+ */
85
+ export declare function useLiveTranscription(source: string | (() => Promise<LiveTranscriptionToken>), options?: UseLiveTranscriptionOptions): UseLiveTranscriptionResult;
86
+ //# sourceMappingURL=useLiveTranscription.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLiveTranscription.d.ts","sourceRoot":"","sources":["../../../ai/voice/useLiveTranscription.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,CAAC;AAE1E,MAAM,MAAM,2BAA2B,GAAG;IACxC;wDACoD;IACpD,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC1B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;0BACsB;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;IAC7C;qDACiD;IACjD,GAAG,CAAC,EAAE,QAAQ,GAAG,qBAAqB,GAAG,KAAK,CAAC;IAC/C,oEAAoE;IACpE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,iCAAiC;IACjC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,uBAAuB,CAAC;IAChC,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,2DAA2D;IAC3D,QAAQ,EAAE,OAAO,CAAC;IAClB,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf;;+CAE2C;IAC3C,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACrC;qEACiE;IACjE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB;;0CAEsC;IACtC,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE;2BACuB;IACvB,MAAM,IAAI,IAAI,CAAC;IACf,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,sDAAsD;IACtD,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,sBAAsB,CAAC,CAAC,EACxD,OAAO,GAAE,2BAAgC,GACxC,0BAA0B,CA6L5B"}
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- import{R as S,T as h,U as u}from"./chunk-mr66zxsz.js";import{ma as q,na as w}from"./chunk-mpqvmge4.js";import"./chunk-a22h5cby.js";import"./chunk-epvpmakp.js";import"./chunk-r3v2cnrj.js";import"./chunk-4278e429.js";import"./chunk-kk18xqgc.js";import"./chunk-sa6cxt3g.js";import{ti as N}from"./chunk-zv5qyrqg.js";import{Lk as y}from"./chunk-x9bk5s21.js";import{_k as C}from"./chunk-99a4e6zf.js";import"./chunk-rnp5k8m3.js";import{join as $J}from"path";import{createServer as zJ}from"vite";import{join as l}from"path";import{IncomingMessage as d,ServerResponse as p}from"http";import{Socket as i}from"net";function c(J){return J.replace(/\x1b\[[0-9;]*m/g,"").replace(/\[[0-9;]*m/g,"").replace(/\n\s*\n/g,`
2
+ import{R as S,T as h,U as u}from"./chunk-c7g1jb3f.js";import{ma as q,na as w}from"./chunk-mpqvmge4.js";import"./chunk-a22h5cby.js";import"./chunk-epvpmakp.js";import"./chunk-r3v2cnrj.js";import"./chunk-4278e429.js";import"./chunk-kk18xqgc.js";import"./chunk-sa6cxt3g.js";import{ti as N}from"./chunk-zv5qyrqg.js";import{Lk as y}from"./chunk-x9bk5s21.js";import{_k as C}from"./chunk-99a4e6zf.js";import"./chunk-rnp5k8m3.js";import{join as $J}from"path";import{createServer as zJ}from"vite";import{join as l}from"path";import{IncomingMessage as d,ServerResponse as p}from"http";import{Socket as i}from"net";function c(J){return J.replace(/\x1b\[[0-9;]*m/g,"").replace(/\[[0-9;]*m/g,"").replace(/\n\s*\n/g,`
3
3
  `).split(`
4
4
  `).map((W)=>W.trim()).filter((W)=>W.length>0).join(`
5
5
  `).trim()}function _(J){return`
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- import{S as R,T as VJ,U as zJ}from"./chunk-mr66zxsz.js";import{oa as M,sa as L,ta as OJ}from"./chunk-a22h5cby.js";import"./chunk-kk18xqgc.js";import{Pd as r,ag as HJ,bg as N}from"./chunk-wevj4981.js";import"./chunk-ff60bzvf.js";import"./chunk-feajsgns.js";import{nh as i,oh as a,rh as t}from"./chunk-0rhf4g12.js";import{ti as GJ}from"./chunk-zv5qyrqg.js";import"./chunk-4t41m5f1.js";import"./chunk-hj88dcss.js";import"./chunk-z61dj480.js";import"./chunk-vnw91fe1.js";import"./chunk-jy9dkgt0.js";import"./chunk-ysc7ykss.js";import"./chunk-jx5zx48j.js";import"./chunk-z9xxthp1.js";import"./chunk-65j32dtn.js";import"./chunk-beb265c9.js";import"./chunk-zypmy8yq.js";import"./chunk-jx2mgvvt.js";import"./chunk-ha15x5ns.js";import{Lk as $J}from"./chunk-x9bk5s21.js";import"./chunk-gqgpwb88.js";import{Zk as KJ}from"./chunk-cxsvzqd4.js";import{_k as D}from"./chunk-99a4e6zf.js";import"./chunk-rnp5k8m3.js";import{createHash as Q2}from"crypto";import{join as E,resolve as xJ,sep as LJ}from"path";import{Duplex as qJ}from"stream";import{constants as _,createBrotliCompress as bJ,createGzip as SJ}from"zlib";var kJ=["br","gzip"],wJ=1024,vJ=5,EJ=19,yJ=6;function gJ(J){if(J===void 0)return 1;if(!/^\d+(\.\d{0,3})?$/.test(J.trim()))return 1;let $=Number(J.trim());return Number.isFinite($)?Math.min($,1):1}function fJ(J){if(J===null||J===void 0)return null;let $=new Map;for(let Z of J.split(",")){let[X,...W]=Z.split(";"),H=X.trim().toLowerCase();if(H==="")continue;let j=1;for(let Y of W){let K=Y.indexOf("=");if(K===-1)continue;if(Y.slice(0,K).trim().toLowerCase()==="q"){j=gJ(Y.slice(K+1));break}}$.set(H,j)}return $}function k(J,$=kJ){let Z=fJ(J);if(!Z)return null;let X=Z.get("*"),W=null,H=0;for(let j of $){let Y=Z.get(j)??X??0;if(Y>H)W=j,H=Y}return W}function mJ(J,$){if(!J)return!1;return J.split(",").some((Z)=>Z.trim().toLowerCase().split("=")[0].trim()===$)}function uJ(J,$){if(J==="HEAD")return!1;if($.status<200||$.status===204||$.status===206||$.status===304)return!1;if($.headers.has("Content-Range"))return!1;let Z=$.headers.get("Content-Type");if(!Z||!/^\s*text\/html\s*(;|$)/i.test(Z))return!1;if(!$.body)return!1;let X=$.headers.get("Content-Encoding");if(X&&X.trim().toLowerCase()!=="identity")return!1;if(mJ($.headers.get("Cache-Control"),"no-transform"))return!1;let W=$.headers.get("Content-Length");if(W!==null&&Number(W)<wJ)return!1;return!0}function hJ(J){let $=J==="br"?bJ({params:{[_.BROTLI_PARAM_QUALITY]:vJ,[_.BROTLI_PARAM_LGWIN]:EJ},flush:_.BROTLI_OPERATION_FLUSH}):SJ({level:yJ,flush:_.Z_SYNC_FLUSH});return qJ.toWeb($)}function cJ(J){let $=J.get("Vary");if(!$){J.set("Vary","Accept-Encoding");return}let Z=$.split(",").map((X)=>X.trim().toLowerCase());if(Z.includes("*")||Z.includes("accept-encoding"))return;J.append("Vary","Accept-Encoding")}function d(J,$){if(!uJ(J.method,$))return $;let Z=new Headers($.headers);cJ(Z);let X=k(J.headers.get("Accept-Encoding"));if(!X)return new Response($.body,{status:$.status,statusText:$.statusText,headers:Z});let W;try{W=hJ(X)}catch{return new Response($.body,{status:$.status,statusText:$.statusText,headers:Z})}return Z.set("Content-Encoding",X),Z.delete("Content-Length"),new Response($.body.pipeThrough(W),{status:$.status,statusText:$.statusText,headers:Z})}import{readdir as aJ,stat as s}from"fs/promises";import{join as tJ}from"path";import{mkdir as V2,readFile as pJ,stat as z2,writeFile as x2}from"fs/promises";import{dirname as C2,join as dJ}from"path";import{promisify as l}from"util";import{brotliCompress as lJ,constants as P2,gzip as oJ}from"zlib";var nJ=".vite/static-assets.json";var o={br:".br",gzip:".gz"},iJ=["br","gzip"],_2=l(lJ),B2=l(oJ);async function n(J){let $;try{$=await pJ(dJ(J,nJ),"utf8")}catch{return null}let Z;try{Z=JSON.parse($)?.files}catch{return null}if(!Z||typeof Z!=="object")return null;let X={files:{}};for(let[W,H]of Object.entries(Z)){if(!Array.isArray(H))continue;X.files[W]=iJ.filter((j)=>H.includes(j))}return X}function w(J){let $=J.toString(),Z=new Bun.CryptoHasher("sha1");return Z.update($),Z.digest("hex")}async function e(J){let $=new Set;async function Z(X,W){let H;try{H=await aJ(X,{withFileTypes:!0})}catch{return}for(let j of H){if(j.name.startsWith("."))continue;if(W&&j.name==="assets")continue;let Y=tJ(X,j.name),K=j.isDirectory(),V=j.isFile();if(j.isSymbolicLink())try{let I=await s(Y);K=I.isDirectory(),V=I.isFile()}catch{continue}if(V)$.add(Y);else if(K&&!j.isSymbolicLink())await Z(Y,!1)}}return await Z(J,!0),$}var rJ="public, max-age=31536000, must-revalidate",sJ="public, max-age=31536000, immutable";async function eJ(J,$,Z){if(Z.length===0||J.headers.has("Range"))return null;let X=k(J.headers.get("Accept-Encoding"),Z);if(!X)return null;let W=$+o[X];try{let H=await s(W);return H.isFile()?{encoding:X,path:W,size:H.size}:null}catch{return null}}async function JJ(J,$,Z={}){let X=Bun.file($),W=Z.encodings??[],H={"Content-Type":X.type||"application/octet-stream","Cache-Control":Z.immutable?sJ:rJ,"Accept-Ranges":"bytes"};if(W.length>0)H.Vary="Accept-Encoding";let j=await eJ(J,$,W);if(j){let V=Bun.file(j.path);return new Response(V.stream(),{headers:{...H,"Content-Encoding":j.encoding,"Content-Length":String(j.size),ETag:`${w(V.lastModified)}-${j.encoding}`}})}let Y=X.size;H.ETag=w(X.lastModified);let K=i(J.headers.get("Range"));if(K){let V=a(K,Y);if(!V)return r(Y);return new Response(X.slice(V.start,V.end+1).stream(),{status:206,headers:{...H,"Content-Range":t(V.start,V.end,Y),"Content-Length":String(V.length)}})}return new Response(X.stream(),{headers:{...H,"Content-Length":String(Y)}})}import{stat as W2}from"fs/promises";var B="app/client.tsx";function ZJ(J,$=M){let Z=J[B]?.file;if(!Z)return;return{module:L(Z,$),preload:A(J,B,$)}}function A(J,$,Z=M){let X=[],W=new Set,H=(j)=>{if(W.has(j))return;W.add(j);let Y=J[j];if(!Y?.file)return;X.push(L(Y.file,Z));for(let K of Y.imports??[])H(K)};return H($),X}function XJ(J,$){let Z=[],X=new Set,W=new Set,H=(j)=>{if(W.has(j))return;W.add(j);let Y=J[j];if(!Y)return;for(let K of Y.imports??[])H(K);for(let K of Y.css??[])if(!X.has(K))X.add(K),Z.push(K)};return H($),Z}function QJ(J,$=M){let Z=new Map;return(X)=>{if(!Z.has(X)){let W=`${X}${$J}`,H=J[W]?.file;Z.set(X,H?{src:L(H,$),preload:A(J,W,$)}:void 0)}return Z.get(X)}}function WJ(J,$=30000){let Z=JSON.stringify("gemi:chunk-reload"),X=JSON.stringify(`Failed to fetch dynamically imported module: ${J}`);return`var r=!1;try{var s=window.sessionStorage,m=JSON.parse(s.getItem(${Z})||"null");var d=m?Date.now()-m.at:NaN;if(navigator.onLine!==!1&&!(d>=0&&d<${$})){s.setItem(${Z},JSON.stringify({url:window.location.href,at:Date.now()}));r=!0}}catch(e){}if(r)window.location.reload();else throw new Error(${X});export {}`}var J2=new RegExp(`^${HJ}/.+\\.m?js$`);function $2(J){return J2.test(J)}function jJ(J){if($2(J))return new Response(WJ(J),{headers:{"Content-Type":"application/javascript","Cache-Control":"no-store"}});if(N(J))return new Response("Not found",{status:404});return}var Z2=`<!DOCTYPE html>
2
+ import{S as R,T as VJ,U as zJ}from"./chunk-c7g1jb3f.js";import{oa as M,sa as L,ta as OJ}from"./chunk-a22h5cby.js";import"./chunk-kk18xqgc.js";import{Pd as r,ag as HJ,bg as N}from"./chunk-wevj4981.js";import"./chunk-ff60bzvf.js";import"./chunk-feajsgns.js";import{nh as i,oh as a,rh as t}from"./chunk-0rhf4g12.js";import{ti as GJ}from"./chunk-zv5qyrqg.js";import"./chunk-4t41m5f1.js";import"./chunk-hj88dcss.js";import"./chunk-z61dj480.js";import"./chunk-vnw91fe1.js";import"./chunk-jy9dkgt0.js";import"./chunk-ysc7ykss.js";import"./chunk-jx5zx48j.js";import"./chunk-z9xxthp1.js";import"./chunk-65j32dtn.js";import"./chunk-beb265c9.js";import"./chunk-zypmy8yq.js";import"./chunk-jx2mgvvt.js";import"./chunk-ha15x5ns.js";import{Lk as $J}from"./chunk-x9bk5s21.js";import"./chunk-gqgpwb88.js";import{Zk as KJ}from"./chunk-cxsvzqd4.js";import{_k as D}from"./chunk-99a4e6zf.js";import"./chunk-rnp5k8m3.js";import{createHash as Q2}from"crypto";import{join as E,resolve as xJ,sep as LJ}from"path";import{Duplex as qJ}from"stream";import{constants as _,createBrotliCompress as bJ,createGzip as SJ}from"zlib";var kJ=["br","gzip"],wJ=1024,vJ=5,EJ=19,yJ=6;function gJ(J){if(J===void 0)return 1;if(!/^\d+(\.\d{0,3})?$/.test(J.trim()))return 1;let $=Number(J.trim());return Number.isFinite($)?Math.min($,1):1}function fJ(J){if(J===null||J===void 0)return null;let $=new Map;for(let Z of J.split(",")){let[X,...W]=Z.split(";"),H=X.trim().toLowerCase();if(H==="")continue;let j=1;for(let Y of W){let K=Y.indexOf("=");if(K===-1)continue;if(Y.slice(0,K).trim().toLowerCase()==="q"){j=gJ(Y.slice(K+1));break}}$.set(H,j)}return $}function k(J,$=kJ){let Z=fJ(J);if(!Z)return null;let X=Z.get("*"),W=null,H=0;for(let j of $){let Y=Z.get(j)??X??0;if(Y>H)W=j,H=Y}return W}function mJ(J,$){if(!J)return!1;return J.split(",").some((Z)=>Z.trim().toLowerCase().split("=")[0].trim()===$)}function uJ(J,$){if(J==="HEAD")return!1;if($.status<200||$.status===204||$.status===206||$.status===304)return!1;if($.headers.has("Content-Range"))return!1;let Z=$.headers.get("Content-Type");if(!Z||!/^\s*text\/html\s*(;|$)/i.test(Z))return!1;if(!$.body)return!1;let X=$.headers.get("Content-Encoding");if(X&&X.trim().toLowerCase()!=="identity")return!1;if(mJ($.headers.get("Cache-Control"),"no-transform"))return!1;let W=$.headers.get("Content-Length");if(W!==null&&Number(W)<wJ)return!1;return!0}function hJ(J){let $=J==="br"?bJ({params:{[_.BROTLI_PARAM_QUALITY]:vJ,[_.BROTLI_PARAM_LGWIN]:EJ},flush:_.BROTLI_OPERATION_FLUSH}):SJ({level:yJ,flush:_.Z_SYNC_FLUSH});return qJ.toWeb($)}function cJ(J){let $=J.get("Vary");if(!$){J.set("Vary","Accept-Encoding");return}let Z=$.split(",").map((X)=>X.trim().toLowerCase());if(Z.includes("*")||Z.includes("accept-encoding"))return;J.append("Vary","Accept-Encoding")}function d(J,$){if(!uJ(J.method,$))return $;let Z=new Headers($.headers);cJ(Z);let X=k(J.headers.get("Accept-Encoding"));if(!X)return new Response($.body,{status:$.status,statusText:$.statusText,headers:Z});let W;try{W=hJ(X)}catch{return new Response($.body,{status:$.status,statusText:$.statusText,headers:Z})}return Z.set("Content-Encoding",X),Z.delete("Content-Length"),new Response($.body.pipeThrough(W),{status:$.status,statusText:$.statusText,headers:Z})}import{readdir as aJ,stat as s}from"fs/promises";import{join as tJ}from"path";import{mkdir as V2,readFile as pJ,stat as z2,writeFile as x2}from"fs/promises";import{dirname as C2,join as dJ}from"path";import{promisify as l}from"util";import{brotliCompress as lJ,constants as P2,gzip as oJ}from"zlib";var nJ=".vite/static-assets.json";var o={br:".br",gzip:".gz"},iJ=["br","gzip"],_2=l(lJ),B2=l(oJ);async function n(J){let $;try{$=await pJ(dJ(J,nJ),"utf8")}catch{return null}let Z;try{Z=JSON.parse($)?.files}catch{return null}if(!Z||typeof Z!=="object")return null;let X={files:{}};for(let[W,H]of Object.entries(Z)){if(!Array.isArray(H))continue;X.files[W]=iJ.filter((j)=>H.includes(j))}return X}function w(J){let $=J.toString(),Z=new Bun.CryptoHasher("sha1");return Z.update($),Z.digest("hex")}async function e(J){let $=new Set;async function Z(X,W){let H;try{H=await aJ(X,{withFileTypes:!0})}catch{return}for(let j of H){if(j.name.startsWith("."))continue;if(W&&j.name==="assets")continue;let Y=tJ(X,j.name),K=j.isDirectory(),V=j.isFile();if(j.isSymbolicLink())try{let I=await s(Y);K=I.isDirectory(),V=I.isFile()}catch{continue}if(V)$.add(Y);else if(K&&!j.isSymbolicLink())await Z(Y,!1)}}return await Z(J,!0),$}var rJ="public, max-age=31536000, must-revalidate",sJ="public, max-age=31536000, immutable";async function eJ(J,$,Z){if(Z.length===0||J.headers.has("Range"))return null;let X=k(J.headers.get("Accept-Encoding"),Z);if(!X)return null;let W=$+o[X];try{let H=await s(W);return H.isFile()?{encoding:X,path:W,size:H.size}:null}catch{return null}}async function JJ(J,$,Z={}){let X=Bun.file($),W=Z.encodings??[],H={"Content-Type":X.type||"application/octet-stream","Cache-Control":Z.immutable?sJ:rJ,"Accept-Ranges":"bytes"};if(W.length>0)H.Vary="Accept-Encoding";let j=await eJ(J,$,W);if(j){let V=Bun.file(j.path);return new Response(V.stream(),{headers:{...H,"Content-Encoding":j.encoding,"Content-Length":String(j.size),ETag:`${w(V.lastModified)}-${j.encoding}`}})}let Y=X.size;H.ETag=w(X.lastModified);let K=i(J.headers.get("Range"));if(K){let V=a(K,Y);if(!V)return r(Y);return new Response(X.slice(V.start,V.end+1).stream(),{status:206,headers:{...H,"Content-Range":t(V.start,V.end,Y),"Content-Length":String(V.length)}})}return new Response(X.stream(),{headers:{...H,"Content-Length":String(Y)}})}import{stat as W2}from"fs/promises";var B="app/client.tsx";function ZJ(J,$=M){let Z=J[B]?.file;if(!Z)return;return{module:L(Z,$),preload:A(J,B,$)}}function A(J,$,Z=M){let X=[],W=new Set,H=(j)=>{if(W.has(j))return;W.add(j);let Y=J[j];if(!Y?.file)return;X.push(L(Y.file,Z));for(let K of Y.imports??[])H(K)};return H($),X}function XJ(J,$){let Z=[],X=new Set,W=new Set,H=(j)=>{if(W.has(j))return;W.add(j);let Y=J[j];if(!Y)return;for(let K of Y.imports??[])H(K);for(let K of Y.css??[])if(!X.has(K))X.add(K),Z.push(K)};return H($),Z}function QJ(J,$=M){let Z=new Map;return(X)=>{if(!Z.has(X)){let W=`${X}${$J}`,H=J[W]?.file;Z.set(X,H?{src:L(H,$),preload:A(J,W,$)}:void 0)}return Z.get(X)}}function WJ(J,$=30000){let Z=JSON.stringify("gemi:chunk-reload"),X=JSON.stringify(`Failed to fetch dynamically imported module: ${J}`);return`var r=!1;try{var s=window.sessionStorage,m=JSON.parse(s.getItem(${Z})||"null");var d=m?Date.now()-m.at:NaN;if(navigator.onLine!==!1&&!(d>=0&&d<${$})){s.setItem(${Z},JSON.stringify({url:window.location.href,at:Date.now()}));r=!0}}catch(e){}if(r)window.location.reload();else throw new Error(${X});export {}`}var J2=new RegExp(`^${HJ}/.+\\.m?js$`);function $2(J){return J2.test(J)}function jJ(J){if($2(J))return new Response(WJ(J),{headers:{"Content-Type":"application/javascript","Cache-Control":"no-store"}});if(N(J))return new Response("Not found",{status:404});return}var Z2=`<!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
5
5
  <meta charset="UTF-8" />