@mentra/engine 3.4.0-dev.1182 → 3.4.0-dev.1183
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/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/DeviceEventRouter.js +2 -2
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +55 -5
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +293 -171
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.d.ts +10 -32
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.js +16 -58
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
- package/build/services/TranscriptionRouting.d.ts +112 -9
- package/build/services/TranscriptionRouting.d.ts.map +1 -1
- package/build/services/TranscriptionRouting.js +159 -21
- package/build/services/TranscriptionRouting.js.map +1 -1
- package/build/services/TranscriptionSubscriptions.d.ts +87 -6
- package/build/services/TranscriptionSubscriptions.d.ts.map +1 -1
- package/build/services/TranscriptionSubscriptions.js +174 -26
- package/build/services/TranscriptionSubscriptions.js.map +1 -1
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/DeviceEventRouter.ts +2 -2
- package/src/services/LocalMiniappRuntime.ts +325 -213
- package/src/services/LocalSttFallbackCoordinator.ts +15 -62
- package/src/services/TranscriptionRouting.ts +200 -30
- package/src/services/TranscriptionSubscriptions.ts +245 -25
|
@@ -14,10 +14,10 @@ export interface LocalSttModelStatus {
|
|
|
14
14
|
reason?: string;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Runs on-device Sherpa STT for local miniapps while LocalMiniappRuntime says
|
|
18
|
+
* a transcription listener needs it (`setDemand`): a `localOnly` listener, a
|
|
19
|
+
* `preferLocal` one the picked model can serve, any listener while the cloud
|
|
20
|
+
* is unreachable, or one still finishing an on-device utterance.
|
|
21
21
|
*
|
|
22
22
|
* There is one recognizer and it always runs the model picked on the Speech
|
|
23
23
|
* page. When on-device transcription is needed and that model is missing, it
|
|
@@ -30,21 +30,13 @@ export interface LocalSttModelStatus {
|
|
|
30
30
|
* `local_transcription` Bridge event; the host wires that event to
|
|
31
31
|
* `localMiniappRuntime.forwardEvent`, with this coordinator providing the
|
|
32
32
|
* subscription-active gate so we never publish transcripts to miniapps that
|
|
33
|
-
* aren't asking for them. LocalMiniappRuntime
|
|
34
|
-
*
|
|
33
|
+
* aren't asking for them. LocalMiniappRuntime routes local and cloud
|
|
34
|
+
* transcripts to each listener.
|
|
35
35
|
*/
|
|
36
36
|
declare class LocalSttFallbackCoordinator {
|
|
37
37
|
private static instance;
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
/**
|
|
41
|
-
* Default to "cloud is up" so we never accidentally activate local STT before
|
|
42
|
-
* the cloud client has had a chance to start. We attach lazily on the first
|
|
43
|
-
* subscription/reconcile pass; once attached, this field reflects the real
|
|
44
|
-
* cloud-client runtime status.
|
|
45
|
-
*/
|
|
46
|
-
private cloudConnected;
|
|
47
|
-
private cloudAdapterAttached;
|
|
38
|
+
/** A miniapp transcription listener needs on-device transcripts. */
|
|
39
|
+
private demand;
|
|
48
40
|
private selectionAttached;
|
|
49
41
|
private hydration?;
|
|
50
42
|
private hydrated;
|
|
@@ -62,12 +54,6 @@ declare class LocalSttFallbackCoordinator {
|
|
|
62
54
|
private readonly statusListeners;
|
|
63
55
|
private constructor();
|
|
64
56
|
static getInstance(): LocalSttFallbackCoordinator;
|
|
65
|
-
/**
|
|
66
|
-
* Lazy-attach the engine cloud connection listener. The coordinator is
|
|
67
|
-
* constructed at module load, before engine.start() brings up the cloud
|
|
68
|
-
* client, so we defer until the first reconcile.
|
|
69
|
-
*/
|
|
70
|
-
private attachCloudAdapterIfReady;
|
|
71
57
|
/** A model change on the Speech page reloads the recognizer while it is in use. */
|
|
72
58
|
private attachSelectionIfReady;
|
|
73
59
|
/** True while local transcripts should reach miniapps (the picked model is loaded). */
|
|
@@ -77,22 +63,14 @@ declare class LocalSttFallbackCoordinator {
|
|
|
77
63
|
getModelStatus(): LocalSttModelStatus;
|
|
78
64
|
/** Follow the model status. The listener gets the current status immediately, then every change. */
|
|
79
65
|
onModelStatusChanged(listener: (status: LocalSttModelStatus) => void): () => void;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* Informational hook, retained for API stability. No longer called: the
|
|
83
|
-
* authoritative switching signal is the cloud-client adapter (local miniapps
|
|
84
|
-
* are powered only by V2). v1 cloud transcripts deliberately do NOT drive this
|
|
85
|
-
* coordinator anymore. Kept for future hysteresis if connection-state alone
|
|
86
|
-
* proves too coarse.
|
|
87
|
-
*/
|
|
88
|
-
onCloudTranscript(): void;
|
|
66
|
+
/** Whether a miniapp transcription listener needs on-device transcripts. */
|
|
67
|
+
setDemand(demand: boolean): void;
|
|
89
68
|
private reconcile;
|
|
90
69
|
private start;
|
|
91
70
|
private fail;
|
|
92
71
|
private stop;
|
|
93
72
|
private setNativeDemand;
|
|
94
73
|
private setStatus;
|
|
95
|
-
private shouldUseLocalStt;
|
|
96
74
|
private log;
|
|
97
75
|
}
|
|
98
76
|
declare const localSttFallbackCoordinator: LocalSttFallbackCoordinator;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalSttFallbackCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LocalSttFallbackCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,aAAa,CAAA;IACnE,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,cAAM,2BAA2B;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IAEpD,oEAAoE;IACpE,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,SAAS,CAAC,CAAkB;IACpC,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,WAAW,CAAQ;IAC3B,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAsB;IACxC,8FAA8F;IAC9F,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,OAAO,CAAC,CAAe;IAC/B,OAAO,CAAC,WAAW,CAAsB;IACzC,uFAAuF;IACvF,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,MAAM,CAA+D;IAC7E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAmD;IAEnF,OAAO;IAQP,MAAM,CAAC,WAAW,IAAI,2BAA2B;IAOjD,mFAAmF;IACnF,OAAO,CAAC,sBAAsB;IAM9B,uFAAuF;IACvF,QAAQ,IAAI,OAAO;IAInB,+EAA+E;IAC/E,iBAAiB,IAAI,MAAM,GAAG,IAAI;IAIlC,cAAc,IAAI,mBAAmB;IAIrC,oGAAoG;IACpG,oBAAoB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,GAAG,MAAM,IAAI;IAQjF,4EAA4E;IAC5E,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAMhC,OAAO,CAAC,SAAS;YAiCH,KAAK;IAyDnB,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,IAAI;YAeE,eAAe;IAO7B,OAAO,CAAC,SAAS;IAqBjB,OAAO,CAAC,GAAG;CAGZ;AAMD,QAAA,MAAM,2BAA2B,6BAA4C,CAAA;AAC7E,eAAe,2BAA2B,CAAA"}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
|
|
2
2
|
import { ISLAND_SETTINGS_KEYS } from "../runtime/config";
|
|
3
3
|
import { useSettingsStore } from "../stores/settings";
|
|
4
|
-
import { cloudClientService } from "./CloudClientService";
|
|
5
4
|
import { ModelRetry } from "./ModelRetry";
|
|
6
5
|
import offlineSpeechModelService from "./OfflineSpeechModelService";
|
|
7
6
|
import sttModelManager from "./STTModelManager";
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Runs on-device Sherpa STT for local miniapps while LocalMiniappRuntime says
|
|
9
|
+
* a transcription listener needs it (`setDemand`): a `localOnly` listener, a
|
|
10
|
+
* `preferLocal` one the picked model can serve, any listener while the cloud
|
|
11
|
+
* is unreachable, or one still finishing an on-device utterance.
|
|
13
12
|
*
|
|
14
13
|
* There is one recognizer and it always runs the model picked on the Speech
|
|
15
14
|
* page. When on-device transcription is needed and that model is missing, it
|
|
@@ -22,21 +21,13 @@ import sttModelManager from "./STTModelManager";
|
|
|
22
21
|
* `local_transcription` Bridge event; the host wires that event to
|
|
23
22
|
* `localMiniappRuntime.forwardEvent`, with this coordinator providing the
|
|
24
23
|
* subscription-active gate so we never publish transcripts to miniapps that
|
|
25
|
-
* aren't asking for them. LocalMiniappRuntime
|
|
26
|
-
*
|
|
24
|
+
* aren't asking for them. LocalMiniappRuntime routes local and cloud
|
|
25
|
+
* transcripts to each listener.
|
|
27
26
|
*/
|
|
28
27
|
class LocalSttFallbackCoordinator {
|
|
29
28
|
static instance;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Default to "cloud is up" so we never accidentally activate local STT before
|
|
34
|
-
* the cloud client has had a chance to start. We attach lazily on the first
|
|
35
|
-
* subscription/reconcile pass; once attached, this field reflects the real
|
|
36
|
-
* cloud-client runtime status.
|
|
37
|
-
*/
|
|
38
|
-
cloudConnected = true;
|
|
39
|
-
cloudAdapterAttached = false;
|
|
29
|
+
/** A miniapp transcription listener needs on-device transcripts. */
|
|
30
|
+
demand = false;
|
|
40
31
|
selectionAttached = false;
|
|
41
32
|
hydration;
|
|
42
33
|
hydrated = false;
|
|
@@ -65,25 +56,6 @@ class LocalSttFallbackCoordinator {
|
|
|
65
56
|
}
|
|
66
57
|
return LocalSttFallbackCoordinator.instance;
|
|
67
58
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Lazy-attach the engine cloud connection listener. The coordinator is
|
|
70
|
-
* constructed at module load, before engine.start() brings up the cloud
|
|
71
|
-
* client, so we defer until the first reconcile.
|
|
72
|
-
*/
|
|
73
|
-
attachCloudAdapterIfReady() {
|
|
74
|
-
if (this.cloudAdapterAttached)
|
|
75
|
-
return;
|
|
76
|
-
this.cloudConnected = cloudClientService.isConnected();
|
|
77
|
-
cloudClientService.onStatusChanged((status) => {
|
|
78
|
-
const connected = status.status === "connected";
|
|
79
|
-
if (this.cloudConnected === connected)
|
|
80
|
-
return;
|
|
81
|
-
this.log(`cloud connection -> ${connected ? "up" : "down"}`);
|
|
82
|
-
this.cloudConnected = connected;
|
|
83
|
-
this.reconcile();
|
|
84
|
-
});
|
|
85
|
-
this.cloudAdapterAttached = true;
|
|
86
|
-
}
|
|
87
59
|
/** A model change on the Speech page reloads the recognizer while it is in use. */
|
|
88
60
|
attachSelectionIfReady() {
|
|
89
61
|
if (this.selectionAttached)
|
|
@@ -110,28 +82,17 @@ class LocalSttFallbackCoordinator {
|
|
|
110
82
|
this.statusListeners.delete(listener);
|
|
111
83
|
};
|
|
112
84
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.log(`
|
|
117
|
-
|
|
118
|
-
this.hasTranscriptionSubscription = hasTranscription;
|
|
119
|
-
this.hasForceLocalSubscription = hasTranscription && hasForceLocal;
|
|
85
|
+
/** Whether a miniapp transcription listener needs on-device transcripts. */
|
|
86
|
+
setDemand(demand) {
|
|
87
|
+
if (this.demand !== demand)
|
|
88
|
+
this.log(`on-device demand -> ${demand}`);
|
|
89
|
+
this.demand = demand;
|
|
120
90
|
this.reconcile();
|
|
121
91
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Informational hook, retained for API stability. No longer called: the
|
|
124
|
-
* authoritative switching signal is the cloud-client adapter (local miniapps
|
|
125
|
-
* are powered only by V2). v1 cloud transcripts deliberately do NOT drive this
|
|
126
|
-
* coordinator anymore. Kept for future hysteresis if connection-state alone
|
|
127
|
-
* proves too coarse.
|
|
128
|
-
*/
|
|
129
|
-
onCloudTranscript() { }
|
|
130
92
|
reconcile() {
|
|
131
|
-
this.attachCloudAdapterIfReady();
|
|
132
93
|
this.attachSelectionIfReady();
|
|
133
|
-
if (!this.
|
|
134
|
-
this.stop(
|
|
94
|
+
if (!this.demand) {
|
|
95
|
+
this.stop("no listener needs it");
|
|
135
96
|
return;
|
|
136
97
|
}
|
|
137
98
|
if (!this.hydrated) {
|
|
@@ -166,7 +127,7 @@ class LocalSttFallbackCoordinator {
|
|
|
166
127
|
}
|
|
167
128
|
async start(code) {
|
|
168
129
|
const generation = ++this.generation;
|
|
169
|
-
const current = () => generation === this.generation && this.
|
|
130
|
+
const current = () => generation === this.generation && this.demand && sttModelManager.getCurrentLanguage() === code;
|
|
170
131
|
if (this.localActive) {
|
|
171
132
|
// Switching models: hold transcripts until the new model is loaded.
|
|
172
133
|
this.log(`switching local stt model ${this.loadedCode} -> ${code}`);
|
|
@@ -269,9 +230,6 @@ class LocalSttFallbackCoordinator {
|
|
|
269
230
|
}
|
|
270
231
|
}
|
|
271
232
|
}
|
|
272
|
-
shouldUseLocalStt() {
|
|
273
|
-
return this.hasTranscriptionSubscription && (this.hasForceLocalSubscription || !this.cloudConnected);
|
|
274
|
-
}
|
|
275
233
|
log(msg) {
|
|
276
234
|
console.log(`[LocalSttFallback] ${msg}`);
|
|
277
235
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalSttFallbackCoordinator.js","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\nimport {ISLAND_SETTINGS_KEYS} from \"../runtime/config\"\nimport {useSettingsStore} from \"../stores/settings\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport {ModelRetry} from \"./ModelRetry\"\nimport offlineSpeechModelService from \"./OfflineSpeechModelService\"\nimport sttModelManager from \"./STTModelManager\"\n\n/**\n * State of the on-device speech-to-text model. `idle` means no miniapp needs\n * on-device transcription right now, so no model is loaded.\n */\nexport interface LocalSttModelStatus {\n /** Speech-page model code, e.g. \"fr\". */\n code: string\n /** BCP-47 language of that model, e.g. \"en-US\". */\n language: string\n state: \"idle\" | \"downloading\" | \"loading\" | \"ready\" | \"unavailable\"\n /** 0–100 while downloading. */\n progress?: number\n /** Why the model is unavailable. */\n reason?: string\n}\n\n/**\n * Decides whether on-device Sherpa STT should produce transcripts for local\n * miniapps, instead of (or in addition to) cloud transcription. Local STT is\n * active when the cloud is unavailable or any subscription requires\n * `forceLocal`.\n *\n * There is one recognizer and it always runs the model picked on the Speech\n * page. When on-device transcription is needed and that model is missing, it\n * is downloaded through the Speech-page system first. A failed download or\n * load retries on the next attempt and on the shared model retry schedule.\n *\n * Activation flips `localSttFallbackActive` in the settings store, which\n * the bluetooth-sdk native side reads to decide whether to feed PCM into\n * the SherpaOnnxTranscriber. Local transcripts then flow back through the\n * `local_transcription` Bridge event; the host wires that event to\n * `localMiniappRuntime.forwardEvent`, with this coordinator providing the\n * subscription-active gate so we never publish transcripts to miniapps that\n * aren't asking for them. LocalMiniappRuntime filters mixed local/cloud\n * delivery per miniapp.\n */\nclass LocalSttFallbackCoordinator {\n private static instance: LocalSttFallbackCoordinator\n\n private hasTranscriptionSubscription = false\n private hasForceLocalSubscription = false\n /**\n * Default to \"cloud is up\" so we never accidentally activate local STT before\n * the cloud client has had a chance to start. We attach lazily on the first\n * subscription/reconcile pass; once attached, this field reflects the real\n * cloud-client runtime status.\n */\n private cloudConnected = true\n private cloudAdapterAttached = false\n private selectionAttached = false\n private hydration?: Promise<unknown>\n private hydrated = false\n private localActive = false\n /** Model code the native recognizer was last confirmed to have loaded for us. */\n private loadedCode: string | null = null\n /** Bumped by every new start and every stop, so a superseded start stops at its next step. */\n private generation = 0\n private running?: Promise<void>\n private runningCode: string | null = null\n /** Model code whose last download or load failed; its retry waits for the schedule. */\n private failedCode: string | null = null\n private readonly retry = new ModelRetry()\n private status: LocalSttModelStatus = {code: \"\", language: \"\", state: \"idle\"}\n private readonly statusListeners = new Set<(status: LocalSttModelStatus) => void>()\n\n private constructor() {\n // Reset the persisted mirror flag on boot — the in-memory state in this\n // coordinator is the source of truth, and a stale \"true\" left from the\n // previous session would cause native to feed Sherpa before any miniapp\n // registered a subscription.\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n }\n\n static getInstance(): LocalSttFallbackCoordinator {\n if (!LocalSttFallbackCoordinator.instance) {\n LocalSttFallbackCoordinator.instance = new LocalSttFallbackCoordinator()\n }\n return LocalSttFallbackCoordinator.instance\n }\n\n /**\n * Lazy-attach the engine cloud connection listener. The coordinator is\n * constructed at module load, before engine.start() brings up the cloud\n * client, so we defer until the first reconcile.\n */\n private attachCloudAdapterIfReady(): void {\n if (this.cloudAdapterAttached) return\n this.cloudConnected = cloudClientService.isConnected()\n cloudClientService.onStatusChanged((status) => {\n const connected = status.status === \"connected\"\n if (this.cloudConnected === connected) return\n this.log(`cloud connection -> ${connected ? \"up\" : \"down\"}`)\n this.cloudConnected = connected\n this.reconcile()\n })\n this.cloudAdapterAttached = true\n }\n\n /** A model change on the Speech page reloads the recognizer while it is in use. */\n private attachSelectionIfReady(): void {\n if (this.selectionAttached) return\n this.selectionAttached = true\n sttModelManager.onSelectionChanged(() => this.reconcile())\n }\n\n /** True while local transcripts should reach miniapps (the picked model is loaded). */\n isActive(): boolean {\n return this.localActive\n }\n\n /** BCP-47 language of the loaded model, or null when local STT is inactive. */\n getActiveLanguage(): string | null {\n return this.localActive && this.loadedCode ? sttModelManager.getBcp47ForLanguage(this.loadedCode) : null\n }\n\n getModelStatus(): LocalSttModelStatus {\n return {...this.status}\n }\n\n /** Follow the model status. The listener gets the current status immediately, then every change. */\n onModelStatusChanged(listener: (status: LocalSttModelStatus) => void): () => void {\n this.statusListeners.add(listener)\n listener(this.getModelStatus())\n return () => {\n this.statusListeners.delete(listener)\n }\n }\n\n onSubscriptionChange(hasTranscription: boolean, hasForceLocal = false): void {\n if (\n this.hasTranscriptionSubscription !== hasTranscription ||\n this.hasForceLocalSubscription !== (hasTranscription && hasForceLocal)\n ) {\n this.log(`onSubscriptionChange(hasTx=${hasTranscription}, forceLocal=${hasForceLocal})`)\n }\n this.hasTranscriptionSubscription = hasTranscription\n this.hasForceLocalSubscription = hasTranscription && hasForceLocal\n this.reconcile()\n }\n\n /**\n * Informational hook, retained for API stability. No longer called: the\n * authoritative switching signal is the cloud-client adapter (local miniapps\n * are powered only by V2). v1 cloud transcripts deliberately do NOT drive this\n * coordinator anymore. Kept for future hysteresis if connection-state alone\n * proves too coarse.\n */\n onCloudTranscript(): void {}\n\n private reconcile(): void {\n this.attachCloudAdapterIfReady()\n this.attachSelectionIfReady()\n if (!this.shouldUseLocalStt()) {\n this.stop(this.hasTranscriptionSubscription ? \"cloud reconnected\" : \"subscription gone\")\n return\n }\n if (!this.hydrated) {\n // Read the saved Speech-page choice first, so a miniapp that subscribes\n // during boot never loads (or downloads) the default model instead.\n this.hydration ??= sttModelManager.getCurrentLanguageFromPreferences().finally(() => {\n this.hydrated = true\n this.reconcile()\n })\n return\n }\n const code = sttModelManager.getCurrentLanguage()\n if (this.localActive && this.loadedCode === code) return\n // A failed model waits for its retry; a different model or new demand tries now.\n if (this.failedCode === code && this.retry.pending) return\n if (this.running && this.runningCode === code) return\n // Anything else supersedes the current run, which bails out at its next step.\n const run = this.start(code)\n this.running = run\n this.runningCode = code\n void run\n .catch((error) => this.log(`start failed unexpectedly: ${error}`))\n .finally(() => {\n if (this.running !== run) return\n this.running = undefined\n this.runningCode = null\n })\n }\n\n private async start(code: string): Promise<void> {\n const generation = ++this.generation\n const current = () =>\n generation === this.generation && this.shouldUseLocalStt() && sttModelManager.getCurrentLanguage() === code\n\n if (this.localActive) {\n // Switching models: hold transcripts until the new model is loaded.\n this.log(`switching local stt model ${this.loadedCode} -> ${code}`)\n this.localActive = false\n }\n this.failedCode = null\n this.retry.cancel()\n\n if (!(await sttModelManager.isModelAvailable(code))) {\n this.log(`local stt model ${code} is missing — downloading`)\n this.setStatus(code, {state: \"downloading\", progress: 0})\n try {\n await offlineSpeechModelService.downloadSttModel(code, (progress) => {\n if (current()) this.setStatus(code, {state: \"downloading\", progress})\n })\n if (!current()) return\n // Persist the picked model natively so the recognizer loads it.\n await sttModelManager.activateLanguage(code)\n } catch (error) {\n if (!current()) return\n this.fail(code, `Could not download the speech model: ${errorMessage(error)}`)\n return\n }\n }\n if (!current()) return\n\n this.log(`loading local stt model ${code}`)\n this.setStatus(code, {state: \"loading\"})\n try {\n // Native loads only while on-device transcription is in demand, so the\n // demand flag must reach it before the restart, not after a debounce.\n await this.setNativeDemand(true)\n await BluetoothSdk.restartTranscriber()\n // Confirm what native loaded rather than trusting promise completion.\n if (!(await BluetoothSdk.isSttModelLoaded(sttModelManager.getModelPath(code)))) {\n throw new Error(\"the native recognizer did not load it\")\n }\n } catch (error) {\n if (!current()) return\n this.log(`loading ${code} failed: ${error}`)\n void this.setNativeDemand(false)\n this.fail(code, `Could not load the speech model: ${errorMessage(error)}`)\n return\n }\n if (!current()) return\n\n this.localActive = true\n this.loadedCode = code\n this.retry.reset()\n this.setStatus(code, {state: \"ready\"})\n this.log(`local stt ready on ${code}`)\n }\n\n private fail(code: string, reason: string): void {\n this.failedCode = code\n const delay = this.retry.schedule(() => this.reconcile())\n this.log(`${reason}; retrying in ${Math.round(delay / 1000)}s`)\n this.setStatus(code, {state: \"unavailable\", reason})\n }\n\n private stop(reason: string): void {\n this.generation++\n this.running = undefined\n this.runningCode = null\n this.retry.reset()\n this.failedCode = null\n if (this.localActive || this.loadedCode || this.status.state !== \"idle\") {\n this.log(`stopping local stt: ${reason}`)\n void this.setNativeDemand(false)\n }\n this.localActive = false\n this.loadedCode = null\n this.setStatus(sttModelManager.getCurrentLanguage(), {state: \"idle\"})\n }\n\n private async setNativeDemand(active: boolean): Promise<void> {\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, active)\n if (active) {\n await BluetoothSdk.updateBluetoothSettings({[ISLAND_SETTINGS_KEYS.localSttFallbackActive]: true})\n }\n }\n\n private setStatus(code: string, next: Omit<LocalSttModelStatus, \"code\" | \"language\">): void {\n const status: LocalSttModelStatus = {code, language: sttModelManager.getBcp47ForLanguage(code), ...next}\n const previous = this.status\n if (\n previous.code === status.code &&\n previous.state === status.state &&\n previous.progress === status.progress &&\n previous.reason === status.reason\n ) {\n return\n }\n this.status = status\n for (const listener of this.statusListeners) {\n try {\n listener(this.getModelStatus())\n } catch (error) {\n this.log(`model status listener threw: ${error}`)\n }\n }\n }\n\n private shouldUseLocalStt(): boolean {\n return this.hasTranscriptionSubscription && (this.hasForceLocalSubscription || !this.cloudConnected)\n }\n\n private log(msg: string): void {\n console.log(`[LocalSttFallback] ${msg}`)\n }\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nconst localSttFallbackCoordinator = LocalSttFallbackCoordinator.getInstance()\nexport default localSttFallbackCoordinator\n"]}
|
|
1
|
+
{"version":3,"file":"LocalSttFallbackCoordinator.js","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\nimport {ISLAND_SETTINGS_KEYS} from \"../runtime/config\"\nimport {useSettingsStore} from \"../stores/settings\"\nimport {ModelRetry} from \"./ModelRetry\"\nimport offlineSpeechModelService from \"./OfflineSpeechModelService\"\nimport sttModelManager from \"./STTModelManager\"\n\n/**\n * State of the on-device speech-to-text model. `idle` means no miniapp needs\n * on-device transcription right now, so no model is loaded.\n */\nexport interface LocalSttModelStatus {\n /** Speech-page model code, e.g. \"fr\". */\n code: string\n /** BCP-47 language of that model, e.g. \"en-US\". */\n language: string\n state: \"idle\" | \"downloading\" | \"loading\" | \"ready\" | \"unavailable\"\n /** 0–100 while downloading. */\n progress?: number\n /** Why the model is unavailable. */\n reason?: string\n}\n\n/**\n * Runs on-device Sherpa STT for local miniapps while LocalMiniappRuntime says\n * a transcription listener needs it (`setDemand`): a `localOnly` listener, a\n * `preferLocal` one the picked model can serve, any listener while the cloud\n * is unreachable, or one still finishing an on-device utterance.\n *\n * There is one recognizer and it always runs the model picked on the Speech\n * page. When on-device transcription is needed and that model is missing, it\n * is downloaded through the Speech-page system first. A failed download or\n * load retries on the next attempt and on the shared model retry schedule.\n *\n * Activation flips `localSttFallbackActive` in the settings store, which\n * the bluetooth-sdk native side reads to decide whether to feed PCM into\n * the SherpaOnnxTranscriber. Local transcripts then flow back through the\n * `local_transcription` Bridge event; the host wires that event to\n * `localMiniappRuntime.forwardEvent`, with this coordinator providing the\n * subscription-active gate so we never publish transcripts to miniapps that\n * aren't asking for them. LocalMiniappRuntime routes local and cloud\n * transcripts to each listener.\n */\nclass LocalSttFallbackCoordinator {\n private static instance: LocalSttFallbackCoordinator\n\n /** A miniapp transcription listener needs on-device transcripts. */\n private demand = false\n private selectionAttached = false\n private hydration?: Promise<unknown>\n private hydrated = false\n private localActive = false\n /** Model code the native recognizer was last confirmed to have loaded for us. */\n private loadedCode: string | null = null\n /** Bumped by every new start and every stop, so a superseded start stops at its next step. */\n private generation = 0\n private running?: Promise<void>\n private runningCode: string | null = null\n /** Model code whose last download or load failed; its retry waits for the schedule. */\n private failedCode: string | null = null\n private readonly retry = new ModelRetry()\n private status: LocalSttModelStatus = {code: \"\", language: \"\", state: \"idle\"}\n private readonly statusListeners = new Set<(status: LocalSttModelStatus) => void>()\n\n private constructor() {\n // Reset the persisted mirror flag on boot — the in-memory state in this\n // coordinator is the source of truth, and a stale \"true\" left from the\n // previous session would cause native to feed Sherpa before any miniapp\n // registered a subscription.\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n }\n\n static getInstance(): LocalSttFallbackCoordinator {\n if (!LocalSttFallbackCoordinator.instance) {\n LocalSttFallbackCoordinator.instance = new LocalSttFallbackCoordinator()\n }\n return LocalSttFallbackCoordinator.instance\n }\n\n /** A model change on the Speech page reloads the recognizer while it is in use. */\n private attachSelectionIfReady(): void {\n if (this.selectionAttached) return\n this.selectionAttached = true\n sttModelManager.onSelectionChanged(() => this.reconcile())\n }\n\n /** True while local transcripts should reach miniapps (the picked model is loaded). */\n isActive(): boolean {\n return this.localActive\n }\n\n /** BCP-47 language of the loaded model, or null when local STT is inactive. */\n getActiveLanguage(): string | null {\n return this.localActive && this.loadedCode ? sttModelManager.getBcp47ForLanguage(this.loadedCode) : null\n }\n\n getModelStatus(): LocalSttModelStatus {\n return {...this.status}\n }\n\n /** Follow the model status. The listener gets the current status immediately, then every change. */\n onModelStatusChanged(listener: (status: LocalSttModelStatus) => void): () => void {\n this.statusListeners.add(listener)\n listener(this.getModelStatus())\n return () => {\n this.statusListeners.delete(listener)\n }\n }\n\n /** Whether a miniapp transcription listener needs on-device transcripts. */\n setDemand(demand: boolean): void {\n if (this.demand !== demand) this.log(`on-device demand -> ${demand}`)\n this.demand = demand\n this.reconcile()\n }\n\n private reconcile(): void {\n this.attachSelectionIfReady()\n if (!this.demand) {\n this.stop(\"no listener needs it\")\n return\n }\n if (!this.hydrated) {\n // Read the saved Speech-page choice first, so a miniapp that subscribes\n // during boot never loads (or downloads) the default model instead.\n this.hydration ??= sttModelManager.getCurrentLanguageFromPreferences().finally(() => {\n this.hydrated = true\n this.reconcile()\n })\n return\n }\n const code = sttModelManager.getCurrentLanguage()\n if (this.localActive && this.loadedCode === code) return\n // A failed model waits for its retry; a different model or new demand tries now.\n if (this.failedCode === code && this.retry.pending) return\n if (this.running && this.runningCode === code) return\n // Anything else supersedes the current run, which bails out at its next step.\n const run = this.start(code)\n this.running = run\n this.runningCode = code\n void run\n .catch((error) => this.log(`start failed unexpectedly: ${error}`))\n .finally(() => {\n if (this.running !== run) return\n this.running = undefined\n this.runningCode = null\n })\n }\n\n private async start(code: string): Promise<void> {\n const generation = ++this.generation\n const current = () => generation === this.generation && this.demand && sttModelManager.getCurrentLanguage() === code\n\n if (this.localActive) {\n // Switching models: hold transcripts until the new model is loaded.\n this.log(`switching local stt model ${this.loadedCode} -> ${code}`)\n this.localActive = false\n }\n this.failedCode = null\n this.retry.cancel()\n\n if (!(await sttModelManager.isModelAvailable(code))) {\n this.log(`local stt model ${code} is missing — downloading`)\n this.setStatus(code, {state: \"downloading\", progress: 0})\n try {\n await offlineSpeechModelService.downloadSttModel(code, (progress) => {\n if (current()) this.setStatus(code, {state: \"downloading\", progress})\n })\n if (!current()) return\n // Persist the picked model natively so the recognizer loads it.\n await sttModelManager.activateLanguage(code)\n } catch (error) {\n if (!current()) return\n this.fail(code, `Could not download the speech model: ${errorMessage(error)}`)\n return\n }\n }\n if (!current()) return\n\n this.log(`loading local stt model ${code}`)\n this.setStatus(code, {state: \"loading\"})\n try {\n // Native loads only while on-device transcription is in demand, so the\n // demand flag must reach it before the restart, not after a debounce.\n await this.setNativeDemand(true)\n await BluetoothSdk.restartTranscriber()\n // Confirm what native loaded rather than trusting promise completion.\n if (!(await BluetoothSdk.isSttModelLoaded(sttModelManager.getModelPath(code)))) {\n throw new Error(\"the native recognizer did not load it\")\n }\n } catch (error) {\n if (!current()) return\n this.log(`loading ${code} failed: ${error}`)\n void this.setNativeDemand(false)\n this.fail(code, `Could not load the speech model: ${errorMessage(error)}`)\n return\n }\n if (!current()) return\n\n this.localActive = true\n this.loadedCode = code\n this.retry.reset()\n this.setStatus(code, {state: \"ready\"})\n this.log(`local stt ready on ${code}`)\n }\n\n private fail(code: string, reason: string): void {\n this.failedCode = code\n const delay = this.retry.schedule(() => this.reconcile())\n this.log(`${reason}; retrying in ${Math.round(delay / 1000)}s`)\n this.setStatus(code, {state: \"unavailable\", reason})\n }\n\n private stop(reason: string): void {\n this.generation++\n this.running = undefined\n this.runningCode = null\n this.retry.reset()\n this.failedCode = null\n if (this.localActive || this.loadedCode || this.status.state !== \"idle\") {\n this.log(`stopping local stt: ${reason}`)\n void this.setNativeDemand(false)\n }\n this.localActive = false\n this.loadedCode = null\n this.setStatus(sttModelManager.getCurrentLanguage(), {state: \"idle\"})\n }\n\n private async setNativeDemand(active: boolean): Promise<void> {\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, active)\n if (active) {\n await BluetoothSdk.updateBluetoothSettings({[ISLAND_SETTINGS_KEYS.localSttFallbackActive]: true})\n }\n }\n\n private setStatus(code: string, next: Omit<LocalSttModelStatus, \"code\" | \"language\">): void {\n const status: LocalSttModelStatus = {code, language: sttModelManager.getBcp47ForLanguage(code), ...next}\n const previous = this.status\n if (\n previous.code === status.code &&\n previous.state === status.state &&\n previous.progress === status.progress &&\n previous.reason === status.reason\n ) {\n return\n }\n this.status = status\n for (const listener of this.statusListeners) {\n try {\n listener(this.getModelStatus())\n } catch (error) {\n this.log(`model status listener threw: ${error}`)\n }\n }\n }\n\n private log(msg: string): void {\n console.log(`[LocalSttFallback] ${msg}`)\n }\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nconst localSttFallbackCoordinator = LocalSttFallbackCoordinator.getInstance()\nexport default localSttFallbackCoordinator\n"]}
|
|
@@ -1,12 +1,115 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Where each miniapp transcription listener gets its text: the cloud or the
|
|
3
|
+
* phone's on-device recognizer.
|
|
4
|
+
*
|
|
5
|
+
* A listener asks for one of three behaviours (the SDK's `processing`):
|
|
6
|
+
* - preferCloud (default): the cloud; on-device only while the cloud is
|
|
7
|
+
* unreachable.
|
|
8
|
+
* - preferLocal: on-device while the running model transcribes the
|
|
9
|
+
* listener's language, otherwise the cloud.
|
|
10
|
+
* - localOnly: on-device only; never the cloud.
|
|
11
|
+
*
|
|
12
|
+
* Each listener receives one source at a time (`handoffStep`). A planned
|
|
13
|
+
* switch, while its current source still works, waits for a pause in which
|
|
14
|
+
* both sources have settled (`UtteranceTracker`), then for a drain of the
|
|
15
|
+
* on-device recognizer (it reports everything it heard so far and starts
|
|
16
|
+
* over). The drain is the boundary: on-device text from before it stays with
|
|
17
|
+
* the old side, on-device text after it is new speech, and the listener's
|
|
18
|
+
* cloud text starts at the cloud audio position where the drain began. So a
|
|
19
|
+
* listener never misses or repeats speech. A model that cannot be drained
|
|
20
|
+
* leaves the quiet window as the boundary. A forced switch, when the current
|
|
21
|
+
* source stops, happens at once; a move to the cloud then starts at the
|
|
22
|
+
* cloud audio position of the last on-device final, so an utterance cut short
|
|
23
|
+
* on-device is completed in the cloud and finished ones are not repeated.
|
|
24
|
+
* The listeners' sources and targets decide what runs: a stream is requested
|
|
25
|
+
* from the cloud only while a listener on it needs the cloud, and the
|
|
26
|
+
* on-device recognizer runs only while a listener needs it.
|
|
27
|
+
*/
|
|
28
|
+
export type SpeechProcessing = "preferCloud" | "preferLocal" | "localOnly";
|
|
29
|
+
export type TranscriptionSource = "cloud" | "local";
|
|
30
|
+
/**
|
|
31
|
+
* A source has settled once it has produced nothing for this long since its
|
|
32
|
+
* last final (or since it started): a pause in speech, longer than the
|
|
33
|
+
* recognizers' own end-of-speech delays.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SOURCE_QUIET_MS = 3000;
|
|
36
|
+
/** An utterance whose final never came has ended after this long without transcripts. */
|
|
37
|
+
export declare const UTTERANCE_STALE_MS = 6000;
|
|
38
|
+
/**
|
|
39
|
+
* The processing a listener, subscription or speak request asks for.
|
|
40
|
+
* `processing` wins; without it, the `forceLocal: true` of SDKs before it
|
|
41
|
+
* means localOnly. Anything else is preferCloud.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readSpeechProcessing(request: {
|
|
44
|
+
processing?: unknown;
|
|
45
|
+
forceLocal?: unknown;
|
|
46
|
+
}): SpeechProcessing;
|
|
47
|
+
export interface RoutedListener {
|
|
48
|
+
/** `transcription:<language>` or `transcription:auto`. */
|
|
49
|
+
stream: string;
|
|
50
|
+
processing: SpeechProcessing;
|
|
4
51
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
52
|
+
/** What the phone can transcribe with right now, as one miniapp sees it. */
|
|
53
|
+
export interface TranscriptionRoutingState {
|
|
54
|
+
cloudConnected: boolean;
|
|
55
|
+
/** On-device speech is allowed by this deployment. */
|
|
56
|
+
onDevice: boolean;
|
|
57
|
+
/** BCP-47 language of the running on-device model, or null while none runs. */
|
|
58
|
+
localLanguage: string | null;
|
|
59
|
+
/** BCP-47 language of the model picked on the Speech page. */
|
|
60
|
+
pickedLanguage: string;
|
|
61
|
+
/** The miniapp's language hints (bare codes, from transcription.configure). */
|
|
62
|
+
hints: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether an on-device model in `language` transcribes what the listener asks
|
|
66
|
+
* for. A specific language must be the model's. Auto-detect follows the
|
|
67
|
+
* model's language, the one the user picked; but a preferLocal listener whose
|
|
68
|
+
* miniapp hinted at another language keeps the cloud, which tells them apart.
|
|
69
|
+
*/
|
|
70
|
+
export declare function onDeviceServes(listener: RoutedListener, language: string, hints: readonly string[]): boolean;
|
|
71
|
+
/** The source the listener should receive now. */
|
|
72
|
+
export declare function targetSource(listener: RoutedListener, state: TranscriptionRoutingState): TranscriptionSource;
|
|
73
|
+
/**
|
|
74
|
+
* What a listener on `current` does on its way to `target`:
|
|
75
|
+
* - "settle": it has no source yet or is already there; take `target`.
|
|
76
|
+
* - "forced": `current` can no longer deliver; move now.
|
|
77
|
+
* - "wait": a source is still busy on its streams; stay.
|
|
78
|
+
* - "drain": both settled, but the on-device recognizer has spoken since its
|
|
79
|
+
* last drain; stay until a drain completes.
|
|
80
|
+
* - "planned": both settled and drained; move now.
|
|
81
|
+
*/
|
|
82
|
+
export type HandoffStep = "settle" | "forced" | "wait" | "drain" | "planned";
|
|
83
|
+
export declare function handoffStep(current: TranscriptionSource | null, target: TranscriptionSource, canDeliver: (source: TranscriptionSource) => boolean, busy: (source: TranscriptionSource) => boolean, drained: boolean): HandoffStep;
|
|
84
|
+
/** Whether the listener needs its stream from the cloud. */
|
|
85
|
+
export declare function needsCloud(listener: RoutedListener, source: TranscriptionSource | null, target: TranscriptionSource): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the listener needs the on-device recognizer running. preferLocal
|
|
88
|
+
* asks for it whenever the picked model transcribes its language, which is
|
|
89
|
+
* what starts (and, when missing, downloads) that model.
|
|
90
|
+
*/
|
|
91
|
+
export declare function needsLocal(listener: RoutedListener, source: TranscriptionSource | null, state: TranscriptionRoutingState): boolean;
|
|
92
|
+
/** When each source last produced a transcript on each stream, to tell when it has settled. */
|
|
93
|
+
export declare class UtteranceTracker {
|
|
94
|
+
private readonly quietMs;
|
|
95
|
+
private readonly staleMs;
|
|
96
|
+
/** `${source} ${stream}` -> its last transcript; stream "*" marks a source start, for every stream. */
|
|
97
|
+
private readonly last;
|
|
98
|
+
constructor(quietMs?: number, staleMs?: number);
|
|
99
|
+
/** Records a transcript: an interim opens (or continues) an utterance, a final ends it. */
|
|
100
|
+
note(source: TranscriptionSource, stream: string, isFinal: boolean, now: number): void;
|
|
101
|
+
/**
|
|
102
|
+
* A source started (the cloud connected, or the on-device model loaded): it
|
|
103
|
+
* may already be working on speech it has not reported, so it settles only
|
|
104
|
+
* after a quiet window.
|
|
105
|
+
*/
|
|
106
|
+
started(source: TranscriptionSource, now: number): void;
|
|
107
|
+
/** Forgets a source that stopped: it never reports anything more. */
|
|
108
|
+
clear(source: TranscriptionSource): void;
|
|
109
|
+
/** Whether `source` may still report speech that a listener on `stream` hears. */
|
|
110
|
+
busy(source: TranscriptionSource, stream: string, now: number): boolean;
|
|
111
|
+
/** When the next busy stream settles, or null when every one has. */
|
|
112
|
+
nextSettle(now: number): number | null;
|
|
113
|
+
private settlesAt;
|
|
8
114
|
}
|
|
9
|
-
export type TranscriptionDeliveryRoute = "default" | "forceLocal" | "all";
|
|
10
|
-
export declare function summarizeTranscriptionRoutes<T>(subscribers: Iterable<T>, routesFor: (subscriber: T) => TranscriptionSubscriberRoutes): TranscriptionRouteSummary;
|
|
11
|
-
export declare function transcriptionDeliveryRoute(source: "cloud" | "local", routes: TranscriptionSubscriberRoutes, cloudConnected: boolean): TranscriptionDeliveryRoute | null;
|
|
12
115
|
//# sourceMappingURL=TranscriptionRouting.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TranscriptionRouting.d.ts","sourceRoot":"","sources":["../../src/services/TranscriptionRouting.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"TranscriptionRouting.d.ts","sourceRoot":"","sources":["../../src/services/TranscriptionRouting.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,24 +1,162 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Where each miniapp transcription listener gets its text: the cloud or the
|
|
3
|
+
* phone's on-device recognizer.
|
|
4
|
+
*
|
|
5
|
+
* A listener asks for one of three behaviours (the SDK's `processing`):
|
|
6
|
+
* - preferCloud (default): the cloud; on-device only while the cloud is
|
|
7
|
+
* unreachable.
|
|
8
|
+
* - preferLocal: on-device while the running model transcribes the
|
|
9
|
+
* listener's language, otherwise the cloud.
|
|
10
|
+
* - localOnly: on-device only; never the cloud.
|
|
11
|
+
*
|
|
12
|
+
* Each listener receives one source at a time (`handoffStep`). A planned
|
|
13
|
+
* switch, while its current source still works, waits for a pause in which
|
|
14
|
+
* both sources have settled (`UtteranceTracker`), then for a drain of the
|
|
15
|
+
* on-device recognizer (it reports everything it heard so far and starts
|
|
16
|
+
* over). The drain is the boundary: on-device text from before it stays with
|
|
17
|
+
* the old side, on-device text after it is new speech, and the listener's
|
|
18
|
+
* cloud text starts at the cloud audio position where the drain began. So a
|
|
19
|
+
* listener never misses or repeats speech. A model that cannot be drained
|
|
20
|
+
* leaves the quiet window as the boundary. A forced switch, when the current
|
|
21
|
+
* source stops, happens at once; a move to the cloud then starts at the
|
|
22
|
+
* cloud audio position of the last on-device final, so an utterance cut short
|
|
23
|
+
* on-device is completed in the cloud and finished ones are not repeated.
|
|
24
|
+
* The listeners' sources and targets decide what runs: a stream is requested
|
|
25
|
+
* from the cloud only while a listener on it needs the cloud, and the
|
|
26
|
+
* on-device recognizer runs only while a listener needs it.
|
|
27
|
+
*/
|
|
28
|
+
const PROCESSING = new Set(["preferCloud", "preferLocal", "localOnly"]);
|
|
29
|
+
const AUTO_STREAM = "transcription:auto";
|
|
30
|
+
const STREAM_PREFIX = "transcription:";
|
|
31
|
+
/**
|
|
32
|
+
* A source has settled once it has produced nothing for this long since its
|
|
33
|
+
* last final (or since it started): a pause in speech, longer than the
|
|
34
|
+
* recognizers' own end-of-speech delays.
|
|
35
|
+
*/
|
|
36
|
+
export const SOURCE_QUIET_MS = 3000;
|
|
37
|
+
/** An utterance whose final never came has ended after this long without transcripts. */
|
|
38
|
+
export const UTTERANCE_STALE_MS = 6000;
|
|
39
|
+
/**
|
|
40
|
+
* The processing a listener, subscription or speak request asks for.
|
|
41
|
+
* `processing` wins; without it, the `forceLocal: true` of SDKs before it
|
|
42
|
+
* means localOnly. Anything else is preferCloud.
|
|
43
|
+
*/
|
|
44
|
+
export function readSpeechProcessing(request) {
|
|
45
|
+
if (PROCESSING.has(request.processing))
|
|
46
|
+
return request.processing;
|
|
47
|
+
return request.forceLocal === true ? "localOnly" : "preferCloud";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether an on-device model in `language` transcribes what the listener asks
|
|
51
|
+
* for. A specific language must be the model's. Auto-detect follows the
|
|
52
|
+
* model's language, the one the user picked; but a preferLocal listener whose
|
|
53
|
+
* miniapp hinted at another language keeps the cloud, which tells them apart.
|
|
54
|
+
*/
|
|
55
|
+
export function onDeviceServes(listener, language, hints) {
|
|
56
|
+
const requested = listener.stream.slice(STREAM_PREFIX.length).toLowerCase();
|
|
57
|
+
if (requested === language.toLowerCase())
|
|
58
|
+
return true;
|
|
59
|
+
if (listener.stream !== AUTO_STREAM)
|
|
60
|
+
return false;
|
|
61
|
+
if (listener.processing !== "preferLocal")
|
|
62
|
+
return true;
|
|
63
|
+
const base = language.split("-")[0].toLowerCase();
|
|
64
|
+
return hints.every((hint) => hint.toLowerCase() === base);
|
|
65
|
+
}
|
|
66
|
+
/** The source the listener should receive now. */
|
|
67
|
+
export function targetSource(listener, state) {
|
|
68
|
+
if (listener.processing === "localOnly" || !state.cloudConnected)
|
|
69
|
+
return "local";
|
|
70
|
+
if (listener.processing === "preferCloud")
|
|
71
|
+
return "cloud";
|
|
72
|
+
return state.onDevice && state.localLanguage !== null && onDeviceServes(listener, state.localLanguage, state.hints)
|
|
73
|
+
? "local"
|
|
74
|
+
: "cloud";
|
|
75
|
+
}
|
|
76
|
+
export function handoffStep(current, target, canDeliver, busy, drained) {
|
|
77
|
+
if (current === null || current === target)
|
|
78
|
+
return "settle";
|
|
79
|
+
if (!canDeliver(current))
|
|
80
|
+
return "forced";
|
|
81
|
+
if (busy(current) || busy(target))
|
|
82
|
+
return "wait";
|
|
83
|
+
return drained ? "planned" : "drain";
|
|
12
84
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
85
|
+
/** Whether the listener needs its stream from the cloud. */
|
|
86
|
+
export function needsCloud(listener, source, target) {
|
|
87
|
+
if (listener.processing === "localOnly")
|
|
88
|
+
return false;
|
|
89
|
+
return listener.processing === "preferCloud" || source === "cloud" || target === "cloud";
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Whether the listener needs the on-device recognizer running. preferLocal
|
|
93
|
+
* asks for it whenever the picked model transcribes its language, which is
|
|
94
|
+
* what starts (and, when missing, downloads) that model.
|
|
95
|
+
*/
|
|
96
|
+
export function needsLocal(listener, source, state) {
|
|
97
|
+
if (source === "local" || listener.processing === "localOnly" || !state.cloudConnected)
|
|
98
|
+
return true;
|
|
99
|
+
return (listener.processing === "preferLocal" &&
|
|
100
|
+
state.onDevice &&
|
|
101
|
+
onDeviceServes(listener, state.pickedLanguage, state.hints));
|
|
102
|
+
}
|
|
103
|
+
/** When each source last produced a transcript on each stream, to tell when it has settled. */
|
|
104
|
+
export class UtteranceTracker {
|
|
105
|
+
quietMs;
|
|
106
|
+
staleMs;
|
|
107
|
+
/** `${source} ${stream}` -> its last transcript; stream "*" marks a source start, for every stream. */
|
|
108
|
+
last = new Map();
|
|
109
|
+
constructor(quietMs = SOURCE_QUIET_MS, staleMs = UTTERANCE_STALE_MS) {
|
|
110
|
+
this.quietMs = quietMs;
|
|
111
|
+
this.staleMs = staleMs;
|
|
112
|
+
}
|
|
113
|
+
/** Records a transcript: an interim opens (or continues) an utterance, a final ends it. */
|
|
114
|
+
note(source, stream, isFinal, now) {
|
|
115
|
+
this.last.set(`${source} ${stream}`, { at: now, final: isFinal });
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A source started (the cloud connected, or the on-device model loaded): it
|
|
119
|
+
* may already be working on speech it has not reported, so it settles only
|
|
120
|
+
* after a quiet window.
|
|
121
|
+
*/
|
|
122
|
+
started(source, now) {
|
|
123
|
+
this.clear(source);
|
|
124
|
+
this.note(source, "*", true, now);
|
|
125
|
+
}
|
|
126
|
+
/** Forgets a source that stopped: it never reports anything more. */
|
|
127
|
+
clear(source) {
|
|
128
|
+
for (const key of [...this.last.keys()]) {
|
|
129
|
+
if (key.startsWith(`${source} `))
|
|
130
|
+
this.last.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Whether `source` may still report speech that a listener on `stream` hears. */
|
|
134
|
+
busy(source, stream, now) {
|
|
135
|
+
for (const [key, last] of this.last) {
|
|
136
|
+
if (now >= this.settlesAt(last))
|
|
137
|
+
continue;
|
|
138
|
+
const [keySource, keyStream] = key.split(" ");
|
|
139
|
+
if (keySource !== source)
|
|
140
|
+
continue;
|
|
141
|
+
if (keyStream === "*" || stream === AUTO_STREAM || keyStream === stream)
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/** When the next busy stream settles, or null when every one has. */
|
|
147
|
+
nextSettle(now) {
|
|
148
|
+
let next = null;
|
|
149
|
+
for (const [key, last] of this.last) {
|
|
150
|
+
const at = this.settlesAt(last);
|
|
151
|
+
if (now >= at)
|
|
152
|
+
this.last.delete(key);
|
|
153
|
+
else if (next === null || at < next)
|
|
154
|
+
next = at;
|
|
155
|
+
}
|
|
156
|
+
return next;
|
|
157
|
+
}
|
|
158
|
+
settlesAt(last) {
|
|
159
|
+
return last.at + (last.final ? this.quietMs : this.staleMs);
|
|
160
|
+
}
|
|
23
161
|
}
|
|
24
162
|
//# sourceMappingURL=TranscriptionRouting.js.map
|