@effect-uai/mistral 0.12.0 → 0.13.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.
- package/dist/Mistral.d.mts +6 -5
- package/dist/Mistral.d.mts.map +1 -1
- package/dist/Mistral.mjs +1 -1
- package/dist/Mistral.mjs.map +1 -1
- package/dist/MistralRealtimeTranscriber.d.mts +1 -3
- package/dist/MistralRealtimeTranscriber.d.mts.map +1 -1
- package/dist/MistralRealtimeTranscriber.mjs +1 -1
- package/dist/MistralRealtimeTranscriber.mjs.map +1 -1
- package/dist/MistralSynthesizer.d.mts +1 -3
- package/dist/MistralSynthesizer.d.mts.map +1 -1
- package/dist/MistralSynthesizer.mjs +4 -2
- package/dist/MistralSynthesizer.mjs.map +1 -1
- package/dist/MistralTranscriber.d.mts +1 -3
- package/dist/MistralTranscriber.d.mts.map +1 -1
- package/dist/MistralTranscriber.mjs +3 -2
- package/dist/MistralTranscriber.mjs.map +1 -1
- package/dist/audioCodec.d.mts +0 -1
- package/dist/audioCodec.d.mts.map +1 -1
- package/dist/audioCodec.mjs.map +1 -1
- package/dist/codec.d.mts +0 -2
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +1 -1
- package/dist/codec.mjs.map +1 -1
- package/dist/codec.test.d.mts +1 -1
- package/dist/codec.test.mjs +1 -1
- package/dist/codec.test.mjs.map +1 -1
- package/dist/http.d.mts +0 -1
- package/dist/http.d.mts.map +1 -1
- package/dist/http.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/{magic-string.es-CaWgF-3p.mjs → magic-string.es-CUAnjKYC.mjs} +14 -13
- package/dist/{magic-string.es-CaWgF-3p.mjs.map → magic-string.es-CUAnjKYC.mjs.map} +1 -1
- package/dist/{models-B1wZuvz5.d.mts → models-gP7kSqeb.d.mts} +1 -1
- package/dist/models-gP7kSqeb.d.mts.map +1 -0
- package/dist/models.d.mts +1 -1
- package/dist/onHalt.test.d.mts +1 -1
- package/dist/onHalt.test.mjs +1 -1
- package/dist/onHalt.test.mjs.map +1 -1
- package/dist/{realtimeStt-BFcctqFC.d.mts → realtimeStt-BcthMc57.d.mts} +3 -3
- package/dist/realtimeStt-BcthMc57.d.mts.map +1 -0
- package/dist/realtimeStt.d.mts +1 -1
- package/dist/realtimeStt.mjs.map +1 -1
- package/dist/{test.DNmyFkvJ-CPKukVWS.mjs → test.DNmyFkvJ-N3QqMRwZ.mjs} +247 -222
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
- package/package.json +6 -6
- package/src/Mistral.ts +4 -7
- package/src/MistralRealtimeTranscriber.ts +4 -8
- package/src/MistralSynthesizer.ts +8 -12
- package/src/MistralTranscriber.ts +9 -11
- package/src/audioCodec.ts +25 -20
- package/dist/models-B1wZuvz5.d.mts.map +0 -1
- package/dist/realtimeStt-BFcctqFC.d.mts.map +0 -1
- package/dist/test.DNmyFkvJ-CPKukVWS.mjs.map +0 -1
- /package/dist/{chunk-CfYAbeIz.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -0
package/dist/realtimeStt.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtimeStt.mjs","names":["WSWebSocket"],"sources":["../src/realtimeStt.ts"],"sourcesContent":["/**\n * Voxtral Realtime STT over WebSocket.\n *\n * Protocol mirrors the `mistralai[realtime]` Python client:\n * - URL: `wss://api.mistral.ai/v1/audio/transcriptions/realtime?model=…`\n * (the model is a query param, not a session field).\n * - Auth: `Authorization: Bearer …` header on the upgrade. The browser\n * `WebSocket` API can't set headers, so this module uses the `ws` peer\n * dep; `ws` is only pulled in transitively via\n * `MistralRealtimeTranscriber` (the sync `MistralTranscriber` stays free\n * of it).\n * - Client → server (JSON): `session.update` (audio_format +\n * target_streaming_delay_ms), then `input_audio.append` with base64 PCM,\n * and `input_audio.end` when the mic stream stops.\n * - Server → client (JSON): `session.created` / `session.updated`,\n * `transcription.text.delta` (`{ text }`), `transcription.done`\n * (`{ text }`), `transcription.segment`, `transcription.language`,\n * and `error` (`{ error: { message, code } }`).\n *\n * Voxtral Realtime is a *continuous* transcriber: it streams text deltas and\n * only emits one `transcription.done` at end-of-audio (no server-side\n * utterance segmentation). To satisfy the `Transcriber` contract's `final`\n * semantics for conversational use, this adapter synthesizes a `final` event\n * when the deltas go quiet for `utteranceSilence`.\n */\nimport {\n Cause,\n Clock,\n Duration,\n Effect,\n Encoding,\n Match,\n Queue,\n Redacted,\n Ref,\n Schema,\n Stream,\n} from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport type { TranscriptEvent } from \"@effect-uai/core/Transcript\"\nimport type { CommonStreamTranscribeRequest } from \"@effect-uai/core/Transcriber\"\nimport { WebSocket as WSWebSocket } from \"ws\"\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n /** Target latency knob (`target_streaming_delay_ms`); lower = faster, less accurate. */\n readonly targetStreamingDelay?: Duration.Duration\n /**\n * Silence gap after which the accumulated deltas are committed as a synthetic\n * `final`. Voxtral Realtime does no utterance segmentation, so this is how the\n * adapter delimits turns. Default 700 ms.\n */\n readonly utteranceSilence?: Duration.Duration\n}\n\nconst DEFAULT_SILENCE = Duration.millis(700)\n\n// ---------------------------------------------------------------------------\n// AudioFormat gate — Voxtral Realtime ingests pcm_s16le @ 16000 mono only.\n// ---------------------------------------------------------------------------\n\nconst ensureInputFormat = (format: AudioFormat): Effect.Effect<void, AiError.AiError> =>\n format.container === \"raw\" && format.encoding === \"pcm_s16le\" && format.sampleRate === 16000\n ? Effect.void\n : Effect.fail(\n new AiError.Unsupported({\n provider: \"mistral\",\n capability: \"inputFormat\",\n reason: `Voxtral Realtime accepts pcm_s16le @ 16000 mono only. Got ${JSON.stringify(format)}.`,\n }),\n )\n\n// ---------------------------------------------------------------------------\n// URL + client frame builders\n// ---------------------------------------------------------------------------\n\nconst buildWsUrl = (cfg: Config, model: string) => {\n const base = (cfg.baseUrl ?? \"https://api.mistral.ai\").replace(/^http/, \"ws\")\n return `${base}/v1/audio/transcriptions/realtime?model=${encodeURIComponent(model)}`\n}\n\nconst sessionUpdateFrame = (cfg: Config) =>\n JSON.stringify({\n type: \"session.update\",\n session: {\n audio_format: { encoding: \"pcm_s16le\", sample_rate: 16000 },\n ...(cfg.targetStreamingDelay !== undefined && {\n target_streaming_delay_ms: Duration.toMillis(cfg.targetStreamingDelay),\n }),\n },\n })\n\nconst audioAppendFrame = (bytes: Uint8Array) =>\n JSON.stringify({ type: \"input_audio.append\", audio: Encoding.encodeBase64(bytes) })\n\nconst audioEndFrame = JSON.stringify({ type: \"input_audio.end\" })\n\n// ---------------------------------------------------------------------------\n// Wire schemas (server → client)\n// ---------------------------------------------------------------------------\n\nconst ServerEvent = Schema.Union([\n Schema.Struct({ type: Schema.Literal(\"session.created\") }),\n Schema.Struct({ type: Schema.Literal(\"session.updated\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.language\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.segment\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.text.delta\"), text: Schema.String }),\n Schema.Struct({ type: Schema.Literal(\"transcription.done\"), text: Schema.String }),\n Schema.Struct({\n type: Schema.Literal(\"error\"),\n error: Schema.Struct({\n message: Schema.String,\n code: Schema.optional(Schema.NullOr(Schema.Number)),\n }),\n }),\n])\nconst decodeServerEvent = Schema.decodeUnknownEffect(ServerEvent)\n\n/** Mutable turn state shared by the message handler and the silence finalizer. */\ntype TurnState = {\n readonly text: Ref.Ref<string>\n readonly lastActivityMs: Ref.Ref<number>\n}\n\nconst emitFinal = (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n state: TurnState,\n override?: string,\n): Effect.Effect<void> =>\n Effect.gen(function* () {\n const acc = yield* Ref.getAndSet(state.text, \"\")\n const text = (override !== undefined && override.length > 0 ? override : acc).trim()\n if (text.length > 0) yield* Queue.offer(queue, { _tag: \"final\", text })\n })\n\nconst handleServerMessage =\n (queue: Queue.Queue<TranscriptEvent, Cause.Done>, state: TurnState) => (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decodeServerEvent(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return\n yield* Match.value(decoded.value).pipe(\n Match.when({ type: \"transcription.text.delta\" }, (m) =>\n Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const next = yield* Ref.updateAndGet(state.text, (t) => t + m.text)\n yield* Ref.set(state.lastActivityMs, now)\n // Cumulative partial so the UI shows the growing sentence.\n yield* Queue.offer(queue, { _tag: \"partial\", text: next })\n }),\n ),\n // End-of-audio: commit whatever's left as the final utterance.\n Match.when({ type: \"transcription.done\" }, (m) => emitFinal(queue, state, m.text)),\n Match.when({ type: \"error\" }, (m) =>\n Queue.offer(queue, {\n _tag: \"error\",\n ...(m.error.code != null && { code: String(m.error.code) }),\n message: m.error.message,\n }),\n ),\n // session.created / .updated / language / segment: no user-visible event.\n Match.orElse(() => Effect.void),\n )\n })\n\n// Background loop: commit a synthetic final once the deltas go quiet.\nconst silenceFinalizer = (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n state: TurnState,\n silence: Duration.Duration,\n): Effect.Effect<never> => {\n const silenceMs = Duration.toMillis(silence)\n return Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const last = yield* Ref.get(state.lastActivityMs)\n const acc = yield* Ref.get(state.text)\n if (acc.trim().length > 0 && now - last >= silenceMs) yield* emitFinal(queue, state)\n yield* Effect.sleep(\"150 millis\")\n }).pipe(Effect.forever)\n}\n\n// ---------------------------------------------------------------------------\n// Stream<Uint8Array> → Stream<TranscriptEvent>\n// ---------------------------------------------------------------------------\n\n// `@types/ws`'s WebSocket extends Node's EventEmitter while\n// `globalThis.WebSocket` extends EventTarget; the browser-style surface\n// Effect's Socket reads (`addEventListener` / `send` / `close`) is identical at\n// runtime, hence the single contained cast.\nconst authedWsConstructor =\n (cfg: Config): Socket.WebSocketConstructor[\"Service\"] =>\n (url) =>\n new WSWebSocket(url, undefined, {\n headers: { Authorization: `Bearer ${Redacted.value(cfg.apiKey)}` },\n }) as unknown as globalThis.WebSocket\n\nexport const streamTranscription =\n (cfg: Config) =>\n <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* ensureInputFormat(request.inputFormat)\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model), {\n // Effect's Socket treats all close codes as errors by default —\n // whitelist standard clean-close codes (1000 / 1001 / 1005).\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg)))\n const queue = yield* Queue.bounded<TranscriptEvent, Cause.Done>(64)\n const state: TurnState = {\n text: yield* Ref.make(\"\"),\n lastActivityMs: yield* Ref.make(0),\n }\n const write = yield* socket.writer\n\n // session.update first, then stream base64 PCM frames, then signal end.\n // Fork-scoped so the Stream's downstream scope tears them down on\n // disconnect / cancel.\n yield* Effect.gen(function* () {\n yield* write(sessionUpdateFrame(cfg))\n yield* Stream.runForEach(audioIn, (bytes) => write(audioAppendFrame(bytes)))\n yield* write(audioEndFrame)\n }).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"[voxtral-realtime] audio send failed\", { cause }),\n ),\n Effect.ignore,\n Effect.forkScoped,\n )\n\n yield* silenceFinalizer(queue, state, cfg.utteranceSilence ?? DEFAULT_SILENCE).pipe(\n Effect.forkScoped,\n )\n\n // `Queue.end` flushes pending events then ends the stream cleanly;\n // `Queue.shutdown` would drop queued items and interrupt takes. A\n // connection/read failure is logged (otherwise the stream would end\n // silently with no transcripts).\n yield* socket.runString(handleServerMessage(queue, state)).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"[voxtral-realtime] socket closed\", { cause }),\n ),\n Effect.ensuring(Queue.end(queue)),\n Effect.forkScoped,\n )\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"realtimeStt.mjs","names":["WSWebSocket"],"sources":["../src/realtimeStt.ts"],"sourcesContent":["/**\n * Voxtral Realtime STT over WebSocket.\n *\n * Protocol mirrors the `mistralai[realtime]` Python client:\n * - URL: `wss://api.mistral.ai/v1/audio/transcriptions/realtime?model=…`\n * (the model is a query param, not a session field).\n * - Auth: `Authorization: Bearer …` header on the upgrade. The browser\n * `WebSocket` API can't set headers, so this module uses the `ws` peer\n * dep; `ws` is only pulled in transitively via\n * `MistralRealtimeTranscriber` (the sync `MistralTranscriber` stays free\n * of it).\n * - Client → server (JSON): `session.update` (audio_format +\n * target_streaming_delay_ms), then `input_audio.append` with base64 PCM,\n * and `input_audio.end` when the mic stream stops.\n * - Server → client (JSON): `session.created` / `session.updated`,\n * `transcription.text.delta` (`{ text }`), `transcription.done`\n * (`{ text }`), `transcription.segment`, `transcription.language`,\n * and `error` (`{ error: { message, code } }`).\n *\n * Voxtral Realtime is a *continuous* transcriber: it streams text deltas and\n * only emits one `transcription.done` at end-of-audio (no server-side\n * utterance segmentation). To satisfy the `Transcriber` contract's `final`\n * semantics for conversational use, this adapter synthesizes a `final` event\n * when the deltas go quiet for `utteranceSilence`.\n */\nimport {\n Cause,\n Clock,\n Duration,\n Effect,\n Encoding,\n Match,\n Queue,\n Redacted,\n Ref,\n Schema,\n Stream,\n} from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport type { TranscriptEvent } from \"@effect-uai/core/Transcript\"\nimport type { CommonStreamTranscribeRequest } from \"@effect-uai/core/Transcriber\"\nimport { WebSocket as WSWebSocket } from \"ws\"\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n /** Target latency knob (`target_streaming_delay_ms`); lower = faster, less accurate. */\n readonly targetStreamingDelay?: Duration.Duration\n /**\n * Silence gap after which the accumulated deltas are committed as a synthetic\n * `final`. Voxtral Realtime does no utterance segmentation, so this is how the\n * adapter delimits turns. Default 700 ms.\n */\n readonly utteranceSilence?: Duration.Duration\n}\n\nconst DEFAULT_SILENCE = Duration.millis(700)\n\n// ---------------------------------------------------------------------------\n// AudioFormat gate — Voxtral Realtime ingests pcm_s16le @ 16000 mono only.\n// ---------------------------------------------------------------------------\n\nconst ensureInputFormat = (format: AudioFormat): Effect.Effect<void, AiError.AiError> =>\n format.container === \"raw\" && format.encoding === \"pcm_s16le\" && format.sampleRate === 16000\n ? Effect.void\n : Effect.fail(\n new AiError.Unsupported({\n provider: \"mistral\",\n capability: \"inputFormat\",\n reason: `Voxtral Realtime accepts pcm_s16le @ 16000 mono only. Got ${JSON.stringify(format)}.`,\n }),\n )\n\n// ---------------------------------------------------------------------------\n// URL + client frame builders\n// ---------------------------------------------------------------------------\n\nconst buildWsUrl = (cfg: Config, model: string) => {\n const base = (cfg.baseUrl ?? \"https://api.mistral.ai\").replace(/^http/, \"ws\")\n return `${base}/v1/audio/transcriptions/realtime?model=${encodeURIComponent(model)}`\n}\n\nconst sessionUpdateFrame = (cfg: Config) =>\n JSON.stringify({\n type: \"session.update\",\n session: {\n audio_format: { encoding: \"pcm_s16le\", sample_rate: 16000 },\n ...(cfg.targetStreamingDelay !== undefined && {\n target_streaming_delay_ms: Duration.toMillis(cfg.targetStreamingDelay),\n }),\n },\n })\n\nconst audioAppendFrame = (bytes: Uint8Array) =>\n JSON.stringify({ type: \"input_audio.append\", audio: Encoding.encodeBase64(bytes) })\n\nconst audioEndFrame = JSON.stringify({ type: \"input_audio.end\" })\n\n// ---------------------------------------------------------------------------\n// Wire schemas (server → client)\n// ---------------------------------------------------------------------------\n\nconst ServerEvent = Schema.Union([\n Schema.Struct({ type: Schema.Literal(\"session.created\") }),\n Schema.Struct({ type: Schema.Literal(\"session.updated\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.language\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.segment\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.text.delta\"), text: Schema.String }),\n Schema.Struct({ type: Schema.Literal(\"transcription.done\"), text: Schema.String }),\n Schema.Struct({\n type: Schema.Literal(\"error\"),\n error: Schema.Struct({\n message: Schema.String,\n code: Schema.optional(Schema.NullOr(Schema.Number)),\n }),\n }),\n])\nconst decodeServerEvent = Schema.decodeUnknownEffect(ServerEvent)\n\n/** Mutable turn state shared by the message handler and the silence finalizer. */\ntype TurnState = {\n readonly text: Ref.Ref<string>\n readonly lastActivityMs: Ref.Ref<number>\n}\n\nconst emitFinal = (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n state: TurnState,\n override?: string,\n): Effect.Effect<void> =>\n Effect.gen(function* () {\n const acc = yield* Ref.getAndSet(state.text, \"\")\n const text = (override !== undefined && override.length > 0 ? override : acc).trim()\n if (text.length > 0) yield* Queue.offer(queue, { _tag: \"final\", text })\n })\n\nconst handleServerMessage =\n (queue: Queue.Queue<TranscriptEvent, Cause.Done>, state: TurnState) => (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decodeServerEvent(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return\n yield* Match.value(decoded.value).pipe(\n Match.when({ type: \"transcription.text.delta\" }, (m) =>\n Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const next = yield* Ref.updateAndGet(state.text, (t) => t + m.text)\n yield* Ref.set(state.lastActivityMs, now)\n // Cumulative partial so the UI shows the growing sentence.\n yield* Queue.offer(queue, { _tag: \"partial\", text: next })\n }),\n ),\n // End-of-audio: commit whatever's left as the final utterance.\n Match.when({ type: \"transcription.done\" }, (m) => emitFinal(queue, state, m.text)),\n Match.when({ type: \"error\" }, (m) =>\n Queue.offer(queue, {\n _tag: \"error\",\n ...(m.error.code != null && { code: String(m.error.code) }),\n message: m.error.message,\n }),\n ),\n // session.created / .updated / language / segment: no user-visible event.\n Match.orElse(() => Effect.void),\n )\n })\n\n// Background loop: commit a synthetic final once the deltas go quiet.\nconst silenceFinalizer = (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n state: TurnState,\n silence: Duration.Duration,\n): Effect.Effect<never> => {\n const silenceMs = Duration.toMillis(silence)\n return Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const last = yield* Ref.get(state.lastActivityMs)\n const acc = yield* Ref.get(state.text)\n if (acc.trim().length > 0 && now - last >= silenceMs) yield* emitFinal(queue, state)\n yield* Effect.sleep(\"150 millis\")\n }).pipe(Effect.forever)\n}\n\n// ---------------------------------------------------------------------------\n// Stream<Uint8Array> → Stream<TranscriptEvent>\n// ---------------------------------------------------------------------------\n\n// `@types/ws`'s WebSocket extends Node's EventEmitter while\n// `globalThis.WebSocket` extends EventTarget; the browser-style surface\n// Effect's Socket reads (`addEventListener` / `send` / `close`) is identical at\n// runtime, hence the single contained cast.\nconst authedWsConstructor =\n (cfg: Config): Socket.WebSocketConstructor[\"Service\"] =>\n (url) =>\n new WSWebSocket(url, undefined, {\n headers: { Authorization: `Bearer ${Redacted.value(cfg.apiKey)}` },\n }) as unknown as globalThis.WebSocket\n\nexport const streamTranscription =\n (cfg: Config) =>\n <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* ensureInputFormat(request.inputFormat)\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model), {\n // Effect's Socket treats all close codes as errors by default —\n // whitelist standard clean-close codes (1000 / 1001 / 1005).\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg)))\n const queue = yield* Queue.bounded<TranscriptEvent, Cause.Done>(64)\n const state: TurnState = {\n text: yield* Ref.make(\"\"),\n lastActivityMs: yield* Ref.make(0),\n }\n const write = yield* socket.writer\n\n // session.update first, then stream base64 PCM frames, then signal end.\n // Fork-scoped so the Stream's downstream scope tears them down on\n // disconnect / cancel.\n yield* Effect.gen(function* () {\n yield* write(sessionUpdateFrame(cfg))\n yield* Stream.runForEach(audioIn, (bytes) => write(audioAppendFrame(bytes)))\n yield* write(audioEndFrame)\n }).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"[voxtral-realtime] audio send failed\", { cause }),\n ),\n Effect.ignore,\n Effect.forkScoped,\n )\n\n yield* silenceFinalizer(queue, state, cfg.utteranceSilence ?? DEFAULT_SILENCE).pipe(\n Effect.forkScoped,\n )\n\n // `Queue.end` flushes pending events then ends the stream cleanly;\n // `Queue.shutdown` would drop queued items and interrupt takes. A\n // connection/read failure is logged (otherwise the stream would end\n // silently with no transcripts).\n yield* socket.runString(handleServerMessage(queue, state)).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"[voxtral-realtime] socket closed\", { cause }),\n ),\n Effect.ensuring(Queue.end(queue)),\n Effect.forkScoped,\n )\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":"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"}
|