@alexkroman1/aai-cli 6.11.0 → 8.0.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.
- package/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -19,18 +19,21 @@
|
|
|
19
19
|
|
|
20
20
|
import { readdir } from "node:fs/promises";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
import { readUpload } from "@alexkroman1/aai/step";
|
|
23
|
+
import {
|
|
24
|
+
installStubReporter,
|
|
25
|
+
installStubStepFetch,
|
|
26
|
+
installStubTranscribe,
|
|
27
|
+
installStubUploads,
|
|
28
|
+
} from "@alexkroman1/aai/testing/vitest";
|
|
29
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
26
30
|
import { FatalError, RetryableError } from "workflow";
|
|
27
31
|
import { z } from "zod";
|
|
28
32
|
import agentDef, { transcribe, transcribeBatch, transcribeStream } from "./agent.ts";
|
|
29
33
|
import { createJob, pollTranscript, uploadToProvider } from "./workflows/batch.ts";
|
|
30
|
-
import {
|
|
34
|
+
import { cuttable, normalizeRecording } from "./workflows/normalize.ts";
|
|
31
35
|
import { expectedSegments, planStreamed, probeUpload } from "./workflows/stream.ts";
|
|
32
36
|
import {
|
|
33
|
-
clock,
|
|
34
37
|
mergeTranscript,
|
|
35
38
|
splitRecording,
|
|
36
39
|
stitchChunks,
|
|
@@ -49,12 +52,8 @@ import {
|
|
|
49
52
|
SEGMENT_SECONDS,
|
|
50
53
|
UnsupportedRecordingError,
|
|
51
54
|
type WavFormat,
|
|
52
|
-
wavWithHeader,
|
|
53
55
|
} from "./workflows/wav.ts";
|
|
54
56
|
|
|
55
|
-
/** Where the sync endpoint lives — the one URL these stubs answer differently. */
|
|
56
|
-
const SYNC_ORIGIN = "https://sync.assemblyai.com";
|
|
57
|
-
|
|
58
57
|
/** The id every spec below uploads under. */
|
|
59
58
|
const UPLOAD_ID = "upl_test";
|
|
60
59
|
|
|
@@ -76,16 +75,12 @@ const STARTED_AT = 1_000_000;
|
|
|
76
75
|
* bytes with no server, no database and no HTTP.
|
|
77
76
|
*/
|
|
78
77
|
function publishRecording(bytes: Uint8Array, name = "standup.wav") {
|
|
79
|
-
|
|
78
|
+
// `installStubUploads` rather than `stubUploads`: the fake registers its own
|
|
79
|
+
// `onTestFinished`, which is what replaced the three hand-kept restore
|
|
80
|
+
// registries this file used to carry.
|
|
81
|
+
installStubUploads({ [UPLOAD_ID]: { bytes, name, type: "audio/wav" } });
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
/** Unpublished between specs — a slot left behind reaches the next file. */
|
|
83
|
-
let restore: (() => void) | undefined;
|
|
84
|
-
afterEach(() => {
|
|
85
|
-
restore?.();
|
|
86
|
-
restore = undefined;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
84
|
/** 16 kHz mono 16-bit — one second of audio is 32,000 bytes. */
|
|
90
85
|
const MONO_16K = { sampleRate: 16_000, channels: 1, bitsPerSample: 16 } as const;
|
|
91
86
|
|
|
@@ -147,8 +142,8 @@ describe("the agent declares its three workflows and nothing else", () => {
|
|
|
147
142
|
// every flow, and the streaming one differs only in that the CLIENT chose the id
|
|
148
143
|
// and PUT the file to it. A divergence here would mean the form had to ask a
|
|
149
144
|
// person how the bytes should travel.
|
|
150
|
-
for (const
|
|
151
|
-
expect(
|
|
145
|
+
for (const wf of [transcribe, transcribeStream, transcribeBatch]) {
|
|
146
|
+
expect(wf.uploads).toEqual(["recording"]);
|
|
152
147
|
}
|
|
153
148
|
});
|
|
154
149
|
|
|
@@ -375,24 +370,6 @@ describe("planSegments decides the fan-out's width", () => {
|
|
|
375
370
|
});
|
|
376
371
|
});
|
|
377
372
|
|
|
378
|
-
describe("wavWithHeader", () => {
|
|
379
|
-
test("writes a header the endpoint can read the rate back out of", () => {
|
|
380
|
-
const samples = new Uint8Array(3200).fill(7);
|
|
381
|
-
const out = wavWithHeader({ ...MONO_16K, dataStart: 44, dataEnd: 3244 }, samples);
|
|
382
|
-
const view = new DataView(out.buffer);
|
|
383
|
-
|
|
384
|
-
expect(String.fromCharCode(...out.subarray(0, 4))).toBe("RIFF");
|
|
385
|
-
expect(String.fromCharCode(...out.subarray(8, 12))).toBe("WAVE");
|
|
386
|
-
expect(view.getUint32(24, true)).toBe(MONO_16K.sampleRate);
|
|
387
|
-
expect(view.getUint16(22, true)).toBe(MONO_16K.channels);
|
|
388
|
-
// The two lengths, which are what a decoder trusts: RIFF counts everything
|
|
389
|
-
// after itself, `data` counts only the samples.
|
|
390
|
-
expect(view.getUint32(4, true)).toBe(36 + samples.length);
|
|
391
|
-
expect(view.getUint32(40, true)).toBe(samples.length);
|
|
392
|
-
expect(out.subarray(44)).toEqual(samples);
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
|
|
396
373
|
describe("stitchTranscript", () => {
|
|
397
374
|
test("removes the words the overlap made duplicates", () => {
|
|
398
375
|
expect(
|
|
@@ -472,13 +449,6 @@ describe("stitchChunks — what the PAGE renders while a run is going", () => {
|
|
|
472
449
|
});
|
|
473
450
|
});
|
|
474
451
|
|
|
475
|
-
describe("clock", () => {
|
|
476
|
-
test("renders a position a reader can find in the recording", () => {
|
|
477
|
-
expect(clock(0)).toBe("0:00");
|
|
478
|
-
expect(clock(65_000)).toBe("1:05");
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
|
|
482
452
|
describe("splitRecording", () => {
|
|
483
453
|
test("plans the segments and reports the duration", async () => {
|
|
484
454
|
const seconds = 200;
|
|
@@ -528,49 +498,40 @@ describe("transcribeSegment", () => {
|
|
|
528
498
|
/**
|
|
529
499
|
* Publishes the recording and answers the sync endpoint.
|
|
530
500
|
*
|
|
531
|
-
* `
|
|
532
|
-
*
|
|
533
|
-
* `
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
501
|
+
* `installStubTranscribe` routes AssemblyAI's four legs off the SDK's own
|
|
502
|
+
* endpoint constants, so this file no longer re-types the wire — and it fills a
|
|
503
|
+
* published `stepFetch` rather than `vi.stubGlobal("fetch", …)`, because the
|
|
504
|
+
* step calls `stepFetch`, which reaches a published slot rather than the global
|
|
505
|
+
* (see `sdk/step-fetch.ts` for why it has to: HTTP/1.1, so a batch of segments
|
|
506
|
+
* gets a socket each instead of N streams on one connection). Stubbing the
|
|
507
|
+
* global still passes, because an unpublished slot falls back to it, and would
|
|
508
|
+
* be asserting against a path production does not take.
|
|
509
|
+
*
|
|
510
|
+
* A refusal is staged as a STATUS, which is what makes the classification specs
|
|
511
|
+
* below a test of the SDK's reading of it rather than of an error a fake minted.
|
|
537
512
|
*/
|
|
538
|
-
function stubProvider(
|
|
539
|
-
sync: { status?: number; body?: unknown; headers?: Record<string, string> } = {},
|
|
540
|
-
) {
|
|
513
|
+
function stubProvider(failure?: { status: number; message: string; retryAfterSeconds?: number }) {
|
|
541
514
|
publishRecording(new Uint8Array(FORMAT.dataEnd));
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}));
|
|
547
|
-
stubs.push(stub.restore);
|
|
548
|
-
return stub.calls;
|
|
515
|
+
return installStubTranscribe({
|
|
516
|
+
text: "hello there",
|
|
517
|
+
failure: failure === undefined ? undefined : { leg: "sync", ...failure },
|
|
518
|
+
}).calls;
|
|
549
519
|
}
|
|
550
520
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
test("sends the segment as a WAV, with the key and the model header", async () => {
|
|
521
|
+
test("sends the segment as a WAV named after its index", async () => {
|
|
522
|
+
// What the SYNC endpoint's request looks like — the URL, the raw-key auth, the
|
|
523
|
+
// multipart envelope — is the SDK's contract and `sdk/step-transcribe*.test.ts`
|
|
524
|
+
// owns it. What is left here is this template's: it re-attaches a header to a
|
|
525
|
+
// window it read, and it names the part after the segment.
|
|
558
526
|
const calls = stubProvider();
|
|
559
527
|
const result = await transcribeSegment(UPLOAD_ID, FORMAT, SEGMENT);
|
|
560
528
|
|
|
561
529
|
expect(result).toEqual({ index: 0, text: "hello there" });
|
|
562
|
-
const
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// BYTES, and multipart — `stepFetch` takes no `FormData`, which is the
|
|
568
|
-
// point: a branded object handed to a fetch from another undici realm goes
|
|
569
|
-
// out as the string `[object FormData]`. `multipartBody` builds the envelope.
|
|
570
|
-
expect(sync?.headers["Content-Type"]).toMatch(/^multipart\/form-data; boundary=/);
|
|
571
|
-
const body = sync?.body;
|
|
572
|
-
expect(body).toBeInstanceOf(Uint8Array);
|
|
573
|
-
const decoded = new TextDecoder().decode(body as Uint8Array);
|
|
530
|
+
const sent = calls.find((call) => call.leg === "sync")?.body;
|
|
531
|
+
// Narrowed by a failing assertion rather than a cast: `expect.fail` returns
|
|
532
|
+
// `never`, and a body that is not bytes is a finding rather than a type error.
|
|
533
|
+
if (!(sent instanceof Uint8Array)) return expect.fail("the sync leg carries bytes");
|
|
534
|
+
const decoded = new TextDecoder().decode(sent);
|
|
574
535
|
expect(decoded).toContain('name="audio"; filename="segment-0.wav"');
|
|
575
536
|
// The WAV really rides in the part, header and all.
|
|
576
537
|
expect(decoded).toContain("RIFF");
|
|
@@ -581,8 +542,7 @@ describe("transcribeSegment", () => {
|
|
|
581
542
|
// page stitches whatever has arrived, so the transcript renders growing
|
|
582
543
|
// instead of appearing when the last segment does. The reporter is the SDK's
|
|
583
544
|
// published slot, which is the same seam `report()` goes through.
|
|
584
|
-
const reported =
|
|
585
|
-
stubs.push(reported.restore);
|
|
545
|
+
const reported = installStubReporter();
|
|
586
546
|
stubProvider();
|
|
587
547
|
|
|
588
548
|
await transcribeSegment(UPLOAD_ID, FORMAT, SEGMENT);
|
|
@@ -612,7 +572,7 @@ describe("transcribeSegment", () => {
|
|
|
612
572
|
// `RetryableError` carrying `retryAfter` is the difference between draining
|
|
613
573
|
// the 429s and re-collecting them `SEGMENT_CONCURRENCY` at a time on a
|
|
614
574
|
// backoff the server did not choose.
|
|
615
|
-
stubProvider({ status: 429,
|
|
575
|
+
stubProvider({ status: 429, message: "slow down", retryAfterSeconds: 30 });
|
|
616
576
|
const failure = await transcribeSegment(UPLOAD_ID, FORMAT, SEGMENT).catch(
|
|
617
577
|
(err: unknown) => err,
|
|
618
578
|
);
|
|
@@ -624,14 +584,14 @@ describe("transcribeSegment", () => {
|
|
|
624
584
|
});
|
|
625
585
|
|
|
626
586
|
test("retries a rate limit that named no delay", async () => {
|
|
627
|
-
stubProvider({ status: 429,
|
|
587
|
+
stubProvider({ status: 429, message: "slow down" });
|
|
628
588
|
await expect(transcribeSegment(UPLOAD_ID, FORMAT, SEGMENT)).rejects.toBeInstanceOf(
|
|
629
589
|
RetryableError,
|
|
630
590
|
);
|
|
631
591
|
});
|
|
632
592
|
|
|
633
593
|
test("fails FATALLY on a rejected request, naming what the endpoint said", async () => {
|
|
634
|
-
stubProvider({ status: 400,
|
|
594
|
+
stubProvider({ status: 400, message: "too short" });
|
|
635
595
|
await expect(transcribeSegment(UPLOAD_ID, FORMAT, SEGMENT)).rejects.toThrow(
|
|
636
596
|
/HTTP 400 — too short/,
|
|
637
597
|
);
|
|
@@ -691,7 +651,7 @@ describe("the streaming flow", () => {
|
|
|
691
651
|
function publishPartial(stored: number, declared: number, complete = false) {
|
|
692
652
|
const bytes = new Uint8Array(44 + stored);
|
|
693
653
|
bytes.set(wavFile(MONO_16K, declared), 0);
|
|
694
|
-
|
|
654
|
+
installStubUploads({
|
|
695
655
|
[UPLOAD_ID]: { bytes, name: "standup.wav", type: "audio/wav", complete },
|
|
696
656
|
});
|
|
697
657
|
}
|
|
@@ -725,12 +685,12 @@ describe("the streaming flow", () => {
|
|
|
725
685
|
// the file has finished — which is exactly what `transcribe` is for.
|
|
726
686
|
const bytes = new Uint8Array(44 + 100);
|
|
727
687
|
bytes.set(wavFile(MONO_16K, 100, { declaredDataSize: 0 }), 0);
|
|
728
|
-
|
|
688
|
+
installStubUploads({ [UPLOAD_ID]: { bytes, complete: false } });
|
|
729
689
|
await expect(planStreamed(UPLOAD_ID)).rejects.toThrow(/declares no data length/);
|
|
730
690
|
});
|
|
731
691
|
|
|
732
692
|
test("planStreamed refuses a file that is not a WAV, terminally", async () => {
|
|
733
|
-
|
|
693
|
+
installStubUploads({ [UPLOAD_ID]: { bytes: new Uint8Array(2000), complete: false } });
|
|
734
694
|
// Fatal, not retryable: three more attempts read the same bytes.
|
|
735
695
|
await expect(planStreamed(UPLOAD_ID)).rejects.toBeInstanceOf(FatalError);
|
|
736
696
|
});
|
|
@@ -761,21 +721,20 @@ describe("the async flow", () => {
|
|
|
761
721
|
vi.stubEnv("ASSEMBLYAI_API_KEY", "sk-test");
|
|
762
722
|
});
|
|
763
723
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
return stub.calls;
|
|
724
|
+
/**
|
|
725
|
+
* Answer the async API, recording what was sent.
|
|
726
|
+
*
|
|
727
|
+
* `installStubTranscribe` routes the three legs off the SDK's own endpoint
|
|
728
|
+
* constants — so a spec cannot pass because the fake and the step agree on a
|
|
729
|
+
* typo — and it restores itself when the test ends.
|
|
730
|
+
*/
|
|
731
|
+
function stubBatch(options: Parameters<typeof installStubTranscribe>[0] = {}) {
|
|
732
|
+
return installStubTranscribe({ audioUrl: "https://cdn.example/abc", ...options }).calls;
|
|
774
733
|
}
|
|
775
734
|
|
|
776
735
|
test("uploadToProvider streams the file and answers with the provider's URL", async () => {
|
|
777
736
|
publishRecording(new Uint8Array(5000), "standup.wav");
|
|
778
|
-
const calls = stubBatch(
|
|
737
|
+
const calls = stubBatch();
|
|
779
738
|
await expect(uploadToProvider(UPLOAD_ID)).resolves.toEqual({
|
|
780
739
|
audioUrl: "https://cdn.example/abc",
|
|
781
740
|
});
|
|
@@ -784,9 +743,9 @@ describe("the async flow", () => {
|
|
|
784
743
|
|
|
785
744
|
test("the file is STREAMED, so a step never holds a whole recording", async () => {
|
|
786
745
|
publishRecording(new Uint8Array(5000), "standup.wav");
|
|
787
|
-
const calls = stubBatch(
|
|
746
|
+
const calls = stubBatch();
|
|
788
747
|
await uploadToProvider(UPLOAD_ID);
|
|
789
|
-
//
|
|
748
|
+
// The fake drains a streaming body into bytes, so what this asserts is that
|
|
790
749
|
// every byte went out — the streaming is what keeps a gigabyte off the heap, and
|
|
791
750
|
// the bytes arriving intact is what says the windowing is right.
|
|
792
751
|
const sent = calls[0]?.body;
|
|
@@ -799,7 +758,7 @@ describe("the async flow", () => {
|
|
|
799
758
|
// thing five times and re-uploaded 24 MB on each attempt. The split is what makes
|
|
800
759
|
// a retry of the cheap half cost the cheap half.
|
|
801
760
|
publishRecording(new Uint8Array(5000));
|
|
802
|
-
const calls = stubBatch(
|
|
761
|
+
const calls = stubBatch({ failure: { leg: "submit", status: 400, message: "bad field" } });
|
|
803
762
|
await expect(createJob("https://cdn.example/abc")).rejects.toBeInstanceOf(FatalError);
|
|
804
763
|
// One call, and it is not the upload.
|
|
805
764
|
expect(calls).toHaveLength(1);
|
|
@@ -808,7 +767,7 @@ describe("the async flow", () => {
|
|
|
808
767
|
|
|
809
768
|
test("createJob asks for `speech_models`, plural — the singular field is a 400", async () => {
|
|
810
769
|
publishRecording(new Uint8Array(10));
|
|
811
|
-
const calls = stubBatch(
|
|
770
|
+
const calls = stubBatch({ jobIdPrefix: "tr_" });
|
|
812
771
|
await expect(createJob("https://cdn.example/abc")).resolves.toEqual({ id: "tr_1" });
|
|
813
772
|
const sent = JSON.parse(String(calls[0]?.body)) as Record<string, unknown>;
|
|
814
773
|
// The async API deprecated `speech_model` and answers 400 for any current model
|
|
@@ -820,7 +779,7 @@ describe("the async flow", () => {
|
|
|
820
779
|
|
|
821
780
|
test("a job the provider gave up on is TERMINAL, not polled forever", async () => {
|
|
822
781
|
publishRecording(new Uint8Array(10));
|
|
823
|
-
stubBatch(
|
|
782
|
+
stubBatch({ jobError: "audio too quiet" });
|
|
824
783
|
// The provider has decided; no number of polls changes it, so this must not come
|
|
825
784
|
// back as "not done yet".
|
|
826
785
|
await expect(pollTranscript(UPLOAD_ID, "tr_1", STARTED_AT)).rejects.toBeInstanceOf(FatalError);
|
|
@@ -828,13 +787,15 @@ describe("the async flow", () => {
|
|
|
828
787
|
|
|
829
788
|
test("pollTranscript answers `done` on completed and not before", async () => {
|
|
830
789
|
publishRecording(new Uint8Array(10));
|
|
831
|
-
stubBatch(
|
|
790
|
+
stubBatch({ pendingPolls: 1 });
|
|
832
791
|
await expect(pollTranscript(UPLOAD_ID, "tr_1", STARTED_AT)).resolves.toEqual({ done: false });
|
|
833
792
|
});
|
|
834
793
|
|
|
835
794
|
test("an unknown status is NOT done, so a new one cannot end a run early", async () => {
|
|
836
795
|
publishRecording(new Uint8Array(10));
|
|
837
|
-
|
|
796
|
+
// Raw rather than `stubTranscribe`: a body with no status at all is not a
|
|
797
|
+
// shape the fake can stage, and it is the whole point of this spec.
|
|
798
|
+
installStubStepFetch(() => ({ body: {} }));
|
|
838
799
|
await expect(pollTranscript(UPLOAD_ID, "tr_1", STARTED_AT)).resolves.toMatchObject({
|
|
839
800
|
done: false,
|
|
840
801
|
});
|
|
@@ -844,9 +805,7 @@ describe("the async flow", () => {
|
|
|
844
805
|
publishRecording(new Uint8Array(10), "standup.wav");
|
|
845
806
|
// It used to poll for a status and then fetch the identical URL again for the
|
|
846
807
|
// text the poll already had in its hand.
|
|
847
|
-
const calls = stubBatch(
|
|
848
|
-
body: { status: "completed", text: " hello there ", audio_duration: 12.5 },
|
|
849
|
-
}));
|
|
808
|
+
const calls = stubBatch({ text: " hello there ", durationSec: 12.5 });
|
|
850
809
|
await expect(pollTranscript(UPLOAD_ID, "tr_1", STARTED_AT)).resolves.toMatchObject({
|
|
851
810
|
done: true,
|
|
852
811
|
transcript: {
|
|
@@ -864,7 +823,7 @@ describe("the async flow", () => {
|
|
|
864
823
|
|
|
865
824
|
test("a rate limit is RETRYABLE, so a busy minute does not fail the run", async () => {
|
|
866
825
|
publishRecording(new Uint8Array(10));
|
|
867
|
-
stubBatch(
|
|
826
|
+
stubBatch({ failure: { leg: "poll", status: 429, message: "slow down" } });
|
|
868
827
|
await expect(pollTranscript(UPLOAD_ID, "tr_1", STARTED_AT)).rejects.toBeInstanceOf(
|
|
869
828
|
RetryableError,
|
|
870
829
|
);
|
|
@@ -872,7 +831,7 @@ describe("the async flow", () => {
|
|
|
872
831
|
|
|
873
832
|
test("all three flows report the same SHAPE, which is what lets one page render any", async () => {
|
|
874
833
|
publishRecording(new Uint8Array(10), "standup.wav");
|
|
875
|
-
stubBatch(
|
|
834
|
+
stubBatch({ text: "hi", durationSec: 1 });
|
|
876
835
|
const progress = await pollTranscript(UPLOAD_ID, "tr_1", STARTED_AT);
|
|
877
836
|
if (!progress.done) return expect.fail("the stub reports a completed job");
|
|
878
837
|
const batched = progress.transcript;
|
|
@@ -943,71 +902,20 @@ describe("normalizing the recording", () => {
|
|
|
943
902
|
// the caller stored. A step that copied it would double the storage every run
|
|
944
903
|
// pays for and would still report success.
|
|
945
904
|
publishRecording(wavFile(MONO_16K, 32_000), "standup.wav");
|
|
946
|
-
const reporter =
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
converted: false,
|
|
951
|
-
});
|
|
952
|
-
expect(reporter.lines.join(" ")).toContain("already linear-PCM WAV");
|
|
953
|
-
} finally {
|
|
954
|
-
reporter.restore();
|
|
955
|
-
}
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
test("a conversion that ffmpeg REFUSED is fatal, so it is not attempted five times", () => {
|
|
959
|
-
// `exit` means ffmpeg read the file and would read it the same way again.
|
|
960
|
-
expect(() =>
|
|
961
|
-
classifyFfmpeg(
|
|
962
|
-
new FfmpegError({
|
|
963
|
-
kind: "exit",
|
|
964
|
-
message: "Invalid data found when processing input",
|
|
965
|
-
binary: "ffmpeg",
|
|
966
|
-
argv: [],
|
|
967
|
-
exitCode: 1,
|
|
968
|
-
}),
|
|
969
|
-
),
|
|
970
|
-
).toThrow(FatalError);
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
test("a conversion that ran out of time keeps its retries, and its argv", () => {
|
|
974
|
-
// Rethrown UNCHANGED rather than wrapped, which is what `toStepError` does
|
|
975
|
-
// with an error carrying no verdict — and the DevKit's default for anything
|
|
976
|
-
// that is not a `FatalError` is to retry. Asserting the class survives is
|
|
977
|
-
// asserting the diagnosis does: `argv` is the command you paste into a shell,
|
|
978
|
-
// and a `new RetryableError(message)` here would throw it away.
|
|
979
|
-
const timedOut = new FfmpegError({
|
|
980
|
-
kind: "timeout",
|
|
981
|
-
message: "timed out",
|
|
982
|
-
binary: "ffmpeg",
|
|
983
|
-
argv: ["-i", "source"],
|
|
905
|
+
const reporter = installStubReporter();
|
|
906
|
+
await expect(normalizeRecording(UPLOAD_ID)).resolves.toEqual({
|
|
907
|
+
recording: UPLOAD_ID,
|
|
908
|
+
converted: false,
|
|
984
909
|
});
|
|
985
|
-
expect((
|
|
986
|
-
expect(() => classifyFfmpeg(timedOut)).not.toThrow(FatalError);
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
test("no ffmpeg at all is fatal — a retry cannot install one", () => {
|
|
990
|
-
// The `aai dev` case. Fatal deliberately: the message already carries the
|
|
991
|
-
// install instructions, and four more attempts only delay a person reading it.
|
|
992
|
-
expect(() =>
|
|
993
|
-
classifyFfmpeg(
|
|
994
|
-
new FfmpegError({
|
|
995
|
-
kind: "missing-binary",
|
|
996
|
-
message: "ffmpeg is not installed",
|
|
997
|
-
binary: "ffmpeg",
|
|
998
|
-
argv: [],
|
|
999
|
-
}),
|
|
1000
|
-
),
|
|
1001
|
-
).toThrow(FatalError);
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
test("something that is not an ffmpeg failure at all is fatal", () => {
|
|
1005
|
-
// The store rejecting a write, say. Fatal rather than retryable because this
|
|
1006
|
-
// step's own `maxRetries` exists for the I/O halves that report themselves as
|
|
1007
|
-
// transient; an unrecognized error has said nothing about being worth another
|
|
1008
|
-
// attempt, and guessing yes is how a run burns its budget before failing.
|
|
1009
|
-
expect(() => classifyFfmpeg(new Error("no space left on device"))).toThrow(FatalError);
|
|
910
|
+
expect(reporter.lines.join(" ")).toContain("already linear-PCM WAV");
|
|
1010
911
|
});
|
|
912
|
+
|
|
913
|
+
// The ffmpeg VERDICT is no longer tested here, and its absence is the change
|
|
914
|
+
// rather than a gap: `throwFfmpegStepError` on `@alexkroman1/aai/step-errors`
|
|
915
|
+
// owns it now, with every arm pinned in `sdk/step-errors.test.ts` — a refused
|
|
916
|
+
// file, a missing binary, a timeout rethrown UNCHANGED so its `argv` survives,
|
|
917
|
+
// and a cause that is not an ffmpeg failure at all. What stays above is what is
|
|
918
|
+
// still this desk's: WHICH files it decides to convert.
|
|
1011
919
|
});
|
|
1012
920
|
|
|
1013
921
|
describe("expectedSegments", () => {
|
|
@@ -1065,19 +973,15 @@ describe("the conversion, up to the spawn", () => {
|
|
|
1065
973
|
new Uint8Array([0, 0, 0, 0x20, 0x66, 0x74, 0x79, 0x70, 0x4d, 0x34, 0x41, 0x20]),
|
|
1066
974
|
"standup.m4a",
|
|
1067
975
|
);
|
|
1068
|
-
const reporter =
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
expect(normalizeRecording.maxRetries).toBe(5);
|
|
1078
|
-
} finally {
|
|
1079
|
-
reporter.restore();
|
|
1080
|
-
}
|
|
976
|
+
const reporter = installStubReporter();
|
|
977
|
+
// Fatal, not retryable: four more attempts find the same missing binary, and
|
|
978
|
+
// the message already carries the install instructions.
|
|
979
|
+
await expect(normalizeRecording(UPLOAD_ID)).rejects.toThrow(/ffprobe/);
|
|
980
|
+
await expect(normalizeRecording(UPLOAD_ID)).rejects.toBeInstanceOf(FatalError);
|
|
981
|
+
// It got as far as deciding the file needs converting — the failure is the
|
|
982
|
+
// binary, not the input.
|
|
983
|
+
expect(reporter.lines.join(" ")).toContain("standup.m4a");
|
|
984
|
+
expect(normalizeRecording.maxRetries).toBe(5);
|
|
1081
985
|
});
|
|
1082
986
|
|
|
1083
987
|
test("leaves no temp directory behind when the conversion fails", async () => {
|
|
@@ -1087,12 +991,8 @@ describe("the conversion, up to the spawn", () => {
|
|
|
1087
991
|
const leaked = (names: string[]) => names.filter((n) => n.startsWith("aai-normalize-"));
|
|
1088
992
|
const before = leaked(await readdir(tmpdir()));
|
|
1089
993
|
publishRecording(new Uint8Array([0, 0, 0, 0x20, 0x66, 0x74, 0x79, 0x70]), "standup.m4a");
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
expect(leaked(await readdir(tmpdir()))).toEqual(before);
|
|
1094
|
-
} finally {
|
|
1095
|
-
reporter.restore();
|
|
1096
|
-
}
|
|
994
|
+
installStubReporter();
|
|
995
|
+
await expect(normalizeRecording(UPLOAD_ID)).rejects.toBeInstanceOf(FatalError);
|
|
996
|
+
expect(leaked(await readdir(tmpdir()))).toEqual(before);
|
|
1097
997
|
});
|
|
1098
998
|
});
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - **`ASSEMBLYAI_API_KEY` in the agent env** — `.env` under `aai dev`,
|
|
17
17
|
* `aai secret put ASSEMBLYAI_API_KEY` once deployed. `requiredEnv` below is
|
|
18
18
|
* what makes a deploy check for it rather than letting the first run find out.
|
|
19
|
-
* A step reads it with `requireStepEnv`; see `@alexkroman1/aai/
|
|
19
|
+
* A step reads it with `requireStepEnv`; see `@alexkroman1/aai/step`.
|
|
20
20
|
* - **Storage** (`aai storage enable`, Settings → Database in the studio, or
|
|
21
21
|
* `DATABASE_URL` under `aai dev`). REQUIRED here, unlike most workflow apps:
|
|
22
22
|
* a run survives without it, but an UPLOAD's record is a row, so the form
|
|
@@ -151,7 +151,8 @@
|
|
|
151
151
|
*/
|
|
152
152
|
|
|
153
153
|
import "@alexkroman1/aai-ui/styles.css";
|
|
154
|
-
import
|
|
154
|
+
import { countWords, formatDuration, plural } from "@alexkroman1/aai/utils";
|
|
155
|
+
import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
155
156
|
import {
|
|
156
157
|
Form,
|
|
157
158
|
isTerminal,
|
|
@@ -162,19 +163,14 @@ import {
|
|
|
162
163
|
useWorkflowRuns,
|
|
163
164
|
useWorkflowStream,
|
|
164
165
|
useWorkflowSubmit,
|
|
166
|
+
WORKFLOW_STATUS_LABELS,
|
|
165
167
|
WorkflowFields,
|
|
166
168
|
WorkflowProgress,
|
|
167
169
|
type WorkflowRun,
|
|
168
170
|
} from "@alexkroman1/aai-ui";
|
|
169
171
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
170
172
|
import type { transcribe } from "./agent.ts";
|
|
171
|
-
import {
|
|
172
|
-
clock,
|
|
173
|
-
countWords,
|
|
174
|
-
stitchChunks,
|
|
175
|
-
TRANSCRIPT_STREAM,
|
|
176
|
-
type TranscriptChunk,
|
|
177
|
-
} from "./workflows/stitch.ts";
|
|
173
|
+
import { stitchChunks, TRANSCRIPT_STREAM, type TranscriptChunk } from "./workflows/stitch.ts";
|
|
178
174
|
|
|
179
175
|
/**
|
|
180
176
|
* What a finished run reports.
|
|
@@ -343,10 +339,10 @@ function TotalLatency({
|
|
|
343
339
|
{running ? "Elapsed" : "Total latency"}
|
|
344
340
|
</h2>
|
|
345
341
|
<span className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
346
|
-
<span className="text-sm tabular-nums">{
|
|
342
|
+
<span className="text-sm tabular-nums">{formatDuration(elapsedMs)}</span>
|
|
347
343
|
{runMs !== undefined && outside !== undefined && (
|
|
348
344
|
<span className="text-xs tabular-nums opacity-60">
|
|
349
|
-
{
|
|
345
|
+
{formatDuration(outside)} before the run · {formatDuration(runMs)} inside it
|
|
350
346
|
</span>
|
|
351
347
|
)}
|
|
352
348
|
</span>
|
|
@@ -647,9 +643,9 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Transcript>; onClear?: ()
|
|
|
647
643
|
{run.status === "completed" && (
|
|
648
644
|
<>
|
|
649
645
|
<p className="text-xs opacity-60">
|
|
650
|
-
{run.output.segments} {run.output.segments
|
|
651
|
-
{
|
|
652
|
-
{run.output.words} words
|
|
646
|
+
{run.output.segments} {plural(run.output.segments, "segment")} ·{" "}
|
|
647
|
+
{formatDuration(run.output.durationMs)} of audio · took{" "}
|
|
648
|
+
{formatDuration(run.output.elapsedMs)} · {run.output.words} words
|
|
653
649
|
</p>
|
|
654
650
|
<pre className="whitespace-pre-wrap text-sm leading-relaxed">{run.output.transcript}</pre>
|
|
655
651
|
</>
|
|
@@ -691,47 +687,26 @@ function LiveTranscript({ runId }: { runId: string }) {
|
|
|
691
687
|
return (
|
|
692
688
|
<div className="flex flex-col gap-2">
|
|
693
689
|
<p className="text-xs opacity-60">
|
|
694
|
-
{countWords(transcript)} words so far · through {
|
|
690
|
+
{countWords(transcript)} words so far · through {formatDuration(covered)}
|
|
695
691
|
</p>
|
|
696
692
|
<pre className="whitespace-pre-wrap text-sm leading-relaxed opacity-80">{transcript}</pre>
|
|
697
693
|
</div>
|
|
698
694
|
);
|
|
699
695
|
}
|
|
700
696
|
|
|
701
|
-
/**
|
|
702
|
-
* A duration a person can read.
|
|
703
|
-
*
|
|
704
|
-
* `${Math.round(ms / 1000)}s` was what this printed, and an hour-long recording came
|
|
705
|
-
* out as `3746s` — which a reader asked whether they should parse as 37.46 seconds.
|
|
706
|
-
* A raw second count stops being readable at about ninety of them, and the recordings
|
|
707
|
-
* this desk is FOR are the ones past that.
|
|
708
|
-
*
|
|
709
|
-
* The hours component is omitted when it is zero rather than padded to `0:02:26`, so
|
|
710
|
-
* a two-minute clip reads as `2:26` and only a long one grows a field.
|
|
711
|
-
*/
|
|
712
|
-
function duration(ms: number): string {
|
|
713
|
-
const total = Math.max(0, Math.round(ms / 1000));
|
|
714
|
-
const seconds = String(total % 60).padStart(2, "0");
|
|
715
|
-
const minutes = Math.floor(total / 60) % 60;
|
|
716
|
-
const hours = Math.floor(total / 3600);
|
|
717
|
-
return hours > 0
|
|
718
|
-
? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}`
|
|
719
|
-
: `${minutes}:${seconds}`;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
697
|
/**
|
|
723
698
|
* One line describing where a run has got to.
|
|
724
699
|
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
700
|
+
* `WORKFLOW_STATUS_LABELS` is the SDK's neutral map — a `Record` keyed by the
|
|
701
|
+
* status union rather than a switch, so a status added upstream is a compile
|
|
702
|
+
* error in one place every page inherits, and spreading a complete record cannot
|
|
703
|
+
* drop a key. Two of these keys are really this desk's: a page knows what its
|
|
704
|
+
* workflow does and the SDK does not.
|
|
728
705
|
*/
|
|
729
|
-
const STATUS_LINE
|
|
730
|
-
|
|
706
|
+
const STATUS_LINE = {
|
|
707
|
+
...WORKFLOW_STATUS_LABELS,
|
|
731
708
|
running: "Transcribing…",
|
|
732
709
|
completed: "Transcript ready",
|
|
733
|
-
failed: "Failed",
|
|
734
|
-
cancelled: "Cancelled",
|
|
735
710
|
};
|
|
736
711
|
|
|
737
712
|
page({ name: "Transcription Desk", component: TranscriptionDesk });
|