@glassly/engine 0.3.2-beta.572 → 0.3.2-beta.597

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 (146) hide show
  1. package/README.md +1 -1
  2. package/build/engine.d.ts +15 -1
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -3
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/dev.d.ts +2 -2
  7. package/build/facades/dev.d.ts.map +1 -1
  8. package/build/facades/dev.js +2 -2
  9. package/build/facades/dev.js.map +1 -1
  10. package/build/facades/glasses.d.ts +20 -0
  11. package/build/facades/glasses.d.ts.map +1 -1
  12. package/build/facades/glasses.js +21 -0
  13. package/build/facades/glasses.js.map +1 -1
  14. package/build/facades/pairing.js +1 -1
  15. package/build/facades/pairing.js.map +1 -1
  16. package/build/facades/reports.js +2 -2
  17. package/build/facades/reports.js.map +1 -1
  18. package/build/facades/session.d.ts +1 -1
  19. package/build/facades/session.js +2 -2
  20. package/build/facades/session.js.map +1 -1
  21. package/build/generated/releaseMetadata.js +3 -3
  22. package/build/generated/releaseMetadata.js.map +1 -1
  23. package/build/index.d.ts +3 -1
  24. package/build/index.d.ts.map +1 -1
  25. package/build/index.js +3 -1
  26. package/build/index.js.map +1 -1
  27. package/build/runtime/bootstrap.d.ts +2 -2
  28. package/build/runtime/bootstrap.d.ts.map +1 -1
  29. package/build/runtime/bootstrap.js.map +1 -1
  30. package/build/services/CloudAudioSubscriptionSync.d.ts +2 -2
  31. package/build/services/CloudAudioSubscriptionSync.js +2 -2
  32. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  33. package/build/services/CloudClientService.d.ts +2 -2
  34. package/build/services/CloudClientService.d.ts.map +1 -1
  35. package/build/services/CloudClientService.js +4 -4
  36. package/build/services/CloudClientService.js.map +1 -1
  37. package/build/services/CloudTranscriptE2EMetrics.d.ts +2 -2
  38. package/build/services/CloudTranscriptE2EMetrics.d.ts.map +1 -1
  39. package/build/services/CloudTranscriptE2EMetrics.js +3 -3
  40. package/build/services/CloudTranscriptE2EMetrics.js.map +1 -1
  41. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  42. package/build/services/DeviceEventRouter.js +5 -0
  43. package/build/services/DeviceEventRouter.js.map +1 -1
  44. package/build/services/GestureDotsService.d.ts +9 -9
  45. package/build/services/GestureDotsService.d.ts.map +1 -1
  46. package/build/services/GestureDotsService.js +75 -50
  47. package/build/services/GestureDotsService.js.map +1 -1
  48. package/build/services/GlassesTouchInput.d.ts +1 -1
  49. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  50. package/build/services/GlassesTouchInput.js +7 -4
  51. package/build/services/GlassesTouchInput.js.map +1 -1
  52. package/build/services/LocalMiniappRuntime.d.ts +1 -1
  53. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  54. package/build/services/LocalMiniappRuntime.js +26 -7
  55. package/build/services/LocalMiniappRuntime.js.map +1 -1
  56. package/build/services/LocalSttFallbackCoordinator.d.ts +5 -2
  57. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  58. package/build/services/LocalSttFallbackCoordinator.js +7 -3
  59. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  60. package/build/services/PhonePhotoCoordinator.d.ts +1 -1
  61. package/build/services/PhonePhotoCoordinator.js +4 -4
  62. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  63. package/build/services/PhoneStreamCoordinator.d.ts +1 -1
  64. package/build/services/PhoneStreamCoordinator.js +1 -1
  65. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  66. package/build/services/SupportProfileSync.d.ts +1 -1
  67. package/build/services/SupportProfileSync.js +2 -2
  68. package/build/services/SupportProfileSync.js.map +1 -1
  69. package/build/services/TranscriptionRouting.d.ts +6 -1
  70. package/build/services/TranscriptionRouting.d.ts.map +1 -1
  71. package/build/services/TranscriptionRouting.js +7 -3
  72. package/build/services/TranscriptionRouting.js.map +1 -1
  73. package/build/services/cloudStreamApi.d.ts +2 -2
  74. package/build/services/cloudStreamApi.js +2 -2
  75. package/build/services/cloudStreamApi.js.map +1 -1
  76. package/build/services/gestureDotGlyphs.d.ts +1 -1
  77. package/build/services/gestureDotGlyphs.d.ts.map +1 -1
  78. package/build/services/gestureDotGlyphs.js +5 -0
  79. package/build/services/gestureDotGlyphs.js.map +1 -1
  80. package/build/services/neuralBand/MetaAuth.d.ts +31 -0
  81. package/build/services/neuralBand/MetaAuth.d.ts.map +1 -0
  82. package/build/services/neuralBand/MetaAuth.js +161 -0
  83. package/build/services/neuralBand/MetaAuth.js.map +1 -0
  84. package/build/services/neuralBand/NeuralBandInput.d.ts +50 -0
  85. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -0
  86. package/build/services/neuralBand/NeuralBandInput.js +114 -0
  87. package/build/services/neuralBand/NeuralBandInput.js.map +1 -0
  88. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts +3 -0
  89. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -0
  90. package/build/services/neuralBand/NeuralBandSettingsSync.js +52 -0
  91. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -0
  92. package/build/services/neuralBand/sha256.d.ts +7 -0
  93. package/build/services/neuralBand/sha256.d.ts.map +1 -0
  94. package/build/services/neuralBand/sha256.js +68 -0
  95. package/build/services/neuralBand/sha256.js.map +1 -0
  96. package/build/services/soniox/SonioxRealtimeClient.d.ts +2 -2
  97. package/build/services/soniox/SonioxRealtimeClient.js +2 -2
  98. package/build/services/soniox/SonioxRealtimeClient.js.map +1 -1
  99. package/build/services/soniox/SonioxTokenAssembler.d.ts +1 -1
  100. package/build/services/soniox/SonioxTokenAssembler.js +1 -1
  101. package/build/services/soniox/SonioxTokenAssembler.js.map +1 -1
  102. package/build/stores/cloudClientStatus.js +1 -1
  103. package/build/stores/cloudClientStatus.js.map +1 -1
  104. package/build/stores/settings.d.ts.map +1 -1
  105. package/build/stores/settings.js +25 -7
  106. package/build/stores/settings.js.map +1 -1
  107. package/build/types/enums.d.ts +2 -1
  108. package/build/types/enums.d.ts.map +1 -1
  109. package/build/types/enums.js +1 -0
  110. package/build/types/enums.js.map +1 -1
  111. package/build/utils/cloudClient/cloudSecureStore.js +2 -2
  112. package/build/utils/cloudClient/cloudSecureStore.js.map +1 -1
  113. package/package.json +7 -7
  114. package/src/engine.ts +7 -3
  115. package/src/facades/dev.ts +2 -2
  116. package/src/facades/glasses.ts +27 -0
  117. package/src/facades/pairing.ts +1 -1
  118. package/src/facades/reports.ts +2 -2
  119. package/src/facades/session.ts +2 -2
  120. package/src/generated/releaseMetadata.ts +3 -3
  121. package/src/index.ts +15 -1
  122. package/src/runtime/bootstrap.ts +2 -2
  123. package/src/services/CloudAudioSubscriptionSync.ts +2 -2
  124. package/src/services/CloudClientService.ts +4 -4
  125. package/src/services/CloudTranscriptE2EMetrics.ts +3 -3
  126. package/src/services/DeviceEventRouter.ts +5 -0
  127. package/src/services/GestureDotsService.ts +72 -49
  128. package/src/services/GlassesTouchInput.ts +13 -5
  129. package/src/services/LocalMiniappRuntime.ts +46 -24
  130. package/src/services/LocalSttFallbackCoordinator.ts +18 -4
  131. package/src/services/PhonePhotoCoordinator.ts +4 -4
  132. package/src/services/PhoneStreamCoordinator.ts +1 -1
  133. package/src/services/SupportProfileSync.ts +2 -2
  134. package/src/services/TranscriptionRouting.ts +12 -4
  135. package/src/services/cloudStreamApi.ts +3 -3
  136. package/src/services/gestureDotGlyphs.ts +6 -1
  137. package/src/services/neuralBand/MetaAuth.ts +192 -0
  138. package/src/services/neuralBand/NeuralBandInput.ts +148 -0
  139. package/src/services/neuralBand/NeuralBandSettingsSync.ts +63 -0
  140. package/src/services/neuralBand/sha256.ts +70 -0
  141. package/src/services/soniox/SonioxRealtimeClient.ts +2 -2
  142. package/src/services/soniox/SonioxTokenAssembler.ts +1 -1
  143. package/src/stores/cloudClientStatus.ts +1 -1
  144. package/src/stores/settings.ts +25 -7
  145. package/src/types/enums.ts +1 -0
  146. package/src/utils/cloudClient/cloudSecureStore.ts +2 -2
@@ -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:")
@@ -286,7 +297,7 @@ const MINIAPP_AUTH_REFRESH_MIN_DELAY_MS = 5_000
286
297
  // (e.g. the cloud client had not finished its first-boot Core token exchange
287
298
  // when the miniapp connected). Exponential from base, capped, so a transient
288
299
  // not-yet-connected client self-heals within seconds instead of needing a full
289
- // manual Cloud V2 reconnect.
300
+ // manual Cloud reconnect.
290
301
  const MINIAPP_AUTH_RETRY_BASE_MS = 1_000
291
302
  const MINIAPP_AUTH_RETRY_MAX_MS = 15_000
292
303
  const FOREGROUND_LIVENESS_PROBE_TIMEOUT_MS = 2_500
@@ -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,
@@ -1826,7 +1838,7 @@ class LocalMiniappRuntime {
1826
1838
  // right after app start. CONNECT_ACK already went out without auth; keep
1827
1839
  // trying to mint (and re-drive on cloud-connect) so the app authenticates
1828
1840
  // to its backend on its own, instead of staying dead until a full manual
1829
- // Cloud V2 reconnect re-runs the whole handshake.
1841
+ // Cloud reconnect re-runs the whole handshake.
1830
1842
  this.deliverInitialMiniappAuth(packageName, authPromise, 0)
1831
1843
  }
1832
1844
  this.sendCloudStatusToMiniapp(packageName)
@@ -1988,7 +2000,7 @@ class LocalMiniappRuntime {
1988
2000
  * When the cloud client (re)connects, immediately re-attempt the initial mint
1989
2001
  * for any handshook app still missing its token, instead of waiting out the
1990
2002
  * backoff. This is what turns "scan a dev app before the cloud is ready" from
1991
- * "dead until you manually reconnect Cloud V2" into "authenticates on its own
2003
+ * "dead until you manually reconnect Cloud" into "authenticates on its own
1992
2004
  * the instant the connection comes up".
1993
2005
  */
1994
2006
  private redriveMiniappAuthDelivery(): void {
@@ -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 {
@@ -5,7 +5,7 @@
5
5
  * miniapp → SDK → LocalMiniappRuntime → photo runtime hook
6
6
  * → coordinator.takePhoto(packageName, opts)
7
7
  * ├── (precheck) glasses connected + hasCamera
8
- * ├── cloudClientService.startManagedPhoto → {requestId, uploadUrl, readUrl} (cloud-v2 runtime presign)
8
+ * ├── cloudClientService.startManagedPhoto → {requestId, uploadUrl, readUrl} (cloud runtime presign)
9
9
  * ├── BluetoothSdk.requestPhoto(requestId, size, uploadUrl, compress, sound)
10
10
  * └── race:
11
11
  * - cloudClientService.awaitManagedPhotoReady(requestId) resolves on photo.ready push
@@ -179,8 +179,8 @@ export class PhonePhotoCoordinator {
179
179
  // high-capacity tier and let the glasses ignore the public size when mode=text.
180
180
  const captureSize = opts.size ?? "medium"
181
181
 
182
- // 1) Presign via the cloud-v2 managed-photo service. Local miniapps use
183
- // ONLY the cloud-v2 path: the runtime presigns upload+read URLs and the
182
+ // 1) Presign via the cloud managed-photo service. Local miniapps use
183
+ // ONLY the cloud path: the runtime presigns upload+read URLs and the
184
184
  // phone (as the device controller) delivers the bytes; the legacy
185
185
  // backend_url mint is gone from this flow.
186
186
  let requestId: string
@@ -419,7 +419,7 @@ export class PhonePhotoCoordinator {
419
419
  // Abort the in-flight long-poll first so we don't double-resolve.
420
420
  entry.abort.abort()
421
421
  entry.reject(new PhotoError(errorCode || "GLASSES_ERROR", errorMessage || "Glasses error", "capture", "ble"))
422
- // cloud-v2 pending photo requests TTL out on their own; nothing to free.
422
+ // cloud pending photo requests TTL out on their own; nothing to free.
423
423
  }
424
424
 
425
425
  private toPhotoError(err: unknown, fallbackCode: string, stage?: PhotoStage, transport?: PhotoTransport): PhotoError {
@@ -4,7 +4,7 @@
4
4
  * Architecture:
5
5
  * miniapp → SDK → LocalMiniappRuntime → coordinator
6
6
  * coordinator → BluetoothSdk (BLE → glasses publisher)
7
- * coordinator ↔ cloudStreamApi (managed only, cloud-v2 runtime provisioning)
7
+ * coordinator ↔ cloudStreamApi (managed only, cloud runtime provisioning)
8
8
  * coordinator ↔ StreamLifecycleController (keep-alive heartbeat)
9
9
  * coordinator → status listeners → routed back to miniapp(s)
10
10
  *
@@ -26,7 +26,7 @@ let lastFailure: {code: string; stage: string} | null = null
26
26
  let lastConnectionState: string | null = null
27
27
 
28
28
  /**
29
- * Keep Cloud V2's canonical support snapshot current without creating an event
29
+ * Keep Cloud's canonical support snapshot current without creating an event
30
30
  * stream from noisy store updates. Only the explicitly selected fields below
31
31
  * leave the phone; Wi-Fi, Bluetooth addresses, location and diagnostics do not.
32
32
  */
@@ -110,7 +110,7 @@ async function sendCurrentSnapshot(force: boolean, generation = syncGeneration):
110
110
  consecutiveSendFailures += 1
111
111
  retryDelayMs = retryDelayForSendFailure(consecutiveSendFailures)
112
112
  console.warn(
113
- `supportProfile: Cloud V2 update failed (retry in ${Math.round(retryDelayMs / 1_000)}s):`,
113
+ `supportProfile: Cloud update failed (retry in ${Math.round(retryDelayMs / 1_000)}s):`,
114
114
  error instanceof Error ? error.message : error,
115
115
  )
116
116
  } finally {
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Managed-stream provisioning over the cloud-v2 runtime (cloud-client REST):
2
+ * Managed-stream provisioning over the cloud runtime (cloud-client REST):
3
3
  *
4
4
  * POST /api/camera/stream -> provision (Cloudflare live input)
5
5
  * GET /api/camera/stream/:id -> provider's ingest status
@@ -61,7 +61,7 @@ export class StreamApiError extends Error {
61
61
  }
62
62
  }
63
63
 
64
- /** The cloud-v2 runtime's ManagedStream wire shape (open records, see protocol). */
64
+ /** The cloud runtime's ManagedStream wire shape (open records, see protocol). */
65
65
  interface ManagedStreamWire {
66
66
  streamId: string
67
67
  ingest: {
@@ -79,7 +79,7 @@ interface ManagedStreamWire {
79
79
  }
80
80
 
81
81
  /**
82
- * Provision a managed stream on the cloud-v2 runtime. Returns ready-to-publish
82
+ * Provision a managed stream on the cloud runtime. Returns ready-to-publish
83
83
  * ingest URLs (credentials embedded) plus the playback URLs.
84
84
  */
85
85
  export async function provisionManagedStream(
@@ -4,12 +4,17 @@
4
4
  */
5
5
  export const GESTURE_DOT_GLYPH_SIZE = 24
6
6
 
7
- export type GestureDotGlyph = "dot" | "up" | "down" | "hold" | "blank"
7
+ export type GestureDotGlyph = "dot" | "double" | "ring" | "doubleRing" | "up" | "down" | "left" | "right" | "hold" | "blank"
8
8
 
9
9
  export const GESTURE_DOT_GLYPHS: Record<GestureDotGlyph, string> = {
10
10
  dot: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALklEQVR4nM2QsQ0AAAjC/P/pmrhqZ+xIF6DqBQyWbwO3gduAmKjQuj7QL/ETEzQmMptl7Q9NBAAAAABJRU5ErkJggg==",
11
+ double: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAG0lEQVR4nGNgGD7gPxjgYkN4yOyBlcDp3GEBAB3sd4lPaQxDAAAAAElFTkSuQmCC",
12
+ ring: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAANElEQVR4nGNgGBTgPxjgEseUgQphyCD4qDL/kSX+Y9WApoWqEjgsx+lc3B7EHSS4A3EgAAC+cEu1JTNREQAAAABJRU5ErkJggg==",
13
+ doubleRing: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAIklEQVR4nGNgGD7gPxjgYkN4yGwGmBA6SV0JHJbjdO6wAADBMlep8zl7TAAAAABJRU5ErkJggg==",
11
14
  up: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAMElEQVR4nGNgoCv4////f1zi2GT+/4ciTHE4RhdHIlDFUUhkcTQKQx9WBw8PCVoBAIcbOcf3xeeBAAAAAElFTkSuQmCC",
12
15
  down: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALUlEQVR4nGNgoC/4/3+4SkBFUCkkNjKJIoMg0GRgGEMGgrC4CQSwuxanR2gEAO1/Ocfgp8uvAAAAAElFTkSuQmCC",
16
+ left: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAI0lEQVR4nGNgoCv4j0sch8RAiTP8RwPESOAwajDI4JKgGQAAKO05x7DqJxIAAAAASUVORK5CYII=",
17
+ right: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAIElEQVR4nGNgoDf4j1NiMMn8RwOEJYizZKDEcQc7bQAAS7w5x+ro2lsAAAAASUVORK5CYII=",
13
18
  hold: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAR0lEQVR4nGNgIAj+gwF2UUy5//+xyyDzcLGRedjMhTEwXAKl0Z0Lof7DKbgMhBpQCZzOxedBHEGCMxAJBTXhKGRAA9hFMQEANr7jHQU9fO0AAAAASUVORK5CYII=",
14
19
  blank: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAD0lEQVR4nGNgGAWjgPoAAAJYAAGgxYwDAAAAAElFTkSuQmCC",
15
20
  }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The Meta account sign-in a Neural Band claim needs, ported from callbacked/kinesis
3
+ * (MIT, Copyright (c) 2026 callbacked): Sources/Kinesis/MetaAuth.swift.
4
+ *
5
+ * The chain: an anonymous tokens query, a WebView sign-in on auth.meta.com whose
6
+ * fb-viewapp://…frl_login redirect carries a token and a blob, a blob decrypt, and
7
+ * the ar-genai exchange that yields the user session. The session then goes to the
8
+ * native Bluetooth SDK (keychain / keystore), which runs the ownership exchanges;
9
+ * nothing here persists it.
10
+ */
11
+ import {sha256Hex} from "./sha256"
12
+
13
+ // Meta's own client identifiers, the same in every copy of Meta's apps. They name the
14
+ // app that asks, not a person. They are not secrets, and no account or session is in them.
15
+ const FRL_CLIENT = "FRL|388177446008673|083800dd7efbbd42eab18c9886d79c18"
16
+ const AR_CLIENT = "AR|306760944872162|a919421a55a8ea18080ab2f10f57f1be"
17
+ const TOKENS_QUERY_URL = "https://meta.graph.meta.com/webview_tokens_query"
18
+ const BLOBS_DECRYPT_URL = "https://meta.graph.meta.com/webview_blobs_decrypt"
19
+ const LOGIN_URL = "https://ar-genai.graph.meta.com/login"
20
+ const AUTH_ENTRY_BASE =
21
+ "https://auth.meta.com/?native_app_id=388177446008673&source_app_id=388177446008673&native_sso_etoken="
22
+
23
+ export class MetaAuthError extends Error {}
24
+
25
+ export interface MetaSsoTokens {
26
+ nativeSsoToken: string
27
+ etoken: string
28
+ }
29
+
30
+ export interface MetaSession {
31
+ accessToken: string
32
+ userId: string
33
+ }
34
+
35
+ export function authEntryUrl(tokens: MetaSsoTokens): string {
36
+ return AUTH_ENTRY_BASE + encodeURIComponent(tokens.etoken)
37
+ }
38
+
39
+ /** The redirect that ends the sign-in: a custom scheme, or the frl_login marker on https. */
40
+ export function isLoginCallback(url: string): boolean {
41
+ return url.toLowerCase().startsWith("fb-viewapp:") || url.includes("frl_login")
42
+ }
43
+
44
+ export function callbackQuery(url: string): {token?: string; blob?: string} {
45
+ const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : ""
46
+ const out: {token?: string; blob?: string} = {}
47
+ for (const pair of query.split("&")) {
48
+ const [key, value = ""] = pair.split("=")
49
+ if (key === "token") out.token = decodeURIComponent(value)
50
+ if (key === "blob") out.blob = decodeURIComponent(value)
51
+ }
52
+ return out
53
+ }
54
+
55
+ /** The first 16 hex digits of the request token's SHA-256, which the callback must present. */
56
+ export function expectedCallbackToken(nativeSsoToken: string): string {
57
+ return sha256Hex(nativeSsoToken).slice(0, 16)
58
+ }
59
+
60
+ export function callbackMatches(token: string | undefined, nativeSsoToken: string): boolean {
61
+ if (!token || token.length !== 16) return false
62
+ const expected = expectedCallbackToken(nativeSsoToken)
63
+ let difference = 0
64
+ for (let i = 0; i < 16; i++) difference |= token.charCodeAt(i) ^ expected.charCodeAt(i)
65
+ return difference === 0
66
+ }
67
+
68
+ export function makeLsd(): string {
69
+ let digits = ""
70
+ for (let i = 0; i < 6; i++) digits += Math.floor(Math.random() * 10)
71
+ return "S0." + digits
72
+ }
73
+
74
+ export function jazoest(lsd: string): string {
75
+ let sum = 0
76
+ for (const char of lsd) sum += char.codePointAt(0) ?? 0
77
+ return "2" + sum
78
+ }
79
+
80
+ export function multipartBody(fields: [string, string][], boundary: string): string {
81
+ let body = ""
82
+ for (const [name, value] of fields) {
83
+ body += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`
84
+ }
85
+ return body + `--${boundary}--\r\n`
86
+ }
87
+
88
+ export function urlForm(fields: [string, string][]): string {
89
+ return fields.map(([name, value]) => `${name}=${encodeURIComponent(value)}`).join("&")
90
+ }
91
+
92
+ type Json = Record<string, unknown>
93
+
94
+ async function post(url: string, headers: Record<string, string>, body: string): Promise<Json> {
95
+ const response = await fetch(url, {method: "POST", headers, body})
96
+ let json: Json | null = null
97
+ try {
98
+ json = (await response.json()) as Json
99
+ } catch {
100
+ json = null
101
+ }
102
+ if (response.status !== 200 || json === null) {
103
+ throw new MetaAuthError(`Meta sign-in failed (HTTP ${response.status}). Try again.`)
104
+ }
105
+ if (json.error !== undefined) {
106
+ const error = json.error as {code?: number; error_subcode?: number} | null
107
+ throw new MetaAuthError(
108
+ `Meta sign-in failed (code ${error?.code ?? "-"}, subcode ${error?.error_subcode ?? "-"}). Try again.`,
109
+ )
110
+ }
111
+ return json
112
+ }
113
+
114
+ async function postMultipart(url: string, fields: [string, string][]): Promise<Json> {
115
+ const boundary = `glassly.form.${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`
116
+ return post(
117
+ url,
118
+ {
119
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
120
+ "Accept": "application/json",
121
+ "Origin": "https://auth.meta.com",
122
+ },
123
+ multipartBody(fields, boundary),
124
+ )
125
+ }
126
+
127
+ /** Step 1: the anonymous WebView tokens query. */
128
+ export async function tokensQuery(): Promise<MetaSsoTokens> {
129
+ const lsd = makeLsd()
130
+ const json = await postMultipart(TOKENS_QUERY_URL, [
131
+ ["access_token", FRL_CLIENT],
132
+ ["lsd", lsd],
133
+ ["jazoest", jazoest(lsd)],
134
+ ])
135
+ const nativeSsoToken = json.native_sso_token
136
+ const etoken = json.native_sso_etoken
137
+ if (typeof nativeSsoToken !== "string" || !nativeSsoToken || typeof etoken !== "string" || !etoken) {
138
+ throw new MetaAuthError("Meta didn't return sign-in tokens. Try again.")
139
+ }
140
+ return {nativeSsoToken, etoken}
141
+ }
142
+
143
+ /** Step 3: decrypt the login blob into the FRL access token. */
144
+ export async function decryptBlob(blob: string, requestToken: string): Promise<string> {
145
+ const lsd = makeLsd()
146
+ const json = await postMultipart(BLOBS_DECRYPT_URL, [
147
+ ["blob", blob],
148
+ ["request_token", requestToken],
149
+ ["access_token", FRL_CLIENT],
150
+ ["lsd", lsd],
151
+ ["jazoest", jazoest(lsd)],
152
+ ])
153
+ const token = json.access_token
154
+ if (typeof token !== "string" || !token) throw new MetaAuthError("Meta didn't confirm the sign-in. Try again.")
155
+ return token
156
+ }
157
+
158
+ function randomId(): string {
159
+ const hex = () =>
160
+ Math.floor(Math.random() * 0x10000)
161
+ .toString(16)
162
+ .padStart(4, "0")
163
+ return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`.toUpperCase()
164
+ }
165
+
166
+ /** Step 4: exchange the FRL token for the ar user session. */
167
+ export async function login(frlAccessToken: string): Promise<MetaSession> {
168
+ const json = await post(
169
+ LOGIN_URL,
170
+ {
171
+ "Content-Type": "application/x-www-form-urlencoded",
172
+ "Accept": "application/json",
173
+ "Authorization": "OAuth " + AR_CLIENT,
174
+ },
175
+ urlForm([
176
+ ["frl_access_token", frlAccessToken],
177
+ ["logging_session_id", randomId()],
178
+ ["format", "json"],
179
+ ["device_id", randomId()],
180
+ ["generate_session_cookies", "1"],
181
+ ["generate_analytics_claim", "1"],
182
+ ["method", "POST"],
183
+ ]),
184
+ )
185
+ const accessToken = json.access_token
186
+ const rawUserId = json.user_id
187
+ const userId = typeof rawUserId === "string" ? rawUserId : typeof rawUserId === "number" ? String(rawUserId) : ""
188
+ if (typeof accessToken !== "string" || !accessToken || !userId) {
189
+ throw new MetaAuthError("Meta didn't return an account session. Try again.")
190
+ }
191
+ return {accessToken, userId}
192
+ }