@alexkroman1/aai-cli 13.2.0 → 14.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/bin.mjs +1 -1
- package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
- package/dist/_config.d.ts +25 -1
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +15 -7
- package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
- package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
- package/dist/_output.d.ts +24 -7
- package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
- package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
- package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
- package/dist/_utils.d.ts +35 -0
- package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
- package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-Mxk8gWvX.mjs +108 -0
- package/dist/build.d.ts +5 -0
- package/dist/cli.mjs +172 -116
- package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
- package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
- package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
- package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
- package/dist/eval.d.ts +2 -0
- package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
- package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
- package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/.env.example +17 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +6 -5
- package/dist/scaffold/server.mjs +129 -25
- package/dist/secret-Cjp912yF.mjs +162 -0
- package/dist/secret.d.ts +48 -13
- package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
- package/dist/templates/briefing-desk/agent.test.ts +7 -3
- package/dist/templates/call-audit/agent.test.ts +25 -9
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +40 -58
- 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/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/embedded-assets/agent.test.ts +7 -1
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.test.ts +8 -4
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +10 -4
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +42 -36
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/agent.test.ts +6 -1
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +33 -16
- 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 +33 -14
- package/dist/templates/podcast-digest/client.tsx +27 -36
- 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 +49 -32
- 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 +8 -2
- package/dist/templates/redline/client.tsx +47 -48
- 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 +17 -8
- 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/registry.test.ts +99 -58
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/simple/agent.test.ts +39 -6
- 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 +21 -12
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +36 -47
- 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/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +36 -20
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +34 -40
- 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/shared.ts +10 -14
- 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/test-D032V2cA.mjs +242 -0
- package/dist/test.d.ts +97 -31
- package/dist/typecheck.mjs +2 -2
- package/dist/worker-bundler.mjs +3 -3
- package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
- package/package.json +10 -10
- package/dist/bin.d.mts +0 -2
- package/dist/build-BLlLOrng.mjs +0 -66
- package/dist/secret-B-3T5tvr.mjs +0 -61
- package/dist/test-CiLab-AA.mjs +0 -182
- package/dist/tsdown.config.d.ts +0 -2
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
import { readdir } from "node:fs/promises";
|
|
22
22
|
import { tmpdir } from "node:os";
|
|
23
|
-
import {
|
|
23
|
+
import { stepReadUpload, type UploadRange } from "@alexkroman1/aai/step";
|
|
24
24
|
import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
|
|
25
|
-
import {
|
|
25
|
+
import { createWorkflowContext } from "@alexkroman1/aai/testing";
|
|
26
26
|
import {
|
|
27
27
|
installStubReporter,
|
|
28
28
|
installStubStepFetch,
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
requestFormat,
|
|
42
42
|
} from "./workflows/downsample.ts";
|
|
43
43
|
import { cuttable, heavierThanNormalized, normalizeRecording } from "./workflows/normalize.ts";
|
|
44
|
+
import { stitchChunks, stitchTranscript, TRANSCRIPT_STREAM } from "./workflows/stitch.ts";
|
|
44
45
|
import {
|
|
45
46
|
expectedSegments,
|
|
46
47
|
nextPollDelay,
|
|
@@ -54,9 +55,6 @@ import {
|
|
|
54
55
|
import {
|
|
55
56
|
mergeTranscript,
|
|
56
57
|
splitRecording,
|
|
57
|
-
stitchChunks,
|
|
58
|
-
stitchTranscript,
|
|
59
|
-
TRANSCRIPT_STREAM,
|
|
60
58
|
type Transcript,
|
|
61
59
|
transcribeFlow,
|
|
62
60
|
transcribeSegment,
|
|
@@ -88,9 +86,9 @@ const UPLOAD_ID = "upl_test";
|
|
|
88
86
|
const STARTED_AT = 1_000_000;
|
|
89
87
|
|
|
90
88
|
/**
|
|
91
|
-
* Publish one in-memory upload, the way `
|
|
89
|
+
* Publish one in-memory upload, the way `createRuntimeServer` publishes a real store.
|
|
92
90
|
*
|
|
93
|
-
* This is the seam that makes a step testable at all: `
|
|
91
|
+
* This is the seam that makes a step testable at all: `stepReadUpload` reads a
|
|
94
92
|
* process-wide slot rather than dialling anything, so a spec supplies its own
|
|
95
93
|
* bytes with no server, no database and no HTTP.
|
|
96
94
|
*/
|
|
@@ -143,15 +141,19 @@ function wavFile(
|
|
|
143
141
|
return head;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
describe("the agent declares its three workflows
|
|
144
|
+
describe("the agent declares its three workflows", () => {
|
|
147
145
|
test("under the names the REST route resolves them by", () => {
|
|
148
146
|
// The page starts a run by these strings, so a rename is a runtime 400 rather
|
|
149
147
|
// than a compile error — which is what makes pinning them worth a test.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
// `arrayContaining` rather than an exact key list: a fourth desk of your own
|
|
149
|
+
// is an invited edit and must not redden a test you did not write. These
|
|
150
|
+
// three NAMES stay pinned, deliberately — the page starts a run by each
|
|
151
|
+
// string, so renaming one is a runtime 400 rather than a compile error, and
|
|
152
|
+
// this is the only thing that says so. Rename here and in `client.tsx`
|
|
153
|
+
// together.
|
|
154
|
+
expect(Object.keys(agentDef.workflows ?? {})).toEqual(
|
|
155
|
+
expect.arrayContaining(["transcribe", "transcribeStream", "transcribeBatch"]),
|
|
156
|
+
);
|
|
155
157
|
expect(agentDef.workflows?.transcribe).toBe(transcribe);
|
|
156
158
|
expect(agentDef.workflows?.transcribeStream).toBe(transcribeStream);
|
|
157
159
|
expect(agentDef.workflows?.transcribeBatch).toBe(transcribeBatch);
|
|
@@ -168,8 +170,12 @@ describe("the agent declares its three workflows and nothing else", () => {
|
|
|
168
170
|
});
|
|
169
171
|
|
|
170
172
|
test("with no tools, because the interface is the page and the API", () => {
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
+
// A PIN, deliberately, and the one assertion in this file you are expected
|
|
174
|
+
// to edit rather than satisfy. `workflowApp()` declares no session, so a
|
|
175
|
+
// tool has no conversation to be called from — a name appearing here means
|
|
176
|
+
// the voice path crept back in and the tool will never run. If you want a
|
|
177
|
+
// voice agent that STARTS a workflow, `research-workflow` is that template,
|
|
178
|
+
// and this line is what tells you you are on the wrong one.
|
|
173
179
|
expect(Object.keys(agentDef.tools ?? {})).toEqual([]);
|
|
174
180
|
});
|
|
175
181
|
|
|
@@ -890,7 +896,7 @@ describe("transcribeSegment", () => {
|
|
|
890
896
|
// What makes the run's answer streamable rather than only its narration: the
|
|
891
897
|
// page stitches whatever has arrived, so the transcript renders growing
|
|
892
898
|
// instead of appearing when the last segment does. The reporter is the SDK's
|
|
893
|
-
// published slot, which is the same seam `
|
|
899
|
+
// published slot, which is the same seam `stepReport()` goes through.
|
|
894
900
|
const reported = installStubReporter();
|
|
895
901
|
stubProvider();
|
|
896
902
|
|
|
@@ -956,7 +962,7 @@ describe("transcribeSegment", () => {
|
|
|
956
962
|
// `transcribeStreamFlow`'s own `transcribeSegment` call carries the same
|
|
957
963
|
// budget and is asserted with that flow, not here — this drives only
|
|
958
964
|
// `transcribeFlow`.
|
|
959
|
-
const ctx =
|
|
965
|
+
const ctx = createWorkflowContext({
|
|
960
966
|
runSteps: false,
|
|
961
967
|
results: {
|
|
962
968
|
normalizeRecording: { recording: UPLOAD_ID, converted: false },
|
|
@@ -1108,7 +1114,7 @@ describe("the streaming flow", () => {
|
|
|
1108
1114
|
|
|
1109
1115
|
test("segmentStored refuses a window that STRADDLES a hole", () => {
|
|
1110
1116
|
// A run is contiguous, so containment in one is the whole test — and it has to
|
|
1111
|
-
// be, because `
|
|
1117
|
+
// be, because `stepReadUpload` clamps to the run a read starts in. A segment
|
|
1112
1118
|
// spanning two runs would come back short and be transcribed as a fragment,
|
|
1113
1119
|
// which is a wrong transcript rather than a failed one.
|
|
1114
1120
|
const at = poll(
|
|
@@ -1183,12 +1189,12 @@ describe("the streaming flow", () => {
|
|
|
1183
1189
|
});
|
|
1184
1190
|
|
|
1185
1191
|
test("a segment reads SHORT rather than failing when its bytes have not landed", async () => {
|
|
1186
|
-
// The property the whole flow rests on, and it predates streaming: `
|
|
1192
|
+
// The property the whole flow rests on, and it predates streaming: `stepReadUpload`
|
|
1187
1193
|
// clamps its window to what is stored. So a body that asks slightly early gets
|
|
1188
1194
|
// what exists — which is why the body checks `end <= size` and can trust the
|
|
1189
1195
|
// clamp for the final segment of a file that came up short.
|
|
1190
1196
|
publishPartial(1000, 320_000);
|
|
1191
|
-
const slice = await
|
|
1197
|
+
const slice = await stepReadUpload(UPLOAD_ID, { start: 44, end: 44 + 320_000 });
|
|
1192
1198
|
expect(slice.bytes.length).toBe(1000);
|
|
1193
1199
|
expect(slice.end).toBe(44 + 1000);
|
|
1194
1200
|
});
|
|
@@ -1348,6 +1354,16 @@ describe("normalizing the recording", () => {
|
|
|
1348
1354
|
* later in `splitRecording` with a message about a header; a `timeout`
|
|
1349
1355
|
* classified as fatal is a run that gives up on work that would have finished.
|
|
1350
1356
|
*/
|
|
1357
|
+
// Point the toolchain at nothing, so this block really is what its doc says:
|
|
1358
|
+
// a unit test that spawns no binary. Without it a developer with ffmpeg
|
|
1359
|
+
// installed runs a different test from CI, which has none — and the one
|
|
1360
|
+
// assertion below that reaches `normalizeRecording` is about a reporter line
|
|
1361
|
+
// emitted BEFORE the spawn. `unstubEnvs` is repo-wide, so there is no teardown.
|
|
1362
|
+
beforeEach(() => {
|
|
1363
|
+
vi.stubEnv("AAI_FFMPEG_PATH", "/nonexistent/ffmpeg");
|
|
1364
|
+
vi.stubEnv("AAI_FFPROBE_PATH", "/nonexistent/ffprobe");
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1351
1367
|
test("a canonical WAV is cuttable, so the desk converts nothing", () => {
|
|
1352
1368
|
expect(cuttable(wavFile(MONO_16K, 32_000), 44 + 32_000)).toBe(true);
|
|
1353
1369
|
});
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* `POST /workflows/uploads` (the browser does this for you: `uploads` below is
|
|
41
41
|
* what makes `<WorkflowFields>` render a file picker, and `useWorkflowSubmit`
|
|
42
42
|
* stores the file before starting the run), the input carries the returned id,
|
|
43
|
-
* and each step reads exactly the window it needs with `
|
|
43
|
+
* and each step reads exactly the window it needs with `stepReadUpload` — which is
|
|
44
44
|
* what keeps sixty steps from moving the same recording sixty times.
|
|
45
45
|
*
|
|
46
46
|
* None of that is this template's code. Uploads are the SDK's, for the reason
|
|
@@ -118,7 +118,7 @@ export const transcribe = workflow({
|
|
|
118
118
|
}),
|
|
119
119
|
// The one line that makes the form take a file: `<WorkflowFields>` renders a
|
|
120
120
|
// picker for this property, `useWorkflowSubmit` stores the chosen file, and
|
|
121
|
-
// the steps read it back with `
|
|
121
|
+
// the steps read it back with `stepReadUpload`.
|
|
122
122
|
uploads: ["recording"],
|
|
123
123
|
run: transcribeFlow,
|
|
124
124
|
});
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
* rather than a fourth option. A single request moves a file at one connection's
|
|
57
57
|
* throughput, which over any distance is a fraction of the link — so the SDK cuts
|
|
58
58
|
* the file into megabyte-aligned parts and sends four at once. Nothing about the
|
|
59
|
-
* workflow changes: the agent reassembles them, `
|
|
59
|
+
* workflow changes: the agent reassembles them, `stepReadUpload` reads the same
|
|
60
60
|
* windows, and the streaming flow still watches the file grow (what it polls is the
|
|
61
61
|
* CONTIGUOUS prefix, which is honest whether one connection or four are filling
|
|
62
62
|
* it).
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
* - **The page stitches with the RUN's own function.** `stitchChunks` is
|
|
84
84
|
* `workflows/stitch.ts`, imported by both, so the live text and the stored one
|
|
85
85
|
* cannot drift into two different transcripts of one recording.
|
|
86
|
-
* - **It is a SEPARATE stream from the progress log.** `
|
|
86
|
+
* - **It is a SEPARATE stream from the progress log.** `stepReport()`'s lines go to
|
|
87
87
|
* the default one, which `<WorkflowProgress>` renders verbatim; objects in
|
|
88
88
|
* there would come out as `[object Object]` between the sentences.
|
|
89
89
|
* - **The finished run wins.** Once `output` exists the panel renders that
|
|
@@ -153,15 +153,17 @@
|
|
|
153
153
|
* ## A reload keeps two of the three runs, and the third CANNOT be kept
|
|
154
154
|
*
|
|
155
155
|
* The run id lives in React state, so a refresh loses it while the fan-out
|
|
156
|
-
* carries on.
|
|
157
|
-
*
|
|
156
|
+
* carries on. A correlation KEY is the handle that survives that, and
|
|
157
|
+
* `useWorkflowSubmit` keeps one and asks for its newest run on mount — which
|
|
158
|
+
* here lands differently PER MODE:
|
|
158
159
|
*
|
|
159
160
|
* - **"After it uploads"** and **"Let the provider do it"** recover. Their input
|
|
160
161
|
* names a recording that is already stored, so a later load adopting the run
|
|
161
162
|
* is adopting something complete: the transcript arrives, the progress log
|
|
162
163
|
* replays, and nobody is asked to send a 600 MB file a second time.
|
|
163
164
|
* - **"While it uploads" does not, and the hook REFUSES the option rather than
|
|
164
|
-
* ignoring it
|
|
165
|
+
* ignoring it** — `useWorkflowStream` omits `recover` from its options type
|
|
166
|
+
* and mints no key. That run's input names an upload id this page load minted and
|
|
165
167
|
* is still filling, so a later load could only adopt a run waiting for bytes
|
|
166
168
|
* nobody is sending — and it is worse than useless: `workflows/stream.ts`
|
|
167
169
|
* fails a run whose upload stops growing (`MAX_IDLE_POLLS`), so the reload
|
|
@@ -171,10 +173,11 @@
|
|
|
171
173
|
* The MODE is remembered too, and that is not decoration: without it a reload
|
|
172
174
|
* opens on the default flow while the recovered run sits behind a radio nobody
|
|
173
175
|
* pressed, so the reader sees an empty form and starts a second run — the exact
|
|
174
|
-
* thing the key exists to prevent. The KEY is
|
|
175
|
-
* minting, the storage and the argument for
|
|
176
|
-
* `?key=` parameter
|
|
177
|
-
*
|
|
176
|
+
* thing the key exists to prevent. The KEY is the hook's own — `use-run-key.ts`
|
|
177
|
+
* owns the minting, the storage and the argument for it being opaque rather
|
|
178
|
+
* than a `?key=` parameter, and the two submit hooks here share one because the
|
|
179
|
+
* slot is keyed by the PAGE; `recover.ts` owns the mode, which is this page's
|
|
180
|
+
* own concept, and the validation on the way back out of storage that turning a
|
|
178
181
|
* stored string into a workflow name obliges.
|
|
179
182
|
*
|
|
180
183
|
* Two smaller consequences worth knowing. Both recovering hooks look up on
|
|
@@ -190,10 +193,9 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
190
193
|
import {
|
|
191
194
|
Form,
|
|
192
195
|
isTerminal,
|
|
193
|
-
|
|
196
|
+
mountPage,
|
|
194
197
|
SubmitButton,
|
|
195
198
|
UploadProgressBar,
|
|
196
|
-
useRunKey,
|
|
197
199
|
useWorkflowRuns,
|
|
198
200
|
useWorkflowStream,
|
|
199
201
|
useWorkflowSubmit,
|
|
@@ -268,38 +270,31 @@ function TranscriptionDesk() {
|
|
|
268
270
|
// piece of state for all three hooks, because it describes the UPLOAD and every
|
|
269
271
|
// mode has one — see the module doc.
|
|
270
272
|
const [parallel, setParallel] = useState(true);
|
|
271
|
-
// This tab's handle on its own runs — minted once and remembered, which is
|
|
272
|
-
// what a later load produces to find the run again.
|
|
273
|
-
const key = useRunKey();
|
|
274
|
-
// Did THIS load press Transcribe? A reload cannot have, and it is the only way
|
|
275
|
-
// the page can tell "working on what you just sent" from "picking up where you
|
|
276
|
-
// left off" — the hooks report the run, not who asked for it.
|
|
277
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
278
273
|
// ALL THREE hooks are called every render, because a hook may not be conditional —
|
|
279
274
|
// and that costs nothing here: none of them does anything until its `submit` is
|
|
280
275
|
// called, and `useWorkflowRun` underneath them holds no id until then either.
|
|
281
276
|
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
277
|
+
// The two submit hooks recover unconditionally rather than on `mode === …`:
|
|
278
|
+
// the lookup is a MOUNT-time act, so arming it when a mode is picked would
|
|
279
|
+
// re-adopt a run the reader had just cleared. They share ONE key without
|
|
280
|
+
// saying so — the hook's slot is keyed by the page — and `find` is scoped by
|
|
281
|
+
// workflow as well as by key, so the two modes recover separate runs. The
|
|
282
|
+
// streaming hook has neither half: it refuses `recover` by type, and mints no
|
|
283
|
+
// key it would never read back.
|
|
287
284
|
const streamed = useWorkflowStream<typeof transcribe>(WORKFLOWS.streaming, { parallel });
|
|
288
|
-
const stored = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.classic, {
|
|
289
|
-
|
|
290
|
-
key,
|
|
291
|
-
recover: true,
|
|
292
|
-
});
|
|
293
|
-
const batched = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.batch, {
|
|
294
|
-
parallel,
|
|
295
|
-
key,
|
|
296
|
-
recover: true,
|
|
297
|
-
});
|
|
285
|
+
const stored = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.classic, { parallel });
|
|
286
|
+
const batched = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.batch, { parallel });
|
|
298
287
|
// The batch flow uploads the same way the classic one does — the id comes from the
|
|
299
288
|
// store — so it is the SAME hook against a different workflow. Only the streaming
|
|
300
289
|
// mode needs the other one, because only it needs the id before the bytes.
|
|
301
290
|
const active = mode === "streaming" ? streamed : mode === "batch" ? batched : stored;
|
|
302
|
-
|
|
291
|
+
// `startedHere` comes off the ACTIVE hook rather than being one flag for the
|
|
292
|
+
// page, and that is a fix rather than a tidy-up: a single piece of page state
|
|
293
|
+
// survived a mode switch, so pressing Transcribe in one mode and then
|
|
294
|
+
// switching made the other mode's panel claim the reader had started its run.
|
|
295
|
+
// Per-hook, switching modes shows the truth about the run being shown.
|
|
296
|
+
const { submitForm, run, upload, pending, error, reset, pauseUpload, resumeUpload, startedHere } =
|
|
297
|
+
active;
|
|
303
298
|
// History is per WORKFLOW, so the list follows the mode: two flows that produce
|
|
304
299
|
// the same output are still two different things to have run, and merging them
|
|
305
300
|
// would put a run under a heading that cannot explain it.
|
|
@@ -356,7 +351,6 @@ function TranscriptionDesk() {
|
|
|
356
351
|
<Form
|
|
357
352
|
onSubmit={(values) => {
|
|
358
353
|
total.start();
|
|
359
|
-
setStartedHere(true);
|
|
360
354
|
// Written at SUBMIT rather than on the radio, so the remembered mode
|
|
361
355
|
// is always the mode a run exists under — which is the only thing the
|
|
362
356
|
// next load can use it for.
|
|
@@ -376,8 +370,8 @@ function TranscriptionDesk() {
|
|
|
376
370
|
</Form>
|
|
377
371
|
|
|
378
372
|
<TotalLatency
|
|
379
|
-
|
|
380
|
-
|
|
373
|
+
startedAt={total.startedAt}
|
|
374
|
+
frozenMs={total.frozenMs}
|
|
381
375
|
runMs={run?.status === "completed" ? run.output.elapsedMs : undefined}
|
|
382
376
|
/>
|
|
383
377
|
|
|
@@ -401,8 +395,8 @@ function TranscriptionDesk() {
|
|
|
401
395
|
onClear={() => {
|
|
402
396
|
// A recovered run is dismissed as deliberately as one this load
|
|
403
397
|
// started: the lookup is a mount-time act, so `reset()` is not
|
|
404
|
-
// undone by a second one and Clear really does clear.
|
|
405
|
-
|
|
398
|
+
// undone by a second one and Clear really does clear. `reset()`
|
|
399
|
+
// also clears `startedHere`, which this page used to mirror here.
|
|
406
400
|
reset();
|
|
407
401
|
total.clear();
|
|
408
402
|
}}
|
|
@@ -507,4 +501,4 @@ function UploadPicker({
|
|
|
507
501
|
);
|
|
508
502
|
}
|
|
509
503
|
|
|
510
|
-
|
|
504
|
+
mountPage({ name: "Transcription Desk", component: TranscriptionDesk });
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `client.tsx` down to the page's shape:
|
|
15
15
|
*
|
|
16
16
|
* - **A template's spec is what makes its exemplar code true**, and none of this
|
|
17
|
-
* can be tested from a `client.tsx`. That file ends in a `
|
|
17
|
+
* can be tested from a `client.tsx`. That file ends in a `mountPage()` call and
|
|
18
18
|
* imports a stylesheet, and this package's suites have no DOM at all: the
|
|
19
19
|
* vitest `include` matches `.test.ts` and not `.test.tsx`, and the scaffold
|
|
20
20
|
* declares no React testing library — so a `client.test.tsx` would be
|
|
@@ -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
|