@agent-native/toolkit 0.13.3 → 0.13.4
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/composer/useRealtimeVoiceMode.d.ts +3 -0
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +2 -0
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useVoiceDictation.browser.spec.d.ts +2 -0
- package/dist/composer/useVoiceDictation.browser.spec.d.ts.map +1 -0
- package/dist/composer/useVoiceDictation.browser.spec.js +220 -0
- package/dist/composer/useVoiceDictation.browser.spec.js.map +1 -0
- package/dist/composer/useVoiceDictation.d.ts +1 -0
- package/dist/composer/useVoiceDictation.d.ts.map +1 -1
- package/dist/composer/useVoiceDictation.js +76 -22
- package/dist/composer/useVoiceDictation.js.map +1 -1
- package/package.json +1 -1
- package/src/composer/useRealtimeVoiceMode.tsx +4 -0
- package/src/composer/useVoiceDictation.browser.spec.tsx +265 -0
- package/src/composer/useVoiceDictation.ts +85 -20
|
@@ -348,7 +348,18 @@ export function voiceDictationStartErrorMessage(error: unknown): string {
|
|
|
348
348
|
return message || "Could not start recording";
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
|
|
351
|
+
/** Retrying these through another provider re-prompts and fails the same way. */
|
|
352
|
+
function isMicPermissionError(error: string | undefined): boolean {
|
|
353
|
+
return (
|
|
354
|
+
error === "not-allowed" ||
|
|
355
|
+
error === "service-not-allowed" ||
|
|
356
|
+
error === "audio-capture"
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function voiceDictationSpeechErrorMessage(
|
|
361
|
+
error: string | undefined,
|
|
362
|
+
): string {
|
|
352
363
|
if (error === "not-allowed" || error === "service-not-allowed") {
|
|
353
364
|
return voiceDictationStartErrorMessage({
|
|
354
365
|
name: "NotAllowedError",
|
|
@@ -358,7 +369,13 @@ function voiceDictationSpeechErrorMessage(error: string | undefined): string {
|
|
|
358
369
|
if (error === "audio-capture") {
|
|
359
370
|
return "No microphone was found. Plug one in or choose a different input, then try again.";
|
|
360
371
|
}
|
|
361
|
-
|
|
372
|
+
if (error === "network") {
|
|
373
|
+
return "Speech recognition couldn't reach its service. Check your connection, or pick a different source in Settings → Voice Transcription.";
|
|
374
|
+
}
|
|
375
|
+
if (error === "aborted" || error === undefined) {
|
|
376
|
+
return "Dictation stopped before it captured any audio. Another app or tab may be holding the microphone — close it, or pick a different source in Settings → Voice Transcription.";
|
|
377
|
+
}
|
|
378
|
+
return `Speech recognition error: ${error}`;
|
|
362
379
|
}
|
|
363
380
|
|
|
364
381
|
export function useVoiceDictation(
|
|
@@ -688,28 +705,20 @@ export function useVoiceDictation(
|
|
|
688
705
|
);
|
|
689
706
|
|
|
690
707
|
const startBrowser = useCallback(
|
|
691
|
-
async (
|
|
708
|
+
async (
|
|
709
|
+
prefs: VoicePrefs,
|
|
710
|
+
/** Return true to take over when the recognizer never opened the mic.
|
|
711
|
+
* Brave ships `webkitSpeechRecognition` with no speech backend, so
|
|
712
|
+
* feature detection alone cannot tell dictation will work. */
|
|
713
|
+
onUnavailable?: (error: string | undefined) => boolean,
|
|
714
|
+
) => {
|
|
692
715
|
const Ctor = getSpeechRecognitionCtor();
|
|
693
716
|
if (!Ctor) {
|
|
694
717
|
throw new Error(
|
|
695
718
|
"Your browser doesn't support speech recognition. Add an OpenAI API key in settings for Whisper transcription.",
|
|
696
719
|
);
|
|
697
720
|
}
|
|
698
|
-
// Still request mic to drive the amplitude meter, so the UI doesn't look
|
|
699
|
-
// dead while the user talks. SpeechRecognition manages its own capture
|
|
700
|
-
// under the hood in most browsers.
|
|
701
|
-
let stream: MediaStream | null = null;
|
|
702
|
-
try {
|
|
703
|
-
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
704
|
-
mediaStreamRef.current = stream;
|
|
705
|
-
startMeter(stream);
|
|
706
|
-
} catch {
|
|
707
|
-
/* non-fatal — recognition can still work without our analyser */
|
|
708
|
-
}
|
|
709
|
-
|
|
710
721
|
if (cancelledRef.current) {
|
|
711
|
-
if (stream) for (const track of stream.getTracks()) track.stop();
|
|
712
|
-
mediaStreamRef.current = null;
|
|
713
722
|
cancelledRef.current = false;
|
|
714
723
|
setState("idle");
|
|
715
724
|
return;
|
|
@@ -723,7 +732,34 @@ export function useVoiceDictation(
|
|
|
723
732
|
speechRef.current = recognition;
|
|
724
733
|
speechTranscriptRef.current = "";
|
|
725
734
|
|
|
735
|
+
let capturing = false;
|
|
736
|
+
let fatal = false;
|
|
737
|
+
let lastError: string | undefined;
|
|
738
|
+
|
|
739
|
+
// Opening our own capture before the speech service has claimed the
|
|
740
|
+
// device makes Chrome abort the session outright. Attach the meter only
|
|
741
|
+
// once recognition is actually listening.
|
|
742
|
+
recognition.onaudiostart = () => {
|
|
743
|
+
capturing = true;
|
|
744
|
+
if (cancelledRef.current) return;
|
|
745
|
+
void Promise.resolve()
|
|
746
|
+
.then(() => navigator.mediaDevices?.getUserMedia({ audio: true }))
|
|
747
|
+
.then((stream) => {
|
|
748
|
+
if (!stream) return;
|
|
749
|
+
if (cancelledRef.current || speechRef.current !== recognition) {
|
|
750
|
+
for (const track of stream.getTracks()) track.stop();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
mediaStreamRef.current = stream;
|
|
754
|
+
startMeter(stream);
|
|
755
|
+
})
|
|
756
|
+
.catch(() => {
|
|
757
|
+
/* the meter is decoration; recognition owns the real capture */
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
|
+
|
|
726
761
|
recognition.onresult = (event: any) => {
|
|
762
|
+
capturing = true;
|
|
727
763
|
let interim = "";
|
|
728
764
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
729
765
|
const result = event.results[i];
|
|
@@ -736,16 +772,30 @@ export function useVoiceDictation(
|
|
|
736
772
|
}
|
|
737
773
|
onLiveUpdateRef.current?.(speechTranscriptRef.current, interim);
|
|
738
774
|
};
|
|
775
|
+
// `end` always follows `error`, so every outcome is decided there. Acting
|
|
776
|
+
// here too would either pre-empt the fallback or be overwritten by it.
|
|
739
777
|
recognition.onerror = (event: any) => {
|
|
778
|
+
lastError = event?.error;
|
|
740
779
|
if (event?.error === "no-speech" || event?.error === "aborted") return;
|
|
741
|
-
|
|
780
|
+
fatal = true;
|
|
742
781
|
};
|
|
743
782
|
recognition.onend = () => {
|
|
744
783
|
const text = speechTranscriptRef.current.trim();
|
|
745
784
|
const wasCancelled = cancelledRef.current;
|
|
746
785
|
cancelledRef.current = false;
|
|
747
786
|
teardown();
|
|
748
|
-
if (wasCancelled
|
|
787
|
+
if (wasCancelled) {
|
|
788
|
+
setState("idle");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (!text) {
|
|
792
|
+
// A recognizer that failed, or that ended before the mic ever opened,
|
|
793
|
+
// produced nothing usable — that is not the same as hearing silence.
|
|
794
|
+
if (fatal || !capturing) {
|
|
795
|
+
if (onUnavailable?.(lastError)) return;
|
|
796
|
+
failWith(voiceDictationSpeechErrorMessage(lastError));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
749
799
|
setState("idle");
|
|
750
800
|
return;
|
|
751
801
|
}
|
|
@@ -1082,7 +1132,22 @@ export function useVoiceDictation(
|
|
|
1082
1132
|
}
|
|
1083
1133
|
await startGoogleRealtime(prefs);
|
|
1084
1134
|
} else {
|
|
1085
|
-
|
|
1135
|
+
// Only "auto" promised a working recognizer of any kind; an explicit
|
|
1136
|
+
// browser preference must surface its own failure instead.
|
|
1137
|
+
await startBrowser(
|
|
1138
|
+
prefs,
|
|
1139
|
+
pref === "auto" && mediaRecorderSupported
|
|
1140
|
+
? (error) => {
|
|
1141
|
+
if (isMicPermissionError(error)) return false;
|
|
1142
|
+
activeProviderRef.current = "openai";
|
|
1143
|
+
setState("starting");
|
|
1144
|
+
void startOpenAi("auto", prefs.instructions).catch((err) =>
|
|
1145
|
+
failWith(voiceDictationStartErrorMessage(err)),
|
|
1146
|
+
);
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
: undefined,
|
|
1150
|
+
);
|
|
1086
1151
|
}
|
|
1087
1152
|
} catch (err) {
|
|
1088
1153
|
if (cancelledRef.current) {
|