@glassly/engine 0.3.2-dev.574 → 0.3.2-dev.578

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.
@@ -2,7 +2,8 @@
2
2
  * Decides whether on-device Sherpa STT should produce transcripts for local
3
3
  * miniapps, instead of (or in addition to) cloud transcription. Local STT is
4
4
  * active when the cloud is unavailable (disconnected or refusing STT for plan
5
- * quota) or any subscription requires `forceLocal`.
5
+ * quota) and some subscriber accepts fallback text, or any subscription
6
+ * requires `forceLocal`.
6
7
  *
7
8
  * Activation flips `localSttFallbackActive` in the settings store, which
8
9
  * the bluetooth-sdk native side reads to decide whether to feed PCM into
@@ -24,6 +25,8 @@ declare class LocalSttFallbackCoordinator {
24
25
  private static instance;
25
26
  private hasTranscriptionSubscription;
26
27
  private hasForceLocalSubscription;
28
+ /** Some default-route subscriber takes on-device text while the primary is down. */
29
+ private hasFallbackSubscription;
27
30
  private activeLanguage;
28
31
  /**
29
32
  * Default to "cloud is up" so we never accidentally activate local STT before
@@ -64,7 +67,7 @@ declare class LocalSttFallbackCoordinator {
64
67
  getActiveLanguage(): string | null;
65
68
  /** The cause of the current activation, or null while inactive. */
66
69
  getReason(): LocalSttFallbackReason | null;
67
- onSubscriptionChange(hasTranscription: boolean, language: string | null, hasForceLocal?: boolean): void;
70
+ onSubscriptionChange(hasTranscription: boolean, language: string | null, hasForceLocal?: boolean, acceptsFallback?: boolean): void;
68
71
  /**
69
72
  * Informational hook, retained for API stability. No longer called: the
70
73
  * authoritative switching signal is the cloud-client adapter (local miniapps
@@ -1 +1 @@
1
- {"version":3,"file":"LocalSttFallbackCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AACH;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAC9B,aAAa,GACb,oBAAoB,GACpB,gBAAgB,GAChB,WAAW,GACX,sBAAsB,CAAA;AAE1B,cAAM,2BAA2B;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IAEpD,OAAO,CAAC,4BAA4B,CAAQ;IAC5C,OAAO,CAAC,yBAAyB,CAAQ;IACzC,OAAO,CAAC,cAAc,CAAsB;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,oBAAoB,CAAQ;IACpC;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,mBAAmB,CAAO;IAClC;;;;;OAKG;IACH,OAAO,CAAC,eAAe,CAAQ;IAE/B,OAAO;YAYO,oBAAoB;IAQlC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAajC,MAAM,CAAC,WAAW,IAAI,2BAA2B;IAOjD,QAAQ,IAAI,OAAO;IAInB,iBAAiB,IAAI,MAAM,GAAG,IAAI;IAIlC,mEAAmE;IACnE,SAAS,IAAI,sBAAsB,GAAG,IAAI;IAI1C,oBAAoB,CAAC,gBAAgB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,aAAa,UAAQ,GAAG,IAAI;IAQrG;;;;;;OAMG;IACH,iBAAiB,IAAI,IAAI;IAEzB;;;;OAIG;IACG,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D,iBAAiB,IAAI,OAAO;IAI5B,uEAAuE;IACvE,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQnE;2CACuC;IACvC,OAAO,CAAC,SAAS;YAWH,SAAS;IAmBvB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;YAQP,aAAa;YASb,gBAAgB;IAqB9B,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,GAAG;CAGZ;AAED,QAAA,MAAM,2BAA2B,6BAA4C,CAAA;AAC7E,eAAe,2BAA2B,CAAA"}
1
+ {"version":3,"file":"LocalSttFallbackCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;GAeG;AACH;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAC9B,aAAa,GACb,oBAAoB,GACpB,gBAAgB,GAChB,WAAW,GACX,sBAAsB,CAAA;AAE1B,cAAM,2BAA2B;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IAEpD,OAAO,CAAC,4BAA4B,CAAQ;IAC5C,OAAO,CAAC,yBAAyB,CAAQ;IACzC,oFAAoF;IACpF,OAAO,CAAC,uBAAuB,CAAQ;IACvC,OAAO,CAAC,cAAc,CAAsB;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,oBAAoB,CAAQ;IACpC;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,mBAAmB,CAAO;IAClC;;;;;OAKG;IACH,OAAO,CAAC,eAAe,CAAQ;IAE/B,OAAO;YAYO,oBAAoB;IAQlC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAajC,MAAM,CAAC,WAAW,IAAI,2BAA2B;IAOjD,QAAQ,IAAI,OAAO;IAInB,iBAAiB,IAAI,MAAM,GAAG,IAAI;IAIlC,mEAAmE;IACnE,SAAS,IAAI,sBAAsB,GAAG,IAAI;IAI1C,oBAAoB,CAClB,gBAAgB,EAAE,OAAO,EACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,aAAa,UAAQ,EACrB,eAAe,UAAO,GACrB,IAAI;IAWP;;;;;;OAMG;IACH,iBAAiB,IAAI,IAAI;IAEzB;;;;OAIG;IACG,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D,iBAAiB,IAAI,OAAO;IAI5B,uEAAuE;IACvE,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQnE;2CACuC;IACvC,OAAO,CAAC,SAAS;YAWH,SAAS;IAmBvB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;YAQP,aAAa;YASb,gBAAgB;IAqB9B,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,GAAG;CAGZ;AAED,QAAA,MAAM,2BAA2B,6BAA4C,CAAA;AAC7E,eAAe,2BAA2B,CAAA"}
@@ -7,6 +7,8 @@ class LocalSttFallbackCoordinator {
7
7
  static instance;
8
8
  hasTranscriptionSubscription = false;
9
9
  hasForceLocalSubscription = false;
10
+ /** Some default-route subscriber takes on-device text while the primary is down. */
11
+ hasFallbackSubscription = false;
10
12
  activeLanguage = null;
11
13
  /**
12
14
  * Default to "cloud is up" so we never accidentally activate local STT before
@@ -87,10 +89,11 @@ class LocalSttFallbackCoordinator {
87
89
  getReason() {
88
90
  return this.localActive ? this.currentReason() : null;
89
91
  }
90
- onSubscriptionChange(hasTranscription, language, hasForceLocal = false) {
91
- this.log(`onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal})`);
92
+ onSubscriptionChange(hasTranscription, language, hasForceLocal = false, acceptsFallback = true) {
93
+ this.log(`onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal}, fallback=${acceptsFallback})`);
92
94
  this.hasTranscriptionSubscription = hasTranscription;
93
95
  this.hasForceLocalSubscription = hasTranscription && hasForceLocal;
96
+ this.hasFallbackSubscription = hasTranscription && acceptsFallback;
94
97
  this.activeLanguage = hasTranscription ? language : null;
95
98
  void this.reconcile();
96
99
  }
@@ -218,7 +221,8 @@ class LocalSttFallbackCoordinator {
218
221
  this.publishReason();
219
222
  }
220
223
  shouldUseLocalStt() {
221
- return this.hasTranscriptionSubscription && (this.hasForceLocalSubscription || !this.primaryUp());
224
+ return (this.hasTranscriptionSubscription &&
225
+ (this.hasForceLocalSubscription || (this.hasFallbackSubscription && !this.primaryUp())));
222
226
  }
223
227
  log(msg) {
224
228
  console.log(`[LocalSttFallback] ${msg}`);
@@ -1 +1 @@
1
- {"version":3,"file":"LocalSttFallbackCoordinator.js","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\nimport {ISLAND_SETTINGS_KEYS} from \"../runtime/config\"\nimport {useSettingsStore} from \"../stores/settings\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport sttModelManager from \"./STTModelManager\"\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 (disconnected or refusing STT for plan\n * quota) or any subscription requires `forceLocal`.\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 */\n/**\n * Why local STT is currently active, mirrored into the\n * `local_stt_fallback_reason` setting (\"\" while inactive) so UI can show it.\n * When several causes hold at once, the primary-path outage wins over\n * `force_local` (a forced subscription would keep local STT on either way).\n */\nexport type LocalSttFallbackReason =\n | \"force_local\"\n | \"cloud_disconnected\"\n | \"cloud_disabled\"\n | \"stt_quota\"\n | \"provider_unreachable\"\n\nclass LocalSttFallbackCoordinator {\n private static instance: LocalSttFallbackCoordinator\n\n private hasTranscriptionSubscription = false\n private hasForceLocalSubscription = false\n private activeLanguage: string | null = null\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 localActive = false\n private starting: Promise<void> | null = null\n private cloudAdapterAttached = false\n /**\n * Soniox-direct (user API key) state, pushed by SonioxDirectService. While\n * enabled, IT is the primary transcription path — cloud connectivity is\n * irrelevant and sherpa engages on Soniox health instead. Defaults healthy\n * for the same reason cloudConnected defaults true: never activate local STT\n * before the primary has had a chance to come up.\n */\n private sonioxDirectEnabled = false\n private sonioxDirectHealthy = true\n /**\n * Cloud STT refused for plan quota (`stream.quota` blocked=true). The cloud\n * connection stays up, so this is tracked separately from `cloudConnected`:\n * for transcription purposes the cloud path is down, and sherpa takes over\n * if its model is on-device. Soniox-direct is unmetered and unaffected.\n */\n private sttQuotaBlocked = false\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 useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, \"\")\n sttModelManager.onModelReady(() => {\n void this.retryAfterModelReady()\n })\n }\n\n private async retryAfterModelReady(): Promise<void> {\n // A model can finish downloading while the initial availability check is\n // still pending. Wait for that attempt before retrying so readiness is\n // never lost and native restarts cannot overlap.\n await this.starting\n await this.reconcile()\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 void this.reconcile()\n })\n this.cloudAdapterAttached = true\n }\n\n static getInstance(): LocalSttFallbackCoordinator {\n if (!LocalSttFallbackCoordinator.instance) {\n LocalSttFallbackCoordinator.instance = new LocalSttFallbackCoordinator()\n }\n return LocalSttFallbackCoordinator.instance\n }\n\n isActive(): boolean {\n return this.localActive\n }\n\n getActiveLanguage(): string | null {\n return this.activeLanguage\n }\n\n /** The cause of the current activation, or null while inactive. */\n getReason(): LocalSttFallbackReason | null {\n return this.localActive ? this.currentReason() : null\n }\n\n onSubscriptionChange(hasTranscription: boolean, language: string | null, hasForceLocal = false): void {\n this.log(`onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal})`)\n this.hasTranscriptionSubscription = hasTranscription\n this.hasForceLocalSubscription = hasTranscription && hasForceLocal\n this.activeLanguage = hasTranscription ? language : null\n void 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 /**\n * Pushed when the cloud announces/clears an STT quota block. Awaitable so\n * the caller can inspect `isActive()` afterwards to phrase its user notice\n * (\"continuing on-device\" vs \"transcription paused\").\n */\n async onSttQuotaBlockedChange(blocked: boolean): Promise<void> {\n if (this.sttQuotaBlocked === blocked) return\n this.log(`stt quota -> ${blocked ? \"blocked\" : \"restored\"}`)\n this.sttQuotaBlocked = blocked\n await this.reconcile()\n }\n\n isSttQuotaBlocked(): boolean {\n return this.sttQuotaBlocked\n }\n\n /** Pushed by SonioxDirectService on every enable/health transition. */\n onSonioxDirectStateChange(enabled: boolean, healthy: boolean): void {\n if (this.sonioxDirectEnabled === enabled && this.sonioxDirectHealthy === healthy) return\n this.log(`soniox-direct -> enabled=${enabled} healthy=${healthy}`)\n this.sonioxDirectEnabled = enabled\n this.sonioxDirectHealthy = healthy\n void this.reconcile()\n }\n\n /** Whether the primary transcription path (Soniox-direct when a user key is\n * set, the cloud otherwise) is up. */\n private primaryUp(): boolean {\n // Cloud turned off in Privacy settings: the cloud path is not merely down,\n // it is never coming back, so on-device STT is the primary. Checked ahead\n // of `cloudConnected` because that field defaults to true (so local STT\n // doesn't pre-empt cloud at boot) and would otherwise report a path that\n // will never connect as up. Soniox-direct is a user-supplied third-party\n // key, not Glassly cloud, so it stays available.\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return false\n return this.sonioxDirectEnabled ? this.sonioxDirectHealthy : this.cloudConnected && !this.sttQuotaBlocked\n }\n\n private async reconcile(): Promise<void> {\n this.attachCloudAdapterIfReady()\n const shouldBeActive = this.shouldUseLocalStt()\n if (shouldBeActive && !this.localActive) {\n if (!this.starting) {\n this.starting = this.startLocalStt().finally(() => {\n this.starting = null\n })\n }\n await this.starting\n } else if (!shouldBeActive && this.localActive) {\n this.stopLocalStt(this.hasTranscriptionSubscription ? \"primary transcription recovered\" : \"subscription gone\")\n } else if (shouldBeActive && this.localActive) {\n // Still active, but the cause may have shifted (e.g. quota block landing\n // while already offline for a disconnect) — keep the mirror current.\n this.publishReason()\n }\n }\n\n /**\n * Ranks the concurrent causes: a dead-forever cloud path first, then the\n * primary provider's own failure, then transient outages, with `force_local`\n * only when the primary is actually up. Mirrors `primaryUp()`.\n */\n private currentReason(): LocalSttFallbackReason {\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return \"cloud_disabled\"\n if (this.sonioxDirectEnabled && !this.sonioxDirectHealthy) return \"provider_unreachable\"\n if (!this.sonioxDirectEnabled && !this.cloudConnected) return \"cloud_disconnected\"\n if (!this.sonioxDirectEnabled && this.sttQuotaBlocked) return \"stt_quota\"\n return \"force_local\"\n }\n\n private publishReason(): void {\n const reason = this.localActive ? this.currentReason() : \"\"\n const store = useSettingsStore.getState()\n if (store.getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason) !== reason) {\n store.setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, reason)\n }\n }\n\n private async startLocalStt(): Promise<void> {\n this.log(\"starting local stt\")\n try {\n await this.activateLocalStt()\n } catch (err) {\n this.log(`local stt startup failed: ${err}`)\n }\n }\n\n private async activateLocalStt(): Promise<void> {\n const modelAvailable = await sttModelManager.isModelAvailable()\n if (!modelAvailable) {\n this.log(\"local stt model is not available yet — skipping activation\")\n return\n }\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed before start completed\")\n return\n }\n // Native rejects unless the recognizer and its processing loop are ready.\n await BluetoothSdk.restartTranscriber()\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed during transcriber restart\")\n return\n }\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, true)\n this.localActive = true\n this.publishReason()\n }\n\n private stopLocalStt(reason: string): void {\n this.log(`stopping local stt: ${reason}`)\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n this.localActive = false\n this.publishReason()\n }\n\n private shouldUseLocalStt(): boolean {\n return this.hasTranscriptionSubscription && (this.hasForceLocalSubscription || !this.primaryUp())\n }\n\n private log(msg: string): void {\n console.log(`[LocalSttFallback] ${msg}`)\n }\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,iCAAiC,CAAA;AAC1D,OAAO,EAAC,oBAAoB,EAAC,MAAM,mBAAmB,CAAA;AACtD,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAA;AACvD,OAAO,eAAe,MAAM,mBAAmB,CAAA;AA+B/C,MAAM,2BAA2B;IACvB,MAAM,CAAC,QAAQ,CAA6B;IAE5C,4BAA4B,GAAG,KAAK,CAAA;IACpC,yBAAyB,GAAG,KAAK,CAAA;IACzC,oFAAoF;IAC5E,uBAAuB,GAAG,KAAK,CAAA;IAC/B,cAAc,GAAkB,IAAI,CAAA;IAC5C;;;;;OAKG;IACK,cAAc,GAAG,IAAI,CAAA;IACrB,WAAW,GAAG,KAAK,CAAA;IACnB,QAAQ,GAAyB,IAAI,CAAA;IACrC,oBAAoB,GAAG,KAAK,CAAA;IACpC;;;;;;OAMG;IACK,mBAAmB,GAAG,KAAK,CAAA;IAC3B,mBAAmB,GAAG,IAAI,CAAA;IAClC;;;;;OAKG;IACK,eAAe,GAAG,KAAK,CAAA;IAE/B;QACE,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,6BAA6B;QAC7B,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAA;QAC1F,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAA;QACvF,eAAe,CAAC,YAAY,CAAC,GAAG,EAAE;YAChC,KAAK,IAAI,CAAC,oBAAoB,EAAE,CAAA;QAClC,CAAC,CAAC,CAAA;IACJ,CAAC;IAEO,KAAK,CAAC,oBAAoB;QAChC,yEAAyE;QACzE,uEAAuE;QACvE,iDAAiD;QACjD,MAAM,IAAI,CAAC,QAAQ,CAAA;QACnB,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;IACxB,CAAC;IAED;;;;OAIG;IACK,yBAAyB;QAC/B,IAAI,IAAI,CAAC,oBAAoB;YAAE,OAAM;QACrC,IAAI,CAAC,cAAc,GAAG,kBAAkB,CAAC,WAAW,EAAE,CAAA;QACtD,kBAAkB,CAAC,eAAe,CAAC,CAAC,MAAM,EAAE,EAAE;YAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,KAAK,WAAW,CAAA;YAC/C,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS;gBAAE,OAAM;YAC7C,IAAI,CAAC,GAAG,CAAC,uBAAuB,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;YAC5D,IAAI,CAAC,cAAc,GAAG,SAAS,CAAA;YAC/B,KAAK,IAAI,CAAC,SAAS,EAAE,CAAA;QACvB,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAA;IAClC,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,QAAQ;QACN,OAAO,IAAI,CAAC,WAAW,CAAA;IACzB,CAAC;IAED,iBAAiB;QACf,OAAO,IAAI,CAAC,cAAc,CAAA;IAC5B,CAAC;IAED,mEAAmE;IACnE,SAAS;QACP,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACvD,CAAC;IAED,oBAAoB,CAClB,gBAAyB,EACzB,QAAuB,EACvB,aAAa,GAAG,KAAK,EACrB,eAAe,GAAG,IAAI;QAEtB,IAAI,CAAC,GAAG,CACN,8BAA8B,gBAAgB,UAAU,QAAQ,gBAAgB,aAAa,cAAc,eAAe,GAAG,CAC9H,CAAA;QACD,IAAI,CAAC,4BAA4B,GAAG,gBAAgB,CAAA;QACpD,IAAI,CAAC,yBAAyB,GAAG,gBAAgB,IAAI,aAAa,CAAA;QAClE,IAAI,CAAC,uBAAuB,GAAG,gBAAgB,IAAI,eAAe,CAAA;QAClE,IAAI,CAAC,cAAc,GAAG,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;QACxD,KAAK,IAAI,CAAC,SAAS,EAAE,CAAA;IACvB,CAAC;IAED;;;;;;OAMG;IACH,iBAAiB,KAAU,CAAC;IAE5B;;;;OAIG;IACH,KAAK,CAAC,uBAAuB,CAAC,OAAgB;QAC5C,IAAI,IAAI,CAAC,eAAe,KAAK,OAAO;YAAE,OAAM;QAC5C,IAAI,CAAC,GAAG,CAAC,gBAAgB,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAA;QAC5D,IAAI,CAAC,eAAe,GAAG,OAAO,CAAA;QAC9B,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;IACxB,CAAC;IAED,iBAAiB;QACf,OAAO,IAAI,CAAC,eAAe,CAAA;IAC7B,CAAC;IAED,uEAAuE;IACvE,yBAAyB,CAAC,OAAgB,EAAE,OAAgB;QAC1D,IAAI,IAAI,CAAC,mBAAmB,KAAK,OAAO,IAAI,IAAI,CAAC,mBAAmB,KAAK,OAAO;YAAE,OAAM;QACxF,IAAI,CAAC,GAAG,CAAC,4BAA4B,OAAO,YAAY,OAAO,EAAE,CAAC,CAAA;QAClE,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAA;QAClC,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAA;QAClC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAA;IACvB,CAAC;IAED;2CACuC;IAC/B,SAAS;QACf,2EAA2E;QAC3E,0EAA0E;QAC1E,wEAAwE;QACxE,yEAAyE;QACzE,yEAAyE;QACzE,iDAAiD;QACjD,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,kBAAkB,CAAC,eAAe,EAAE;YAAE,OAAO,KAAK,CAAA;QACnF,OAAO,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,IAAI,CAAC,eAAe,CAAA;IAC3G,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,IAAI,CAAC,yBAAyB,EAAE,CAAA;QAChC,MAAM,cAAc,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAA;QAC/C,IAAI,cAAc,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACnB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;oBAChD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;gBACtB,CAAC,CAAC,CAAA;YACJ,CAAC;YACD,MAAM,IAAI,CAAC,QAAQ,CAAA;QACrB,CAAC;aAAM,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC/C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAA;QAChH,CAAC;aAAM,IAAI,cAAc,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC9C,yEAAyE;YACzE,qEAAqE;YACrE,IAAI,CAAC,aAAa,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,aAAa;QACnB,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,kBAAkB,CAAC,eAAe,EAAE;YAAE,OAAO,gBAAgB,CAAA;QAC9F,IAAI,IAAI,CAAC,mBAAmB,IAAI,CAAC,IAAI,CAAC,mBAAmB;YAAE,OAAO,sBAAsB,CAAA;QACxF,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,IAAI,CAAC,cAAc;YAAE,OAAO,oBAAoB,CAAA;QAClF,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,eAAe;YAAE,OAAO,WAAW,CAAA;QACzE,OAAO,aAAa,CAAA;IACtB,CAAC;IAEO,aAAa;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QAC3D,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,EAAE,CAAA;QACzC,IAAI,KAAK,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,CAAC,KAAK,MAAM,EAAE,CAAC;YAC7E,KAAK,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QAC9B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAA;QAC/B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,gBAAgB;QAC5B,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC,gBAAgB,EAAE,CAAA;QAC/D,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,IAAI,CAAC,GAAG,CAAC,4DAA4D,CAAC,CAAA;YACtE,OAAM;QACR,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,sEAAsE,CAAC,CAAA;YAChF,OAAM;QACR,CAAC;QACD,0EAA0E;QAC1E,MAAM,YAAY,CAAC,kBAAkB,EAAE,CAAA;QACvC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,0EAA0E,CAAC,CAAA;YACpF,OAAM;QACR,CAAC;QACD,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAA;QACzF,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAEO,YAAY,CAAC,MAAc;QACjC,IAAI,CAAC,GAAG,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAA;QACzC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAA;QAC1F,IAAI,CAAC,WAAW,GAAG,KAAK,CAAA;QACxB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAEO,iBAAiB;QACvB,OAAO,CACL,IAAI,CAAC,4BAA4B;YACjC,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,IAAI,CAAC,uBAAuB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CACxF,CAAA;IACH,CAAC;IAEO,GAAG,CAAC,GAAW;QACrB,OAAO,CAAC,GAAG,CAAC,sBAAsB,GAAG,EAAE,CAAC,CAAA;IAC1C,CAAC;CACF;AAED,MAAM,2BAA2B,GAAG,2BAA2B,CAAC,WAAW,EAAE,CAAA;AAC7E,eAAe,2BAA2B,CAAA","sourcesContent":["import BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\nimport {ISLAND_SETTINGS_KEYS} from \"../runtime/config\"\nimport {useSettingsStore} from \"../stores/settings\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport sttModelManager from \"./STTModelManager\"\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 (disconnected or refusing STT for plan\n * quota) and some subscriber accepts fallback text, or any subscription\n * requires `forceLocal`.\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 */\n/**\n * Why local STT is currently active, mirrored into the\n * `local_stt_fallback_reason` setting (\"\" while inactive) so UI can show it.\n * When several causes hold at once, the primary-path outage wins over\n * `force_local` (a forced subscription would keep local STT on either way).\n */\nexport type LocalSttFallbackReason =\n | \"force_local\"\n | \"cloud_disconnected\"\n | \"cloud_disabled\"\n | \"stt_quota\"\n | \"provider_unreachable\"\n\nclass LocalSttFallbackCoordinator {\n private static instance: LocalSttFallbackCoordinator\n\n private hasTranscriptionSubscription = false\n private hasForceLocalSubscription = false\n /** Some default-route subscriber takes on-device text while the primary is down. */\n private hasFallbackSubscription = false\n private activeLanguage: string | null = null\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 localActive = false\n private starting: Promise<void> | null = null\n private cloudAdapterAttached = false\n /**\n * Soniox-direct (user API key) state, pushed by SonioxDirectService. While\n * enabled, IT is the primary transcription path — cloud connectivity is\n * irrelevant and sherpa engages on Soniox health instead. Defaults healthy\n * for the same reason cloudConnected defaults true: never activate local STT\n * before the primary has had a chance to come up.\n */\n private sonioxDirectEnabled = false\n private sonioxDirectHealthy = true\n /**\n * Cloud STT refused for plan quota (`stream.quota` blocked=true). The cloud\n * connection stays up, so this is tracked separately from `cloudConnected`:\n * for transcription purposes the cloud path is down, and sherpa takes over\n * if its model is on-device. Soniox-direct is unmetered and unaffected.\n */\n private sttQuotaBlocked = false\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 useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, \"\")\n sttModelManager.onModelReady(() => {\n void this.retryAfterModelReady()\n })\n }\n\n private async retryAfterModelReady(): Promise<void> {\n // A model can finish downloading while the initial availability check is\n // still pending. Wait for that attempt before retrying so readiness is\n // never lost and native restarts cannot overlap.\n await this.starting\n await this.reconcile()\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 void this.reconcile()\n })\n this.cloudAdapterAttached = true\n }\n\n static getInstance(): LocalSttFallbackCoordinator {\n if (!LocalSttFallbackCoordinator.instance) {\n LocalSttFallbackCoordinator.instance = new LocalSttFallbackCoordinator()\n }\n return LocalSttFallbackCoordinator.instance\n }\n\n isActive(): boolean {\n return this.localActive\n }\n\n getActiveLanguage(): string | null {\n return this.activeLanguage\n }\n\n /** The cause of the current activation, or null while inactive. */\n getReason(): LocalSttFallbackReason | null {\n return this.localActive ? this.currentReason() : null\n }\n\n onSubscriptionChange(\n hasTranscription: boolean,\n language: string | null,\n hasForceLocal = false,\n acceptsFallback = true,\n ): void {\n this.log(\n `onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal}, fallback=${acceptsFallback})`,\n )\n this.hasTranscriptionSubscription = hasTranscription\n this.hasForceLocalSubscription = hasTranscription && hasForceLocal\n this.hasFallbackSubscription = hasTranscription && acceptsFallback\n this.activeLanguage = hasTranscription ? language : null\n void 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 /**\n * Pushed when the cloud announces/clears an STT quota block. Awaitable so\n * the caller can inspect `isActive()` afterwards to phrase its user notice\n * (\"continuing on-device\" vs \"transcription paused\").\n */\n async onSttQuotaBlockedChange(blocked: boolean): Promise<void> {\n if (this.sttQuotaBlocked === blocked) return\n this.log(`stt quota -> ${blocked ? \"blocked\" : \"restored\"}`)\n this.sttQuotaBlocked = blocked\n await this.reconcile()\n }\n\n isSttQuotaBlocked(): boolean {\n return this.sttQuotaBlocked\n }\n\n /** Pushed by SonioxDirectService on every enable/health transition. */\n onSonioxDirectStateChange(enabled: boolean, healthy: boolean): void {\n if (this.sonioxDirectEnabled === enabled && this.sonioxDirectHealthy === healthy) return\n this.log(`soniox-direct -> enabled=${enabled} healthy=${healthy}`)\n this.sonioxDirectEnabled = enabled\n this.sonioxDirectHealthy = healthy\n void this.reconcile()\n }\n\n /** Whether the primary transcription path (Soniox-direct when a user key is\n * set, the cloud otherwise) is up. */\n private primaryUp(): boolean {\n // Cloud turned off in Privacy settings: the cloud path is not merely down,\n // it is never coming back, so on-device STT is the primary. Checked ahead\n // of `cloudConnected` because that field defaults to true (so local STT\n // doesn't pre-empt cloud at boot) and would otherwise report a path that\n // will never connect as up. Soniox-direct is a user-supplied third-party\n // key, not Glassly cloud, so it stays available.\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return false\n return this.sonioxDirectEnabled ? this.sonioxDirectHealthy : this.cloudConnected && !this.sttQuotaBlocked\n }\n\n private async reconcile(): Promise<void> {\n this.attachCloudAdapterIfReady()\n const shouldBeActive = this.shouldUseLocalStt()\n if (shouldBeActive && !this.localActive) {\n if (!this.starting) {\n this.starting = this.startLocalStt().finally(() => {\n this.starting = null\n })\n }\n await this.starting\n } else if (!shouldBeActive && this.localActive) {\n this.stopLocalStt(this.hasTranscriptionSubscription ? \"primary transcription recovered\" : \"subscription gone\")\n } else if (shouldBeActive && this.localActive) {\n // Still active, but the cause may have shifted (e.g. quota block landing\n // while already offline for a disconnect) — keep the mirror current.\n this.publishReason()\n }\n }\n\n /**\n * Ranks the concurrent causes: a dead-forever cloud path first, then the\n * primary provider's own failure, then transient outages, with `force_local`\n * only when the primary is actually up. Mirrors `primaryUp()`.\n */\n private currentReason(): LocalSttFallbackReason {\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return \"cloud_disabled\"\n if (this.sonioxDirectEnabled && !this.sonioxDirectHealthy) return \"provider_unreachable\"\n if (!this.sonioxDirectEnabled && !this.cloudConnected) return \"cloud_disconnected\"\n if (!this.sonioxDirectEnabled && this.sttQuotaBlocked) return \"stt_quota\"\n return \"force_local\"\n }\n\n private publishReason(): void {\n const reason = this.localActive ? this.currentReason() : \"\"\n const store = useSettingsStore.getState()\n if (store.getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason) !== reason) {\n store.setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, reason)\n }\n }\n\n private async startLocalStt(): Promise<void> {\n this.log(\"starting local stt\")\n try {\n await this.activateLocalStt()\n } catch (err) {\n this.log(`local stt startup failed: ${err}`)\n }\n }\n\n private async activateLocalStt(): Promise<void> {\n const modelAvailable = await sttModelManager.isModelAvailable()\n if (!modelAvailable) {\n this.log(\"local stt model is not available yet — skipping activation\")\n return\n }\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed before start completed\")\n return\n }\n // Native rejects unless the recognizer and its processing loop are ready.\n await BluetoothSdk.restartTranscriber()\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed during transcriber restart\")\n return\n }\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, true)\n this.localActive = true\n this.publishReason()\n }\n\n private stopLocalStt(reason: string): void {\n this.log(`stopping local stt: ${reason}`)\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n this.localActive = false\n this.publishReason()\n }\n\n private shouldUseLocalStt(): boolean {\n return (\n this.hasTranscriptionSubscription &&\n (this.hasForceLocalSubscription || (this.hasFallbackSubscription && !this.primaryUp()))\n )\n }\n\n private log(msg: string): void {\n console.log(`[LocalSttFallback] ${msg}`)\n }\n}\n\nconst localSttFallbackCoordinator = LocalSttFallbackCoordinator.getInstance()\nexport default localSttFallbackCoordinator\n"]}
@@ -1,12 +1,17 @@
1
1
  export interface TranscriptionRouteSummary {
2
2
  hasCloudSubscriber: boolean;
3
3
  hasForceLocalSubscriber: boolean;
4
+ /** A default-route subscriber that takes on-device text while the primary is down. */
5
+ hasFallbackSubscriber: boolean;
4
6
  }
5
7
  export interface TranscriptionSubscriberRoutes {
6
8
  cloud: boolean;
7
9
  forceLocal: boolean;
10
+ /** Default-route fallback acceptance; absent means accepted. */
11
+ localFallback?: boolean;
8
12
  }
9
- export type TranscriptionDeliveryRoute = "default" | "forceLocal" | "all";
13
+ /** "fallback" is on-device text serving the default route because the primary is down. */
14
+ export type TranscriptionDeliveryRoute = "default" | "forceLocal" | "fallback" | "all";
10
15
  export declare function summarizeTranscriptionRoutes<T>(subscribers: Iterable<T>, routesFor: (subscriber: T) => TranscriptionSubscriberRoutes): TranscriptionRouteSummary;
11
16
  /**
12
17
  * Decide which subscription route(s) a transcript reaches.
@@ -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;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,OAAO,GAAG,OAAO,EACzB,MAAM,EAAE,6BAA6B,EACrC,SAAS,EAAE,OAAO,GACjB,0BAA0B,GAAG,IAAI,CAMnC"}
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;IAChC,sFAAsF;IACtF,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;IACnB,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,0FAA0F;AAC1F,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,YAAY,GAAG,UAAU,GAAG,KAAK,CAAA;AAEtF,wBAAgB,4BAA4B,CAAC,CAAC,EAC5C,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,EACxB,SAAS,EAAE,CAAC,UAAU,EAAE,CAAC,KAAK,6BAA6B,GAC1D,yBAAyB,CAW3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,OAAO,GAAG,OAAO,EACzB,MAAM,EAAE,6BAA6B,EACrC,SAAS,EAAE,OAAO,GACjB,0BAA0B,GAAG,IAAI,CAOnC"}
@@ -1,14 +1,17 @@
1
1
  export function summarizeTranscriptionRoutes(subscribers, routesFor) {
2
2
  let hasCloudSubscriber = false;
3
3
  let hasForceLocalSubscriber = false;
4
+ let hasFallbackSubscriber = false;
4
5
  for (const subscriber of subscribers) {
5
6
  const routes = routesFor(subscriber);
6
7
  if (routes.cloud)
7
8
  hasCloudSubscriber = true;
8
9
  if (routes.forceLocal)
9
10
  hasForceLocalSubscriber = true;
11
+ if (routes.cloud && routes.localFallback !== false)
12
+ hasFallbackSubscriber = true;
10
13
  }
11
- return { hasCloudSubscriber, hasForceLocalSubscriber };
14
+ return { hasCloudSubscriber, hasForceLocalSubscriber, hasFallbackSubscriber };
12
15
  }
13
16
  /**
14
17
  * Decide which subscription route(s) a transcript reaches.
@@ -25,10 +28,11 @@ export function transcriptionDeliveryRoute(source, routes, primaryUp) {
25
28
  return routes.cloud ? "default" : null;
26
29
  if (primaryUp)
27
30
  return routes.forceLocal ? "forceLocal" : null;
28
- if (routes.cloud && routes.forceLocal)
31
+ const fallback = routes.cloud && routes.localFallback !== false;
32
+ if (fallback && routes.forceLocal)
29
33
  return "all";
30
34
  if (routes.forceLocal)
31
35
  return "forceLocal";
32
- return routes.cloud ? "default" : null;
36
+ return fallback ? "fallback" : null;
33
37
  }
34
38
  //# sourceMappingURL=TranscriptionRouting.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TranscriptionRouting.js","sourceRoot":"","sources":["../../src/services/TranscriptionRouting.ts"],"names":[],"mappings":"AAYA,MAAM,UAAU,4BAA4B,CAC1C,WAAwB,EACxB,SAA2D;IAE3D,IAAI,kBAAkB,GAAG,KAAK,CAAA;IAC9B,IAAI,uBAAuB,GAAG,KAAK,CAAA;IACnC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;QACpC,IAAI,MAAM,CAAC,KAAK;YAAE,kBAAkB,GAAG,IAAI,CAAA;QAC3C,IAAI,MAAM,CAAC,UAAU;YAAE,uBAAuB,GAAG,IAAI,CAAA;IACvD,CAAC;IACD,OAAO,EAAC,kBAAkB,EAAE,uBAAuB,EAAC,CAAA;AACtD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CACxC,MAAyB,EACzB,MAAqC,EACrC,SAAkB;IAElB,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9D,IAAI,SAAS;QAAE,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7D,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,UAAU;QAAE,OAAO,KAAK,CAAA;IACnD,IAAI,MAAM,CAAC,UAAU;QAAE,OAAO,YAAY,CAAA;IAC1C,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA;AACxC,CAAC","sourcesContent":["export interface TranscriptionRouteSummary {\n hasCloudSubscriber: boolean\n hasForceLocalSubscriber: boolean\n}\n\nexport interface TranscriptionSubscriberRoutes {\n cloud: boolean\n forceLocal: boolean\n}\n\nexport type TranscriptionDeliveryRoute = \"default\" | \"forceLocal\" | \"all\"\n\nexport function summarizeTranscriptionRoutes<T>(\n subscribers: Iterable<T>,\n routesFor: (subscriber: T) => TranscriptionSubscriberRoutes,\n): TranscriptionRouteSummary {\n let hasCloudSubscriber = false\n let hasForceLocalSubscriber = false\n for (const subscriber of subscribers) {\n const routes = routesFor(subscriber)\n if (routes.cloud) hasCloudSubscriber = true\n if (routes.forceLocal) hasForceLocalSubscriber = true\n }\n return {hasCloudSubscriber, hasForceLocalSubscriber}\n}\n\n/**\n * Decide which subscription route(s) a transcript reaches.\n *\n * `source` is who produced the transcript: \"cloud\" is the PRIMARY provider\n * (the cloud runtime, or Soniox-direct when a user API key is set — both\n * deliver through the default route); \"local\" is on-device sherpa.\n * `primaryUp` is whether that primary path is currently believed usable\n * (cloud connectivity, or Soniox-direct session health) — when it is down,\n * local transcripts also serve default-route subscribers.\n */\nexport function transcriptionDeliveryRoute(\n source: \"cloud\" | \"local\",\n routes: TranscriptionSubscriberRoutes,\n primaryUp: boolean,\n): TranscriptionDeliveryRoute | null {\n if (source === \"cloud\") return routes.cloud ? \"default\" : null\n if (primaryUp) return routes.forceLocal ? \"forceLocal\" : null\n if (routes.cloud && routes.forceLocal) return \"all\"\n if (routes.forceLocal) return \"forceLocal\"\n return routes.cloud ? \"default\" : null\n}\n"]}
1
+ {"version":3,"file":"TranscriptionRouting.js","sourceRoot":"","sources":["../../src/services/TranscriptionRouting.ts"],"names":[],"mappings":"AAiBA,MAAM,UAAU,4BAA4B,CAC1C,WAAwB,EACxB,SAA2D;IAE3D,IAAI,kBAAkB,GAAG,KAAK,CAAA;IAC9B,IAAI,uBAAuB,GAAG,KAAK,CAAA;IACnC,IAAI,qBAAqB,GAAG,KAAK,CAAA;IACjC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;QACpC,IAAI,MAAM,CAAC,KAAK;YAAE,kBAAkB,GAAG,IAAI,CAAA;QAC3C,IAAI,MAAM,CAAC,UAAU;YAAE,uBAAuB,GAAG,IAAI,CAAA;QACrD,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,aAAa,KAAK,KAAK;YAAE,qBAAqB,GAAG,IAAI,CAAA;IAClF,CAAC;IACD,OAAO,EAAC,kBAAkB,EAAE,uBAAuB,EAAE,qBAAqB,EAAC,CAAA;AAC7E,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CACxC,MAAyB,EACzB,MAAqC,EACrC,SAAkB;IAElB,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9D,IAAI,SAAS;QAAE,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,aAAa,KAAK,KAAK,CAAA;IAC/D,IAAI,QAAQ,IAAI,MAAM,CAAC,UAAU;QAAE,OAAO,KAAK,CAAA;IAC/C,IAAI,MAAM,CAAC,UAAU;QAAE,OAAO,YAAY,CAAA;IAC1C,OAAO,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA;AACrC,CAAC","sourcesContent":["export interface TranscriptionRouteSummary {\n hasCloudSubscriber: boolean\n hasForceLocalSubscriber: boolean\n /** A default-route subscriber that takes on-device text while the primary is down. */\n hasFallbackSubscriber: boolean\n}\n\nexport interface TranscriptionSubscriberRoutes {\n cloud: boolean\n forceLocal: boolean\n /** Default-route fallback acceptance; absent means accepted. */\n localFallback?: boolean\n}\n\n/** \"fallback\" is on-device text serving the default route because the primary is down. */\nexport type TranscriptionDeliveryRoute = \"default\" | \"forceLocal\" | \"fallback\" | \"all\"\n\nexport function summarizeTranscriptionRoutes<T>(\n subscribers: Iterable<T>,\n routesFor: (subscriber: T) => TranscriptionSubscriberRoutes,\n): TranscriptionRouteSummary {\n let hasCloudSubscriber = false\n let hasForceLocalSubscriber = false\n let hasFallbackSubscriber = false\n for (const subscriber of subscribers) {\n const routes = routesFor(subscriber)\n if (routes.cloud) hasCloudSubscriber = true\n if (routes.forceLocal) hasForceLocalSubscriber = true\n if (routes.cloud && routes.localFallback !== false) hasFallbackSubscriber = true\n }\n return {hasCloudSubscriber, hasForceLocalSubscriber, hasFallbackSubscriber}\n}\n\n/**\n * Decide which subscription route(s) a transcript reaches.\n *\n * `source` is who produced the transcript: \"cloud\" is the PRIMARY provider\n * (the cloud runtime, or Soniox-direct when a user API key is set — both\n * deliver through the default route); \"local\" is on-device sherpa.\n * `primaryUp` is whether that primary path is currently believed usable\n * (cloud connectivity, or Soniox-direct session health) — when it is down,\n * local transcripts also serve default-route subscribers.\n */\nexport function transcriptionDeliveryRoute(\n source: \"cloud\" | \"local\",\n routes: TranscriptionSubscriberRoutes,\n primaryUp: boolean,\n): TranscriptionDeliveryRoute | null {\n if (source === \"cloud\") return routes.cloud ? \"default\" : null\n if (primaryUp) return routes.forceLocal ? \"forceLocal\" : null\n const fallback = routes.cloud && routes.localFallback !== false\n if (fallback && routes.forceLocal) return \"all\"\n if (routes.forceLocal) return \"forceLocal\"\n return fallback ? \"fallback\" : null\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.2-dev.574",
3
+ "version": "0.3.2-dev.578",
4
4
  "description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,17 +97,17 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@glassly/bluetooth-sdk": "0.3.2-dev.574",
101
- "@glassly/cloud-client": "0.3.2-dev.574",
102
- "@glassly/cloud-protocol": "0.3.2-dev.574",
103
- "@glassly/crust": "0.3.2-dev.574",
104
- "@glassly/miniapp": "0.3.2-dev.574",
100
+ "@glassly/bluetooth-sdk": "0.3.2-dev.578",
101
+ "@glassly/cloud-client": "0.3.2-dev.578",
102
+ "@glassly/cloud-protocol": "0.3.2-dev.578",
103
+ "@glassly/crust": "0.3.2-dev.578",
104
+ "@glassly/miniapp": "0.3.2-dev.578",
105
105
  "buffer": "^6.0.3",
106
106
  "events": "^3.3.0",
107
107
  "react-native-marked": "8.1.1",
108
108
  "semver": "^7.7.4",
109
109
  "typesafe-ts": "^1.7.1",
110
- "@glassly/llm-providers": "0.3.2-dev.574"
110
+ "@glassly/llm-providers": "0.3.2-dev.578"
111
111
  },
112
112
  "devDependencies": {
113
113
  "@glassly/llm-providers": "workspace:*",
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "0.3.2",
15
- "releaseIdentity": "0.3.2-dev.574",
16
- "releaseSetId": "glassly-0.3.2-dev.574",
17
- "sourceCommit": "959420ded485d85438cc44e085ba61388cbdd72a",
15
+ "releaseIdentity": "0.3.2-dev.578",
16
+ "releaseSetId": "glassly-0.3.2-dev.578",
17
+ "sourceCommit": "4258797cfc6dfb58a598e33bec9efa2e9cdbfbf4",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -54,6 +54,7 @@ import {
54
54
  import {normalizeAiTier, resolveAiTier} from "./AiTierResolution"
55
55
  import type {AiTier, AiTierProvider, SavedLlmKeys} from "./AiTierResolution"
56
56
  import {HttpError} from "@glassly/cloud-client"
57
+ import type {QuotaStatusPayload} from "@glassly/cloud-protocol"
57
58
  import localDisplayManager from "./LocalDisplayManager"
58
59
  import sttModelManager from "./STTModelManager"
59
60
  import localTranslationService, {type LocalTranslationSubscription} from "./LocalTranslationService"
@@ -108,7 +109,12 @@ import {phonePhotoCoordinator} from "./PhonePhotoCoordinator"
108
109
  import {phoneStreamCoordinator} from "./PhoneStreamCoordinator"
109
110
  import {phoneVideoCoordinator} from "./PhoneVideoCoordinator"
110
111
  import {runSentenceTtsPipeline} from "./SentenceTtsPipeline"
111
- import {summarizeTranscriptionRoutes, transcriptionDeliveryRoute} from "./TranscriptionRouting"
112
+ import {
113
+ summarizeTranscriptionRoutes,
114
+ transcriptionDeliveryRoute,
115
+ type TranscriptionDeliveryRoute,
116
+ type TranscriptionSubscriberRoutes,
117
+ } from "./TranscriptionRouting"
112
118
  import {prepareTtsSentences} from "./TtsTextSanitizer"
113
119
  import {handleWifiAdbRequest} from "./WifiAdbRequest"
114
120
  import {cloudClientService, setBlobNamespaceMigrator} from "./CloudClientService"
@@ -171,6 +177,8 @@ interface ConnectedMiniapp {
171
177
  forceLocalTranscriptionStreams: Set<string>
172
178
  /** Transcription/translation streams with at least one listener using the default/cloud route. */
173
179
  cloudTranscriptionStreams: Set<string>
180
+ /** Transcription streams whose default-route listeners all refuse on-device fallback text. */
181
+ noFallbackTranscriptionStreams: Set<string>
174
182
  sendMessage: (raw: string) => void
175
183
  lastPongAt: number
176
184
  unansweredPingRounds: number
@@ -213,6 +221,9 @@ interface SpeechRun {
213
221
 
214
222
  type TranscriptionEventSource = "cloud" | "local"
215
223
 
224
+ /** The cloud snapshot miniapps see: the phone's connection state plus plan-quota blocks. */
225
+ type MiniappCloudStatus = CloudClientStatusSnapshot & {quotaBlocked: QuotaStatusPayload["feature"][]}
226
+
216
227
  /** Streams that carry speech text and so have a cloud route and an on-device route. */
217
228
  const isSpeechTextStream = (stream: string): boolean =>
218
229
  stream.startsWith("transcription:") || stream.startsWith("translation:")
@@ -1048,6 +1059,7 @@ class LocalMiniappRuntime {
1048
1059
  subscriptions: new Set(),
1049
1060
  forceLocalTranscriptionStreams: new Set(),
1050
1061
  cloudTranscriptionStreams: new Set(),
1062
+ noFallbackTranscriptionStreams: new Set(),
1051
1063
  sendMessage: sendFn,
1052
1064
  lastPongAt: Date.now(),
1053
1065
  unansweredPingRounds: 0,
@@ -2101,7 +2113,10 @@ class LocalMiniappRuntime {
2101
2113
  if (!app) return
2102
2114
 
2103
2115
  const rawStreams = (payload.subscriptions ?? payload.streams) as
2104
- | (string | {stream: string; rate?: string; forceLocal?: boolean; includeCloud?: boolean})[]
2116
+ | (
2117
+ | string
2118
+ | {stream: string; rate?: string; forceLocal?: boolean; includeCloud?: boolean; localFallback?: boolean}
2119
+ )[]
2105
2120
  | undefined
2106
2121
  // Normalize: objects like {stream: "location_stream", rate: "realtime"} → extract stream name
2107
2122
  // "location_stream" is the rate-bearing alias for "location_update".
@@ -2110,6 +2125,7 @@ class LocalMiniappRuntime {
2110
2125
  let requestedLocationRate: string | null = null
2111
2126
  const forceLocalTranscriptionStreams = new Set<string>()
2112
2127
  const cloudTranscriptionStreams = new Set<string>()
2128
+ const noFallbackTranscriptionStreams = new Set<string>()
2113
2129
  const streams = rawStreams?.map((s) => {
2114
2130
  if (typeof s === "object" && s !== null) {
2115
2131
  if (s.stream === "location_stream") {
@@ -2123,6 +2139,7 @@ class LocalMiniappRuntime {
2123
2139
  }
2124
2140
  if (isSpeechTextStream(s.stream) && (s.forceLocal !== true || s.includeCloud === true)) {
2125
2141
  cloudTranscriptionStreams.add(s.stream)
2142
+ if (s.localFallback === false) noFallbackTranscriptionStreams.add(s.stream)
2126
2143
  }
2127
2144
  return s.stream
2128
2145
  }
@@ -2190,6 +2207,9 @@ class LocalMiniappRuntime {
2190
2207
  app.cloudTranscriptionStreams = new Set(
2191
2208
  [...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
2192
2209
  )
2210
+ app.noFallbackTranscriptionStreams = new Set(
2211
+ [...noFallbackTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
2212
+ )
2193
2213
  this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
2194
2214
  // The overlay app presents phone notifications on the glasses while it
2195
2215
  // listens to them; recorded for every dashboard so the role can move.
@@ -4863,6 +4883,7 @@ class LocalMiniappRuntime {
4863
4883
  const previousSubscriptions = app.subscriptions
4864
4884
  app.subscriptions = new Set(streams)
4865
4885
  app.forceLocalTranscriptionStreams.clear()
4886
+ app.noFallbackTranscriptionStreams.clear()
4866
4887
  app.cloudTranscriptionStreams = new Set(streams.filter(isSpeechTextStream))
4867
4888
  this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
4868
4889
 
@@ -4929,6 +4950,7 @@ class LocalMiniappRuntime {
4929
4950
  let transcriptionLang: string | null = null
4930
4951
  let forceLocalTranscriptionLang: string | null = null
4931
4952
  let hasForceLocalTranscription = false
4953
+ let hasFallbackTranscription = false
4932
4954
  const localTranslations: LocalTranslationSubscription[] = []
4933
4955
  for (const [stream, subscribers] of this.activeStreamSubscribers()) {
4934
4956
  if (subscribers.size === 0) continue
@@ -4960,6 +4982,7 @@ class LocalMiniappRuntime {
4960
4982
  hasForceLocalTranscription = true
4961
4983
  if (forceLocalTranscriptionLang === null) forceLocalTranscriptionLang = language
4962
4984
  }
4985
+ if (routes.hasFallbackSubscriber) hasFallbackTranscription = true
4963
4986
  if (routes.hasCloudSubscriber) cloudStreams.add(stream)
4964
4987
  }
4965
4988
  }
@@ -4974,6 +4997,7 @@ class LocalMiniappRuntime {
4974
4997
  transcriptionLang !== null || localSourceLang !== null,
4975
4998
  forceLocalTranscriptionLang ?? transcriptionLang ?? localSourceLang,
4976
4999
  hasForceLocalTranscription || hasForceLocalTranslation,
5000
+ hasFallbackTranscription || localSourceLang !== null,
4977
5001
  )
4978
5002
 
4979
5003
  // Mirror the same set as typed AudioSubscription[]. When Soniox-direct is
@@ -5134,12 +5158,15 @@ class LocalMiniappRuntime {
5134
5158
  if (q.feature !== "transcription") return
5135
5159
  if (!q.blocked) {
5136
5160
  this.lastSttQuotaNoticeAt = 0
5137
- void localSttFallbackCoordinator.onSttQuotaBlockedChange(false)
5161
+ void localSttFallbackCoordinator.onSttQuotaBlockedChange(false).then(() => this.broadcastCloudStatus())
5138
5162
  this.onQuotaRestored?.("transcription")
5139
5163
  return
5140
5164
  }
5141
5165
  console.warn(`${LOG_TAG}: cloud transcription blocked by plan quota`)
5142
5166
  void localSttFallbackCoordinator.onSttQuotaBlockedChange(true).then(() => {
5167
+ // Miniapps learn of the block through the status snapshot; the lens
5168
+ // notice below is for the wearer.
5169
+ this.broadcastCloudStatus()
5143
5170
  const localSttActive = localSttFallbackCoordinator.isActive()
5144
5171
  const now = Date.now()
5145
5172
  if (now - this.lastSttQuotaNoticeAt >= STT_QUOTA_NOTICE_DEBOUNCE_MS) {
@@ -5383,7 +5410,7 @@ class LocalMiniappRuntime {
5383
5410
  )
5384
5411
  }
5385
5412
 
5386
- private currentCloudStatus(): CloudClientStatusSnapshot {
5413
+ private currentCloudStatus(): MiniappCloudStatus {
5387
5414
  const base = cloudClientService.getStatus()
5388
5415
  const fallbackActive =
5389
5416
  (useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive) as boolean | undefined) ===
@@ -5394,6 +5421,7 @@ class LocalMiniappRuntime {
5394
5421
  return {
5395
5422
  status: base.status,
5396
5423
  audioTransport: fallbackActive || sonioxDegraded ? "offline" : base.audioTransport,
5424
+ quotaBlocked: localSttFallbackCoordinator.isSttQuotaBlocked() ? ["transcription"] : [],
5397
5425
  }
5398
5426
  }
5399
5427
 
@@ -5543,7 +5571,7 @@ class LocalMiniappRuntime {
5543
5571
 
5544
5572
  for (const packageName of matchedSubs) {
5545
5573
  if (only !== undefined && packageName !== only) continue
5546
- let transcriptionRoute: "default" | "forceLocal" | "all" | undefined
5574
+ let transcriptionRoute: TranscriptionDeliveryRoute | undefined
5547
5575
  if (normalizedStream.startsWith("transcription:") && transcriptionSource) {
5548
5576
  const routes = this.appTranscriptionRoutesForEvent(packageName, normalizedStream)
5549
5577
  transcriptionRoute =
@@ -5638,17 +5666,12 @@ class LocalMiniappRuntime {
5638
5666
  })
5639
5667
  }
5640
5668
 
5641
- private appTranscriptionRoutesForSubscription(
5642
- packageName: string,
5643
- stream: string,
5644
- ): {
5645
- cloud: boolean
5646
- forceLocal: boolean
5647
- } {
5669
+ private appTranscriptionRoutesForSubscription(packageName: string, stream: string): TranscriptionSubscriberRoutes {
5648
5670
  const app = this.connectedApps.get(packageName)
5649
5671
  return {
5650
5672
  cloud: app?.cloudTranscriptionStreams.has(stream) === true,
5651
5673
  forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true,
5674
+ localFallback: app?.noFallbackTranscriptionStreams.has(stream) !== true,
5652
5675
  }
5653
5676
  }
5654
5677
 
@@ -5666,21 +5689,20 @@ class LocalMiniappRuntime {
5666
5689
  }
5667
5690
  }
5668
5691
 
5669
- private appTranscriptionRoutesForEvent(
5670
- packageName: string,
5671
- stream: string,
5672
- ): {
5673
- cloud: boolean
5674
- forceLocal: boolean
5675
- } {
5692
+ private appTranscriptionRoutesForEvent(packageName: string, stream: string): TranscriptionSubscriberRoutes {
5676
5693
  const app = this.connectedApps.get(packageName)
5677
5694
  const autoStream = `${MiniappStreamType.TRANSCRIPTION}:auto`
5695
+ const cloudExact = app?.cloudTranscriptionStreams.has(stream) === true
5696
+ const cloudAuto = app?.cloudTranscriptionStreams.has(autoStream) === true
5678
5697
  return {
5679
- cloud:
5680
- app?.cloudTranscriptionStreams.has(stream) === true || app?.cloudTranscriptionStreams.has(autoStream) === true,
5698
+ cloud: cloudExact || cloudAuto,
5681
5699
  forceLocal:
5682
5700
  app?.forceLocalTranscriptionStreams.has(stream) === true ||
5683
5701
  app?.forceLocalTranscriptionStreams.has(autoStream) === true,
5702
+ // Fallback stays accepted while any matching default subscription takes it.
5703
+ localFallback:
5704
+ (cloudExact && app?.noFallbackTranscriptionStreams.has(stream) !== true) ||
5705
+ (cloudAuto && app?.noFallbackTranscriptionStreams.has(autoStream) !== true),
5684
5706
  }
5685
5707
  }
5686
5708
 
@@ -8,7 +8,8 @@ import sttModelManager from "./STTModelManager"
8
8
  * Decides whether on-device Sherpa STT should produce transcripts for local
9
9
  * miniapps, instead of (or in addition to) cloud transcription. Local STT is
10
10
  * active when the cloud is unavailable (disconnected or refusing STT for plan
11
- * quota) or any subscription requires `forceLocal`.
11
+ * quota) and some subscriber accepts fallback text, or any subscription
12
+ * requires `forceLocal`.
12
13
  *
13
14
  * Activation flips `localSttFallbackActive` in the settings store, which
14
15
  * the bluetooth-sdk native side reads to decide whether to feed PCM into
@@ -37,6 +38,8 @@ class LocalSttFallbackCoordinator {
37
38
 
38
39
  private hasTranscriptionSubscription = false
39
40
  private hasForceLocalSubscription = false
41
+ /** Some default-route subscriber takes on-device text while the primary is down. */
42
+ private hasFallbackSubscription = false
40
43
  private activeLanguage: string | null = null
41
44
  /**
42
45
  * Default to "cloud is up" so we never accidentally activate local STT before
@@ -123,10 +126,18 @@ class LocalSttFallbackCoordinator {
123
126
  return this.localActive ? this.currentReason() : null
124
127
  }
125
128
 
126
- onSubscriptionChange(hasTranscription: boolean, language: string | null, hasForceLocal = false): void {
127
- this.log(`onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal})`)
129
+ onSubscriptionChange(
130
+ hasTranscription: boolean,
131
+ language: string | null,
132
+ hasForceLocal = false,
133
+ acceptsFallback = true,
134
+ ): void {
135
+ this.log(
136
+ `onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal}, fallback=${acceptsFallback})`,
137
+ )
128
138
  this.hasTranscriptionSubscription = hasTranscription
129
139
  this.hasForceLocalSubscription = hasTranscription && hasForceLocal
140
+ this.hasFallbackSubscription = hasTranscription && acceptsFallback
130
141
  this.activeLanguage = hasTranscription ? language : null
131
142
  void this.reconcile()
132
143
  }
@@ -256,7 +267,10 @@ class LocalSttFallbackCoordinator {
256
267
  }
257
268
 
258
269
  private shouldUseLocalStt(): boolean {
259
- return this.hasTranscriptionSubscription && (this.hasForceLocalSubscription || !this.primaryUp())
270
+ return (
271
+ this.hasTranscriptionSubscription &&
272
+ (this.hasForceLocalSubscription || (this.hasFallbackSubscription && !this.primaryUp()))
273
+ )
260
274
  }
261
275
 
262
276
  private log(msg: string): void {
@@ -1,14 +1,19 @@
1
1
  export interface TranscriptionRouteSummary {
2
2
  hasCloudSubscriber: boolean
3
3
  hasForceLocalSubscriber: boolean
4
+ /** A default-route subscriber that takes on-device text while the primary is down. */
5
+ hasFallbackSubscriber: boolean
4
6
  }
5
7
 
6
8
  export interface TranscriptionSubscriberRoutes {
7
9
  cloud: boolean
8
10
  forceLocal: boolean
11
+ /** Default-route fallback acceptance; absent means accepted. */
12
+ localFallback?: boolean
9
13
  }
10
14
 
11
- export type TranscriptionDeliveryRoute = "default" | "forceLocal" | "all"
15
+ /** "fallback" is on-device text serving the default route because the primary is down. */
16
+ export type TranscriptionDeliveryRoute = "default" | "forceLocal" | "fallback" | "all"
12
17
 
13
18
  export function summarizeTranscriptionRoutes<T>(
14
19
  subscribers: Iterable<T>,
@@ -16,12 +21,14 @@ export function summarizeTranscriptionRoutes<T>(
16
21
  ): TranscriptionRouteSummary {
17
22
  let hasCloudSubscriber = false
18
23
  let hasForceLocalSubscriber = false
24
+ let hasFallbackSubscriber = false
19
25
  for (const subscriber of subscribers) {
20
26
  const routes = routesFor(subscriber)
21
27
  if (routes.cloud) hasCloudSubscriber = true
22
28
  if (routes.forceLocal) hasForceLocalSubscriber = true
29
+ if (routes.cloud && routes.localFallback !== false) hasFallbackSubscriber = true
23
30
  }
24
- return {hasCloudSubscriber, hasForceLocalSubscriber}
31
+ return {hasCloudSubscriber, hasForceLocalSubscriber, hasFallbackSubscriber}
25
32
  }
26
33
 
27
34
  /**
@@ -41,7 +48,8 @@ export function transcriptionDeliveryRoute(
41
48
  ): TranscriptionDeliveryRoute | null {
42
49
  if (source === "cloud") return routes.cloud ? "default" : null
43
50
  if (primaryUp) return routes.forceLocal ? "forceLocal" : null
44
- if (routes.cloud && routes.forceLocal) return "all"
51
+ const fallback = routes.cloud && routes.localFallback !== false
52
+ if (fallback && routes.forceLocal) return "all"
45
53
  if (routes.forceLocal) return "forceLocal"
46
- return routes.cloud ? "default" : null
54
+ return fallback ? "fallback" : null
47
55
  }