1import {
2 VoiceTranscriptionError,
3 VOICE_RECORDING_LIMIT_SECONDS,
4 throwIfVoiceTranscriptionAborted,
5 type VoiceTranscriber,
6} from "@t3tools/client-runtime/voice-input";
7
8const pcmBytesPerSecond = 16_000 * 2;
9const maxPcmBytes = pcmBytesPerSecond * VOICE_RECORDING_LIMIT_SECONDS;
10
11export function createEnvironmentTranscriber(input: {
12 readonly canSend: () => boolean;
13 readonly prepare: (signal: AbortSignal) => Promise<string>;
14 readonly decode: (uri: string) => Promise<Uint8Array>;
15 readonly upload: (pcm: Uint8Array, signal: AbortSignal) => Promise<string>;
16}): VoiceTranscriber {
17 function check(signal: AbortSignal) {
18 throwIfVoiceTranscriptionAborted(signal);
19 if (!input.canSend()) {
20 throw new VoiceTranscriptionError({
21 code: "cancelled",
22 message: "Your transcription choice changed.",
23 });
24 }
25 }
26 return {
27 async prepare({ signal }) {
28 check(signal);
29 const locale = await input.prepare(signal);
30 check(signal);
31 return {
32 locale,
33 async transcribe(uri, { signal }) {
34 check(signal);
35 const decoded = await input.decode(uri);
36 check(signal);
37 if (
38 !decoded.length ||
39 decoded.length % 2 ||
40 decoded.length > maxPcmBytes + pcmBytesPerSecond
41 ) {
42 throw new VoiceTranscriptionError({
43 code: "transcription-failed",
44 message: "This recording couldn’t be read. Try recording again.",
45 });
46 }
47 // A timed native stop can include a final encoder packet beyond the recording limit.
48 const bytes = decoded.subarray(0, maxPcmBytes);
49 const samples =
50 bytes.byteOffset % 2 === 0
51 ? new Int16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2)
52 : new Int16Array(Uint8Array.from(bytes).buffer);
53 const pcm = new Float32Array(samples.length);
54 for (let i = 0; i < samples.length; i++) pcm[i] = samples[i]! / 32768;
55 const text = await input.upload(new Uint8Array(pcm.buffer), signal);
56 check(signal);
57 return text;
58 },
59 };
60 },
61 };
62}