@alexkroman1/aai-cli 6.10.0 → 6.11.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/scaffold/CLAUDE.md +58 -0
- package/dist/scaffold/package.json +3 -3
- package/dist/scaffold/server.mjs +12 -3
- package/dist/scaffold/vite.config.ts +1 -1
- package/dist/templates/call-audit/agent.test.ts +965 -0
- package/dist/templates/call-audit/agent.ts +158 -0
- package/dist/templates/call-audit/client.tsx +235 -0
- package/dist/templates/call-audit/workflows/audit.ts +305 -0
- package/dist/templates/call-audit/workflows/ingest.ts +259 -0
- package/dist/templates/call-audit/workflows/media.ts +647 -0
- package/dist/templates/call-audit/workflows/summarize.ts +206 -0
- package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
- package/dist/templates/call-audit/workflows/temp-media.ts +138 -0
- package/dist/templates/recap-workflow/agent.test.ts +11 -3
- package/dist/templates/recap-workflow/workflows/recap.ts +19 -8
- package/dist/templates/spoken-summary/agent.test.ts +343 -0
- package/dist/templates/spoken-summary/agent.ts +142 -0
- package/dist/templates/spoken-summary/client.tsx +225 -0
- package/dist/templates/spoken-summary/workflows/summarize.ts +242 -0
- package/dist/templates/spoken-summary/workflows/transcribe.ts +145 -0
- package/dist/templates/transcription-workflow/agent.test.ts +241 -18
- package/dist/templates/transcription-workflow/agent.ts +20 -6
- package/dist/templates/transcription-workflow/workflows/batch.ts +75 -173
- package/dist/templates/transcription-workflow/workflows/normalize.ts +343 -0
- package/dist/templates/transcription-workflow/workflows/stream.ts +6 -4
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -14
- package/dist/templates/transcription-workflow/workflows/wav.ts +31 -0
- package/package.json +3 -3
|
@@ -151,6 +151,7 @@ import {
|
|
|
151
151
|
transcribeSegment,
|
|
152
152
|
} from "./transcribe.ts";
|
|
153
153
|
import {
|
|
154
|
+
HEADER_PROBE_BYTES,
|
|
154
155
|
offsetToMs,
|
|
155
156
|
parseWav,
|
|
156
157
|
planSegments,
|
|
@@ -176,9 +177,6 @@ const POLL_INTERVAL = "5s";
|
|
|
176
177
|
*/
|
|
177
178
|
const MAX_IDLE_POLLS = 60;
|
|
178
179
|
|
|
179
|
-
/** Bytes probed for the WAV header — the same window `splitRecording` uses. */
|
|
180
|
-
const HEADER_PROBE_BYTES = 64 * 1024;
|
|
181
|
-
|
|
182
180
|
/** What one poll of the upload found. */
|
|
183
181
|
export type UploadProgressView = {
|
|
184
182
|
/** Bytes stored so far. */
|
|
@@ -339,8 +337,12 @@ export async function planStreamed(id: string): Promise<StreamPlan> {
|
|
|
339
337
|
* Not `plan.segments.length`: the plan came from the header's declared length, and a
|
|
340
338
|
* recording that came up short has segments that start past the end of the file.
|
|
341
339
|
* Counting those would leave the run waiting for audio nobody is going to send.
|
|
340
|
+
*
|
|
341
|
+
* Exported for its spec. It is the one piece of this flow's exit condition that is
|
|
342
|
+
* a pure function of journaled values, so it is the one a test can pin — and the
|
|
343
|
+
* failure it guards is a run that never ends rather than one that fails.
|
|
342
344
|
*/
|
|
343
|
-
function expectedSegments(plan: StreamPlan, size: number): number {
|
|
345
|
+
export function expectedSegments(plan: StreamPlan, size: number): number {
|
|
344
346
|
return plan.segments.filter((segment) => segment.start < size).length;
|
|
345
347
|
}
|
|
346
348
|
|
|
@@ -1,35 +1,25 @@
|
|
|
1
1
|
// Copyright 2026 the AAI authors. MIT license.
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* What both SYNC flows share, now that the endpoint itself is the SDK's.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the multipart shape, and the three-way failure classification.
|
|
5
|
+
* This module used to BE the endpoint: the URL, the model header, the raw-key auth,
|
|
6
|
+
* the deadline, the multipart shape and the three-way failure classification. All of
|
|
7
|
+
* that is `stepTranscribeSync` on `@alexkroman1/aai/utils` — the same request, with
|
|
8
|
+
* `transcription-workflow`'s own hard-won details (the unprefixed key, the
|
|
9
|
+
* `X-AAI-Model` header, `stepFetch` rather than `fetch` so a fan-out's rate limit
|
|
10
|
+
* arrives as a status rather than as a stream reset) carried into it.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* What is left is what belongs to the CALLER rather than to the endpoint, and it is
|
|
13
|
+
* all measurement: both flows time each request, because per-part latency is the one
|
|
14
|
+
* number that says which bound is actually binding.
|
|
15
|
+
*
|
|
16
|
+
* No directive, which is what lets it live under `workflows/` beside the bodies: the
|
|
17
|
+
* WDK builder scans this directory and transforms only what carries one (`wav.ts` is
|
|
18
|
+
* the same shape). It is called FROM steps, so it inherits their environment.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
/** The synchronous transcription endpoint. Global — it routes to the nearest region. */
|
|
23
|
-
const SYNC_ENDPOINT = "https://sync.assemblyai.com/transcribe";
|
|
24
|
-
|
|
25
|
-
/** Required on every sync request; the endpoint routes on it. */
|
|
26
|
-
const SYNC_MODEL = "universal-3-5-pro";
|
|
27
|
-
|
|
28
|
-
/** The key a step reads out of the agent env. Declared in `agent.ts`'s `requiredEnv`. */
|
|
29
|
-
const API_KEY_ENV = "ASSEMBLYAI_API_KEY";
|
|
30
|
-
|
|
31
|
-
/** The endpoint's own per-request deadline, plus room to upload. */
|
|
32
|
-
const SYNC_TIMEOUT_MS = 60_000;
|
|
21
|
+
import { throwStepError } from "@alexkroman1/aai/step-errors";
|
|
22
|
+
import { stepTranscribeSync } from "@alexkroman1/aai/utils";
|
|
33
23
|
|
|
34
24
|
/**
|
|
35
25
|
* Time one transcription, so the progress log carries LATENCY.
|
|
@@ -65,6 +55,14 @@ export function elapsed(ms: number): string {
|
|
|
65
55
|
* callers arrive at that differently: one re-attaches a header to a window it
|
|
66
56
|
* read, the other is handed parts that already carry one.
|
|
67
57
|
*
|
|
58
|
+
* `.catch(throwStepError)` is the whole of what this adds to the SDK call, and it is
|
|
59
|
+
* where the three-way call is made: a `FatalError` stops the DevKit retrying
|
|
60
|
+
* something that will answer the same way, a bare `RetryableError` retries in ONE
|
|
61
|
+
* SECOND (that class's own default), and a `RetryableError` carrying `retryAfter`
|
|
62
|
+
* waits exactly as long as the far side asked. The last matters here because a whole
|
|
63
|
+
* batch hits the rate limit together — a second later all of them ask again, where
|
|
64
|
+
* on the server's number they drain.
|
|
65
|
+
*
|
|
68
66
|
* @param label - How this piece is named in a failure. The CALLER's vocabulary
|
|
69
67
|
* (a segment's timestamp, a part's index), because it is what a reader of the
|
|
70
68
|
* log has in front of them.
|
|
@@ -74,72 +72,6 @@ export async function transcribeWav(
|
|
|
74
72
|
filename: string,
|
|
75
73
|
label: string,
|
|
76
74
|
): Promise<string> {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// `stepFetch`, not `fetch`, and here it is load-bearing rather than tidy:
|
|
81
|
-
// `fetch` speaks HTTP/2 wherever the far side offers it, which puts a whole
|
|
82
|
-
// batch of segments on ONE connection — and a capacity limit then arrives as a
|
|
83
|
-
// stream reset carrying no HTTP status for `toStepError` below to read. A
|
|
84
|
-
// fan-out is exactly the shape that breaks on. `sdk/step-fetch.ts` holds the
|
|
85
|
-
// measurements; a `StepTransportError` out of here is already retryable and
|
|
86
|
-
// already names its cause.
|
|
87
|
-
const response = await stepFetch(SYNC_ENDPOINT, {
|
|
88
|
-
method: "POST",
|
|
89
|
-
headers: {
|
|
90
|
-
// The raw key — this endpoint takes it unprefixed, and a `Bearer ` in
|
|
91
|
-
// front of it is a 401 that reads like a wrong key.
|
|
92
|
-
Authorization: apiKey,
|
|
93
|
-
"X-AAI-Model": SYNC_MODEL,
|
|
94
|
-
...part.headers,
|
|
95
|
-
},
|
|
96
|
-
body: part.body,
|
|
97
|
-
// Nothing here has a deadline of its own, and a hung upload inside a step is
|
|
98
|
-
// a run that never finishes rather than one that retries.
|
|
99
|
-
signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
|
|
100
|
-
});
|
|
101
|
-
if (!response.ok) throw await syncFailure(response, label);
|
|
102
|
-
|
|
103
|
-
const body = (await response.json()) as { text?: string };
|
|
104
|
-
return (body.text ?? "").trim();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The API key, or a terminal failure — three more attempts find the same gap.
|
|
109
|
-
*
|
|
110
|
-
* Exported because `batch.ts` calls the same provider on the same key and had
|
|
111
|
-
* written this, and its own `API_KEY_ENV`, again. The key belongs to the
|
|
112
|
-
* PROVIDER; only the endpoint and its failure shapes belong to this module.
|
|
113
|
-
*/
|
|
114
|
-
export function apiKeyOrFatal(): string {
|
|
115
|
-
try {
|
|
116
|
-
return requireStepEnv(API_KEY_ENV);
|
|
117
|
-
} catch (err: unknown) {
|
|
118
|
-
// `throwFatalStepError` rather than `throw new FatalError(…)`: that class
|
|
119
|
-
// takes only a message — no `cause` — so constructing one inside a `catch`
|
|
120
|
-
// loses the original where the linter (rightly) expects it preserved. Here
|
|
121
|
-
// the original is the ARGUMENT, and nothing is swallowed.
|
|
122
|
-
return throwFatalStepError(err);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* The sync endpoint's failure, with whatever it said about it.
|
|
128
|
-
*
|
|
129
|
-
* `toStepError` makes the three-way call: a `FatalError` stops the DevKit
|
|
130
|
-
* retrying something that will answer the same way, a bare `RetryableError`
|
|
131
|
-
* retries in ONE SECOND (that class's own default), and a `RetryableError`
|
|
132
|
-
* carrying `retryAfter` waits exactly as long as the far side asked. The last
|
|
133
|
-
* matters here because a whole batch hits the rate limit together — a second
|
|
134
|
-
* later all of them ask again, where on the server's number they drain.
|
|
135
|
-
*/
|
|
136
|
-
async function syncFailure(response: Response, label: string): Promise<Error> {
|
|
137
|
-
// Two shapes, documented: `{ error_code, message }` for a request problem and
|
|
138
|
-
// `{ detail }` for auth and rate limits.
|
|
139
|
-
const body = (await response.json().catch(() => ({}))) as { message?: string; detail?: string };
|
|
140
|
-
const detail = body.message ?? body.detail;
|
|
141
|
-
return toStepError(
|
|
142
|
-
response,
|
|
143
|
-
`${label} failed: HTTP ${response.status}${detail ? ` — ${detail}` : ""}`,
|
|
144
|
-
);
|
|
75
|
+
const { text } = await stepTranscribeSync(bytes, { filename, label }).catch(throwStepError);
|
|
76
|
+
return text;
|
|
145
77
|
}
|
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
* every directive body obeys — replayed from the top, so no live handles and no
|
|
8
8
|
* undurable decisions; step arguments and return values are serialized, so pass
|
|
9
9
|
* an id and not a payload — and both hold here unchanged. What this template
|
|
10
|
-
* adds is the shape a real provider limit forces on a workflow, and it is
|
|
10
|
+
* adds is the shape a real provider limit forces on a workflow, and it is four
|
|
11
11
|
* steps in a straight line:
|
|
12
12
|
*
|
|
13
13
|
* ```text
|
|
14
|
+
* normalizeRecording one step → an upload id in a format that can be cut
|
|
14
15
|
* splitRecording one step → the format + a byte range per segment
|
|
15
16
|
* transcribeSegment N steps → one sync API request each, bounded
|
|
16
17
|
* mergeTranscript one step → the stitched transcript
|
|
17
18
|
* ```
|
|
18
19
|
*
|
|
20
|
+
* The first is the newest and the one a reader is least likely to expect, since
|
|
21
|
+
* everything below it is arithmetic over a WAV and real recordings are not WAVs.
|
|
22
|
+
* `normalize.ts` is where ffmpeg enters, and its module doc carries why the
|
|
23
|
+
* conversion is file-to-file and why a temp file may not outlive its step.
|
|
24
|
+
*
|
|
19
25
|
* ## Why the SYNC endpoint, and why that forces a fan-out
|
|
20
26
|
*
|
|
21
27
|
* AssemblyAI has two pre-recorded APIs. The BATCH one takes a job and a webhook
|
|
@@ -59,6 +65,7 @@
|
|
|
59
65
|
|
|
60
66
|
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
61
67
|
import { emit, mapConcurrent, readUpload, report, uploadInfo } from "@alexkroman1/aai/utils";
|
|
68
|
+
import { normalizeRecording } from "./normalize.ts";
|
|
62
69
|
import {
|
|
63
70
|
clock,
|
|
64
71
|
countWords,
|
|
@@ -69,6 +76,7 @@ import {
|
|
|
69
76
|
import { elapsed, timed, transcribeWav } from "./sync-api.ts";
|
|
70
77
|
import {
|
|
71
78
|
bytesPerSecond,
|
|
79
|
+
HEADER_PROBE_BYTES,
|
|
72
80
|
parseWav,
|
|
73
81
|
planSegments,
|
|
74
82
|
SEGMENT_OVERLAP_SECONDS,
|
|
@@ -186,15 +194,6 @@ export function segmentConcurrency(format: WavFormat): number {
|
|
|
186
194
|
return Math.max(1, Math.min(MAX_SEGMENT_CONCURRENCY, Math.floor(BYTES_IN_FLIGHT / perSegment)));
|
|
187
195
|
}
|
|
188
196
|
|
|
189
|
-
/**
|
|
190
|
-
* Bytes probed for the WAV header.
|
|
191
|
-
*
|
|
192
|
-
* The canonical header is 44 bytes; a recorder that writes a `LIST` or `bext`
|
|
193
|
-
* chunk in front of the samples pushes the `data` chunk further out, and 64 KB
|
|
194
|
-
* covers every such file anyone has produced by accident.
|
|
195
|
-
*/
|
|
196
|
-
const HEADER_PROBE_BYTES = 64 * 1024;
|
|
197
|
-
|
|
198
197
|
/**
|
|
199
198
|
* What a finished run reports, whichever flow produced it.
|
|
200
199
|
*
|
|
@@ -234,7 +233,16 @@ export async function transcribeFlow(input: { recording: string }) {
|
|
|
234
233
|
// round trip instead of two before any audio is read. The ORDER is still a
|
|
235
234
|
// pure function of this line — the two calls go out synchronously, left to
|
|
236
235
|
// right — which is what a replay reproduces.
|
|
237
|
-
|
|
236
|
+
//
|
|
237
|
+
// The clock starts before the conversion rather than after it, because a
|
|
238
|
+
// reader comparing the three flows over one file is comparing what the desk
|
|
239
|
+
// COST them, and re-encoding an m4a is part of that.
|
|
240
|
+
const [startedAt, ready] = await Promise.all([startClock(), normalizeRecording(input.recording)]);
|
|
241
|
+
|
|
242
|
+
// `ready.recording` from here on, not `input.recording`: a converted file is a
|
|
243
|
+
// DIFFERENT upload, and cutting the original by offsets planned against the
|
|
244
|
+
// converted one is a fan-out of garbage that still reports success.
|
|
245
|
+
const plan = await splitRecording(ready.recording);
|
|
238
246
|
|
|
239
247
|
// One step per segment, bounded, in an order a replay reproduces exactly.
|
|
240
248
|
// A failed segment fails the RUN, deliberately: every sibling that finished is
|
|
@@ -242,11 +250,12 @@ export async function transcribeFlow(input: { recording: string }) {
|
|
|
242
250
|
// what is missing, where catching here to salvage a partial transcript would
|
|
243
251
|
// return a recording with a silent hole in it and report success.
|
|
244
252
|
const parts = await mapConcurrent(plan.segments, segmentConcurrency(plan.format), (segment) =>
|
|
245
|
-
transcribeSegment(
|
|
253
|
+
transcribeSegment(ready.recording, plan.format, segment),
|
|
246
254
|
);
|
|
247
255
|
|
|
248
|
-
//
|
|
249
|
-
//
|
|
256
|
+
// The ORIGINAL id, and only here: `mergeTranscript` uses it for the filename a
|
|
257
|
+
// reader sees, and `standup.m4a` is the recording they uploaded — where the
|
|
258
|
+
// converted copy is an artifact of how the desk works.
|
|
250
259
|
return await mergeTranscript(input.recording, plan.durationMs, parts, startedAt);
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -26,6 +26,21 @@
|
|
|
26
26
|
* anything else by name rather than trying to be clever — an MP3 or an M4A frame
|
|
27
27
|
* boundary cannot be found by arithmetic, and finding it means shipping a
|
|
28
28
|
* decoder into a step.
|
|
29
|
+
*
|
|
30
|
+
* ## Who READS the refusals below
|
|
31
|
+
*
|
|
32
|
+
* {@link UnsupportedRecordingError} is raised from here and handled in two
|
|
33
|
+
* completely different ways, which is worth knowing before editing a message:
|
|
34
|
+
*
|
|
35
|
+
* - The plain `transcribe` flow CONVERTS rather than refusing. `normalize.ts`
|
|
36
|
+
* calls {@link parseWav} as a QUESTION — a throw is its signal to hand the file
|
|
37
|
+
* to ffmpeg — so on that path no message here reaches a person, and a file this
|
|
38
|
+
* module rejects for a huge {@link MAX_BYTES_PER_SECOND} is one that
|
|
39
|
+
* normalization fixes by downsampling.
|
|
40
|
+
* - `transcribeStream` still refuses, and has to: it cuts a recording while the
|
|
41
|
+
* bytes are still arriving, and a partial file is not something ffmpeg can
|
|
42
|
+
* transcode. There the sentences below are the whole of what a person is told,
|
|
43
|
+
* so they keep naming the `ffmpeg` line that fixes the file.
|
|
29
44
|
*/
|
|
30
45
|
|
|
31
46
|
/** What the sync endpoint will accept in one request. */
|
|
@@ -57,6 +72,22 @@ export const MAX_SEGMENT_BYTES = 40 * 1024 * 1024;
|
|
|
57
72
|
/** The sync endpoint refuses audio shorter than this. */
|
|
58
73
|
export const MIN_SEGMENT_MS = 80;
|
|
59
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Bytes probed for the WAV header.
|
|
77
|
+
*
|
|
78
|
+
* The canonical header is 44 bytes; a recorder that writes a `LIST` or `bext`
|
|
79
|
+
* chunk in front of the samples pushes the `data` chunk further out, and 64 KB
|
|
80
|
+
* covers every such file anyone has produced by accident.
|
|
81
|
+
*
|
|
82
|
+
* Declared HERE because three callers need the same window, and the third one is
|
|
83
|
+
* what made a shared constant the rule rather than a preference: `splitRecording`
|
|
84
|
+
* cuts on it, `planFromHead` in `stream.ts` cuts on it, and `normalize.ts` decides
|
|
85
|
+
* whether to CONVERT on it. Two of those probing a different amount than the third
|
|
86
|
+
* is a file the desk converts and then cannot cut, or refuses and then converts —
|
|
87
|
+
* both silent, and neither reproducible from any one module.
|
|
88
|
+
*/
|
|
89
|
+
export const HEADER_PROBE_BYTES = 64 * 1024;
|
|
90
|
+
|
|
60
91
|
/**
|
|
61
92
|
* The largest `bytesPerSecond` this desk will cut, and the reason it is checked
|
|
62
93
|
* at PARSE time rather than at cut time.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aai": "bin.mjs"
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"p-timeout": "^7.0.1",
|
|
45
45
|
"vite": "^8.2.1",
|
|
46
46
|
"zod": "^4.4.3",
|
|
47
|
-
"@alexkroman1/aai": "6.
|
|
48
|
-
"@alexkroman1/aai-ui": "6.
|
|
47
|
+
"@alexkroman1/aai": "6.11.0",
|
|
48
|
+
"@alexkroman1/aai-ui": "6.11.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"playwright": "^1.62.1",
|