@alexkroman1/aai-cli 6.10.1 → 7.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/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
- package/dist/cli.mjs +30 -25
- 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-DvYeD8ia.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
- package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
- package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +93 -14
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +13 -4
- package/dist/scaffold/vite.config.ts +1 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
- package/dist/templates/call-audit/agent.test.ts +818 -0
- package/dist/templates/call-audit/agent.ts +171 -0
- package/dist/templates/call-audit/client.tsx +198 -0
- package/dist/templates/call-audit/workflows/audit.ts +298 -0
- package/dist/templates/call-audit/workflows/ingest.ts +251 -0
- package/dist/templates/call-audit/workflows/media.ts +637 -0
- package/dist/templates/call-audit/workflows/summarize.ts +201 -0
- package/dist/templates/call-audit/workflows/sync-api.ts +44 -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.test.ts +17 -14
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.test.ts +4 -6
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- 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/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/pizza-ordering/agent.test.ts +27 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- 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.test.ts +746 -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 +411 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
- package/dist/templates/recap-workflow/agent.test.ts +107 -96
- 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 +48 -41
- 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.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/agent.test.ts +26 -23
- package/dist/templates/retail/client.tsx +226 -117
- package/dist/templates/retail/registry.test.ts +38 -6
- package/dist/templates/retail/store.test.ts +82 -15
- package/dist/templates/retail/store.ts +174 -47
- package/dist/templates/retail/system-prompt.md +11 -2
- package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
- 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 +2 -2
- package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
- package/dist/templates/retail/tools/modify_user_address.ts +2 -2
- package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
- 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.test.ts +314 -0
- package/dist/templates/spoken-summary/agent.ts +155 -0
- package/dist/templates/spoken-summary/client.tsx +185 -0
- package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
- package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -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/tools/answer_question.ts +17 -4
- package/dist/templates/transcription-workflow/agent.test.ts +246 -123
- package/dist/templates/transcription-workflow/agent.ts +21 -7
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
- package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
- package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
- package/dist/templates/travel-concierge/agent.test.ts +64 -33
- 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 +70 -3
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
- package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- 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
|
@@ -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 });
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
* the two ways to arrange that. The async API has no such cap. You submit a job, it
|
|
9
9
|
* answers with an id in milliseconds, and the transcript is ready minutes later.
|
|
10
10
|
*
|
|
11
|
-
* So this flow is
|
|
11
|
+
* So this flow is three steps and no arithmetic:
|
|
12
12
|
*
|
|
13
13
|
* ```text
|
|
14
14
|
* uploadToProvider one step → the file, streamed, and the URL it answered
|
|
15
15
|
* createJob one step → the transcript id
|
|
16
|
-
* pollTranscript one step + a durable sleep, until
|
|
17
|
-
* readTranscript one step → the text
|
|
16
|
+
* pollTranscript one step + a durable sleep, until the text comes back
|
|
18
17
|
* ```
|
|
19
18
|
*
|
|
20
19
|
* **It is here to be compared against the other two, and it usually wins.** No
|
|
@@ -24,6 +23,21 @@
|
|
|
24
23
|
* inside: the latency is the provider's queue rather than your fan-out, and there is
|
|
25
24
|
* nothing to report between "submitted" and "done" except the job's own status.
|
|
26
25
|
*
|
|
26
|
+
* ## The endpoint is the SDK's; the STEPS are ours
|
|
27
|
+
*
|
|
28
|
+
* `stepTranscribeUpload` / `stepTranscribeSubmit` / `stepTranscribePoll` on
|
|
29
|
+
* `@alexkroman1/aai/step` own the URL, the raw-key auth, the windowed streaming
|
|
30
|
+
* upload, the PLURAL `speech_models` field and the failure classification. This file
|
|
31
|
+
* used to spell all of that out, and so did `spoken-summary` — the same ~200 lines
|
|
32
|
+
* twice, reworded, identical in behaviour, and drifting apart at the edges.
|
|
33
|
+
*
|
|
34
|
+
* What stays here is what a dependency cannot decide: how many steps to cut the job
|
|
35
|
+
* into, and therefore what is journaled and what a retry repeats. That is also
|
|
36
|
+
* structural rather than stylistic — the Workflow DevKit's builder transforms
|
|
37
|
+
* exactly the files under this `workflows/` directory, so a `"use step"` shipped
|
|
38
|
+
* inside the SDK would be transformed by nothing and would run inline with no
|
|
39
|
+
* journal and no retry, silently.
|
|
40
|
+
*
|
|
27
41
|
* ## The one thing that makes this a WORKFLOW rather than a request
|
|
28
42
|
*
|
|
29
43
|
* The wait. A job takes minutes, and nothing about an HTTP request survives minutes:
|
|
@@ -32,56 +46,23 @@
|
|
|
32
46
|
* shape and its module doc carries the argument; this is the same pattern with the
|
|
33
47
|
* poll bounded by attempts rather than by a deadline.
|
|
34
48
|
*
|
|
35
|
-
* ##
|
|
49
|
+
* ## Three steps, not four, and the fourth was a wasted round trip
|
|
36
50
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* That is also why the step that does it is the step the DevKit retries: a streaming
|
|
42
|
-
* body is consumed once, so a retry has to re-read the upload from the start, which
|
|
43
|
-
* it does. One window of READ-AHEAD keeps the store and the socket busy at the same
|
|
44
|
-
* time; `windows` carries the argument.
|
|
51
|
+
* This used to poll `GET /v2/transcript/:id` for a status and then fetch the
|
|
52
|
+
* identical URL a second time to read the text the poll already had in its hand.
|
|
53
|
+
* {@link pollTranscript} answers with the transcript, so a finished job costs one
|
|
54
|
+
* request rather than two and the value journaled by the last poll IS the result.
|
|
45
55
|
*/
|
|
46
56
|
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
57
|
+
import { report, TRANSCRIBE_API, uploadInfo } from "@alexkroman1/aai/step";
|
|
58
|
+
import {
|
|
59
|
+
stepTranscribePollClassified,
|
|
60
|
+
stepTranscribeSubmitClassified,
|
|
61
|
+
stepTranscribeUploadClassified,
|
|
62
|
+
} from "@alexkroman1/aai/step-errors";
|
|
63
|
+
import { countWords, formatBytes } from "@alexkroman1/aai/utils";
|
|
49
64
|
import { sleep } from "workflow";
|
|
50
|
-
import {
|
|
51
|
-
import { countWords, startClock, type Transcript } from "./transcribe.ts";
|
|
52
|
-
|
|
53
|
-
/** The async API's base. */
|
|
54
|
-
const API = "https://api.assemblyai.com";
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* The models this desk asks for, best first.
|
|
58
|
-
*
|
|
59
|
-
* `speech_models`, PLURAL and an array. The singular `speech_model` is deprecated on
|
|
60
|
-
* the async API and answers **400** for any current model name — which is how this
|
|
61
|
-
* was found: the first live run of this flow failed on it, and the API said so in
|
|
62
|
-
* exactly those words. Note the streaming API still uses the singular field, so the
|
|
63
|
-
* two are not interchangeable and neither is "the" spelling.
|
|
64
|
-
*
|
|
65
|
-
* Omitting it entirely is also legal and routes to the default; naming it is what
|
|
66
|
-
* pins the model so a default change does not silently move this template's output.
|
|
67
|
-
*/
|
|
68
|
-
const MODELS = ["universal-3-5-pro"];
|
|
69
|
-
|
|
70
|
-
/** How much of our stored upload one outbound window carries. */
|
|
71
|
-
const UPLOAD_WINDOW_BYTES = 4 * 1024 * 1024;
|
|
72
|
-
|
|
73
|
-
/** How long a single request may take. The upload is not one of these — see below. */
|
|
74
|
-
const REQUEST_TIMEOUT_MS = 60_000;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* How long the upload leg may take.
|
|
78
|
-
*
|
|
79
|
-
* Its own budget because it is the one request whose duration is a function of the
|
|
80
|
-
* FILE rather than of the service: a gigabyte at 8 MB/s is over two minutes, and a
|
|
81
|
-
* deadline sized for a JSON round trip would cancel exactly the uploads this flow
|
|
82
|
-
* exists to handle.
|
|
83
|
-
*/
|
|
84
|
-
const UPLOAD_TIMEOUT_MS = 30 * 60_000;
|
|
65
|
+
import { startClock, type Transcript } from "./transcribe.ts";
|
|
85
66
|
|
|
86
67
|
/** How long between polls of a submitted job. */
|
|
87
68
|
const POLL_INTERVAL = "10s";
|
|
@@ -89,9 +70,9 @@ const POLL_INTERVAL = "10s";
|
|
|
89
70
|
/**
|
|
90
71
|
* Polls before the run gives up on a job.
|
|
91
72
|
*
|
|
92
|
-
* At {@link POLL_INTERVAL} this is an hour,
|
|
93
|
-
*
|
|
94
|
-
*
|
|
73
|
+
* At {@link POLL_INTERVAL} this is an hour, well past what the async API takes for
|
|
74
|
+
* any recording it accepts. Bounded rather than endless because a job that never
|
|
75
|
+
* leaves `queued` is a run that would otherwise be replayed forever.
|
|
95
76
|
*/
|
|
96
77
|
const MAX_POLLS = 360;
|
|
97
78
|
|
|
@@ -109,15 +90,15 @@ export async function transcribeBatchFlow(input: { recording: string }): Promise
|
|
|
109
90
|
const job = await createJob(audioUrl);
|
|
110
91
|
|
|
111
92
|
for (let poll = 0; poll < MAX_POLLS; poll += 1) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
93
|
+
const progress = await pollTranscript(input.recording, job.id, startedAt);
|
|
94
|
+
if (progress.done) return progress.transcript;
|
|
114
95
|
await sleep(POLL_INTERVAL);
|
|
115
96
|
}
|
|
116
97
|
// A plain throw: this is the BODY, where the fatal/retryable distinction has
|
|
117
98
|
// nothing to apply to — see `stream.ts`'s `abandon` for the same reasoning.
|
|
118
99
|
throw new Error(
|
|
119
100
|
`Transcript ${job.id} was still unfinished after ${MAX_POLLS} polls. It is not lost — ` +
|
|
120
|
-
`read it directly with GET ${
|
|
101
|
+
`read it directly with GET ${TRANSCRIBE_API}/v2/transcript/${job.id}.`,
|
|
121
102
|
);
|
|
122
103
|
}
|
|
123
104
|
|
|
@@ -134,28 +115,21 @@ export async function transcribeBatchFlow(input: { recording: string }): Promise
|
|
|
134
115
|
* So the URL is journaled after all. The risk that made that look wrong is real but
|
|
135
116
|
* far smaller: if it expires before the next step runs, the run fails and a fresh one
|
|
136
117
|
* re-uploads — which is what would have happened anyway, once, instead of five times.
|
|
118
|
+
*
|
|
119
|
+
* The `Classified` callers on `@alexkroman1/aai/step-errors` are the SDK's own
|
|
120
|
+
* `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns the
|
|
121
|
+
* SDK's `TranscribeError` into the DevKit's verdict: a missing key and a 400 stop, a
|
|
122
|
+
* 429 waits as long as the service asked. Every step here ends the same way for the
|
|
123
|
+
* same reason.
|
|
137
124
|
*/
|
|
138
125
|
export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
|
|
139
126
|
"use step";
|
|
140
127
|
|
|
141
|
-
const apiKey = apiKeyOrFatal();
|
|
142
128
|
const stored = await uploadInfo(uploadId);
|
|
143
|
-
await report(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
headers: { Authorization: apiKey, "Content-Type": "application/octet-stream" },
|
|
148
|
-
// An async iterable, not bytes: this file may be gigabytes, and nothing here holds
|
|
149
|
-
// more than one window of it. See the module doc.
|
|
150
|
-
body: windows(uploadId, stored.size),
|
|
151
|
-
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
|
|
152
|
-
});
|
|
153
|
-
if (!uploaded.ok) throw await failure(uploaded, "Upload");
|
|
154
|
-
const { upload_url: audioUrl } = (await uploaded.json()) as { upload_url?: string };
|
|
155
|
-
if (!audioUrl) {
|
|
156
|
-
return throwFatalStepError(new Error("The async API accepted the upload but named no URL."));
|
|
157
|
-
}
|
|
158
|
-
return { audioUrl };
|
|
129
|
+
await report(
|
|
130
|
+
`Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) to the async API.`,
|
|
131
|
+
);
|
|
132
|
+
return await stepTranscribeUploadClassified(uploadId);
|
|
159
133
|
}
|
|
160
134
|
|
|
161
135
|
/** Retries beyond the default 3: an upload is the one call here worth another attempt. */
|
|
@@ -165,126 +139,51 @@ uploadToProvider.maxRetries = 5;
|
|
|
165
139
|
export async function createJob(audioUrl: string): Promise<{ id: string }> {
|
|
166
140
|
"use step";
|
|
167
141
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
body: JSON.stringify({ audio_url: audioUrl, speech_models: MODELS }),
|
|
172
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
173
|
-
});
|
|
174
|
-
if (!created.ok) throw await failure(created, "Submit");
|
|
175
|
-
const { id } = (await created.json()) as { id?: string };
|
|
176
|
-
if (!id) return throwFatalStepError(new Error("The async API created no transcript id."));
|
|
177
|
-
|
|
178
|
-
await report(`Submitted transcript ${id}.`);
|
|
179
|
-
return { id };
|
|
142
|
+
const job = await stepTranscribeSubmitClassified(audioUrl);
|
|
143
|
+
await report(`Submitted — job ${job.id}.`);
|
|
144
|
+
return job;
|
|
180
145
|
}
|
|
181
146
|
|
|
182
147
|
/**
|
|
183
|
-
* Ask once whether the job has finished.
|
|
148
|
+
* Ask once whether the job has finished, and read it when it has.
|
|
184
149
|
*
|
|
185
150
|
* `done` rather than the raw status, because the BODY branches on it and a body must
|
|
186
|
-
* not be
|
|
187
|
-
* otherwise
|
|
188
|
-
*
|
|
151
|
+
* not be where a provider's vocabulary is interpreted — a new status string would
|
|
152
|
+
* otherwise read as "not done yet" forever. A failed job is a terminal failure
|
|
153
|
+
* inside the SDK call, not a `done: true` the caller has to re-check.
|
|
189
154
|
*/
|
|
190
|
-
export async function pollTranscript(
|
|
191
|
-
"use step";
|
|
192
|
-
|
|
193
|
-
const res = await stepFetch(`${API}/v2/transcript/${id}`, {
|
|
194
|
-
headers: { Authorization: apiKeyOrFatal() },
|
|
195
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
196
|
-
});
|
|
197
|
-
if (!res.ok) throw await failure(res, `Transcript ${id}`);
|
|
198
|
-
const body = (await res.json()) as { status?: string; error?: string };
|
|
199
|
-
const status = body.status ?? "unknown";
|
|
200
|
-
if (status === "error") {
|
|
201
|
-
// The provider has decided; no number of polls changes it.
|
|
202
|
-
return throwFatalStepError(
|
|
203
|
-
new Error(
|
|
204
|
-
`The async API could not transcribe that recording: ${body.error ?? "no reason given"}`,
|
|
205
|
-
),
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
await report(`Transcript ${id} is ${status}.`);
|
|
209
|
-
return { done: status === "completed", status };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Read the finished transcript, and report it the way both sync flows do. */
|
|
213
|
-
export async function readTranscript(
|
|
155
|
+
export async function pollTranscript(
|
|
214
156
|
uploadId: string,
|
|
215
157
|
id: string,
|
|
216
158
|
startedAt: number,
|
|
217
|
-
): Promise<Transcript> {
|
|
159
|
+
): Promise<{ done: false } | { done: true; transcript: Transcript }> {
|
|
218
160
|
"use step";
|
|
219
161
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const body = (await res.json()) as { text?: string; audio_duration?: number };
|
|
226
|
-
const transcript = (body.text ?? "").trim();
|
|
227
|
-
const stored = await uploadInfo(uploadId);
|
|
162
|
+
const progress = await stepTranscribePollClassified(id);
|
|
163
|
+
if (!progress.done) {
|
|
164
|
+
await report(`Transcript ${id} is ${progress.status}.`);
|
|
165
|
+
return { done: false };
|
|
166
|
+
}
|
|
228
167
|
|
|
168
|
+
const stored = await uploadInfo(uploadId);
|
|
169
|
+
const transcript = progress.transcript.text;
|
|
229
170
|
return {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
171
|
+
done: true,
|
|
172
|
+
transcript: {
|
|
173
|
+
source: stored.name || uploadId,
|
|
174
|
+
// ONE, and it is not a fudge: the async API transcribed the recording in one
|
|
175
|
+
// piece, which is the difference this flow is here to show. A reader comparing
|
|
176
|
+
// the three sees 7 segments, 7 segments, and 1.
|
|
177
|
+
segments: 1,
|
|
178
|
+
// The provider's own measurement — the only one of the three flows that does
|
|
179
|
+
// not have to derive this from byte offsets.
|
|
180
|
+
durationMs: progress.transcript.durationMs,
|
|
181
|
+
// Wall clock, the same way both sync flows measure it — see `startClock`. For
|
|
182
|
+
// this flow it is mostly the provider's queue, which is exactly the thing a
|
|
183
|
+
// reader comparing the three wants to see.
|
|
184
|
+
elapsedMs: Date.now() - startedAt,
|
|
185
|
+
words: countWords(transcript),
|
|
186
|
+
transcript,
|
|
187
|
+
},
|
|
244
188
|
};
|
|
245
189
|
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* The stored upload as a sequence of windows, with the next one already in flight.
|
|
249
|
-
*
|
|
250
|
-
* A generator rather than one `readUpload`, because the whole point is that the file
|
|
251
|
-
* is never held: each window is read, sent, and dropped. `readUpload` clamps to what
|
|
252
|
-
* is stored, so the loop ends on the real end of the file even if `size` moved.
|
|
253
|
-
*
|
|
254
|
-
* **One window of READ-AHEAD**, which is the whole concurrency available here: the
|
|
255
|
-
* consumer is a socket and the producer is the app's own store, and read-then-send
|
|
256
|
-
* makes them strictly alternate — the store idles while bytes go out, and the socket
|
|
257
|
-
* idles while the next window is fetched. Starting the next read before yielding the
|
|
258
|
-
* current window overlaps them, so a gigabyte upload pays the larger of the two
|
|
259
|
-
* rather than their sum. Exactly one, not a queue: a deeper buffer holds more of a
|
|
260
|
-
* file this generator exists to avoid holding, and there is nothing to gain past
|
|
261
|
-
* keeping both ends busy.
|
|
262
|
-
*/
|
|
263
|
-
async function* windows(uploadId: string, size: number): AsyncGenerator<Uint8Array> {
|
|
264
|
-
const read = (at: number): Promise<Uint8Array> =>
|
|
265
|
-
readUpload(uploadId, { start: at, end: at + UPLOAD_WINDOW_BYTES }).then((slice) => slice.bytes);
|
|
266
|
-
let at = 0;
|
|
267
|
-
let next = at < size ? read(at) : undefined;
|
|
268
|
-
while (next !== undefined) {
|
|
269
|
-
const bytes = await next;
|
|
270
|
-
if (bytes.length === 0) return;
|
|
271
|
-
at += UPLOAD_WINDOW_BYTES;
|
|
272
|
-
// Issued BEFORE the yield, so the store is fetching while the socket sends.
|
|
273
|
-
next = at < size ? read(at) : undefined;
|
|
274
|
-
yield bytes;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** A failed call, classified for the DevKit — see `sync-api.ts` for the three-way rule. */
|
|
279
|
-
async function failure(res: Response, what: string): Promise<Error> {
|
|
280
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
281
|
-
return toStepError(
|
|
282
|
-
res,
|
|
283
|
-
`${what} failed: HTTP ${res.status}${body.error ? ` — ${body.error}` : ""}`,
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** A size a person can read, because the number that matters is the scale. */
|
|
288
|
-
function mb(bytes: number): string {
|
|
289
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
290
|
-
}
|