@aithos/sdk 0.1.0-alpha.43 → 0.1.0-alpha.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/src/compute.d.ts +218 -0
- package/dist/src/compute.js +457 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/react/index.d.ts +1 -0
- package/dist/src/react/index.js +1 -0
- package/dist/src/react/use-transcribe-pending.d.ts +21 -0
- package/dist/src/react/use-transcribe-pending.js +47 -0
- package/dist/src/transcribe-resilience.d.ts +57 -0
- package/dist/src/transcribe-resilience.js +203 -0
- package/dist/test/transcribe-invoke.test.d.ts +2 -0
- package/dist/test/transcribe-invoke.test.js +204 -0
- package/dist/test/transcribe.test.d.ts +2 -0
- package/dist/test/transcribe.test.js +186 -0
- package/package.json +1 -1
package/dist/src/compute.js
CHANGED
|
@@ -20,6 +20,7 @@ import { buildSignedEnvelope, } from "@aithos/protocol-client";
|
|
|
20
20
|
import { computeInvokeUrl, } from "./endpoints.js";
|
|
21
21
|
import { delegateKeyPair, ownerKeyPair, } from "./internal/protocol-client-bridge.js";
|
|
22
22
|
import { AithosSDKError } from "./types.js";
|
|
23
|
+
import { LocalPendingTranscribeTracker, TranscribeDraftStore, } from "./transcribe-resilience.js";
|
|
23
24
|
/**
|
|
24
25
|
* `sdk.compute` namespace. Constructed once by the {@link AithosSDK}
|
|
25
26
|
* constructor; reads the active owner from the supplied
|
|
@@ -28,9 +29,23 @@ import { AithosSDKError } from "./types.js";
|
|
|
28
29
|
*/
|
|
29
30
|
export class ComputeNamespace {
|
|
30
31
|
#deps;
|
|
32
|
+
// Lazily-created browser resilience helpers (see transcribe-resilience.ts).
|
|
33
|
+
// Created on first access only — the core invoke path never touches them.
|
|
34
|
+
#pendingTracker = null;
|
|
35
|
+
#draftStore = null;
|
|
31
36
|
constructor(deps) {
|
|
32
37
|
this.#deps = deps;
|
|
33
38
|
}
|
|
39
|
+
#tracker() {
|
|
40
|
+
if (!this.#pendingTracker)
|
|
41
|
+
this.#pendingTracker = new LocalPendingTranscribeTracker();
|
|
42
|
+
return this.#pendingTracker;
|
|
43
|
+
}
|
|
44
|
+
#draft() {
|
|
45
|
+
if (!this.#draftStore)
|
|
46
|
+
this.#draftStore = new TranscribeDraftStore();
|
|
47
|
+
return this.#draftStore;
|
|
48
|
+
}
|
|
34
49
|
/**
|
|
35
50
|
* Invoke a Bedrock model through the compute proxy. See
|
|
36
51
|
* {@link InvokeBedrockArgs} and {@link InvokeBedrockResult}.
|
|
@@ -234,6 +249,307 @@ export class ComputeNamespace {
|
|
|
234
249
|
signal: args.signal,
|
|
235
250
|
});
|
|
236
251
|
}
|
|
252
|
+
/* ---------------------------------------------------------------------- */
|
|
253
|
+
/* Transcription — low-level (advanced) API */
|
|
254
|
+
/* */
|
|
255
|
+
/* Four thin wrappers over the JSON-RPC methods. Use these when you want */
|
|
256
|
+
/* to own the upload + polling loop; otherwise prefer `invokeTranscribe` */
|
|
257
|
+
/* which composes them. All four are isomorphic (sign + POST only). */
|
|
258
|
+
/* ---------------------------------------------------------------------- */
|
|
259
|
+
/**
|
|
260
|
+
* Provision a transcription job and get a pre-signed S3 URL to PUT the
|
|
261
|
+
* audio to. No wallet debit. `mandateId` only selects the delegate
|
|
262
|
+
* signer (it is not part of the wire params for prepare).
|
|
263
|
+
*/
|
|
264
|
+
async prepareTranscribe(args) {
|
|
265
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
266
|
+
const choice = this.#resolveSigner(args.mandateId);
|
|
267
|
+
const url = computeInvokeUrl(endpoints);
|
|
268
|
+
const params = {
|
|
269
|
+
app_did: this.#deps.appDid,
|
|
270
|
+
content_type: args.contentType,
|
|
271
|
+
};
|
|
272
|
+
if (args.durationSecEstimate !== undefined) {
|
|
273
|
+
params.duration_sec_estimate = args.durationSecEstimate;
|
|
274
|
+
}
|
|
275
|
+
const r = await this.#signAndPost({
|
|
276
|
+
url,
|
|
277
|
+
method: "aithos.compute_transcribe_prepare",
|
|
278
|
+
params,
|
|
279
|
+
choice,
|
|
280
|
+
fetchImpl,
|
|
281
|
+
signal: args.signal,
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
jobId: r.job_id,
|
|
285
|
+
uploadUrl: r.upload_url,
|
|
286
|
+
s3ObjectKey: r.s3_object_key,
|
|
287
|
+
expiresAt: r.expires_at,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Verify the uploaded audio, pre-debit the wallet, and launch the AWS
|
|
292
|
+
* Transcribe job. Returns immediately with `status: "running"`.
|
|
293
|
+
*/
|
|
294
|
+
async startTranscribe(args) {
|
|
295
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
296
|
+
const choice = this.#resolveSigner(args.mandateId);
|
|
297
|
+
const url = computeInvokeUrl(endpoints);
|
|
298
|
+
const params = {
|
|
299
|
+
app_did: this.#deps.appDid,
|
|
300
|
+
mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
|
|
301
|
+
job_id: args.jobId,
|
|
302
|
+
model: args.model,
|
|
303
|
+
duration_sec: args.durationSec,
|
|
304
|
+
};
|
|
305
|
+
if (args.languageCode !== undefined)
|
|
306
|
+
params.language_code = args.languageCode;
|
|
307
|
+
if (args.diarization !== undefined)
|
|
308
|
+
params.diarization = args.diarization;
|
|
309
|
+
if (args.idempotencyKey !== undefined)
|
|
310
|
+
params.idempotency_key = args.idempotencyKey;
|
|
311
|
+
const r = await this.#signAndPost({
|
|
312
|
+
url,
|
|
313
|
+
method: "aithos.compute_transcribe_start",
|
|
314
|
+
params,
|
|
315
|
+
choice,
|
|
316
|
+
fetchImpl,
|
|
317
|
+
signal: args.signal,
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
jobId: r.job_id,
|
|
321
|
+
status: "running",
|
|
322
|
+
estimatedCredits: r.estimated_credits,
|
|
323
|
+
walletBalance: r.walletBalance,
|
|
324
|
+
...(r.fundedBy ? { fundedBy: r.fundedBy } : {}),
|
|
325
|
+
...(r.receiptId ? { receiptId: r.receiptId } : {}),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Poll a job's status. On completion the server finalises (reconcile +
|
|
330
|
+
* audit) and returns the transcript; a resumed poll after reconnect
|
|
331
|
+
* re-reads the transcript while it's still in the 24h output window.
|
|
332
|
+
*/
|
|
333
|
+
async getTranscribeStatus(args) {
|
|
334
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
335
|
+
const choice = this.#resolveSigner(args.mandateId);
|
|
336
|
+
const url = computeInvokeUrl(endpoints);
|
|
337
|
+
const params = { job_id: args.jobId };
|
|
338
|
+
const r = await this.#signAndPost({
|
|
339
|
+
url,
|
|
340
|
+
method: "aithos.compute_transcribe_status",
|
|
341
|
+
params,
|
|
342
|
+
choice,
|
|
343
|
+
fetchImpl,
|
|
344
|
+
signal: args.signal,
|
|
345
|
+
});
|
|
346
|
+
return mapTranscribeStatus(args.jobId, r);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* List the caller's transcription jobs. Excludes terminal `completed`
|
|
350
|
+
* jobs unless `includeCompleted` is set — the resilience "what's still
|
|
351
|
+
* pending server-side" query.
|
|
352
|
+
*/
|
|
353
|
+
async listPendingTranscribes(args) {
|
|
354
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
355
|
+
const choice = this.#resolveSigner(args?.mandateId);
|
|
356
|
+
const url = computeInvokeUrl(endpoints);
|
|
357
|
+
const params = {};
|
|
358
|
+
if (args?.includeCompleted !== undefined) {
|
|
359
|
+
params.include_completed = args.includeCompleted;
|
|
360
|
+
}
|
|
361
|
+
const r = await this.#signAndPost({
|
|
362
|
+
url,
|
|
363
|
+
method: "aithos.compute_transcribe_list_pending",
|
|
364
|
+
params,
|
|
365
|
+
choice,
|
|
366
|
+
fetchImpl,
|
|
367
|
+
signal: args?.signal,
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
jobs: (r.jobs ?? []).map((j) => ({
|
|
371
|
+
jobId: String(j.job_id),
|
|
372
|
+
status: j.status,
|
|
373
|
+
createdAt: Number(j.created_at),
|
|
374
|
+
...(j.estimated_credits !== undefined
|
|
375
|
+
? { estimatedCredits: Number(j.estimated_credits) }
|
|
376
|
+
: {}),
|
|
377
|
+
...(j.creditsCharged !== undefined
|
|
378
|
+
? { creditsCharged: Number(j.creditsCharged) }
|
|
379
|
+
: {}),
|
|
380
|
+
})),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/* ---------------------------------------------------------------------- */
|
|
384
|
+
/* Transcription — high-level one-call API */
|
|
385
|
+
/* ---------------------------------------------------------------------- */
|
|
386
|
+
/**
|
|
387
|
+
* Transcribe an audio Blob to text in one call. Composes the four
|
|
388
|
+
* low-level methods: prepare → direct S3 upload → start → poll. Returns
|
|
389
|
+
* the transcript and stores NOTHING server-side beyond the ephemeral
|
|
390
|
+
* job — the consumer decides what to do with the result.
|
|
391
|
+
*
|
|
392
|
+
* Isomorphic: depends only on `Blob`, `fetch`/`XMLHttpRequest` and
|
|
393
|
+
* timers. On a backend, pass `durationSecOverride` (no Blob duration
|
|
394
|
+
* probing is possible without a DOM); in a browser the duration is
|
|
395
|
+
* probed automatically when omitted.
|
|
396
|
+
*
|
|
397
|
+
* Resilience: the job id is recorded in a localStorage tracker (browser)
|
|
398
|
+
* before upload, so `listLocalPendingTranscribes()` / `resumeTranscribe()`
|
|
399
|
+
* can recover a job whose result never arrived. In Node the tracker is a
|
|
400
|
+
* harmless in-memory no-op.
|
|
401
|
+
*/
|
|
402
|
+
async invokeTranscribe(args) {
|
|
403
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
404
|
+
const model = args.model ?? "transcribe:aws-fr-standard";
|
|
405
|
+
const contentType = args.audio.type || "audio/webm";
|
|
406
|
+
const durationSec = args.durationSecOverride ?? (await probeBlobDuration(args.audio));
|
|
407
|
+
args.onProgress?.({ phase: "queued" });
|
|
408
|
+
// 1. Prepare — pre-signed S3 URL + job id.
|
|
409
|
+
const prepared = await this.prepareTranscribe({
|
|
410
|
+
contentType,
|
|
411
|
+
durationSecEstimate: durationSec,
|
|
412
|
+
...(args.mandateId ? { mandateId: args.mandateId } : {}),
|
|
413
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
414
|
+
});
|
|
415
|
+
const tracker = this.#tracker();
|
|
416
|
+
tracker.upsert(prepared.jobId, "uploading", { model, contentType });
|
|
417
|
+
// 2. Upload the blob straight to S3 (the proxy never sees the bytes).
|
|
418
|
+
try {
|
|
419
|
+
await uploadToS3WithProgress(prepared.uploadUrl, args.audio, contentType, {
|
|
420
|
+
onProgress: (b, t) => args.onProgress?.({ phase: "uploading", bytesUploaded: b, totalBytes: t }),
|
|
421
|
+
signal: args.signal,
|
|
422
|
+
fetchImpl: this.#deps.fetch,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
tracker.upsert(prepared.jobId, "failed");
|
|
427
|
+
throw e instanceof AithosSDKError
|
|
428
|
+
? e
|
|
429
|
+
: new AithosSDKError("transcribe_upload_failed", e.message);
|
|
430
|
+
}
|
|
431
|
+
// 3. Start — pre-debit + launch AWS Transcribe.
|
|
432
|
+
args.onProgress?.({ phase: "starting" });
|
|
433
|
+
await this.startTranscribe({
|
|
434
|
+
jobId: prepared.jobId,
|
|
435
|
+
...(args.mandateId ? { mandateId: args.mandateId } : {}),
|
|
436
|
+
model,
|
|
437
|
+
durationSec,
|
|
438
|
+
...(args.languageCode ? { languageCode: args.languageCode } : {}),
|
|
439
|
+
...(args.diarization !== undefined ? { diarization: args.diarization } : {}),
|
|
440
|
+
idempotencyKey,
|
|
441
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
442
|
+
});
|
|
443
|
+
tracker.upsert(prepared.jobId, "running");
|
|
444
|
+
// 4. Poll to completion.
|
|
445
|
+
const result = await this.#pollUntilTerminal(prepared.jobId, {
|
|
446
|
+
...(args.mandateId ? { mandateId: args.mandateId } : {}),
|
|
447
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
448
|
+
...(args.onProgress ? { onProgress: args.onProgress } : {}),
|
|
449
|
+
...(args.pollIntervalMs ? { pollIntervalMs: args.pollIntervalMs } : {}),
|
|
450
|
+
});
|
|
451
|
+
tracker.remove(prepared.jobId);
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Resume polling an in-flight job by id — for recovery after a reload or
|
|
456
|
+
* crash. Returns the final result and clears the job from the local
|
|
457
|
+
* pending tracker. Throws if the job has already failed.
|
|
458
|
+
*/
|
|
459
|
+
async resumeTranscribe(jobId, opts) {
|
|
460
|
+
const result = await this.#pollUntilTerminal(jobId, {
|
|
461
|
+
...(opts?.mandateId ? { mandateId: opts.mandateId } : {}),
|
|
462
|
+
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
463
|
+
...(opts?.onProgress ? { onProgress: opts.onProgress } : {}),
|
|
464
|
+
...(opts?.pollIntervalMs ? { pollIntervalMs: opts.pollIntervalMs } : {}),
|
|
465
|
+
});
|
|
466
|
+
this.#tracker().remove(jobId);
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
async #pollUntilTerminal(jobId, opts) {
|
|
470
|
+
const startedAt = Date.now();
|
|
471
|
+
let backoffMs = opts.pollIntervalMs ?? 2000;
|
|
472
|
+
for (;;) {
|
|
473
|
+
if (opts.signal?.aborted) {
|
|
474
|
+
throw new AithosSDKError("aborted", "transcription aborted");
|
|
475
|
+
}
|
|
476
|
+
const st = await this.getTranscribeStatus({
|
|
477
|
+
jobId,
|
|
478
|
+
...(opts.mandateId ? { mandateId: opts.mandateId } : {}),
|
|
479
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
480
|
+
});
|
|
481
|
+
if (st.status === "completed") {
|
|
482
|
+
opts.onProgress?.({ phase: "completed" });
|
|
483
|
+
return {
|
|
484
|
+
text: st.text,
|
|
485
|
+
segments: st.segments,
|
|
486
|
+
words: st.words,
|
|
487
|
+
durationSec: st.durationSec,
|
|
488
|
+
languageCode: st.languageCode,
|
|
489
|
+
creditsCharged: st.creditsCharged,
|
|
490
|
+
walletBalance: st.walletBalance,
|
|
491
|
+
auditId: st.auditId,
|
|
492
|
+
jobId: st.jobId,
|
|
493
|
+
...(st.fundedBy ? { fundedBy: st.fundedBy } : {}),
|
|
494
|
+
...(st.sponsoredBy ? { sponsoredBy: st.sponsoredBy } : {}),
|
|
495
|
+
...(st.receiptId ? { receiptId: st.receiptId } : {}),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (st.status === "failed") {
|
|
499
|
+
this.#tracker().upsert(jobId, "failed");
|
|
500
|
+
throw new AithosSDKError(st.error.code || "transcribe_failed", st.error.message);
|
|
501
|
+
}
|
|
502
|
+
opts.onProgress?.({
|
|
503
|
+
phase: "processing",
|
|
504
|
+
elapsedSec: Math.floor((Date.now() - startedAt) / 1000),
|
|
505
|
+
});
|
|
506
|
+
await sleep(backoffMs, opts.signal);
|
|
507
|
+
backoffMs = Math.min(15_000, Math.ceil(backoffMs * 1.5));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/* ---------------------------------------------------------------------- */
|
|
511
|
+
/* Transcription — browser resilience (framework-agnostic) */
|
|
512
|
+
/* ---------------------------------------------------------------------- */
|
|
513
|
+
/** Snapshot of locally-tracked in-flight jobs (stable ref between mutations). */
|
|
514
|
+
listLocalPendingTranscribes() {
|
|
515
|
+
return this.#tracker().list();
|
|
516
|
+
}
|
|
517
|
+
/** Stable snapshot for `useSyncExternalStore`-style consumers. */
|
|
518
|
+
getLocalPendingTranscribesSnapshot() {
|
|
519
|
+
return this.#tracker().getSnapshot();
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Subscribe to changes in the local pending-jobs registry. Returns an
|
|
523
|
+
* unsubscribe function. Framework-agnostic: wrap it in a React
|
|
524
|
+
* `useSyncExternalStore`, a Vue effect, a Svelte store, etc.
|
|
525
|
+
*/
|
|
526
|
+
subscribeLocalPendingTranscribes(listener) {
|
|
527
|
+
return this.#tracker().subscribe(listener);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* IndexedDB-backed draft queue: persist a recording before any network
|
|
531
|
+
* call, upload it when the user confirms. Browser-only (methods reject
|
|
532
|
+
* with TranscribeDraftUnavailableError when IndexedDB is absent).
|
|
533
|
+
*/
|
|
534
|
+
get transcribeDraft() {
|
|
535
|
+
const store = this.#draft();
|
|
536
|
+
const self = this;
|
|
537
|
+
return {
|
|
538
|
+
save: (blob, meta) => store.save(blob, meta),
|
|
539
|
+
list: () => store.list(),
|
|
540
|
+
get: (draftId) => store.get(draftId),
|
|
541
|
+
delete: (draftId) => store.delete(draftId),
|
|
542
|
+
upload: async (draftId, args) => {
|
|
543
|
+
const rec = await store.get(draftId);
|
|
544
|
+
if (!rec) {
|
|
545
|
+
throw new AithosSDKError("draft_not_found", `no transcription draft '${draftId}'`);
|
|
546
|
+
}
|
|
547
|
+
const result = await self.invokeTranscribe({ ...args, audio: rec.blob });
|
|
548
|
+
await store.delete(draftId);
|
|
549
|
+
return result;
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
237
553
|
/**
|
|
238
554
|
* Resolve the active signer (owner takes precedence over delegate).
|
|
239
555
|
*
|
|
@@ -363,6 +679,147 @@ function generateIdempotencyKey() {
|
|
|
363
679
|
}
|
|
364
680
|
return hex;
|
|
365
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Probe an audio Blob's duration (seconds) in a browser. Returns 0 when no
|
|
684
|
+
* DOM is available (Node / SSR) or probing fails — the caller should pass
|
|
685
|
+
* `durationSecOverride` on a backend. Never throws.
|
|
686
|
+
*/
|
|
687
|
+
async function probeBlobDuration(blob) {
|
|
688
|
+
try {
|
|
689
|
+
if (typeof document === "undefined" ||
|
|
690
|
+
typeof URL === "undefined" ||
|
|
691
|
+
typeof Audio === "undefined") {
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
return await new Promise((resolve) => {
|
|
695
|
+
const url = URL.createObjectURL(blob);
|
|
696
|
+
const audio = new Audio();
|
|
697
|
+
let settled = false;
|
|
698
|
+
const done = (v) => {
|
|
699
|
+
if (settled)
|
|
700
|
+
return;
|
|
701
|
+
settled = true;
|
|
702
|
+
try {
|
|
703
|
+
URL.revokeObjectURL(url);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
/* ignore */
|
|
707
|
+
}
|
|
708
|
+
resolve(Number.isFinite(v) && v > 0 ? v : 0);
|
|
709
|
+
};
|
|
710
|
+
audio.preload = "metadata";
|
|
711
|
+
audio.onloadedmetadata = () => done(audio.duration);
|
|
712
|
+
audio.onerror = () => done(0);
|
|
713
|
+
audio.src = url;
|
|
714
|
+
setTimeout(() => done(0), 5000);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
return 0;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* PUT a Blob to a pre-signed S3 URL. Uses XMLHttpRequest in browsers for
|
|
723
|
+
* real upload-progress events; falls back to `fetch` on Node (coarse
|
|
724
|
+
* 0→100% progress). Honors an AbortSignal.
|
|
725
|
+
*/
|
|
726
|
+
async function uploadToS3WithProgress(url, blob, contentType, opts) {
|
|
727
|
+
const total = blob.size;
|
|
728
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
729
|
+
await new Promise((resolve, reject) => {
|
|
730
|
+
const xhr = new XMLHttpRequest();
|
|
731
|
+
xhr.open("PUT", url, true);
|
|
732
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
733
|
+
xhr.upload.onprogress = (e) => {
|
|
734
|
+
if (e.lengthComputable)
|
|
735
|
+
opts.onProgress?.(e.loaded, e.total);
|
|
736
|
+
};
|
|
737
|
+
xhr.onload = () => {
|
|
738
|
+
if (xhr.status >= 200 && xhr.status < 300)
|
|
739
|
+
resolve();
|
|
740
|
+
else
|
|
741
|
+
reject(new AithosSDKError("transcribe_upload_failed", `S3 upload failed: HTTP ${xhr.status}`));
|
|
742
|
+
};
|
|
743
|
+
xhr.onerror = () => reject(new AithosSDKError("transcribe_upload_failed", "S3 upload network error"));
|
|
744
|
+
xhr.onabort = () => reject(new AithosSDKError("aborted", "S3 upload aborted"));
|
|
745
|
+
if (opts.signal) {
|
|
746
|
+
if (opts.signal.aborted) {
|
|
747
|
+
xhr.abort();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
opts.signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
751
|
+
}
|
|
752
|
+
xhr.send(blob);
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// Node / no XHR — use the injected fetch (falls back to global fetch).
|
|
757
|
+
opts.onProgress?.(0, total);
|
|
758
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
759
|
+
const res = await doFetch(url, {
|
|
760
|
+
method: "PUT",
|
|
761
|
+
headers: { "content-type": contentType },
|
|
762
|
+
body: blob,
|
|
763
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
764
|
+
});
|
|
765
|
+
if (!res.ok) {
|
|
766
|
+
throw new AithosSDKError("transcribe_upload_failed", `S3 upload failed: HTTP ${res.status}`);
|
|
767
|
+
}
|
|
768
|
+
opts.onProgress?.(total, total);
|
|
769
|
+
}
|
|
770
|
+
/** Sleep `ms`, rejecting early if the signal aborts. */
|
|
771
|
+
function sleep(ms, signal) {
|
|
772
|
+
return new Promise((resolve, reject) => {
|
|
773
|
+
const t = setTimeout(resolve, ms);
|
|
774
|
+
if (signal) {
|
|
775
|
+
if (signal.aborted) {
|
|
776
|
+
clearTimeout(t);
|
|
777
|
+
reject(new AithosSDKError("aborted", "aborted"));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
signal.addEventListener("abort", () => {
|
|
781
|
+
clearTimeout(t);
|
|
782
|
+
reject(new AithosSDKError("aborted", "aborted"));
|
|
783
|
+
}, { once: true });
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
/** Coerce a raw `transcribe_status` JSON-RPC result into the typed union. */
|
|
788
|
+
function mapTranscribeStatus(jobId, r) {
|
|
789
|
+
const status = r.status;
|
|
790
|
+
if (status === "completed") {
|
|
791
|
+
const fundedBy = r.fundedBy;
|
|
792
|
+
return {
|
|
793
|
+
jobId,
|
|
794
|
+
status: "completed",
|
|
795
|
+
text: String(r.text ?? ""),
|
|
796
|
+
segments: r.segments ?? [],
|
|
797
|
+
words: r.words ?? [],
|
|
798
|
+
durationSec: Number(r.duration_sec_actual ?? 0),
|
|
799
|
+
languageCode: String(r.language_code ?? ""),
|
|
800
|
+
creditsCharged: Number(r.creditsCharged ?? 0),
|
|
801
|
+
walletBalance: Number(r.walletBalance ?? 0),
|
|
802
|
+
auditId: String(r.auditId ?? ""),
|
|
803
|
+
...(fundedBy === "sponsored" || fundedBy === "grant" || fundedBy === "purchase"
|
|
804
|
+
? { fundedBy }
|
|
805
|
+
: {}),
|
|
806
|
+
...(typeof r.sponsoredBy === "string" ? { sponsoredBy: r.sponsoredBy } : {}),
|
|
807
|
+
...(typeof r.receiptId === "string" ? { receiptId: r.receiptId } : {}),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
if (status === "failed") {
|
|
811
|
+
const err = r.error ?? {};
|
|
812
|
+
return {
|
|
813
|
+
jobId,
|
|
814
|
+
status: "failed",
|
|
815
|
+
error: {
|
|
816
|
+
code: String(err.code ?? "transcribe_failed"),
|
|
817
|
+
message: String(err.message ?? ""),
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
return { jobId, status: "running", elapsedSec: Number(r.elapsed_sec ?? 0) };
|
|
822
|
+
}
|
|
366
823
|
/**
|
|
367
824
|
* Encode an ArrayBuffer as base64 in environments where `Buffer` is
|
|
368
825
|
* not available (browser). Uses btoa over a binary string — safe for
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
1
|
+
export declare const VERSION = "0.1.0-alpha.44";
|
|
2
2
|
export { AithosSDK } from "./sdk.js";
|
|
3
3
|
export type { AithosSDKConfig } from "./types.js";
|
|
4
4
|
export { AithosSDKError } from "./types.js";
|
|
5
5
|
export { AithosRpcError } from "@aithos/protocol-client";
|
|
6
6
|
export type { AithosSdkEndpoints } from "./endpoints.js";
|
|
7
7
|
export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
|
|
8
|
-
export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, } from "./compute.js";
|
|
8
|
+
export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, TranscribeModelId, TranscribeProgressState, TranscribeSegment, TranscribeWord, InvokeTranscribeArgs, InvokeTranscribeResult, PrepareTranscribeArgs, PrepareTranscribeResult, StartTranscribeArgs, StartTranscribeResult, TranscribeStatusResult, TranscribeJobSummary, } from "./compute.js";
|
|
9
9
|
export { ComputeNamespace } from "./compute.js";
|
|
10
|
+
export type { LocalPendingEntry, LocalPendingStatus, TranscribeDraftMeta, TranscribeDraftRecord, } from "./transcribe-resilience.js";
|
|
11
|
+
export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
|
|
10
12
|
export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
|
|
11
13
|
export { WalletNamespace } from "./wallet.js";
|
|
12
14
|
export type { ComponentStyle, ExtractArgs, ExtractContent, ExtractData, ExtractForm, ExtractFormField, ExtractHeading, ExtractIconDeclaration, ExtractImage, ExtractLink, ExtractLogo, ExtractMeta, ExtractResult, ExtractSection, ExtractStructure, ExtractStyles, FetchAssetArgs, FetchAssetResult, PaletteEntry, VisualSignature, WebNamespaceDeps, } from "./web.js";
|
package/dist/src/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
|
|
18
18
|
// are exported from here. Endpoint config (`AithosSdkEndpoints`,
|
|
19
19
|
// `DEFAULT_SDK_ENDPOINTS`) likewise.
|
|
20
|
-
export const VERSION = "0.1.0-alpha.
|
|
20
|
+
export const VERSION = "0.1.0-alpha.44";
|
|
21
21
|
export { AithosSDK } from "./sdk.js";
|
|
22
22
|
export { AithosSDKError } from "./types.js";
|
|
23
23
|
// Re-export protocol-client's JSON-RPC error type so consumers can
|
|
@@ -26,6 +26,7 @@ export { AithosSDKError } from "./types.js";
|
|
|
26
26
|
export { AithosRpcError } from "@aithos/protocol-client";
|
|
27
27
|
export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
|
|
28
28
|
export { ComputeNamespace } from "./compute.js";
|
|
29
|
+
export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
|
|
29
30
|
export { WalletNamespace } from "./wallet.js";
|
|
30
31
|
export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
|
|
31
32
|
// Sign-up, sign-in, sign-in-with-Google. Lives outside the AithosSDK
|
|
@@ -25,4 +25,5 @@
|
|
|
25
25
|
export { AssetsClientProvider, useAssetsClient, type AssetsClientProviderProps, } from "./context.js";
|
|
26
26
|
export { useAithosAsset, type UseAithosAssetState, type UseAithosAssetOptions, } from "./use-aithos-asset.js";
|
|
27
27
|
export { AithosAsset, type AithosAssetProps, type AithosImageProps, type AithosVideoProps, type AithosAudioProps, type AithosDownloadProps, } from "./AithosAsset.js";
|
|
28
|
+
export { useAithosTranscribePendingJobs, type UseAithosTranscribePendingJobs, } from "./use-transcribe-pending.js";
|
|
28
29
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/react/index.js
CHANGED
|
@@ -27,4 +27,5 @@
|
|
|
27
27
|
export { AssetsClientProvider, useAssetsClient, } from "./context.js";
|
|
28
28
|
export { useAithosAsset, } from "./use-aithos-asset.js";
|
|
29
29
|
export { AithosAsset, } from "./AithosAsset.js";
|
|
30
|
+
export { useAithosTranscribePendingJobs, } from "./use-transcribe-pending.js";
|
|
30
31
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComputeNamespace, InvokeTranscribeResult, TranscribeProgressState } from "../compute.js";
|
|
2
|
+
import type { LocalPendingEntry } from "../transcribe-resilience.js";
|
|
3
|
+
export interface UseAithosTranscribePendingJobs {
|
|
4
|
+
/** Locally-tracked in-flight jobs (survives reloads via localStorage). */
|
|
5
|
+
readonly pending: readonly LocalPendingEntry[];
|
|
6
|
+
/** Resume polling a job by id; resolves with the final transcript. */
|
|
7
|
+
readonly resume: (jobId: string, opts?: {
|
|
8
|
+
readonly mandateId?: string;
|
|
9
|
+
readonly onProgress?: (state: TranscribeProgressState) => void;
|
|
10
|
+
readonly signal?: AbortSignal;
|
|
11
|
+
readonly pollIntervalMs?: number;
|
|
12
|
+
}) => Promise<InvokeTranscribeResult>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe a React component to the SDK's local pending-transcription
|
|
16
|
+
* registry. Re-renders whenever a job is added, advances, or clears.
|
|
17
|
+
*
|
|
18
|
+
* @param compute the SDK compute namespace (`sdk.compute`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function useAithosTranscribePendingJobs(compute: ComputeNamespace): UseAithosTranscribePendingJobs;
|
|
21
|
+
//# sourceMappingURL=use-transcribe-pending.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* `useAithosTranscribePendingJobs(sdk.compute)` — a thin React adapter over
|
|
5
|
+
* the framework-agnostic local pending-jobs tracker exposed by the compute
|
|
6
|
+
* namespace. The tracker itself (subscribe/getSnapshot) is plain vanilla JS
|
|
7
|
+
* and works with any framework; this hook just wires it into React's
|
|
8
|
+
* `useSyncExternalStore`. Vue/Svelte/vanilla users can call
|
|
9
|
+
* `sdk.compute.subscribeLocalPendingTranscribes` directly.
|
|
10
|
+
*
|
|
11
|
+
* function PendingBanner({ sdk }) {
|
|
12
|
+
* const { pending, resume } = useAithosTranscribePendingJobs(sdk.compute);
|
|
13
|
+
* if (pending.length === 0) return null;
|
|
14
|
+
* return (
|
|
15
|
+
* <div>
|
|
16
|
+
* {pending.map((p) => (
|
|
17
|
+
* <button key={p.jobId} onClick={() => resume(p.jobId)}>
|
|
18
|
+
* Resume {p.jobId} ({p.status})
|
|
19
|
+
* </button>
|
|
20
|
+
* ))}
|
|
21
|
+
* </div>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
import { useCallback, useEffect, useState } from "react";
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe a React component to the SDK's local pending-transcription
|
|
28
|
+
* registry. Re-renders whenever a job is added, advances, or clears.
|
|
29
|
+
*
|
|
30
|
+
* @param compute the SDK compute namespace (`sdk.compute`).
|
|
31
|
+
*/
|
|
32
|
+
export function useAithosTranscribePendingJobs(compute) {
|
|
33
|
+
const [pending, setPending] = useState(() => compute.getLocalPendingTranscribesSnapshot());
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// Sync immediately (the snapshot may have changed before mount) then
|
|
36
|
+
// subscribe. getSnapshot returns a stable reference between mutations,
|
|
37
|
+
// so identical states bail out of a re-render.
|
|
38
|
+
setPending(compute.getLocalPendingTranscribesSnapshot());
|
|
39
|
+
const unsubscribe = compute.subscribeLocalPendingTranscribes(() => {
|
|
40
|
+
setPending(compute.getLocalPendingTranscribesSnapshot());
|
|
41
|
+
});
|
|
42
|
+
return unsubscribe;
|
|
43
|
+
}, [compute]);
|
|
44
|
+
const resume = useCallback((jobId, opts) => compute.resumeTranscribe(jobId, opts), [compute]);
|
|
45
|
+
return { pending, resume };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=use-transcribe-pending.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type LocalPendingStatus = "uploading" | "running" | "completed" | "failed";
|
|
2
|
+
export interface LocalPendingEntry {
|
|
3
|
+
readonly jobId: string;
|
|
4
|
+
readonly status: LocalPendingStatus;
|
|
5
|
+
readonly createdAt: number;
|
|
6
|
+
readonly updatedAt: number;
|
|
7
|
+
readonly meta?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Framework-agnostic observable registry of in-flight transcription jobs.
|
|
11
|
+
* Persisted to localStorage when available (so it survives reloads), with
|
|
12
|
+
* an in-memory fallback otherwise. Subscribe with `subscribe(listener)`;
|
|
13
|
+
* read with `getSnapshot()` (stable reference between mutations, so it
|
|
14
|
+
* plugs directly into React's `useSyncExternalStore`).
|
|
15
|
+
*/
|
|
16
|
+
export declare class LocalPendingTranscribeTracker {
|
|
17
|
+
#private;
|
|
18
|
+
constructor();
|
|
19
|
+
/** Current entries. Stable reference until the next mutation. */
|
|
20
|
+
getSnapshot(): readonly LocalPendingEntry[];
|
|
21
|
+
list(): readonly LocalPendingEntry[];
|
|
22
|
+
/** Subscribe to changes. Returns an unsubscribe function. */
|
|
23
|
+
subscribe(listener: () => void): () => void;
|
|
24
|
+
upsert(jobId: string, status: LocalPendingStatus, meta?: Record<string, unknown>): void;
|
|
25
|
+
remove(jobId: string): void;
|
|
26
|
+
clear(): void;
|
|
27
|
+
}
|
|
28
|
+
export interface TranscribeDraftMeta {
|
|
29
|
+
readonly title?: string;
|
|
30
|
+
readonly tag?: string;
|
|
31
|
+
readonly contentType?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface TranscribeDraftRecord {
|
|
34
|
+
readonly draftId: string;
|
|
35
|
+
readonly blob: Blob;
|
|
36
|
+
readonly metadata: TranscribeDraftMeta;
|
|
37
|
+
readonly createdAt: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class TranscribeDraftUnavailableError extends Error {
|
|
40
|
+
constructor();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* IndexedDB-backed queue of recorded audio Blobs. Save a recording the
|
|
44
|
+
* instant it finishes (before any network), then `upload` it when the
|
|
45
|
+
* user confirms — so a flaky network or a closed tab never loses audio.
|
|
46
|
+
* Browser-only: methods reject with {@link TranscribeDraftUnavailableError}
|
|
47
|
+
* when IndexedDB is absent.
|
|
48
|
+
*/
|
|
49
|
+
export declare class TranscribeDraftStore {
|
|
50
|
+
save(blob: Blob, meta?: TranscribeDraftMeta): Promise<{
|
|
51
|
+
readonly draftId: string;
|
|
52
|
+
}>;
|
|
53
|
+
list(): Promise<readonly TranscribeDraftRecord[]>;
|
|
54
|
+
get(draftId: string): Promise<TranscribeDraftRecord | null>;
|
|
55
|
+
delete(draftId: string): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=transcribe-resilience.d.ts.map
|