@alexkroman1/aai-cli 13.3.0 → 15.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-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/_artifacts-BJOYGQPp.mjs +21 -0
- package/dist/_artifacts.d.ts +16 -0
- package/dist/_build-target.d.ts +172 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +1 -1
- package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/_server-common-De0haHr9.mjs +70 -0
- package/dist/_server-common.d.ts +20 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
- package/dist/_templates.d.ts +1 -1
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/_vercel-output.d.ts +63 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-BhEaxBPu.mjs +481 -0
- package/dist/build.d.ts +23 -10
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +155 -138
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
- package/dist/client-bundler.d.ts +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
- package/dist/init.d.ts +14 -0
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +155 -76
- package/dist/scaffold/package.json +6 -6
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/start.d.ts +112 -0
- package/dist/start.mjs +156 -0
- package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
- package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/code-interpreter/agent.test.ts +103 -0
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +81 -19
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/math-buddy/agent.test.ts +126 -0
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/personal-finance/agent.test.ts +127 -0
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +27 -23
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +10 -7
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/agent.ts +8 -0
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/routing.ts +64 -55
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
- package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/templates/web-researcher/agent.test.ts +130 -0
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.d.ts +1 -1
- package/dist/worker-bundler.mjs +8 -8
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +9 -4
- package/dist/_server-common-BV4_CrP3.mjs +0 -43
- package/dist/build-CWyFGe-W.mjs +0 -69
- package/dist/scaffold/server.mjs +0 -204
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { countWords, formatDuration, plural } from "@alexkroman1/aai/utils";
|
|
22
22
|
import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
23
23
|
import {
|
|
24
|
+
Facts,
|
|
24
25
|
isTerminal,
|
|
25
26
|
useWorkflowProgress,
|
|
26
27
|
WORKFLOW_STATUS_LABELS,
|
|
@@ -136,7 +137,7 @@ export function RunPanel({ run, onClear }: { run: WorkflowRun<Transcript>; onCle
|
|
|
136
137
|
above, and the reason both exist: the status is `running` for the whole
|
|
137
138
|
fan-out, so a sixty-segment recording and a one-segment recording look
|
|
138
139
|
identical while they run. These lines come from the run itself
|
|
139
|
-
(`
|
|
140
|
+
(`stepReport()` in `workflows/transcribe.ts`), and they REPLAY, so looking a
|
|
140
141
|
finished run up in the panel below shows how it got there. */}
|
|
141
142
|
<WorkflowProgress runId={run.runId} />
|
|
142
143
|
|
|
@@ -150,15 +151,25 @@ export function RunPanel({ run, onClear }: { run: WorkflowRun<Transcript>; onCle
|
|
|
150
151
|
object with optional fields. */}
|
|
151
152
|
{run.status === "completed" && (
|
|
152
153
|
<>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
<Facts
|
|
155
|
+
size="xs"
|
|
156
|
+
items={[
|
|
157
|
+
`${run.output.segments} ${plural(run.output.segments, "segment")}`,
|
|
158
|
+
`${formatDuration(run.output.durationMs)} of audio`,
|
|
159
|
+
`took ${formatDuration(run.output.elapsedMs)}`,
|
|
160
|
+
`${run.output.words} words`,
|
|
161
|
+
]}
|
|
162
|
+
/>
|
|
158
163
|
<pre className="whitespace-pre-wrap text-sm leading-relaxed">{run.output.transcript}</pre>
|
|
159
164
|
</>
|
|
160
165
|
)}
|
|
161
|
-
{
|
|
166
|
+
{/* `role="alert"`, the same contract `<Form>` gives the submit error in
|
|
167
|
+
`client.tsx`: this is the outcome the reader waited minutes for. */}
|
|
168
|
+
{run.status === "failed" && (
|
|
169
|
+
<p role="alert" className="text-red-600">
|
|
170
|
+
{run.error}
|
|
171
|
+
</p>
|
|
172
|
+
)}
|
|
162
173
|
</section>
|
|
163
174
|
);
|
|
164
175
|
}
|
|
@@ -186,17 +197,26 @@ function LiveTranscript({ runId }: { runId: string }) {
|
|
|
186
197
|
// Memoized on the ARRAY, which the hook appends to per read: stitching is a
|
|
187
198
|
// seam search per segment, and a fan-out re-renders this panel on every
|
|
188
199
|
// progress poll whether or not anything arrived.
|
|
189
|
-
|
|
200
|
+
//
|
|
201
|
+
// The word count and the coverage are derived in the SAME memo, not below
|
|
202
|
+
// it: both are functions of `progress` alone, and `countWords` rescans a
|
|
203
|
+
// transcript that reaches ~14k words on a feature-length recording.
|
|
204
|
+
//
|
|
205
|
+
// `covered` is the furthest point reached, not the count: segments land out
|
|
206
|
+
// of order, so "6 segments" says nothing about how much is covered.
|
|
207
|
+
const { transcript, words, covered } = useMemo(() => {
|
|
208
|
+
const stitched = stitchChunks(progress);
|
|
209
|
+
return {
|
|
210
|
+
transcript: stitched,
|
|
211
|
+
words: countWords(stitched),
|
|
212
|
+
covered: progress.reduce((furthest, chunk) => Math.max(furthest, chunk.endMs), 0),
|
|
213
|
+
};
|
|
214
|
+
}, [progress]);
|
|
190
215
|
if (progress.length === 0) return null;
|
|
191
216
|
|
|
192
|
-
// The furthest point reached, not the count: segments land out of order, so
|
|
193
|
-
// "6 segments" says nothing about how much of the recording is covered.
|
|
194
|
-
const covered = Math.max(...progress.map((chunk) => chunk.endMs));
|
|
195
217
|
return (
|
|
196
218
|
<div className="flex flex-col gap-2">
|
|
197
|
-
<
|
|
198
|
-
{countWords(transcript)} words so far · through {formatDuration(covered)}
|
|
199
|
-
</p>
|
|
219
|
+
<Facts size="xs" items={[`${words} words so far`, `through ${formatDuration(covered)}`]} />
|
|
200
220
|
<pre className="whitespace-pre-wrap text-sm leading-relaxed opacity-80">{transcript}</pre>
|
|
201
221
|
</div>
|
|
202
222
|
);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { formatDuration } from "@alexkroman1/aai/utils";
|
|
18
|
+
import { Facts } from "@alexkroman1/aai-ui";
|
|
18
19
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -29,12 +30,17 @@ const STOPWATCH_TICK_MS = 250;
|
|
|
29
30
|
/** What {@link useTotalLatency} reports. */
|
|
30
31
|
export type TotalLatency = {
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
33
|
+
* The mark the clock started at, or undefined before the first submit.
|
|
34
|
+
*
|
|
35
|
+
* The raw mark rather than an elapsed duration, and that is the load-bearing
|
|
36
|
+
* part: an elapsed number is stale the instant it is computed, so a page
|
|
37
|
+
* holding one has to re-render to keep it true — which is what used to drive
|
|
38
|
+
* the whole tree at 4 Hz. {@link TotalLatency} turns these two marks into a
|
|
39
|
+
* duration, and only that section re-renders.
|
|
34
40
|
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
41
|
+
startedAt: number | undefined;
|
|
42
|
+
/** The elapsed time at the moment the run settled, once it has. */
|
|
43
|
+
frozenMs: number | undefined;
|
|
38
44
|
/** Start (or restart) the clock. Called from the form's own submit handler. */
|
|
39
45
|
start: () => void;
|
|
40
46
|
/** Drop it, for a panel that no longer describes the submission it timed. */
|
|
@@ -53,7 +59,10 @@ export type TotalLatency = {
|
|
|
53
59
|
*
|
|
54
60
|
* - **The interval re-renders; it does not accumulate.** The elapsed time is read
|
|
55
61
|
* from the clock at render, so a tick the tab throttled or dropped cannot make
|
|
56
|
-
* the number lag behind real time.
|
|
62
|
+
* the number lag behind real time. The interval lives in {@link TotalLatency}
|
|
63
|
+
* rather than here, because a hook called from the page makes its ticks the
|
|
64
|
+
* PAGE's re-renders — the form, the upload bar, the run panel and the whole
|
|
65
|
+
* history list, four times a second, to move one duration string.
|
|
57
66
|
* - **`performance.now()`, not `Date.now()`.** It is monotonic, so a clock
|
|
58
67
|
* correction (NTP, a laptop waking up) cannot make a transcription look
|
|
59
68
|
* instant — or negative.
|
|
@@ -61,8 +70,6 @@ export type TotalLatency = {
|
|
|
61
70
|
export function useTotalLatency(inFlight: boolean): TotalLatency {
|
|
62
71
|
const [startedAt, setStartedAt] = useState<number | undefined>(undefined);
|
|
63
72
|
const [frozenMs, setFrozenMs] = useState<number | undefined>(undefined);
|
|
64
|
-
// Re-render trigger only — see the doc above.
|
|
65
|
-
const [, tick] = useState(0);
|
|
66
73
|
// Whether `inFlight` has been seen true since the last `start()`. Without it,
|
|
67
74
|
// a start that lands one render before the submission reports itself in flight
|
|
68
75
|
// would freeze the clock at zero instead of running it.
|
|
@@ -72,8 +79,7 @@ export function useTotalLatency(inFlight: boolean): TotalLatency {
|
|
|
72
79
|
if (startedAt === undefined || frozenMs !== undefined) return;
|
|
73
80
|
if (inFlight) {
|
|
74
81
|
began.current = true;
|
|
75
|
-
|
|
76
|
-
return () => clearInterval(id);
|
|
82
|
+
return;
|
|
77
83
|
}
|
|
78
84
|
// Measured here rather than at render, so the frozen number is the one at the
|
|
79
85
|
// moment the run settled rather than whenever this page next drew.
|
|
@@ -93,8 +99,8 @@ export function useTotalLatency(inFlight: boolean): TotalLatency {
|
|
|
93
99
|
}, []);
|
|
94
100
|
|
|
95
101
|
return {
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
startedAt,
|
|
103
|
+
frozenMs,
|
|
98
104
|
start,
|
|
99
105
|
clear,
|
|
100
106
|
};
|
|
@@ -114,17 +120,31 @@ export function useTotalLatency(inFlight: boolean): TotalLatency {
|
|
|
114
120
|
* notices it finished. Clamped at zero, because the two numbers come from two
|
|
115
121
|
* different clocks on two different machines and a few milliseconds the wrong way
|
|
116
122
|
* would otherwise print a negative.
|
|
123
|
+
*
|
|
124
|
+
* The stopwatch interval lives HERE rather than in {@link useTotalLatency}, so a
|
|
125
|
+
* running clock re-renders this section and nothing else.
|
|
117
126
|
*/
|
|
118
127
|
export function TotalLatency({
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
startedAt,
|
|
129
|
+
frozenMs,
|
|
121
130
|
runMs,
|
|
122
131
|
}: {
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
startedAt: number | undefined;
|
|
133
|
+
frozenMs: number | undefined;
|
|
125
134
|
runMs: number | undefined;
|
|
126
135
|
}) {
|
|
127
|
-
|
|
136
|
+
// Re-render trigger only: the elapsed time is read from the clock below, so a
|
|
137
|
+
// tick the tab throttled or dropped cannot make the number lag real time.
|
|
138
|
+
const [, tick] = useState(0);
|
|
139
|
+
const running = startedAt !== undefined && frozenMs === undefined;
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!running) return;
|
|
142
|
+
const id = setInterval(() => tick((n) => n + 1), STOPWATCH_TICK_MS);
|
|
143
|
+
return () => clearInterval(id);
|
|
144
|
+
}, [running]);
|
|
145
|
+
|
|
146
|
+
if (startedAt === undefined) return null;
|
|
147
|
+
const elapsedMs = frozenMs ?? performance.now() - startedAt;
|
|
128
148
|
const outside = runMs === undefined ? undefined : Math.max(0, elapsedMs - runMs);
|
|
129
149
|
return (
|
|
130
150
|
<section className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 rounded-md border px-5 py-3">
|
|
@@ -134,9 +154,15 @@ export function TotalLatency({
|
|
|
134
154
|
<span className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
135
155
|
<span className="text-sm tabular-nums">{formatDuration(elapsedMs)}</span>
|
|
136
156
|
{runMs !== undefined && outside !== undefined && (
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
<Facts
|
|
158
|
+
size="xs"
|
|
159
|
+
as="span"
|
|
160
|
+
className="tabular-nums"
|
|
161
|
+
items={[
|
|
162
|
+
`${formatDuration(outside)} before the run`,
|
|
163
|
+
`${formatDuration(runMs)} inside it`,
|
|
164
|
+
]}
|
|
165
|
+
/>
|
|
140
166
|
)}
|
|
141
167
|
</span>
|
|
142
168
|
</section>
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
* request rather than two and the value journaled by the last poll IS the result.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
import type {
|
|
57
|
-
import {
|
|
56
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
57
|
+
import { stepReport, stepUploadInfo, TRANSCRIBE_API } from "@alexkroman1/aai/step";
|
|
58
58
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
stepTranscribePollOrFail,
|
|
60
|
+
stepTranscribeSubmitOrFail,
|
|
61
|
+
stepTranscribeUploadOrFail,
|
|
62
62
|
} from "@alexkroman1/aai/step-errors";
|
|
63
63
|
import { countWords, formatBytes } from "@alexkroman1/aai/utils";
|
|
64
64
|
import type { Transcript } from "./transcribe.ts";
|
|
@@ -78,7 +78,7 @@ const MAX_POLLS = 360;
|
|
|
78
78
|
/** Transcribe a recording through the async API. */
|
|
79
79
|
export async function transcribeBatchFlow(
|
|
80
80
|
input: { recording: string },
|
|
81
|
-
ctx:
|
|
81
|
+
ctx: WorkflowContext,
|
|
82
82
|
): Promise<Transcript> {
|
|
83
83
|
// Both at once: the clock does not depend on the upload, and issuing them
|
|
84
84
|
// together costs one round trip instead of two before a byte moves. Their issue
|
|
@@ -122,24 +122,24 @@ export async function transcribeBatchFlow(
|
|
|
122
122
|
* far smaller: if it expires before the next step runs, the run fails and a fresh one
|
|
123
123
|
* re-uploads — which is what would have happened anyway, once, instead of five times.
|
|
124
124
|
*
|
|
125
|
-
* The `
|
|
125
|
+
* The `OrFail` callers on `@alexkroman1/aai/step-errors` are the SDK's own
|
|
126
126
|
* `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns the
|
|
127
127
|
* SDK's `TranscribeError` into the DevKit's verdict: a missing key and a 400 stop, a
|
|
128
128
|
* 429 waits as long as the service asked. Every step here ends the same way for the
|
|
129
129
|
* same reason.
|
|
130
130
|
*/
|
|
131
131
|
export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
|
|
132
|
-
const stored = await
|
|
133
|
-
await
|
|
132
|
+
const stored = await stepUploadInfo(uploadId);
|
|
133
|
+
await stepReport(
|
|
134
134
|
`Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) to the async API.`,
|
|
135
135
|
);
|
|
136
|
-
return await
|
|
136
|
+
return await stepTranscribeUploadOrFail(uploadId);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** Create the transcription job, and answer with the id that outlives this run. */
|
|
140
140
|
export async function createJob(audioUrl: string): Promise<{ id: string }> {
|
|
141
|
-
const job = await
|
|
142
|
-
await
|
|
141
|
+
const job = await stepTranscribeSubmitOrFail(audioUrl);
|
|
142
|
+
await stepReport(`Submitted — job ${job.id}.`);
|
|
143
143
|
return job;
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -156,13 +156,13 @@ export async function pollTranscript(
|
|
|
156
156
|
id: string,
|
|
157
157
|
startedAt: number,
|
|
158
158
|
): Promise<{ done: false } | { done: true; transcript: Transcript }> {
|
|
159
|
-
const progress = await
|
|
159
|
+
const progress = await stepTranscribePollOrFail(id);
|
|
160
160
|
if (!progress.done) {
|
|
161
|
-
await
|
|
161
|
+
await stepReport(`Transcript ${id} is ${progress.status}.`);
|
|
162
162
|
return { done: false };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
const stored = await
|
|
165
|
+
const stored = await stepUploadInfo(uploadId);
|
|
166
166
|
const transcript = progress.transcript.text;
|
|
167
167
|
return {
|
|
168
168
|
done: true,
|
|
@@ -152,7 +152,7 @@ export function downsampleSegment(
|
|
|
152
152
|
const bind = sampleReaderFor(from);
|
|
153
153
|
const frame = blockAlign(from);
|
|
154
154
|
const inFrames = Math.floor(bytes.length / frame);
|
|
155
|
-
// A CALLER ERROR, and stated as one. `
|
|
155
|
+
// A CALLER ERROR, and stated as one. `stepReadUpload` clamps a window to what is
|
|
156
156
|
// stored, so the streaming flow can hand a short read down here; with no whole
|
|
157
157
|
// frame the averaging window below is zero wide, `total / 0` is `NaN`, and
|
|
158
158
|
// `setInt16` writes that as a 0 — a two-byte WAV of silence, transcribed and
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
72
|
import { basename, extname, join } from "node:path";
|
|
73
|
-
import { probeMedia, runFfmpeg, wavEncodeArgs } from "@alexkroman1/aai/ffmpeg";
|
|
74
|
-
import {
|
|
73
|
+
import { ffmpegBaseArgs, probeMedia, runFfmpeg, wavEncodeArgs } from "@alexkroman1/aai/ffmpeg";
|
|
74
|
+
import { stepReadUpload, stepReport, stepRequireCompleteUpload } from "@alexkroman1/aai/step";
|
|
75
75
|
import { throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
|
|
76
76
|
import { readUploadToFile, withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
|
|
77
77
|
import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
|
|
@@ -122,16 +122,16 @@ export type NormalizedRecording = {
|
|
|
122
122
|
* file that already exists instead of paying for a second one.
|
|
123
123
|
*/
|
|
124
124
|
export async function normalizeRecording(uploadId: string): Promise<NormalizedRecording> {
|
|
125
|
-
// `
|
|
125
|
+
// `stepRequireCompleteUpload`, not `stepUploadInfo`: `size` is the readable PREFIX, and
|
|
126
126
|
// every judgement below — cuttable, heavier-per-second, the byte count copied to
|
|
127
127
|
// disk — is about the WHOLE file.
|
|
128
|
-
const stored = await
|
|
129
|
-
const head = await
|
|
128
|
+
const stored = await stepRequireCompleteUpload(uploadId);
|
|
129
|
+
const head = await stepReadUpload(uploadId, { end: HEADER_PROBE_BYTES });
|
|
130
130
|
|
|
131
131
|
if (cuttable(head.bytes, stored.size) && !heavierThanNormalized(head.bytes, stored.size)) {
|
|
132
132
|
// No subprocess, no copy, no second upload. The overwhelmingly common case
|
|
133
133
|
// for a desk whose form says WAV, and the reason the check is a 64 KB read.
|
|
134
|
-
await
|
|
134
|
+
await stepReport(`${stored.name || uploadId} is already linear-PCM WAV — cutting it as it is.`);
|
|
135
135
|
return { recording: uploadId, converted: false };
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -144,7 +144,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
144
144
|
// WAV that is fine but too heavy to cut at this rate. Reporting "not a WAV we
|
|
145
145
|
// can cut" for the second one is a line that contradicts the file they
|
|
146
146
|
// uploaded.
|
|
147
|
-
await
|
|
147
|
+
await stepReport(
|
|
148
148
|
`Converting ${stored.name || uploadId} (${formatBytes(stored.size)}) — ` +
|
|
149
149
|
(cuttable(head.bytes, stored.size)
|
|
150
150
|
? `heavier per second than ${NORMALIZED_SAMPLE_RATE / 1000} kHz mono.`
|
|
@@ -164,7 +164,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
164
164
|
// `STEP_FILE_READ_CONCURRENCY` of them. Passing `size` means "I am judging
|
|
165
165
|
// completeness myself", which is what a body polling a still-arriving
|
|
166
166
|
// upload needs and is the opposite of what happened above: this step has
|
|
167
|
-
// already called `
|
|
167
|
+
// already called `stepRequireCompleteUpload`, so the file IS whole and the
|
|
168
168
|
// windows may land in any order. Omitting it lets `readUploadToFile`
|
|
169
169
|
// establish that for itself and fan out. The cost is one metadata round
|
|
170
170
|
// trip, against the dozens of window reads it overlaps.
|
|
@@ -177,22 +177,18 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
177
177
|
const info = await probeMedia(source, { timeoutMs: CONVERT_TIMEOUT_MS }).catch(
|
|
178
178
|
throwFfmpegStepError,
|
|
179
179
|
);
|
|
180
|
-
await
|
|
180
|
+
await stepReport(
|
|
181
181
|
`It is ${describeSource(info.audio?.codec, info.durationSec)} — re-encoding to ` +
|
|
182
182
|
`${NORMALIZED_SAMPLE_RATE / 1000} kHz mono WAV.`,
|
|
183
183
|
);
|
|
184
184
|
|
|
185
185
|
await runFfmpeg(
|
|
186
186
|
[
|
|
187
|
-
// The argv is the caller's, verbatim — `runFfmpeg` adds nothing
|
|
188
|
-
// standing flags are
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
"-loglevel",
|
|
193
|
-
"error",
|
|
194
|
-
"-nostdin",
|
|
195
|
-
"-y",
|
|
187
|
+
// The argv is the caller's, verbatim — `runFfmpeg` adds nothing — so
|
|
188
|
+
// the standing flags are the caller's too. This copy used to omit
|
|
189
|
+
// `-nostats`, which is the one that keeps ffmpeg's progress spam from
|
|
190
|
+
// evicting the error out of the captured stderr tail.
|
|
191
|
+
...ffmpegBaseArgs(),
|
|
196
192
|
"-i",
|
|
197
193
|
source,
|
|
198
194
|
...wavEncodeArgs({
|
|
@@ -212,7 +208,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
212
208
|
type: "audio/wav",
|
|
213
209
|
});
|
|
214
210
|
|
|
215
|
-
await
|
|
211
|
+
await stepReport(
|
|
216
212
|
`Converted to ${formatBytes(written.size)} of WAV (from ${formatBytes(stored.size)}).`,
|
|
217
213
|
);
|
|
218
214
|
return { recording: written.id, converted: true };
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* first byte with `complete: false` and its `size` grows as bytes land.
|
|
23
23
|
*
|
|
24
24
|
* The reader needed almost nothing for this, which is why this flow is so close to
|
|
25
|
-
* the other one: `
|
|
25
|
+
* the other one: `stepReadUpload` already clamped its window to what is stored (so a
|
|
26
26
|
* plan computed from a header could end one byte past the file), and that clamp is
|
|
27
27
|
* exactly "read what has arrived". So `transcribeSegment` below is `transcribe.ts`'s
|
|
28
28
|
* OWN step, unchanged, called on windows this body has checked are present.
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* 0.9 MB/s: `size` was 0 at every poll for 45 seconds and then the whole file, so
|
|
51
51
|
* the run planned nothing, transcribed nothing, and did its entire fan-out after
|
|
52
52
|
* the upload — the classic flow, with extra steps. `segmentStored` reads `ranges`
|
|
53
|
-
* instead, and `
|
|
53
|
+
* instead, and `stepReadUpload` clamps to the run a read starts in rather than to the
|
|
54
54
|
* prefix, so a window that has landed is a window this flow can work on.
|
|
55
55
|
* - **The stall test on the prefix would then FAIL a healthy upload.** A parts
|
|
56
56
|
* upload moving at full speed reports the same prefix at every poll, which is
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
* what keeps that order a pure function of journaled values.
|
|
168
168
|
*/
|
|
169
169
|
|
|
170
|
-
import type {
|
|
170
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
171
171
|
import {
|
|
172
172
|
mapConcurrent,
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
stepReadUpload,
|
|
174
|
+
stepReport,
|
|
175
|
+
stepUploadInfo,
|
|
175
176
|
type UploadRange,
|
|
176
|
-
uploadInfo,
|
|
177
177
|
} from "@alexkroman1/aai/step";
|
|
178
178
|
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
179
179
|
import { formatDuration, omitUndefined, plural } from "@alexkroman1/aai/utils";
|
|
@@ -308,7 +308,7 @@ export type StreamPlan = {
|
|
|
308
308
|
* an upload id exactly as in the classic flow; what differs is that the client chose
|
|
309
309
|
* it and the bytes are still on their way.
|
|
310
310
|
*/
|
|
311
|
-
export async function transcribeStreamFlow(input: { recording: string }, ctx:
|
|
311
|
+
export async function transcribeStreamFlow(input: { recording: string }, ctx: WorkflowContext) {
|
|
312
312
|
// `ctx.now()`, not a step: the engine journals the read under its own key, so
|
|
313
313
|
// every walk of this line sees the instant the first one did.
|
|
314
314
|
const startedAt = await ctx.now();
|
|
@@ -364,7 +364,7 @@ export async function transcribeStreamFlow(input: { recording: string }, ctx: Wo
|
|
|
364
364
|
// A segment is READY when its whole window is stored — except once the upload
|
|
365
365
|
// is complete, where `at.size` is the true total and the plan came from the
|
|
366
366
|
// header's DECLARED length: a recording that came up short leaves a final
|
|
367
|
-
// segment ending past the file, and `
|
|
367
|
+
// segment ending past the file, and `stepReadUpload` clamping is what makes that
|
|
368
368
|
// the right answer rather than an error.
|
|
369
369
|
const ready = plan.segments.filter(
|
|
370
370
|
(segment) =>
|
|
@@ -443,7 +443,7 @@ export async function transcribeStreamFlow(input: { recording: string }, ctx: Wo
|
|
|
443
443
|
* from.
|
|
444
444
|
*/
|
|
445
445
|
export async function probeUpload(id: string): Promise<UploadProgressView> {
|
|
446
|
-
const info = await
|
|
446
|
+
const info = await stepUploadInfo(id);
|
|
447
447
|
return {
|
|
448
448
|
size: info.size,
|
|
449
449
|
complete: info.complete,
|
|
@@ -609,7 +609,7 @@ export function storedBytes(size: number, ranges: readonly UploadRange[] | undef
|
|
|
609
609
|
* only the prefix and the run has nothing to do until the upload is over, which
|
|
610
610
|
* is the entire wait this flow exists to remove.
|
|
611
611
|
*
|
|
612
|
-
* A window has to be covered WHOLE by one run: `
|
|
612
|
+
* A window has to be covered WHOLE by one run: `stepReadUpload` clamps to the run a
|
|
613
613
|
* read starts in, so a segment straddling a hole would come back short and be
|
|
614
614
|
* transcribed as a fragment. `rangesOf` merges adjacent windows, so a run really
|
|
615
615
|
* is a contiguous stretch and one containment test is the whole check.
|
|
@@ -636,7 +636,7 @@ export function segmentStored(segment: Segment, at: UploadProgressView): boolean
|
|
|
636
636
|
* which is what the classic flow is for.
|
|
637
637
|
*/
|
|
638
638
|
export async function planStreamed(id: string): Promise<StreamPlan> {
|
|
639
|
-
const head = await
|
|
639
|
+
const head = await stepReadUpload(id, { end: HEADER_PROBE_BYTES });
|
|
640
640
|
const format = fatalOnUnsupported(() => parseWav(head.bytes, Number.POSITIVE_INFINITY));
|
|
641
641
|
if (!Number.isFinite(format.dataEnd)) {
|
|
642
642
|
return throwFatalStepError(
|
|
@@ -647,7 +647,7 @@ export async function planStreamed(id: string): Promise<StreamPlan> {
|
|
|
647
647
|
);
|
|
648
648
|
}
|
|
649
649
|
const segments = fatalOnUnsupported(() => planSegments(format));
|
|
650
|
-
await
|
|
650
|
+
await stepReport(
|
|
651
651
|
`Planned ${formatDuration(segments.at(-1)?.endMs ?? 0)} of audio as ` +
|
|
652
652
|
`${segments.length} ${plural(segments.length, "segment")} while it uploads.`,
|
|
653
653
|
);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* the same shape). It is called FROM steps, so it inherits their environment.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { stepTranscribeSyncOrFail } from "@alexkroman1/aai/step-errors";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Time one transcription, so the progress log carries LATENCY.
|
|
@@ -57,7 +57,7 @@ export function elapsed(ms: number): string {
|
|
|
57
57
|
* concatenated straight into the request body rather than into an intermediate
|
|
58
58
|
* buffer that doubles the segment's footprint.
|
|
59
59
|
*
|
|
60
|
-
* `
|
|
60
|
+
* `stepTranscribeSyncOrFail` — the SDK's own `stepTranscribeSync` plus
|
|
61
61
|
* `throwStepError`, and nothing else — is the whole of what this adds to the SDK
|
|
62
62
|
* call, and it is where the three-way call is made: a `FatalError` stops the DevKit retrying
|
|
63
63
|
* something that will answer the same way, a bare `RetryableError` retries in ONE
|
|
@@ -75,6 +75,6 @@ export async function transcribeWav(
|
|
|
75
75
|
filename: string,
|
|
76
76
|
label: string,
|
|
77
77
|
): Promise<string> {
|
|
78
|
-
const { text } = await
|
|
78
|
+
const { text } = await stepTranscribeSyncOrFail(bytes, { filename, label });
|
|
79
79
|
return text;
|
|
80
80
|
}
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* - **The audio is addressed by BYTE RANGE, never carried.** A workflow's input
|
|
49
49
|
* is journaled and replayed on every resume, so the recording lives in the
|
|
50
50
|
* app's own upload store and the run carries only its id; each step reads
|
|
51
|
-
* exactly its own window with `
|
|
51
|
+
* exactly its own window with `stepReadUpload`. Sixty steps therefore move the
|
|
52
52
|
* recording once between them, not sixty times.
|
|
53
53
|
* - **The fan-out is bounded by `mapConcurrent`, and the bound is not a detail.**
|
|
54
54
|
* The DevKit correlates a journal entry to a step call by the ORDER the call
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
* calls settle. Its module doc carries the argument; what matters here is that
|
|
58
58
|
* there is no barrier, so a slow segment costs only itself.
|
|
59
59
|
* - **The transcript STREAMS as it is produced.** Each segment is emitted the
|
|
60
|
-
* moment it lands (`
|
|
60
|
+
* moment it lands (`stepEmit(TRANSCRIPT_STREAM, …)`), so the page renders the
|
|
61
61
|
* answer growing rather than a status line and then everything at once. That is
|
|
62
62
|
* the difference a fan-out can make to a reader and a run output cannot: an
|
|
63
63
|
* `output` exists only when the last segment does.
|
|
64
64
|
*/
|
|
65
65
|
|
|
66
|
-
import type {
|
|
66
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
67
67
|
import {
|
|
68
|
-
emit,
|
|
69
68
|
mapConcurrent,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
stepEmit,
|
|
70
|
+
stepReadUpload,
|
|
71
|
+
stepReport,
|
|
72
|
+
stepRequireCompleteUpload,
|
|
73
|
+
stepUploadInfo,
|
|
74
74
|
wavHeader,
|
|
75
75
|
} from "@alexkroman1/aai/step";
|
|
76
76
|
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
@@ -248,7 +248,7 @@ export type SegmentTranscript = {
|
|
|
248
248
|
* The input is what `POST /workflows/runs` carries — see `agent.ts` for the
|
|
249
249
|
* schema it is validated against before a run exists.
|
|
250
250
|
*/
|
|
251
|
-
export async function transcribeFlow(input: { recording: string }, ctx:
|
|
251
|
+
export async function transcribeFlow(input: { recording: string }, ctx: WorkflowContext) {
|
|
252
252
|
// Both at once: neither needs the other, and issued together they are one
|
|
253
253
|
// round trip instead of two before any audio is read. The ORDER is still a
|
|
254
254
|
// pure function of this line — the two calls go out synchronously, left to
|
|
@@ -313,13 +313,13 @@ export async function splitRecording(uploadId: string): Promise<{
|
|
|
313
313
|
// half-arrived recording this planned a fan-out over the first half and the run
|
|
314
314
|
// returned a transcript of it, reporting success. `stream.ts` is the flow for a
|
|
315
315
|
// recording that is still landing; this one wants all of it.
|
|
316
|
-
const stored = await
|
|
317
|
-
const head = await
|
|
316
|
+
const stored = await stepRequireCompleteUpload(uploadId);
|
|
317
|
+
const head = await stepReadUpload(uploadId, { end: HEADER_PROBE_BYTES });
|
|
318
318
|
const format = fatalOnUnsupported(() => parseWav(head.bytes, stored.size));
|
|
319
319
|
const segments = fatalOnUnsupported(() => planSegments(format));
|
|
320
320
|
const durationMs = segments.at(-1)?.endMs ?? 0;
|
|
321
321
|
|
|
322
|
-
await
|
|
322
|
+
await stepReport(
|
|
323
323
|
`Split ${formatDuration(durationMs)} of audio into ${segments.length} ${plural(segments.length, "segment")}.`,
|
|
324
324
|
);
|
|
325
325
|
return { format, segments, durationMs };
|
|
@@ -345,12 +345,14 @@ export async function transcribeSegment(
|
|
|
345
345
|
// calls together, so their lines interleave by completion — the page renders a
|
|
346
346
|
// log, not a sequence, and `segment.index` is what puts the TRANSCRIPT back in
|
|
347
347
|
// order.
|
|
348
|
-
await
|
|
348
|
+
await stepReport(
|
|
349
|
+
`Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`,
|
|
350
|
+
);
|
|
349
351
|
|
|
350
352
|
// `[start, end)`, the same half-open pair `planSegments` produced — the store
|
|
351
353
|
// owns the conversion to HTTP's inclusive range, so there is no `- 1` here to
|
|
352
354
|
// get wrong.
|
|
353
|
-
const audio = await
|
|
355
|
+
const audio = await stepReadUpload(uploadId, { start: segment.start, end: segment.end });
|
|
354
356
|
|
|
355
357
|
// The audio and nothing else. A `config` part carrying `language_code` used
|
|
356
358
|
// to ride along, and it is gone with the picker that fed it: the model detects
|
|
@@ -401,15 +403,15 @@ export async function transcribeSegment(
|
|
|
401
403
|
);
|
|
402
404
|
// The LATENCY, which is what says whether the concurrency bound or the endpoint
|
|
403
405
|
// is the thing limiting the run — see `timed`'s doc.
|
|
404
|
-
await
|
|
406
|
+
await stepReport(
|
|
405
407
|
`Transcribed ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)} in ${elapsed(ms)}.`,
|
|
406
408
|
);
|
|
407
409
|
// And the WORDS, into their own stream, which is what makes this run's answer
|
|
408
410
|
// streamable rather than only its narration: the page stitches whatever has
|
|
409
411
|
// arrived and renders the transcript growing, minutes before `output` exists.
|
|
410
|
-
// Its own namespace because `
|
|
411
|
-
// verbatim — see `
|
|
412
|
-
await
|
|
412
|
+
// Its own namespace because `stepReport`'s stream carries sentences a page prints
|
|
413
|
+
// verbatim — see `stepEmit`'s doc.
|
|
414
|
+
await stepEmit(TRANSCRIPT_STREAM, {
|
|
413
415
|
index: segment.index,
|
|
414
416
|
startMs: segment.startMs,
|
|
415
417
|
endMs: segment.endMs,
|
|
@@ -422,7 +424,7 @@ export async function transcribeSegment(
|
|
|
422
424
|
* Stitch the segments into one transcript.
|
|
423
425
|
*
|
|
424
426
|
* A step rather than a pure call in the body, and the reason is the narration:
|
|
425
|
-
* the body replays from the top on every resume, so a `
|
|
427
|
+
* the body replays from the top on every resume, so a `stepReport()` written there
|
|
426
428
|
* is re-emitted on each one. Journaling the finished transcript also means a
|
|
427
429
|
* caller re-reading a completed run gets the same bytes rather than a value
|
|
428
430
|
* recomputed from parts.
|
|
@@ -433,7 +435,7 @@ export async function mergeTranscript(
|
|
|
433
435
|
parts: readonly SegmentTranscript[],
|
|
434
436
|
startedAt: number,
|
|
435
437
|
): Promise<Transcript> {
|
|
436
|
-
await
|
|
438
|
+
await stepReport(`Stitching ${parts.length} ${plural(parts.length, "segment")} together.`);
|
|
437
439
|
|
|
438
440
|
// `mapConcurrent` resolves in ITEM order however the calls settled, so this is
|
|
439
441
|
// already ordered — sorted anyway, because the merge is where an ordering
|
|
@@ -443,7 +445,7 @@ export async function mergeTranscript(
|
|
|
443
445
|
|
|
444
446
|
// The FILENAME, not the id: the page prints this, and `upl_9f3…` tells a
|
|
445
447
|
// reader nothing about which recording they are looking at.
|
|
446
|
-
const source = (await
|
|
448
|
+
const source = (await stepUploadInfo(uploadId)).name || uploadId;
|
|
447
449
|
return {
|
|
448
450
|
source,
|
|
449
451
|
segments: parts.length,
|
|
@@ -457,19 +459,6 @@ export async function mergeTranscript(
|
|
|
457
459
|
};
|
|
458
460
|
}
|
|
459
461
|
|
|
460
|
-
// Re-exported rather than re-declared: `stream.ts` and `batch.ts` already import
|
|
461
|
-
// these from this module, and the split that let the PAGE stitch a partial
|
|
462
|
-
// transcript should not ripple through every flow. `clock` and `countWords` used
|
|
463
|
-
// to be in this list and are `formatDuration`/`countWords` on
|
|
464
|
-
// `@alexkroman1/aai/utils` now — a run narrates itself and the page renders the
|
|
465
|
-
// same run, so those two were a private copy of a formatter the SDK ships.
|
|
466
|
-
export {
|
|
467
|
-
stitchChunks,
|
|
468
|
-
stitchTranscript,
|
|
469
|
-
TRANSCRIPT_STREAM,
|
|
470
|
-
type TranscriptChunk,
|
|
471
|
-
} from "./stitch.ts";
|
|
472
|
-
|
|
473
462
|
// ---- I/O helpers ------------------------------------------------------------
|
|
474
463
|
|
|
475
464
|
/**
|