@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.
Files changed (27) hide show
  1. package/build/generated/releaseMetadata.js +5 -5
  2. package/build/generated/releaseMetadata.js.map +1 -1
  3. package/build/services/DeviceEventRouter.js +2 -2
  4. package/build/services/DeviceEventRouter.js.map +1 -1
  5. package/build/services/LocalMiniappRuntime.d.ts +55 -5
  6. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  7. package/build/services/LocalMiniappRuntime.js +293 -171
  8. package/build/services/LocalMiniappRuntime.js.map +1 -1
  9. package/build/services/LocalSttFallbackCoordinator.d.ts +10 -32
  10. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  11. package/build/services/LocalSttFallbackCoordinator.js +16 -58
  12. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  13. package/build/services/TranscriptionRouting.d.ts +112 -9
  14. package/build/services/TranscriptionRouting.d.ts.map +1 -1
  15. package/build/services/TranscriptionRouting.js +159 -21
  16. package/build/services/TranscriptionRouting.js.map +1 -1
  17. package/build/services/TranscriptionSubscriptions.d.ts +87 -6
  18. package/build/services/TranscriptionSubscriptions.d.ts.map +1 -1
  19. package/build/services/TranscriptionSubscriptions.js +174 -26
  20. package/build/services/TranscriptionSubscriptions.js.map +1 -1
  21. package/package.json +8 -8
  22. package/src/generated/releaseMetadata.ts +5 -5
  23. package/src/services/DeviceEventRouter.ts +2 -2
  24. package/src/services/LocalMiniappRuntime.ts +325 -213
  25. package/src/services/LocalSttFallbackCoordinator.ts +15 -62
  26. package/src/services/TranscriptionRouting.ts +200 -30
  27. package/src/services/TranscriptionSubscriptions.ts +245 -25
@@ -14,10 +14,10 @@ export interface LocalSttModelStatus {
14
14
  reason?: string;
15
15
  }
16
16
  /**
17
- * Decides whether on-device Sherpa STT should produce transcripts for local
18
- * miniapps, instead of (or in addition to) cloud transcription. Local STT is
19
- * active when the cloud is unavailable or any subscription requires
20
- * `forceLocal`.
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 filters mixed local/cloud
34
- * delivery per miniapp.
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
- private hasTranscriptionSubscription;
39
- private hasForceLocalSubscription;
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
- onSubscriptionChange(hasTranscription: boolean, hasForceLocal?: boolean): void;
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":"AAQA;;;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,OAAO,CAAC,4BAA4B,CAAQ;IAC5C,OAAO,CAAC,yBAAyB,CAAQ;IACzC;;;;;OAKG;IACH,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,oBAAoB,CAAQ;IACpC,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;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAajC,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,oBAAoB,CAAC,gBAAgB,EAAE,OAAO,EAAE,aAAa,UAAQ,GAAG,IAAI;IAY5E;;;;;;OAMG;IACH,iBAAiB,IAAI,IAAI;IAEzB,OAAO,CAAC,SAAS;YAkCH,KAAK;IA0DnB,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,IAAI;YAeE,eAAe;IAO7B,OAAO,CAAC,SAAS;IAqBjB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,GAAG;CAGZ;AAMD,QAAA,MAAM,2BAA2B,6BAA4C,CAAA;AAC7E,eAAe,2BAA2B,CAAA"}
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
- * Decides whether on-device Sherpa STT should produce transcripts for local
10
- * miniapps, instead of (or in addition to) cloud transcription. Local STT is
11
- * active when the cloud is unavailable or any subscription requires
12
- * `forceLocal`.
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 filters mixed local/cloud
26
- * delivery per miniapp.
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
- hasTranscriptionSubscription = false;
31
- hasForceLocalSubscription = false;
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
- onSubscriptionChange(hasTranscription, hasForceLocal = false) {
114
- if (this.hasTranscriptionSubscription !== hasTranscription ||
115
- this.hasForceLocalSubscription !== (hasTranscription && hasForceLocal)) {
116
- this.log(`onSubscriptionChange(hasTx=${hasTranscription}, forceLocal=${hasForceLocal})`);
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.shouldUseLocalStt()) {
134
- this.stop(this.hasTranscriptionSubscription ? "cloud reconnected" : "subscription gone");
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.shouldUseLocalStt() && sttModelManager.getCurrentLanguage() === code;
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":"AAAA,OAAO,YAAY,MAAM,gCAAgC,CAAA;AACzD,OAAO,EAAC,oBAAoB,EAAC,MAAM,mBAAmB,CAAA;AACtD,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAA;AACvC,OAAO,yBAAyB,MAAM,6BAA6B,CAAA;AACnE,OAAO,eAAe,MAAM,mBAAmB,CAAA;AAkB/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,2BAA2B;IACvB,MAAM,CAAC,QAAQ,CAA6B;IAEpD,oEAAoE;IAC5D,MAAM,GAAG,KAAK,CAAA;IACd,iBAAiB,GAAG,KAAK,CAAA;IACzB,SAAS,CAAmB;IAC5B,QAAQ,GAAG,KAAK,CAAA;IAChB,WAAW,GAAG,KAAK,CAAA;IAC3B,iFAAiF;IACzE,UAAU,GAAkB,IAAI,CAAA;IACxC,8FAA8F;IACtF,UAAU,GAAG,CAAC,CAAA;IACd,OAAO,CAAgB;IACvB,WAAW,GAAkB,IAAI,CAAA;IACzC,uFAAuF;IAC/E,UAAU,GAAkB,IAAI,CAAA;IACvB,KAAK,GAAG,IAAI,UAAU,EAAE,CAAA;IACjC,MAAM,GAAwB,EAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAC,CAAA;IAC5D,eAAe,GAAG,IAAI,GAAG,EAAyC,CAAA;IAEnF;QACE,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,6BAA6B;QAC7B,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAA;IAC5F,CAAC;IAED,MAAM,CAAC,WAAW;QAChB,IAAI,CAAC,2BAA2B,CAAC,QAAQ,EAAE,CAAC;YAC1C,2BAA2B,CAAC,QAAQ,GAAG,IAAI,2BAA2B,EAAE,CAAA;QAC1E,CAAC;QACD,OAAO,2BAA2B,CAAC,QAAQ,CAAA;IAC7C,CAAC;IAED,mFAAmF;IAC3E,sBAAsB;QAC5B,IAAI,IAAI,CAAC,iBAAiB;YAAE,OAAM;QAClC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;QAC7B,eAAe,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;IAC5D,CAAC;IAED,uFAAuF;IACvF,QAAQ;QACN,OAAO,IAAI,CAAC,WAAW,CAAA;IACzB,CAAC;IAED,+EAA+E;IAC/E,iBAAiB;QACf,OAAO,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,CAAC,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1G,CAAC;IAED,cAAc;QACZ,OAAO,EAAC,GAAG,IAAI,CAAC,MAAM,EAAC,CAAA;IACzB,CAAC;IAED,oGAAoG;IACpG,oBAAoB,CAAC,QAA+C;QAClE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAClC,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAA;QAC/B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QACvC,CAAC,CAAA;IACH,CAAC;IAED,4EAA4E;IAC5E,SAAS,CAAC,MAAe;QACvB,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM;YAAE,IAAI,CAAC,GAAG,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAA;QACrE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,SAAS,EAAE,CAAA;IAClB,CAAC;IAEO,SAAS;QACf,IAAI,CAAC,sBAAsB,EAAE,CAAA;QAC7B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAA;YACjC,OAAM;QACR,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,wEAAwE;YACxE,oEAAoE;YACpE,IAAI,CAAC,SAAS,KAAK,eAAe,CAAC,iCAAiC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;gBAClF,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;gBACpB,IAAI,CAAC,SAAS,EAAE,CAAA;YAClB,CAAC,CAAC,CAAA;YACF,OAAM;QACR,CAAC;QACD,MAAM,IAAI,GAAG,eAAe,CAAC,kBAAkB,EAAE,CAAA;QACjD,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI;YAAE,OAAM;QACxD,iFAAiF;QACjF,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAM;QAC1D,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;YAAE,OAAM;QACrD,8EAA8E;QAC9E,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC5B,IAAI,CAAC,OAAO,GAAG,GAAG,CAAA;QAClB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,KAAK,GAAG;aACL,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;aACjE,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,IAAI,CAAC,OAAO,KAAK,GAAG;gBAAE,OAAM;YAChC,IAAI,CAAC,OAAO,GAAG,SAAS,CAAA;YACxB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACzB,CAAC,CAAC,CAAA;IACN,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,IAAY;QAC9B,MAAM,UAAU,GAAG,EAAE,IAAI,CAAC,UAAU,CAAA;QACpC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,IAAI,eAAe,CAAC,kBAAkB,EAAE,KAAK,IAAI,CAAA;QAEpH,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,oEAAoE;YACpE,IAAI,CAAC,GAAG,CAAC,6BAA6B,IAAI,CAAC,UAAU,OAAO,IAAI,EAAE,CAAC,CAAA;YACnE,IAAI,CAAC,WAAW,GAAG,KAAK,CAAA;QAC1B,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACtB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;QAEnB,IAAI,CAAC,CAAC,MAAM,eAAe,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,GAAG,CAAC,mBAAmB,IAAI,2BAA2B,CAAC,CAAA;YAC5D,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,EAAC,CAAC,CAAA;YACzD,IAAI,CAAC;gBACH,MAAM,yBAAyB,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE;oBAClE,IAAI,OAAO,EAAE;wBAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAC,CAAC,CAAA;gBACvE,CAAC,CAAC,CAAA;gBACF,IAAI,CAAC,OAAO,EAAE;oBAAE,OAAM;gBACtB,gEAAgE;gBAChE,MAAM,eAAe,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAA;YAC9C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,OAAO,EAAE;oBAAE,OAAM;gBACtB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,wCAAwC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;gBAC9E,OAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,CAAC,OAAO,EAAE;YAAE,OAAM;QAEtB,IAAI,CAAC,GAAG,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAA;QAC3C,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAC,KAAK,EAAE,SAAS,EAAC,CAAC,CAAA;QACxC,IAAI,CAAC;YACH,uEAAuE;YACvE,sEAAsE;YACtE,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;YAChC,MAAM,YAAY,CAAC,kBAAkB,EAAE,CAAA;YACvC,sEAAsE;YACtE,IAAI,CAAC,CAAC,MAAM,YAAY,CAAC,gBAAgB,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/E,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;YAC1D,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,OAAO,EAAE;gBAAE,OAAM;YACtB,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,YAAY,KAAK,EAAE,CAAC,CAAA;YAC5C,KAAK,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;YAChC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,oCAAoC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YAC1E,OAAM;QACR,CAAC;QACD,IAAI,CAAC,OAAO,EAAE;YAAE,OAAM;QAEtB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACtB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QAClB,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAC,KAAK,EAAE,OAAO,EAAC,CAAC,CAAA;QACtC,IAAI,CAAC,GAAG,CAAC,sBAAsB,IAAI,EAAE,CAAC,CAAA;IACxC,CAAC;IAEO,IAAI,CAAC,IAAY,EAAE,MAAc;QACvC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;QACzD,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,iBAAiB,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QAC/D,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAC,CAAC,CAAA;IACtD,CAAC;IAEO,IAAI,CAAC,MAAc;QACzB,IAAI,CAAC,UAAU,EAAE,CAAA;QACjB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAA;QACxB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QAClB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACtB,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;YACxE,IAAI,CAAC,GAAG,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAA;YACzC,KAAK,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;QAClC,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,KAAK,CAAA;QACxB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACtB,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,kBAAkB,EAAE,EAAE,EAAC,KAAK,EAAE,MAAM,EAAC,CAAC,CAAA;IACvE,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,MAAe;QAC3C,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAA;QAC3F,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,YAAY,CAAC,uBAAuB,CAAC,EAAC,CAAC,oBAAoB,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAC,CAAC,CAAA;QACnG,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,IAAY,EAAE,IAAoD;QAClF,MAAM,MAAM,GAAwB,EAAC,IAAI,EAAE,QAAQ,EAAE,eAAe,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAC,CAAA;QACxG,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAA;QAC5B,IACE,QAAQ,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI;YAC7B,QAAQ,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK;YAC/B,QAAQ,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ;YACrC,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EACjC,CAAC;YACD,OAAM;QACR,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC5C,IAAI,CAAC;gBACH,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAA;YACjC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,GAAG,CAAC,gCAAgC,KAAK,EAAE,CAAC,CAAA;YACnD,CAAC;QACH,CAAC;IACH,CAAC;IAEO,GAAG,CAAC,GAAW;QACrB,OAAO,CAAC,GAAG,CAAC,sBAAsB,GAAG,EAAE,CAAC,CAAA;IAC1C,CAAC;CACF;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC/D,CAAC;AAED,MAAM,2BAA2B,GAAG,2BAA2B,CAAC,WAAW,EAAE,CAAA;AAC7E,eAAe,2BAA2B,CAAA","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
- export interface TranscriptionRouteSummary {
2
- hasCloudSubscriber: boolean;
3
- hasForceLocalSubscriber: boolean;
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
- export interface TranscriptionSubscriberRoutes {
6
- cloud: boolean;
7
- forceLocal: boolean;
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,yBAAyB;IACxC,kBAAkB,EAAE,OAAO,CAAA;IAC3B,uBAAuB,EAAE,OAAO,CAAA;CACjC;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,YAAY,GAAG,KAAK,CAAA;AAEzE,wBAAgB,4BAA4B,CAAC,CAAC,EAC5C,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,EACxB,SAAS,EAAE,CAAC,UAAU,EAAE,CAAC,KAAK,6BAA6B,GAC1D,yBAAyB,CAS3B;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,OAAO,GAAG,OAAO,EACzB,MAAM,EAAE,6BAA6B,EACrC,cAAc,EAAE,OAAO,GACtB,0BAA0B,GAAG,IAAI,CAMnC"}
1
+ {"version":3,"file":"TranscriptionRouting.d.ts","sourceRoot":"","sources":["../../src/services/TranscriptionRouting.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,aAAa,GAAG,WAAW,CAAA;AAC1E,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,OAAO,CAAA;AAMnD;;;;GAIG;AACH,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,OAAO,CAAA;AAEtC;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAC,GAAG,gBAAgB,CAG5G;AAED,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,gBAAgB,CAAA;CAC7B;AAED,4EAA4E;AAC5E,MAAM,WAAW,yBAAyB;IACxC,cAAc,EAAE,OAAO,CAAA;IACvB,sDAAsD;IACtD,QAAQ,EAAE,OAAO,CAAA;IACjB,+EAA+E;IAC/E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,8DAA8D;IAC9D,cAAc,EAAE,MAAM,CAAA;IACtB,+EAA+E;IAC/E,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAO5G;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,yBAAyB,GAAG,mBAAmB,CAM5G;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;AAE5E,wBAAgB,WAAW,CACzB,OAAO,EAAE,mBAAmB,GAAG,IAAI,EACnC,MAAM,EAAE,mBAAmB,EAC3B,UAAU,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,EACpD,IAAI,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,EAC9C,OAAO,EAAE,OAAO,GACf,WAAW,CAKb;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CACxB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAClC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAGT;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAClC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAOT;AAED,+FAA+F;AAC/F,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,uGAAuG;IACvG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkD;gBAGpD,OAAO,SAAkB,EACzB,OAAO,SAAqB;IAG/C,2FAA2F;IAC3F,IAAI,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAItF;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAKvD,qEAAqE;IACrE,KAAK,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAMxC,kFAAkF;IAClF,IAAI,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAUvE,qEAAqE;IACrE,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAUtC,OAAO,CAAC,SAAS;CAGlB"}
@@ -1,24 +1,162 @@
1
- export function summarizeTranscriptionRoutes(subscribers, routesFor) {
2
- let hasCloudSubscriber = false;
3
- let hasForceLocalSubscriber = false;
4
- for (const subscriber of subscribers) {
5
- const routes = routesFor(subscriber);
6
- if (routes.cloud)
7
- hasCloudSubscriber = true;
8
- if (routes.forceLocal)
9
- hasForceLocalSubscriber = true;
10
- }
11
- return { hasCloudSubscriber, hasForceLocalSubscriber };
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
- export function transcriptionDeliveryRoute(source, routes, cloudConnected) {
14
- if (source === "cloud")
15
- return routes.cloud ? "default" : null;
16
- if (cloudConnected)
17
- return routes.forceLocal ? "forceLocal" : null;
18
- if (routes.cloud && routes.forceLocal)
19
- return "all";
20
- if (routes.forceLocal)
21
- return "forceLocal";
22
- return routes.cloud ? "default" : null;
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