@alexkroman1/aai-cli 9.2.0 → 10.0.1
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/{_bundler-CDuYl5Gb.mjs → _bundler-B31MqYaD.mjs} +7 -33
- package/dist/_bundler.d.ts +3 -41
- package/dist/{_dev-server-BAR8Ya43.mjs → _dev-server-Bo5dSBzQ.mjs} +140 -44
- package/dist/_dev-server.d.ts +1 -8
- package/dist/_dev-vite-config.d.ts +21 -0
- package/dist/{_init-BNotRgV1.mjs → _init-BURbdHNM.mjs} +1 -1
- package/dist/{_slug-api--ZBmIGH-.mjs → _slug-api-CfGQq1Tt.mjs} +1 -1
- package/dist/{_templates-4WcKOjS5.mjs → _templates-0Zr2z6yA.mjs} +37 -2
- package/dist/_templates.d.ts +29 -0
- package/dist/{build-DvDdSwAS.mjs → build-DKC2e9o_.mjs} +1 -2
- package/dist/cli.mjs +46 -22
- package/dist/{delete-8pjH8n8n.mjs → delete-X_CRgCf6.mjs} +1 -1
- package/dist/{deploy-CjU3e-3P.mjs → deploy-CchW5pmw.mjs} +2 -2
- package/dist/{dev-dFzdvwa7.mjs → dev-BfVl1uBA.mjs} +1 -1
- package/dist/{init-DK7GpIF9.mjs → init-BRfoc2EK.mjs} +2 -2
- package/dist/{logs-D2Yhqe2w.mjs → logs-hWnJ66Bl.mjs} +2 -2
- package/dist/scaffold/CLAUDE.md +162 -54
- package/dist/scaffold/package.json +4 -6
- package/dist/scaffold/server.mjs +12 -23
- package/dist/{secret-CwgDLpSy.mjs → secret-B-3T5tvr.mjs} +2 -2
- package/dist/{studio-D3-HWIfk.mjs → studio-wVWNLREn.mjs} +4 -3
- package/dist/templates/call-audit/agent.eval.test.ts +1 -1
- package/dist/templates/call-audit/agent.test.ts +80 -8
- package/dist/templates/call-audit/client.tsx +87 -3
- package/dist/templates/call-audit/workflows/audit.ts +52 -32
- package/dist/templates/call-audit/workflows/ingest.ts +15 -15
- package/dist/templates/call-audit/workflows/media.ts +1 -1
- package/dist/templates/call-audit/workflows/summarize.ts +0 -4
- package/dist/templates/link-digest/agent.eval.test.ts +4 -2
- package/dist/templates/link-digest/agent.test.ts +25 -5
- package/dist/templates/link-digest/client.tsx +65 -7
- package/dist/templates/link-digest/workflows/digest.ts +39 -29
- package/dist/templates/podcast-digest/agent.eval.test.ts +5 -5
- package/dist/templates/podcast-digest/agent.test.ts +108 -2
- package/dist/templates/podcast-digest/client.tsx +89 -8
- package/dist/templates/podcast-digest/workflows/digest.ts +67 -48
- package/dist/templates/podcast-digest/workflows/feeds.ts +1 -4
- package/dist/templates/podcast-digest/workflows/slack.ts +5 -6
- package/dist/templates/recap-workflow/agent.eval.test.ts +72 -8
- package/dist/templates/recap-workflow/agent.test.ts +323 -83
- package/dist/templates/recap-workflow/shared.ts +4 -3
- package/dist/templates/recap-workflow/workflows/recap.ts +322 -86
- package/dist/templates/recap-workflow/workflows/tokens.ts +21 -0
- package/dist/templates/redline/agent.eval.test.ts +1 -1
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +83 -3
- package/dist/templates/redline/workflows/redline.ts +26 -23
- package/dist/templates/research-workflow/agent.eval.test.ts +9 -8
- package/dist/templates/research-workflow/agent.test.ts +32 -6
- package/dist/templates/research-workflow/agent.ts +4 -4
- package/dist/templates/research-workflow/shared.ts +4 -3
- package/dist/templates/research-workflow/workflows/prompts.ts +1 -1
- package/dist/templates/research-workflow/workflows/research.ts +35 -38
- package/dist/templates/spoken-summary/agent.eval.test.ts +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +10 -8
- package/dist/templates/spoken-summary/client.tsx +72 -3
- package/dist/templates/spoken-summary/workflows/summarize.ts +25 -21
- package/dist/templates/spoken-summary/workflows/transcribe.ts +15 -18
- package/dist/templates/transcription-workflow/agent.eval.test.ts +1 -1
- package/dist/templates/transcription-workflow/agent.test.ts +626 -14
- package/dist/templates/transcription-workflow/agent.ts +6 -4
- package/dist/templates/transcription-workflow/client.tsx +106 -308
- package/dist/templates/transcription-workflow/recover.test.ts +141 -0
- package/dist/templates/transcription-workflow/recover.ts +130 -0
- package/dist/templates/transcription-workflow/run-panel.tsx +203 -0
- package/dist/templates/transcription-workflow/total-latency.tsx +144 -0
- package/dist/templates/transcription-workflow/workflows/batch.ts +21 -24
- package/dist/templates/transcription-workflow/workflows/downsample.ts +273 -0
- package/dist/templates/transcription-workflow/workflows/normalize.ts +17 -40
- package/dist/templates/transcription-workflow/workflows/stream.ts +177 -24
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +69 -32
- package/dist/templates/transcription-workflow/workflows/wav.ts +15 -4
- package/dist/worker-bundler.d.ts +0 -14
- package/dist/worker-bundler.mjs +149 -1
- package/dist/{workflow-BflATMmU.mjs → workflow-BBu5oEDw.mjs} +84 -10
- package/dist/workflow.d.ts +1 -0
- package/package.json +4 -6
- package/dist/_workflow-scan.d.ts +0 -108
- package/dist/worker-bundler-CCVEDjm1.mjs +0 -650
- package/dist/workflow-bundler.d.ts +0 -138
- package/dist/{_studio-CKrsixd0.mjs → _studio-B1waXMuR.mjs} +1 -1
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
* Specs for the spoken-summary app's declaration and its four legs.
|
|
4
4
|
*
|
|
5
5
|
* **The body itself is not driven here**, and that is a property of what a
|
|
6
|
-
* workflow template demonstrates rather than a gap:
|
|
7
|
-
*
|
|
8
|
-
* function — so its HTTP handling, its fatal/retryable classification and what
|
|
6
|
+
* workflow template demonstrates rather than a gap: a step is an ordinary
|
|
7
|
+
* exported async function — so its HTTP handling, its fatal/retryable classification and what
|
|
9
8
|
* it returns are all testable, while durability, suspension and replay are not.
|
|
10
9
|
* A body test that looked like a durability test would be the worse failure;
|
|
11
10
|
* the real thing is exercised end to end by `aai-cli`'s
|
|
@@ -18,6 +17,8 @@
|
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
19
|
import { readUpload, uploadInfo } from "@alexkroman1/aai/step";
|
|
20
|
+
import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
|
|
21
|
+
import { createWorkflowCtx } from "@alexkroman1/aai/testing";
|
|
21
22
|
import {
|
|
22
23
|
installStubGateway,
|
|
23
24
|
installStubReporter,
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
installStubUploads,
|
|
27
28
|
} from "@alexkroman1/aai/testing/vitest";
|
|
28
29
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
29
|
-
import { FatalError, RetryableError } from "workflow";
|
|
30
30
|
import agentDef, { spokenSummary } from "./agent.ts";
|
|
31
31
|
import { speak, spokenSummaryFlow, summarize } from "./workflows/summarize.ts";
|
|
32
32
|
import { createJob, pollTranscript, uploadToProvider } from "./workflows/transcribe.ts";
|
|
@@ -233,7 +233,7 @@ describe("the whole run", () => {
|
|
|
233
233
|
/**
|
|
234
234
|
* Answer every leg's HTTP, so the BODY can be driven end to end.
|
|
235
235
|
*
|
|
236
|
-
* Imported through vitest
|
|
236
|
+
* Imported through vitest, a workflow
|
|
237
237
|
* function is an ordinary async function — so what this exercises is the
|
|
238
238
|
* ORDER the legs are wired in and the shape they hand each other, which is
|
|
239
239
|
* the one thing the per-leg specs above cannot see. Durability, suspension
|
|
@@ -276,7 +276,7 @@ describe("the whole run", () => {
|
|
|
276
276
|
installStubReporter();
|
|
277
277
|
installStubSpeech();
|
|
278
278
|
|
|
279
|
-
const summary = await spokenSummaryFlow({ recording: UPLOAD_ID });
|
|
279
|
+
const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx());
|
|
280
280
|
|
|
281
281
|
expect(summary).toEqual({
|
|
282
282
|
source: "standup.wav",
|
|
@@ -298,7 +298,7 @@ describe("the whole run", () => {
|
|
|
298
298
|
installStubReporter();
|
|
299
299
|
const speech = installStubSpeech();
|
|
300
300
|
|
|
301
|
-
await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" });
|
|
301
|
+
await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowCtx());
|
|
302
302
|
|
|
303
303
|
expect(speech.calls[0]).toMatchObject({ text: "Spoken.", voice: "michael" });
|
|
304
304
|
});
|
|
@@ -308,6 +308,8 @@ describe("the whole run", () => {
|
|
|
308
308
|
installStubReporter();
|
|
309
309
|
installStubSpeech();
|
|
310
310
|
|
|
311
|
-
await expect(spokenSummaryFlow({ recording: UPLOAD_ID })).rejects.toThrow(
|
|
311
|
+
await expect(spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx())).rejects.toThrow(
|
|
312
|
+
"corrupt audio",
|
|
313
|
+
);
|
|
312
314
|
});
|
|
313
315
|
});
|
|
@@ -30,6 +30,36 @@
|
|
|
30
30
|
* so the file picker exists because `agent.ts` declares `recording` in
|
|
31
31
|
* `uploads`, and the voice SELECT exists because it declares `voice` as an
|
|
32
32
|
* enum. Adding a field there adds a control here with no edit.
|
|
33
|
+
*
|
|
34
|
+
* ## A reload used to lose a run whose input was already paid for
|
|
35
|
+
*
|
|
36
|
+
* A `runId` names a run for as long as something holds it, and this page held it
|
|
37
|
+
* in React state — so a refresh lost it while the recording carried on being
|
|
38
|
+
* transcribed, summarized and spoken. The bytes were already stored, so the
|
|
39
|
+
* expensive half had happened; what an empty form invites is somebody uploading
|
|
40
|
+
* the recording again and paying for all four legs twice. `key` is the handle
|
|
41
|
+
* that survives a reload, `recover: true` is what reads it back, and the upload
|
|
42
|
+
* half is already the SDK's — picking the same file again sends only the windows
|
|
43
|
+
* that did not land.
|
|
44
|
+
*
|
|
45
|
+
* **The key is `useRunKey()`** — opaque, and in `sessionStorage`, which is the
|
|
46
|
+
* same lifetime as that upload recall, so both halves of a reload make the same
|
|
47
|
+
* promise. What that rules out is a `?key=` parameter, and it is worth being
|
|
48
|
+
* plain about the trade because this template exists to produce something
|
|
49
|
+
* sendable: a summary you can listen to is exactly the sort of thing somebody
|
|
50
|
+
* forwards, and a URL is how people forward things. But there is no per-user
|
|
51
|
+
* filtering behind `find`, so the key IS the access control — a leaked one
|
|
52
|
+
* grants, on this agent, the whole TRANSCRIPT of the recording, the summary,
|
|
53
|
+
* the synthesized audio, and every other run filed under the same key. And URLs
|
|
54
|
+
* leak by ordinary use: chats, history, referrers. Sharing the artifact needs
|
|
55
|
+
* none of that — the page offers `Download summary.wav` and renders the words,
|
|
56
|
+
* so a person sends the file to exactly who they meant. Deriving the key from
|
|
57
|
+
* the recording would be worse again: two people summarizing one file would
|
|
58
|
+
* recover each other's runs.
|
|
59
|
+
*
|
|
60
|
+
* A real app with accounts passes the account's own id instead, and then a
|
|
61
|
+
* summary follows the person to another device — a promise only a login can
|
|
62
|
+
* keep.
|
|
33
63
|
*/
|
|
34
64
|
|
|
35
65
|
import "@alexkroman1/aai-ui/styles.css";
|
|
@@ -44,10 +74,12 @@ import {
|
|
|
44
74
|
SubmitButton,
|
|
45
75
|
UploadProgressBar,
|
|
46
76
|
useDownloadUrl,
|
|
77
|
+
useRunKey,
|
|
47
78
|
useWorkflowSubmit,
|
|
48
79
|
WorkflowFields,
|
|
49
80
|
WorkflowProgress,
|
|
50
81
|
} from "@alexkroman1/aai-ui";
|
|
82
|
+
import { useState } from "react";
|
|
51
83
|
import type { spokenSummary } from "./agent.ts";
|
|
52
84
|
|
|
53
85
|
/**
|
|
@@ -67,6 +99,20 @@ const WORKFLOW = "spokenSummary";
|
|
|
67
99
|
*/
|
|
68
100
|
const api = createWorkflowApi();
|
|
69
101
|
|
|
102
|
+
/**
|
|
103
|
+
* What the page says while a run is in flight — three situations, one line
|
|
104
|
+
* each.
|
|
105
|
+
*
|
|
106
|
+
* The reload case gets its own words deliberately: somebody who did not press
|
|
107
|
+
* the button is owed an explanation for a summary appearing in front of them,
|
|
108
|
+
* and it is the line that keeps them from uploading the recording again.
|
|
109
|
+
*/
|
|
110
|
+
function pendingNote(startedHere: boolean, found: boolean): string {
|
|
111
|
+
if (startedHere) return "Reloading is safe — this page will find the summary again.";
|
|
112
|
+
if (!found) return "Looking for a summary this tab started earlier…";
|
|
113
|
+
return "Still working on a recording this tab uploaded earlier — no need to send it again.";
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
/**
|
|
71
117
|
* The spoken text as a one-cue WebVTT track, inline.
|
|
72
118
|
*
|
|
@@ -82,11 +128,20 @@ function captionsUrl(text: string, durationMs: number): string {
|
|
|
82
128
|
}
|
|
83
129
|
|
|
84
130
|
export function App() {
|
|
131
|
+
// This tab's handle on its own summaries, in the store the upload recall uses
|
|
132
|
+
// — see the module doc for why a `?key=` is the wrong trade here.
|
|
133
|
+
const key = useRunKey();
|
|
134
|
+
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
135
|
+
// the page can tell "working on what you just sent" from "picking up where
|
|
136
|
+
// you left off" — the hook reports the run, not who asked for it.
|
|
137
|
+
const [startedHere, setStartedHere] = useState(false);
|
|
85
138
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
86
|
-
// `run.output` instead of `unknown`.
|
|
139
|
+
// `run.output` instead of `unknown`. Neither half of the recovery is useful
|
|
140
|
+
// alone: without `key` there is nothing to find the run by, and without
|
|
141
|
+
// `recover` the key is only ever written.
|
|
87
142
|
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
|
|
88
143
|
typeof spokenSummary
|
|
89
|
-
>(WORKFLOW, { api });
|
|
144
|
+
>(WORKFLOW, { api, key, recover: true });
|
|
90
145
|
const output = run?.status === "completed" ? run.output : undefined;
|
|
91
146
|
// `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
|
|
92
147
|
// bytes have to be FETCHED and handed to the element as an object URL — and the
|
|
@@ -102,7 +157,14 @@ export function App() {
|
|
|
102
157
|
</p>
|
|
103
158
|
</header>
|
|
104
159
|
|
|
105
|
-
<Form
|
|
160
|
+
<Form
|
|
161
|
+
onSubmit={(values) => {
|
|
162
|
+
setStartedHere(true);
|
|
163
|
+
return submitForm(values);
|
|
164
|
+
}}
|
|
165
|
+
error={error}
|
|
166
|
+
className="flex flex-col gap-4"
|
|
167
|
+
>
|
|
106
168
|
{/* Every control, from the workflow's own input schema. See the module doc. */}
|
|
107
169
|
<WorkflowFields workflow={WORKFLOW} />
|
|
108
170
|
<SubmitButton pending={pending} pendingLabel="Working…">
|
|
@@ -110,6 +172,13 @@ export function App() {
|
|
|
110
172
|
</SubmitButton>
|
|
111
173
|
</Form>
|
|
112
174
|
|
|
175
|
+
{/* `pending` covers the RUN rather than the request, and on a reload it is
|
|
176
|
+
also true while the run is being looked up by key — the stretch where
|
|
177
|
+
an empty form would invite a second upload of the same recording. */}
|
|
178
|
+
{pending && (
|
|
179
|
+
<p className="text-sm opacity-70">{pendingNote(startedHere, run !== undefined)}</p>
|
|
180
|
+
)}
|
|
181
|
+
|
|
113
182
|
{/* The upload is its own wait, and the one nothing else can describe: the
|
|
114
183
|
run does not EXIST until the bytes are in, so there is no run id and
|
|
115
184
|
nothing for `<WorkflowProgress>` to read. */}
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* expensive one.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
+
import type { WorkflowCtx } from "@alexkroman1/aai";
|
|
52
53
|
import { report, stepSpeak, TRANSCRIBE_API, writeUpload } from "@alexkroman1/aai/step";
|
|
53
54
|
import { stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
|
|
54
55
|
import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
|
|
@@ -56,13 +57,12 @@ import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
|
|
|
56
57
|
// a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
|
|
57
58
|
// for `WorkflowOutputOf`.
|
|
58
59
|
import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
|
|
59
|
-
import { sleep } from "workflow";
|
|
60
60
|
import { z } from "zod";
|
|
61
61
|
import type { spokenSummary } from "../agent.ts";
|
|
62
62
|
import {
|
|
63
63
|
createJob,
|
|
64
64
|
MAX_POLLS,
|
|
65
|
-
|
|
65
|
+
POLL_INTERVAL_MS,
|
|
66
66
|
pollTranscript,
|
|
67
67
|
type Transcript,
|
|
68
68
|
uploadToProvider,
|
|
@@ -128,12 +128,11 @@ export type SpokenSummary = {
|
|
|
128
128
|
/** Transcribe a recording, summarize it, and read the summary back. */
|
|
129
129
|
export async function spokenSummaryFlow(
|
|
130
130
|
input: WorkflowInputOf<typeof spokenSummary>,
|
|
131
|
+
ctx: WorkflowCtx,
|
|
131
132
|
): Promise<SpokenSummary> {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const summary = await summarize(transcript.text);
|
|
136
|
-
const spoken = await speak(summary.spoken, input.voice);
|
|
133
|
+
const transcript = await transcribe(input.recording, ctx);
|
|
134
|
+
const summary = await ctx.step("summarize", () => summarize(transcript.text));
|
|
135
|
+
const spoken = await ctx.step("speak", () => speak(summary.spoken, input.voice));
|
|
137
136
|
|
|
138
137
|
return {
|
|
139
138
|
source: transcript.source,
|
|
@@ -151,20 +150,29 @@ export async function spokenSummaryFlow(
|
|
|
151
150
|
/**
|
|
152
151
|
* The whole first leg, factored out of the body.
|
|
153
152
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
153
|
+
* Part of the BODY rather than a step, and NOT because it is small: it CALLS
|
|
154
|
+
* steps and it sleeps durably between polls, neither of which may happen inside
|
|
155
|
+
* one. So it is replayed with the body — which is legal for the ordinary reason,
|
|
156
|
+
* that everything it does is either a `ctx.step` or a `ctx.sleep`, so a replay
|
|
157
|
+
* re-derives exactly the same sequence. It takes the `ctx` for that reason: a
|
|
158
|
+
* helper that reaches the journal has to be handed the handle.
|
|
159
159
|
*/
|
|
160
|
-
async function transcribe(recording: string): Promise<Transcript> {
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
async function transcribe(recording: string, ctx: WorkflowCtx): Promise<Transcript> {
|
|
161
|
+
// `maxAttempts: 6` was `uploadToProvider.maxRetries = 5` — five retries after
|
|
162
|
+
// the first attempt. It is the one step here worth extra patience: it streams
|
|
163
|
+
// the whole recording, so a transient failure is expensive to reach again.
|
|
164
|
+
const { audioUrl } = await ctx.step("uploadToProvider", () => uploadToProvider(recording), {
|
|
165
|
+
maxAttempts: 6,
|
|
166
|
+
});
|
|
167
|
+
const job = await ctx.step("createJob", () => createJob(audioUrl));
|
|
163
168
|
|
|
164
169
|
for (let poll = 0; poll < MAX_POLLS; poll += 1) {
|
|
165
|
-
|
|
170
|
+
// One call site in a loop, so each poll is its own journal entry
|
|
171
|
+
// (`pollTranscript#0`, `#1`, …) and a resume replays the polls already made
|
|
172
|
+
// rather than starting the wait over.
|
|
173
|
+
const progress = await ctx.step("pollTranscript", () => pollTranscript(recording, job.id));
|
|
166
174
|
if (progress.done) return progress.transcript;
|
|
167
|
-
await sleep(
|
|
175
|
+
await ctx.sleep(POLL_INTERVAL_MS);
|
|
168
176
|
}
|
|
169
177
|
// A plain throw: this is the BODY, where the fatal/retryable distinction has
|
|
170
178
|
// nothing to apply to. The transcript is not lost, so the message says where
|
|
@@ -179,8 +187,6 @@ async function transcribe(recording: string): Promise<Transcript> {
|
|
|
179
187
|
export async function summarize(
|
|
180
188
|
text: string,
|
|
181
189
|
): Promise<{ headline: string; points: string[]; spoken: string }> {
|
|
182
|
-
"use step";
|
|
183
|
-
|
|
184
190
|
await report("Summarizing the transcript.");
|
|
185
191
|
const reply = await stepGenerateJsonClassified(
|
|
186
192
|
"Summarize this transcript of a recording.\n\n" +
|
|
@@ -219,8 +225,6 @@ export async function speak(
|
|
|
219
225
|
script: string,
|
|
220
226
|
voice?: string,
|
|
221
227
|
): Promise<{ audio: string; durationMs: number }> {
|
|
222
|
-
"use step";
|
|
223
|
-
|
|
224
228
|
const spoken = await stepSpeak(script, omitUndefined({ voice }));
|
|
225
229
|
const stored = await writeUpload(spoken.audio, {
|
|
226
230
|
// Named, because this is what a person sees on the download link rather
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
*
|
|
23
23
|
* ## The steps are still OURS, and they have to be
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* A step is a `ctx.step(name, fn)` in the BODY, and only the body holds a `ctx` —
|
|
26
|
+
* so the SDK could not declare one even if it wanted to. That is a better
|
|
27
|
+
* boundary than the one it replaced: under the DevKit a step directive shipped
|
|
28
|
+
* inside a dependency was transformed by nothing and ran inline with no journal
|
|
29
|
+
* and no retry, SILENTLY, and the rule was a convention this comment had to
|
|
30
|
+
* state. Now the SDK owns what happens INSIDE a step and the app owns which
|
|
31
|
+
* steps exist — which is to say what gets journaled and what a retry repeats —
|
|
32
|
+
* and neither can accidentally take the other's half.
|
|
31
33
|
*
|
|
32
34
|
* **The async API rather than the sync one, and the choice is about the FORM.**
|
|
33
35
|
* The sync endpoint (`stepTranscribeSync`) answers inside the request and pays
|
|
@@ -46,13 +48,17 @@ import {
|
|
|
46
48
|
} from "@alexkroman1/aai/step-errors";
|
|
47
49
|
import { countWords, formatBytes } from "@alexkroman1/aai/utils";
|
|
48
50
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* How long between polls of a submitted job.
|
|
53
|
+
*
|
|
54
|
+
* Milliseconds: `ctx.sleep` takes a number or a `Date` and no duration STRING.
|
|
55
|
+
*/
|
|
56
|
+
export const POLL_INTERVAL_MS = 10_000;
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
59
|
* Polls before the run gives up on a job.
|
|
54
60
|
*
|
|
55
|
-
* At {@link
|
|
61
|
+
* At {@link POLL_INTERVAL_MS} this is an hour, well past what the async API takes
|
|
56
62
|
* for any recording it accepts. Bounded rather than endless because a job that
|
|
57
63
|
* never leaves `queued` is a run that would otherwise be replayed forever.
|
|
58
64
|
*/
|
|
@@ -86,8 +92,6 @@ export type Transcript = {
|
|
|
86
92
|
* same way for the same reason.
|
|
87
93
|
*/
|
|
88
94
|
export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
|
|
89
|
-
"use step";
|
|
90
|
-
|
|
91
95
|
const stored = await uploadInfo(uploadId);
|
|
92
96
|
await report(
|
|
93
97
|
`Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
|
|
@@ -95,13 +99,8 @@ export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: st
|
|
|
95
99
|
return await stepTranscribeUploadClassified(uploadId);
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
/** Retries beyond the default 3: an upload is the one call here worth another attempt. */
|
|
99
|
-
uploadToProvider.maxRetries = 5;
|
|
100
|
-
|
|
101
102
|
/** Create the transcription job, and answer with the id that outlives this run. */
|
|
102
103
|
export async function createJob(audioUrl: string): Promise<{ id: string }> {
|
|
103
|
-
"use step";
|
|
104
|
-
|
|
105
104
|
const job = await stepTranscribeSubmitClassified(audioUrl);
|
|
106
105
|
await report(`Transcribing — job ${job.id}.`);
|
|
107
106
|
return job;
|
|
@@ -120,8 +119,6 @@ export async function pollTranscript(
|
|
|
120
119
|
uploadId: string,
|
|
121
120
|
id: string,
|
|
122
121
|
): Promise<{ done: false } | { done: true; transcript: Transcript }> {
|
|
123
|
-
"use step";
|
|
124
|
-
|
|
125
122
|
const progress = await stepTranscribePollClassified(id);
|
|
126
123
|
if (!progress.done) return { done: false };
|
|
127
124
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// that answers "is the transcript really of the recording".
|
|
25
25
|
//
|
|
26
26
|
// WHAT NO EVAL HERE COVERS: durability. Imported through vitest with no bundler
|
|
27
|
-
// in the path, a
|
|
27
|
+
// in the path, a workflow body is an ordinary async function — no
|
|
28
28
|
// journal, no replay, and no per-step retry, so the resume-after-segment-27
|
|
29
29
|
// property this template exists to demonstrate is NOT exercised here, and a
|
|
30
30
|
// rate-limited live run fails where a deployed one would have ridden it out.
|