Open History to see the last commit
1import { requireOptionalNativeModule } from "expo";
2import type { RecorderState, RecordingStatus } from "expo-audio";
3import { Platform } from "react-native";
4import type { NativeVoiceRecorder } from "../features/voice-input/lazyVoiceRecorder";
5
6import {
7 VoiceTranscriptionError,
8 throwIfVoiceTranscriptionAborted,
9 type VoiceTranscriber,
10} from "@t3tools/client-runtime/voice-input";
11
12type PreparedGoogleTranscription = { readonly locale: string; readonly mode: number };
13
14type GoogleTranscriptionModule = {
15 checkAvailability(): Promise<boolean>;
16 prepare(id: number): Promise<PreparedGoogleTranscription>;
17 transcribe(id: number, uri: string, locale: string, mode: number): Promise<string>;
18 cancel(id: number): Promise<void>;
19 readonly LiveVoiceRecorder?: new () => NativeVoiceRecorder<RecordingStatus, RecorderState>;
20 startLive?(id: number, uri: string, locale: string, mode: number): Promise<string>;
21};
22
23const native = requireOptionalNativeModule<GoogleTranscriptionModule>("T3VoiceTranscription");
24
25const nativeErrorMessages: Readonly<Record<string, string>> = {
26 unavailable: "On-device voice transcription is not available for this device language.",
27 storage: "Not enough storage for on-device dictation. Free device storage and retry.",
28 quota: "On-device speech quota reached. Let the device rest or charge it, then retry.",
29 "no-speech": "No speech was detected. Speak again and retry dictation.",
30 busy: "On-device speech is busy. Wait for it to finish, then retry dictation.",
31 "needs-update": "Update Android and its on-device speech services, then retry dictation.",
32 "download-failed": "Speech model download failed. Check your network connection and retry.",
33 "audio-overflow":
34 "The speech engine could not process the complete recording. No text was added. Retry dictation.",
35 "incomplete-audio":
36 "Speech recognition could not process the complete recording. No text was added. Retry dictation.",
37};
38
39let nextOperationId = 0;
40
41export function createLiveVoiceRecorder() {
42 return Number(Platform.Version) >= 31 && native?.LiveVoiceRecorder
43 ? new native.LiveVoiceRecorder()
44 : null;
45}
46
47export async function getLocalVoiceTranscriber(): Promise<VoiceTranscriber | null> {
48 if (!native || !(await native.checkAvailability())) return null;
49 const module = native;
50 return {
51 prepare: async ({ signal }) => {
52 const prepared = await runOperation(module, signal, "preparation-failed", (id) =>
53 module.prepare(id),
54 );
55 const startLive = module.startLive?.bind(module);
56 return {
57 locale: prepared.locale,
58 ...(module.LiveVoiceRecorder && startLive
59 ? {
60 start: (uri: string, { signal }: { signal: AbortSignal }) =>
61 runOperation(module, signal, "transcription-failed", (id) =>
62 startLive(id, uri, prepared.locale, prepared.mode),
63 ),
64 }
65 : {}),
66 transcribe: (uri, { signal }) =>
67 runOperation(module, signal, "transcription-failed", (id) =>
68 module.transcribe(id, uri, prepared.locale, prepared.mode),
69 ),
70 };
71 },
72 };
73}
74
75async function runOperation<T>(
76 module: GoogleTranscriptionModule,
77 signal: AbortSignal,
78 fallbackCode: "preparation-failed" | "transcription-failed",
79 operation: (id: number) => Promise<T>,
80): Promise<T> {
81 throwIfVoiceTranscriptionAborted(signal);
82 const id = ++nextOperationId;
83 let cancellation: Promise<void> | undefined;
84 const abort = () => {
85 cancellation = module.cancel(id).catch(() => {});
86 };
87 signal.addEventListener("abort", abort, { once: true });
88 try {
89 const result = await operation(id);
90 throwIfVoiceTranscriptionAborted(signal);
91 return result;
92 } catch (error) {
93 throwIfVoiceTranscriptionAborted(signal);
94 const code =
95 typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
96 throw new VoiceTranscriptionError({
97 code: code === "unavailable" || code === "unsupported-locale" ? code : fallbackCode,
98 message:
99 (typeof code === "string" ? nativeErrorMessages[code] : undefined) ??
100 (fallbackCode === "preparation-failed"
101 ? "Voice transcription could not prepare this language. Check Google speech services and try again."
102 : "Voice transcription failed."),
103 cause: error,
104 });
105 } finally {
106 signal.removeEventListener("abort", abort);
107 await cancellation;
108 }
109}