diff --git a/packages/flow-schema/src/parse.ts b/packages/flow-schema/src/parse.ts index 5009689..7029520 100644 --- a/packages/flow-schema/src/parse.ts +++ b/packages/flow-schema/src/parse.ts @@ -145,11 +145,18 @@ function decodePrompt(value: unknown, label: string, issues: Issue[]): Prompt | if (isScalar(value) && typeof value.value === 'string') return value.value; if (isMap(value)) { const fields = fieldsOf(value, label, issues); - warnUnknownFields(fields, ['audio'], label, issues); + // `transcript` is the words the clip speaks, carried for display and + // traces (model `Audio.transcript`); advisory only — playback still uses + // `audio`. Known here so it neither warns nor gets dropped on decode. + warnUnknownFields(fields, ['audio', 'transcript'], label, issues); const audio = fields.get('audio'); if (audio) { const ref = asString(audio.value, `${label} audio`, issues); - return ref === undefined ? undefined : { audio: ref }; + if (ref === undefined) return undefined; + const transcriptField = fields.get('transcript'); + if (!transcriptField) return { audio: ref }; + const transcript = asString(transcriptField.value, `${label} transcript`, issues); + return transcript === undefined ? undefined : { audio: ref, transcript }; } } error(issues, 'bad_prompt', `${label} must be text or { audio: ref }`, value); diff --git a/packages/flow-schema/test/parse.test.ts b/packages/flow-schema/test/parse.test.ts index 227e9c3..6cea7e9 100644 --- a/packages/flow-schema/test/parse.test.ts +++ b/packages/flow-schema/test/parse.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it } from 'vitest'; import { parseFlow } from '../src/parse.js'; -import { requiredAssets } from '../src/model.js'; +import { promptTranscript, requiredAssets } from '../src/model.js'; // The doc 48 "flow document, sketched" example — kept in sync with the // Rust side's `model::tests::LUIGIS` on purpose: both must stay valid. @@ -123,6 +123,39 @@ nodes: expect(flow ? requiredAssets(flow) : []).toEqual(['bye.wav']); }); + it('carries an audio prompt transcript through decode without warning', () => { + const { flow, issues } = parseFlow(` +schema_version: 1 +id: f +name: n +entry: g +nodes: + g: + kind: hangup + prompt: { audio: vprompt_ab12, transcript: Thanks for calling! } +`); + const node = flow?.nodes['g']; + const prompt = node?.kind === 'hangup' ? node.prompt : undefined; + expect(prompt).toEqual({ audio: 'vprompt_ab12', transcript: 'Thanks for calling!' }); + expect(prompt && promptTranscript(prompt)).toBe('Thanks for calling!'); + // The field the schema added in #30 must not read as unknown. + expect(issues.filter((issue) => issue.code === 'unknown_field')).toHaveLength(0); + }); + + it('rejects a non-string audio prompt transcript', () => { + const { issues } = parseFlow(` +schema_version: 1 +id: f +name: n +entry: g +nodes: + g: + kind: hangup + prompt: { audio: vprompt_ab12, transcript: 42 } +`); + expect(issues.some((issue) => issue.code === 'expected_string')).toBe(true); + }); + it('rejects duplicate keys at any nesting level', () => { const { flow, issues } = parseFlow(` schema_version: 1