arcane-os 0.46.0 → 0.47.0

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.
@@ -2081,8 +2081,10 @@ class AI {
2081
2081
  #assertDeviceSpeechConfiguration(configuration){
2082
2082
  for(const routeName of ['default','localOnly']){
2083
2083
  const selection=configuration?.stt?.[routeName];
2084
- if(selection&&selection.localOnly!==true){
2085
- const error=new TypeError('Speech recognition supports on-device providers only.');
2084
+ const liveCapture=selection
2085
+ &&this.#providerRuntime.supportsTranscriptionCapture(selection.providerId);
2086
+ if(selection&&selection.localOnly!==true&&!liveCapture){
2087
+ const error=new TypeError('Speech recognition requires an on-device or explicitly selected live-capture provider.');
2086
2088
  error.code='AI_STT_DEVICE_ONLY';
2087
2089
  throw error;
2088
2090
  }
@@ -3430,8 +3432,9 @@ class AI {
3430
3432
  if(provider!==null&&provider?.role!==role){
3431
3433
  throw new TypeError('The speech provider must implement the selected role.');
3432
3434
  }
3433
- if(role==='stt'&&provider&&provider.localOnly!==true){
3434
- const error=new TypeError('Speech recognition supports on-device providers only.');
3435
+ if(role==='stt'&&provider&&provider.localOnly!==true
3436
+ &&!is.function(provider.createCapture)){
3437
+ const error=new TypeError('Speech recognition requires an on-device or explicitly selected live-capture provider.');
3435
3438
  error.code='AI_STT_DEVICE_ONLY';
3436
3439
  throw error;
3437
3440
  }
@@ -6875,6 +6878,14 @@ class AI {
6875
6878
  }
6876
6879
  }
6877
6880
 
6881
+ supportsTranscriptionCapture(){
6882
+ return this.#providerRuntime.supportsTranscriptionCapture();
6883
+ }
6884
+
6885
+ createTranscriptionCapture(options={}){
6886
+ return this.#providerRuntime.createTranscriptionCapture(options);
6887
+ }
6888
+
6878
6889
  async fetchSTT(audioFile,signal=null){
6879
6890
  const callId=this.#traceSpeech('fetchSTT.call',{audioFile,aborted:signal?.aborted});
6880
6891
  try{
@@ -1366,6 +1366,9 @@ function validateProvider(provider) {
1366
1366
  for (const method of PROVIDER_METHODS) {
1367
1367
  record[method] = provider[method].bind(provider);
1368
1368
  }
1369
+ if (provider.role === 'stt' && is.function(provider.createCapture)) {
1370
+ record.createCapture = provider.createCapture.bind(provider);
1371
+ }
1369
1372
  const admitted = completeValue(record);
1370
1373
  PROVIDER_RECORDS.set(provider, admitted);
1371
1374
  return admitted;
@@ -4103,6 +4106,121 @@ export class AIProviderRuntime {
4103
4106
  return this.#roleRequestAlias('llm', 'stream', payload, options);
4104
4107
  }
4105
4108
 
4109
+ supportsTranscriptionCapture(providerId = null) {
4110
+ const slot = this.#slots.stt;
4111
+ const provider = providerId === null
4112
+ ? this.#providerFor(slot)
4113
+ : this.#providers.get(providerKey('stt', providerId));
4114
+ return is.function(provider?.createCapture);
4115
+ }
4116
+
4117
+ createTranscriptionCapture(options = {}) {
4118
+ this.#assertOpen();
4119
+ this.#assertNotConfiguring();
4120
+ const slot = this.#slots.stt;
4121
+ const provider = this.#providerFor(slot);
4122
+ if (!is.function(provider?.createCapture)) return null;
4123
+
4124
+ const runtime = this;
4125
+ const generation = slot.generation;
4126
+ const capture = provider.createCapture(options);
4127
+ const controller = new AbortController();
4128
+ let requestRecord = null;
4129
+ let detachSignal = null;
4130
+ let started = false;
4131
+ let closed = false;
4132
+ const done = Promise.resolve(capture.done).finally(
4133
+ function releaseTranscriptionCapture() {
4134
+ closed = true;
4135
+ detachSignal?.();
4136
+ if (!requestRecord) return;
4137
+ slot.activeRequests.delete(requestRecord.requestSequence);
4138
+ runtime.#drainRoleRequestQueue(slot);
4139
+ if (slot.generation !== generation || slot.unloadPromise) return;
4140
+ if (slot.activeRequests.size || slot.requestQueue.length) {
4141
+ runtime.#publishRoleRequestState(slot);
4142
+ return;
4143
+ }
4144
+ const status = validateProviderStatus(provider.status());
4145
+ slot.ready = status.state === 'ready' && status.loaded && !status.busy;
4146
+ publishAIRuntimeRoleState(
4147
+ 'stt',
4148
+ roleRecord(
4149
+ 'stt',
4150
+ slot.selection,
4151
+ {state: slot.ready ? 'ready' : 'unloaded', loaded: status.loaded}
4152
+ )
4153
+ );
4154
+ }
4155
+ );
4156
+ done.catch(
4157
+ function observeTranscriptionCaptureCleanupFailure(error) {
4158
+ arcaneLogging.error('Transcription capture cleanup failed.', error);
4159
+ }
4160
+ );
4161
+ // Capture has its own synchronous start boundary so native microphone
4162
+ // permission retains the originating user gesture. It is never queued.
4163
+ return {
4164
+ done,
4165
+ start({signal} = {}) {
4166
+ if (started || closed) return Promise.resolve(false);
4167
+ try {
4168
+ runtime.#assertOpen();
4169
+ runtime.#assertNotConfiguring();
4170
+ const status = validateProviderStatus(provider.status());
4171
+ if (slot.generation !== generation || !slot.ready
4172
+ || slot.unloadPromise || slot.disposePromise
4173
+ || slot.activeRequests.size || slot.requestQueue.length
4174
+ || status.state !== 'ready' || !status.loaded || status.busy) {
4175
+ throw operationError(
4176
+ 'The selected transcription capture provider is not ready.',
4177
+ 'ARCANE_AI_ROLE_NOT_READY'
4178
+ );
4179
+ }
4180
+ assertAbortSignal(signal);
4181
+ started = true;
4182
+ detachSignal = runtime.#forwardAbort(signal, controller);
4183
+ const requestSequence = runtime.#nextRequestSequence(slot);
4184
+ requestRecord = {
4185
+ controller,
4186
+ requestSequence,
4187
+ operationId: runtime.#nextOperationId(slot, 'capture'),
4188
+ promise: done,
4189
+ cancel: function cancelOwnedTranscriptionCapture(reason) {
4190
+ controller.abort(reason);
4191
+ capture.cancel();
4192
+ return done;
4193
+ }
4194
+ };
4195
+ slot.activeRequests.set(requestSequence, requestRecord);
4196
+ runtime.#publishRoleRequestState(slot);
4197
+ const starting = capture.start({signal: controller.signal});
4198
+ return Promise.resolve(starting).catch(
4199
+ function releaseFailedTranscriptionCaptureStart(error) {
4200
+ capture.cancel();
4201
+ throw error;
4202
+ }
4203
+ );
4204
+ } catch (error) {
4205
+ capture.cancel();
4206
+ throw error;
4207
+ }
4208
+ },
4209
+ stop() {
4210
+ capture.stop();
4211
+ return done;
4212
+ },
4213
+ cancel() {
4214
+ controller.abort();
4215
+ capture.cancel();
4216
+ },
4217
+ destroy() {
4218
+ controller.abort();
4219
+ capture.destroy();
4220
+ }
4221
+ };
4222
+ }
4223
+
4106
4224
  transcribe(payload, options = {}) {
4107
4225
  arcaneLogging.debug('[Arcane speech runtime] transcribe', payload, options);
4108
4226
  return this.#roleRequestAlias('stt', 'transcribe', payload, options);