@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
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Splitting steps buys a cheaper retry: a failure re-runs one stage instead of
|
|
20
20
|
* five. It costs a MATERIALIZATION each, because a temp file cannot cross a step
|
|
21
|
-
* boundary (see
|
|
21
|
+
* boundary (see `@alexkroman1/aai/step-files`) — so a five-step version reads the whole
|
|
22
22
|
* recording out of the upload store five times, and on a 700 MB file that is the
|
|
23
23
|
* expensive part by an order of magnitude. The decode passes are cheap: ffmpeg
|
|
24
24
|
* resamples two orders of magnitude faster than realtime, so a two-hour recording
|
|
@@ -42,16 +42,33 @@
|
|
|
42
42
|
* printed last, so a capped tail holds it) and the pauses arrive in a **file**
|
|
43
43
|
* (one event per pause, so their size grows with the recording and a tail would
|
|
44
44
|
* silently drop the earliest ones). Both are read here.
|
|
45
|
+
*
|
|
46
|
+
* ## Everything ffmpeg-shaped is named from inside the step BODY
|
|
47
|
+
*
|
|
48
|
+
* `@alexkroman1/aai/ffmpeg` and `@alexkroman1/aai/step-files` both reach a
|
|
49
|
+
* `node:` builtin, and a name this module holds at MODULE scope keeps its import
|
|
50
|
+
* in the workflow bundle — which is compiled as a `node:vm` Script with no
|
|
51
|
+
* `require`. The import statements are at the top, as the SDK's own examples
|
|
52
|
+
* write them, and every name they bind is referenced only inside
|
|
53
|
+
* {@link ingestRecording}'s body, which the workflow transform removes along
|
|
54
|
+
* with the imports it is the only user of. A module-scope FUNCTION naming one is
|
|
55
|
+
* what breaks a run at replay; this template used to carry a whole
|
|
56
|
+
* `ffmpeg-verdict.ts` because of it, and `throwFfmpegStepError` — which reaches
|
|
57
|
+
* no `node:` builtin at all — is what dissolved the boundary.
|
|
58
|
+
*
|
|
59
|
+
* `analyse` stays here for the other half of that rule: everything IT names is
|
|
60
|
+
* pure, so it may survive into the bundle.
|
|
45
61
|
*/
|
|
46
62
|
|
|
47
63
|
import { readFile, stat } from "node:fs/promises";
|
|
48
64
|
import { join } from "node:path";
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
65
|
+
import { probeMedia, runFfmpeg } from "@alexkroman1/aai/ffmpeg";
|
|
66
|
+
import { pcmDurationMs, report, uploadInfo } from "@alexkroman1/aai/step";
|
|
67
|
+
import { throwFatalStepError, throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
|
|
68
|
+
import { readUploadToFile, withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
|
|
69
|
+
import { formatBytes, formatDuration, plural } from "@alexkroman1/aai/utils";
|
|
52
70
|
import {
|
|
53
71
|
ANALYSIS_FORMAT,
|
|
54
|
-
clock,
|
|
55
72
|
type Loudness,
|
|
56
73
|
MediaAnalysisError,
|
|
57
74
|
measureLoudnessArgs,
|
|
@@ -61,14 +78,13 @@ import {
|
|
|
61
78
|
type Silence,
|
|
62
79
|
speechFraction,
|
|
63
80
|
} from "./media.ts";
|
|
64
|
-
import { fileChunks, materializeUpload, withTempDir } from "./temp-media.ts";
|
|
65
81
|
|
|
66
82
|
/**
|
|
67
83
|
* How long any one ffmpeg invocation may run before it is killed.
|
|
68
84
|
*
|
|
69
85
|
* Well past what the work takes, because the reason for a bound at all is a file
|
|
70
86
|
* that makes a decoder pathological rather than one that is merely long. A
|
|
71
|
-
* `timeout` is retryable and an `exit` is not;
|
|
87
|
+
* `timeout` is retryable and an `exit` is not; `throwFfmpegStepError` decides.
|
|
72
88
|
*/
|
|
73
89
|
const FFMPEG_TIMEOUT_MS = 20 * 60_000;
|
|
74
90
|
|
|
@@ -108,119 +124,99 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
|
|
|
108
124
|
"use step";
|
|
109
125
|
|
|
110
126
|
const stored = await uploadInfo(uploadId);
|
|
111
|
-
await report(`Reading ${stored.name || uploadId} (${
|
|
112
|
-
|
|
113
|
-
return await withTempDir(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
127
|
+
await report(`Reading ${stored.name || uploadId} (${formatBytes(stored.size)}).`);
|
|
128
|
+
|
|
129
|
+
return await withTempDir(
|
|
130
|
+
async (dir) => {
|
|
131
|
+
const source = join(dir, "source");
|
|
132
|
+
const normalized = join(dir, "audio.pcm");
|
|
133
|
+
const silenceLog = join(dir, "silence.txt");
|
|
134
|
+
|
|
135
|
+
await readUploadToFile(uploadId, source, { size: stored.size });
|
|
136
|
+
|
|
137
|
+
// What it WAS, for the progress log and the page. Worth one ffprobe: "41
|
|
138
|
+
// minutes of aac" explains the shape of the run, where "the recording" leaves
|
|
139
|
+
// a reader guessing what the desk decided. On a temp FILE rather than a pipe,
|
|
140
|
+
// so a trailing index is readable.
|
|
141
|
+
const probed = await probeMedia(source, { timeoutMs: FFMPEG_TIMEOUT_MS }).catch(
|
|
142
|
+
throwFfmpegStepError,
|
|
143
|
+
);
|
|
144
|
+
const codec = probed.audio?.codec ?? "unknown";
|
|
145
|
+
await report(
|
|
146
|
+
`Levelling ${describeSource(codec, probed.durationSec)} to ${ANALYSIS_FORMAT.sampleRate / 1000} kHz mono.`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Pass one: measure. `-f null -` decodes every frame and writes no audio, so
|
|
150
|
+
// this costs a decode and produces five numbers.
|
|
151
|
+
const measured = await runFfmpeg(measureLoudnessArgs(source), {
|
|
152
|
+
timeoutMs: FFMPEG_TIMEOUT_MS,
|
|
153
|
+
}).catch(throwFfmpegStepError);
|
|
154
|
+
const loudness = analyse(() => parseLoudness(measured.stderr));
|
|
155
|
+
|
|
156
|
+
// Pass two: apply the measurement, find the pauses, write the audio.
|
|
157
|
+
await runFfmpeg(normalizeArgs(source, loudness, normalized, silenceLog), {
|
|
158
|
+
timeoutMs: FFMPEG_TIMEOUT_MS,
|
|
159
|
+
}).catch(throwFfmpegStepError);
|
|
160
|
+
|
|
161
|
+
// The duration comes from the BYTE COUNT, not from the original's header or
|
|
162
|
+
// from ffprobe. It is the only measurement that agrees with the byte offsets
|
|
163
|
+
// the fan-out will use — a container's declared duration can disagree with
|
|
164
|
+
// what was actually decoded (an AAC file's encoder padding puts this one ~16ms
|
|
165
|
+
// over), and a segment planned against the wrong one runs off the end.
|
|
166
|
+
const bytes = (await stat(normalized)).size;
|
|
167
|
+
const durationMs = pcmDurationMs(bytes, ANALYSIS_FORMAT);
|
|
168
|
+
|
|
169
|
+
// Verified on ffmpeg 6.1: `ametadata` creates the file at filter-init, so a
|
|
170
|
+
// recording with no pause in it leaves an EMPTY log rather than no log. A
|
|
171
|
+
// missing file here is therefore a real failure and not a case to tolerate.
|
|
172
|
+
const log = await readFile(silenceLog, "utf-8");
|
|
173
|
+
const silences = analyse(() => parseSilences(log, durationMs / 1000));
|
|
174
|
+
|
|
175
|
+
const written = await writeUploadFromFile(normalized, {
|
|
176
|
+
// Named after the original, so a download reads as the recording it came
|
|
177
|
+
// from. `.pcm` because that is what it is — raw samples with no header, and
|
|
178
|
+
// a `.wav` name on a headerless file is one no player will open.
|
|
179
|
+
name: `${baseName(stored.name || uploadId)}.pcm`,
|
|
180
|
+
// Not `audio/wav`: the type is served back on the byte route, and claiming a
|
|
181
|
+
// container this file does not have would be a lie a browser acts on. Not
|
|
182
|
+
// `audio/L16` either, which looks right and is not — that type is defined as
|
|
183
|
+
// BIG-endian 16-bit PCM, where this is `s16le`. Nothing plays this file; the
|
|
184
|
+
// fan-out reads byte ranges out of it and puts a real header back on each one
|
|
185
|
+
// with `encodeWav`.
|
|
186
|
+
type: "application/octet-stream",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await report(
|
|
190
|
+
`Levelled ${formatDuration(durationMs)} from ${loudness.inputLufs} LUFS, ` +
|
|
191
|
+
`${Math.round(speechFraction(silences, durationMs / 1000) * 100)}% speech across ` +
|
|
192
|
+
`${silences.length} ${plural(silences.length, "pause")}.`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
audio: written.id,
|
|
197
|
+
source: stored.name || uploadId,
|
|
198
|
+
codec,
|
|
199
|
+
durationMs,
|
|
200
|
+
bytes,
|
|
201
|
+
loudness,
|
|
202
|
+
silences,
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
{ prefix: "aai-call-audit-" },
|
|
206
|
+
);
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
/**
|
|
189
210
|
* Retries beyond the default 3.
|
|
190
211
|
*
|
|
191
212
|
* Not because a conversion is flaky — a corrupt file fails identically forever,
|
|
192
|
-
* and
|
|
213
|
+
* and `throwFfmpegStepError` is what stops the DevKit retrying that. It is the
|
|
193
214
|
* two I/O halves that are worth another attempt: this step reads a whole
|
|
194
215
|
* recording out of the store and writes a whole one back, and either can lose a
|
|
195
216
|
* connection on a file this size.
|
|
196
217
|
*/
|
|
197
218
|
ingestRecording.maxRetries = 5;
|
|
198
219
|
|
|
199
|
-
/**
|
|
200
|
-
* Turn an ffmpeg failure into the DevKit's verdict.
|
|
201
|
-
*
|
|
202
|
-
* The whole reason `FfmpegError.kind` exists, used the way it was meant to be: an
|
|
203
|
-
* `exit` is ffmpeg having read the file and refused it, so every retry re-reads
|
|
204
|
-
* the same bytes and reaches the same conclusion while burning the budget a real
|
|
205
|
-
* transient needs. A `timeout` or an `aborted` is worth another attempt, and a
|
|
206
|
-
* `missing-binary` is `aai dev` on a laptop with no ffmpeg — fatal, and already
|
|
207
|
-
* carrying the install instructions in its message.
|
|
208
|
-
*
|
|
209
|
-
* **The retryable arm goes through `throwStepError` even though it classifies
|
|
210
|
-
* nothing**, which is deliberate. `toStepError` reaches a verdict from a
|
|
211
|
-
* `Response` or from an SDK error that already carries one; an `FfmpegError` is
|
|
212
|
-
* neither, so it is rethrown UNCHANGED — which the DevKit treats as retryable by
|
|
213
|
-
* default, the outcome this arm wants. Constructing a `RetryableError` here
|
|
214
|
-
* instead would replace ffmpeg's own message and its `argv` with a sentence, and
|
|
215
|
-
* the argv is the thing you paste into a shell.
|
|
216
|
-
*/
|
|
217
|
-
export function classifyFfmpeg(err: unknown): never {
|
|
218
|
-
if (isFfmpegError(err) && (err.kind === "timeout" || err.kind === "aborted")) {
|
|
219
|
-
return throwStepError(err);
|
|
220
|
-
}
|
|
221
|
-
return throwFatalStepError(err);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
220
|
/**
|
|
225
221
|
* Run a `media.ts` reader, turning "I cannot read this analysis" into a terminal
|
|
226
222
|
* failure.
|
|
@@ -243,7 +239,8 @@ export function analyse<T>(read: () => T): T {
|
|
|
243
239
|
|
|
244
240
|
/** `41:20 of aac`, or as much of that as ffprobe would say. */
|
|
245
241
|
function describeSource(codec: string, durationSec: number | undefined): string {
|
|
246
|
-
const length =
|
|
242
|
+
const length =
|
|
243
|
+
durationSec === undefined ? undefined : formatDuration(Math.round(durationSec * 1000));
|
|
247
244
|
return length === undefined ? codec : `${length} of ${codec}`;
|
|
248
245
|
}
|
|
249
246
|
|
|
@@ -252,8 +249,3 @@ function baseName(name: string): string {
|
|
|
252
249
|
const dot = name.lastIndexOf(".");
|
|
253
250
|
return dot > 0 ? name.slice(0, dot) : name;
|
|
254
251
|
}
|
|
255
|
-
|
|
256
|
-
/** A size a person can read, because the number that matters is the scale. */
|
|
257
|
-
function mb(bytes: number): string {
|
|
258
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
259
|
-
}
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
* longer than the cap gets the blind cut, and says so.
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
|
-
import {
|
|
61
|
+
import type { PcmFormat } from "@alexkroman1/aai/step";
|
|
62
|
+
import { isRecord } from "@alexkroman1/aai/utils";
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* The format every recording is converted to before anything measures it.
|
|
@@ -578,17 +579,6 @@ export function speechFraction(silences: readonly Silence[], durationSec: number
|
|
|
578
579
|
return Math.max(0, Math.min(1, (durationSec - quiet) / durationSec));
|
|
579
580
|
}
|
|
580
581
|
|
|
581
|
-
/** `1:04:09`, or `4:09` under an hour — the shape a reader scans for. */
|
|
582
|
-
export function clock(ms: number): string {
|
|
583
|
-
const total = Math.max(0, Math.round(ms / 1000));
|
|
584
|
-
const seconds = String(total % 60).padStart(2, "0");
|
|
585
|
-
const minutes = Math.floor(total / 60) % 60;
|
|
586
|
-
const hours = Math.floor(total / 3600);
|
|
587
|
-
return hours > 0
|
|
588
|
-
? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}`
|
|
589
|
-
: `${minutes}:${seconds}`;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
582
|
/**
|
|
593
583
|
* A second, as a byte offset on a sample-frame boundary.
|
|
594
584
|
*
|
|
@@ -35,18 +35,12 @@
|
|
|
35
35
|
import { stat, writeFile } from "node:fs/promises";
|
|
36
36
|
import { join } from "node:path";
|
|
37
37
|
import { runFfmpeg } from "@alexkroman1/aai/ffmpeg";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
stepGenerateJson,
|
|
43
|
-
stepSpeak,
|
|
44
|
-
writeUpload,
|
|
45
|
-
} from "@alexkroman1/aai/utils";
|
|
38
|
+
import { report, stepSpeak } from "@alexkroman1/aai/step";
|
|
39
|
+
import { stepGenerateJsonClassified, throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
|
|
40
|
+
import { withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
|
|
41
|
+
import { formatBytes, formatDuration, omitUndefined, plural } from "@alexkroman1/aai/utils";
|
|
46
42
|
import { z } from "zod";
|
|
47
|
-
import {
|
|
48
|
-
import { clock, masterArgs } from "./media.ts";
|
|
49
|
-
import { fileChunks, withTempDir } from "./temp-media.ts";
|
|
43
|
+
import { masterArgs } from "./media.ts";
|
|
50
44
|
|
|
51
45
|
/** Risks the summary is reduced to. Enough to be useful, few enough to act on. */
|
|
52
46
|
const MAX_RISKS = 4;
|
|
@@ -114,8 +108,8 @@ export async function summarize(
|
|
|
114
108
|
"use step";
|
|
115
109
|
|
|
116
110
|
await report("Reading the transcript.");
|
|
117
|
-
const reply = await
|
|
118
|
-
`Audit this transcript of a recorded call (${source}, ${
|
|
111
|
+
const reply = await stepGenerateJsonClassified(
|
|
112
|
+
`Audit this transcript of a recorded call (${source}, ${formatDuration(durationMs)}).\n\n` +
|
|
119
113
|
"Answer with JSON only, in this shape:\n" +
|
|
120
114
|
`{"headline": "...", "risks": ["..."], "actions": ["..."], "spoken": "..."}\n\n` +
|
|
121
115
|
"- headline: one line naming what the call was about.\n" +
|
|
@@ -133,13 +127,14 @@ export async function summarize(
|
|
|
133
127
|
system: "You audit recorded calls. You answer with JSON and nothing else.",
|
|
134
128
|
schema: AuditReply,
|
|
135
129
|
},
|
|
136
|
-
// Classified
|
|
137
|
-
//
|
|
138
|
-
|
|
130
|
+
// The `Classified` caller is `stepGenerateJson` plus `throwStepError`, which is
|
|
131
|
+
// what reads the gateway's own status: a 429 is worth another attempt and a 400
|
|
132
|
+
// is not, and that is what tells the DevKit which.
|
|
133
|
+
);
|
|
139
134
|
|
|
140
135
|
await report(
|
|
141
|
-
`Found ${reply.risks.length}
|
|
142
|
-
`${reply.actions.length}
|
|
136
|
+
`Found ${reply.risks.length} ${plural(reply.risks.length, "risk")} and ` +
|
|
137
|
+
`${reply.actions.length} ${plural(reply.actions.length, "action")}.`,
|
|
143
138
|
);
|
|
144
139
|
return {
|
|
145
140
|
headline: reply.headline,
|
|
@@ -156,7 +151,7 @@ export async function summarize(
|
|
|
156
151
|
* records: a step is replayed by its RETURN VALUE, so an id is replayed and bytes
|
|
157
152
|
* are not. Split in two, the audio would have to cross the queue between them —
|
|
158
153
|
* megabytes of it, on every resume — and the temp file the mastering pass needs
|
|
159
|
-
* cannot cross a step boundary at all (see
|
|
154
|
+
* cannot cross a step boundary at all (see `@alexkroman1/aai/step-files`). Together, a resumed
|
|
160
155
|
* run replays the id and re-reads a file that is already there.
|
|
161
156
|
*
|
|
162
157
|
* The cost is that a retried step writes a second upload and abandons the first.
|
|
@@ -170,37 +165,37 @@ export async function narrate(
|
|
|
170
165
|
|
|
171
166
|
const spoken = await stepSpeak(script, omitUndefined({ voice }));
|
|
172
167
|
|
|
173
|
-
return await withTempDir(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
168
|
+
return await withTempDir(
|
|
169
|
+
async (dir) => {
|
|
170
|
+
const wav = join(dir, "spoken.wav");
|
|
171
|
+
const mp3 = join(dir, "summary.mp3");
|
|
172
|
+
|
|
173
|
+
// `writeFile` rather than a stream, and this is the one place in the template
|
|
174
|
+
// where holding the whole thing in memory is right: `stepSpeak` already
|
|
175
|
+
// returned it as a single `Uint8Array`, so streaming it to disk would be
|
|
176
|
+
// copying from the heap to the heap on the way. It is bounded by the script,
|
|
177
|
+
// which the schema keeps under 150 words.
|
|
178
|
+
await writeFile(wav, spoken.audio);
|
|
179
|
+
|
|
180
|
+
await runFfmpeg(masterArgs(wav, mp3), { timeoutMs: MASTER_TIMEOUT_MS }).catch(
|
|
181
|
+
throwFfmpegStepError,
|
|
182
|
+
);
|
|
183
|
+
const bytes = (await stat(mp3)).size;
|
|
184
|
+
|
|
185
|
+
const stored = await writeUploadFromFile(mp3, {
|
|
186
|
+
// Named, because this is what a person sees on the download link rather than
|
|
187
|
+
// an opaque id — and typed, because the byte route serves the type it was
|
|
188
|
+
// given and a browser will not play a file it was handed as bytes.
|
|
189
|
+
name: "audit.mp3",
|
|
190
|
+
type: "audio/mpeg",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await report(
|
|
194
|
+
`Recorded a ${Math.round(spoken.durationMs / 1000)}s audit in ${spoken.voice}'s voice — ` +
|
|
195
|
+
`${formatBytes(bytes)} of MP3, from ${formatBytes(spoken.audio.byteLength)} of WAV.`,
|
|
196
|
+
);
|
|
197
|
+
return { audio: stored.id, durationMs: spoken.durationMs, bytes };
|
|
198
|
+
},
|
|
199
|
+
{ prefix: "aai-call-audit-" },
|
|
200
|
+
);
|
|
206
201
|
}
|
|
@@ -7,12 +7,11 @@
|
|
|
7
7
|
* `transcription-workflow` has one: `stepTranscribeSync` is the SDK's — the URL,
|
|
8
8
|
* the raw-key auth (no `Bearer`, which is a 401 that reads like a wrong key), the
|
|
9
9
|
* multipart shape, the deadline and the three-way failure verdict all live there —
|
|
10
|
-
* so what is left at the call site is the
|
|
11
|
-
* DevKit, and that belongs somewhere a spec can reach it.
|
|
10
|
+
* so what is left at the call site is the classification that hands the verdict to
|
|
11
|
+
* the DevKit, and that belongs somewhere a spec can reach it.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import { stepTranscribeSync } from "@alexkroman1/aai/utils";
|
|
14
|
+
import { stepTranscribeSyncClassified } from "@alexkroman1/aai/step-errors";
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Transcribe one complete WAV.
|
|
@@ -22,8 +21,9 @@ import { stepTranscribeSync } from "@alexkroman1/aai/utils";
|
|
|
22
21
|
* stores headerless PCM on purpose (see `media.ts`) and puts a header back with
|
|
23
22
|
* `encodeWav` for exactly this call.
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* `stepTranscribeSyncClassified` — the SDK's own `stepTranscribeSync` plus
|
|
25
|
+
* `throwStepError`, and nothing else — is the whole of what this adds, and it is
|
|
26
|
+
* where the three-way call is made: a `FatalError` stops the DevKit retrying something that
|
|
27
27
|
* will answer the same way, a bare `RetryableError` retries in ONE SECOND (that
|
|
28
28
|
* class's own default), and a `RetryableError` carrying `retryAfter` waits exactly
|
|
29
29
|
* as long as the far side asked. The last matters here because a whole fan-out
|
|
@@ -39,6 +39,6 @@ export async function transcribeSpan(
|
|
|
39
39
|
filename: string,
|
|
40
40
|
label: string,
|
|
41
41
|
): Promise<string> {
|
|
42
|
-
const { text } = await
|
|
42
|
+
const { text } = await stepTranscribeSyncClassified(bytes, { filename, label });
|
|
43
43
|
return text;
|
|
44
44
|
}
|