From 78fd3eb092bc7ec8e9da07712892048f88e8922f Mon Sep 17 00:00:00 2001 From: Amin Mousavi Date: Fri, 11 Sep 2026 19:33:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(voice):=20hear=20"=DB=8C=D8=A7=D8=AF=D8=AF?= =?UTF-8?q?=D8=A7=D8=B4=D8=AA"=20as=20a=20note=20request,=20and=20stop=20t?= =?UTF-8?q?he=20note=20at=20the=20next=20instruction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reports from live dictation in fa. The locale trigger vocabulary listed only verbs that ask for a note ("بنویس", "یادداشت کن"). Naming the field itself — "توی یادداشت‌ها بنویس", "در توضیحات" — reported no commentTrigger, and the resolver discards a comment without one, so the note was silently dropped. Added the field-name shape to fa, en and nl. Where the dictated note ends was never stated, so the model answered it inconsistently: "توی توضیحات بنویس سلام چطوری و بفرست برای لابراتوآر سانشاین" sometimes put the lab dispatch inside the comment. Added a no-overlap rule — a clause reported as a lab, tooth, prosthesis or deadline is an instruction and must not appear in comment as well — with that sentence as a worked example. Both rules go in the prompt and in the JSON-schema field descriptions, because the schema description is what the model reads while filling the field. No code enforcement for the second: stripping an extracted clause out of free-speech note text by string matching is guesswork, and the review sheet shows the note before it is saved. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/modules/voice/extraction.prompt.ts | 18 ++++++++++--- backend/src/modules/voice/extraction.wire.ts | 9 ++++--- docs/specs/voice-treatment-entry/progress.md | 26 +++++++++++++++++++ docs/specs/voice-treatment-entry/spec.md | 16 +++++++++++- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/backend/src/modules/voice/extraction.prompt.ts b/backend/src/modules/voice/extraction.prompt.ts index 66af5fb..30b8100 100644 --- a/backend/src/modules/voice/extraction.prompt.ts +++ b/backend/src/modules/voice/extraction.prompt.ts @@ -10,14 +10,17 @@ const LOCALE_NOTES: Record = { '"شش بالا راست" = upper right six -> arch "upper", side "patient_right", position 6.', 'A jaw is spoken as "فک بالا" (upper jaw) or "فک پایین" (lower jaw), sometimes just', '"بالا"/"پایین" in context, or "هر دو فک" (both jaws).', - 'A note is asked for with "بنویس", "یادداشت کن", "این را یادداشت کن", "بنویس که",', - '"در توضیحات بنویس". Anything else the clinician says is not a note.', + 'A note is asked for with "بنویس", "بنویس که", "یادداشت کن", "این را یادداشت کن",', + 'or by naming the field the words go into: "یادداشت", "یادداشت‌ها", "توضیحات",', + '"کامنت", "کامنت‌ها" — as in "توی یادداشت‌ها بنویس", "در توضیحات بنویس",', + '"یادداشت: ...". Anything else the clinician says is not a note.', ].join(' '), nl: [ 'The clinician is speaking Dutch, where FDI is standard. "zesentwintig" and "26" are', 'tooth 26. The descriptive form is "rechtsboven zes" = upper right six. A jaw is', '"bovenkaak" (upper) or "onderkaak" (lower), or "beide kaken" (both).', - 'A note is asked for with "schrijf op", "noteer", "zet in de notities". Anything else', + 'A note is asked for with "schrijf op", "noteer", "zet in de notities", or by naming', + 'the field the words go into ("notitie", "notities", "opmerkingen"). Anything else', 'the clinician says is not a note.', ].join(' '), en: [ @@ -25,7 +28,8 @@ const LOCALE_NOTES: Record = { 'all tooth 26. The descriptive form is "upper right six". A jaw is "upper jaw"/"lower', 'jaw", or "both jaws".', 'A note is asked for with "write this in the notes", "note that", "add a note",', - '"put in the comments". Anything else the clinician says is not a note.', + '"put in the comments", or by naming the field the words go into ("note", "notes",', + '"comments", "remarks"). Anything else the clinician says is not a note.', ].join(' '), }; @@ -176,6 +180,12 @@ export function buildExtractionPrompt( ' If nobody asked, BOTH are null. Never sweep up leftover speech, filler, small talk or', ' a diagnosis nobody asked you to record. A comment without a trigger is discarded, so', ' guessing costs the clinician the note.', + ' WHERE THE NOTE ENDS: the dictated words end where the next instruction begins. Any', + ' clause you also reported in another field — a lab, a tooth, a prosthesis, a deadline —', + ' is that instruction, so it never belongs in "comment" as well. Nothing may appear in', + ' two fields. Example: "توی توضیحات بنویس سلام چطوری و بفرست برای لابراتوآر سانشاین" ->', + ' commentTrigger "توی توضیحات بنویس", comment "سلام چطوری", and the lab goes to labId.', + ' "و بفرست برای لابراتوآر سانشاین" is an instruction, not part of the note.', '- labMatchExact: true only when the spoken name matched a lab name exactly.', ].join('\n'); diff --git a/backend/src/modules/voice/extraction.wire.ts b/backend/src/modules/voice/extraction.wire.ts index 284d247..e242dc6 100644 --- a/backend/src/modules/voice/extraction.wire.ts +++ b/backend/src/modules/voice/extraction.wire.ts @@ -159,14 +159,17 @@ export const VOICE_INTENT_JSON_SCHEMA = { description: 'The note the clinician explicitly dictated, in the spoken language, WITHOUT the ' + 'words that asked for it. Null unless they actually asked for a note. Never put ' + - 'leftover speech here.', + 'leftover speech here. It ends where the next instruction begins: a clause you ' + + 'also reported as a lab, a tooth, a prosthesis or a deadline is that instruction, ' + + 'and must not appear here too.', }, commentTrigger: { type: ['string', 'null'], description: 'The exact words that asked for a note, copied from the transcript (e.g. ' + - '"بنویس که", "write this in the notes"). Null when nobody asked. A comment with ' + - 'no trigger is discarded.', + '"بنویس که", "توی یادداشت‌ها بنویس", "write this in the notes"). Naming the field ' + + 'itself counts as asking ("یادداشت", "توضیحات", "notes", "comments"). Null when ' + + 'nobody asked. A comment with no trigger is discarded.', }, prosthesis: { type: 'array', diff --git a/docs/specs/voice-treatment-entry/progress.md b/docs/specs/voice-treatment-entry/progress.md index 7ce29f8..300e703 100644 --- a/docs/specs/voice-treatment-entry/progress.md +++ b/docs/specs/voice-treatment-entry/progress.md @@ -524,6 +524,32 @@ step — only build and deploy. Combined with `nest build` excluding `**/*spec.t running transpile-only under `isolatedModules`, nothing in this repo would ever catch a type error in a spec file. That is how two of them reached this branch unnoticed. +## 2026-09-11 — note triggers and where a note ends (dyolink) + +Two reports from live dictation in `fa`, both prompt-side, no resolver change. + +1. «یادداشت» / «یادداشت‌ها» produced no trigger, so the note was discarded. The locale notes + listed only verbs that ask («بنویس», «یادداشت کن»); naming the field itself was not in the + vocabulary. Added the field-name shape for all three locales — fa «یادداشت/یادداشت‌ها/توضیحات/ + کامنت», en "note/notes/comments/remarks", nl "notitie/notities/opmerkingen". +2. The note swallowed the next instruction. For «توی توضیحات بنویس سلام چطوری و بفرست برای + لابراتوآر سانشاین» the model sometimes put the whole tail in `comment`, sometimes only + «سلام چطوری». Added a no-overlap invariant: a clause reported as a lab, tooth, prosthesis or + deadline is an instruction and must not also appear in `comment`, plus that exact sentence as + a worked example. + +Both rules are stated twice on purpose — in `extraction.prompt.ts` and in the `comment` / +`commentTrigger` descriptions in `extraction.wire.ts`, since the schema description is what the +model reads while filling the field. + +No code enforcement for (2). Stripping an extracted clause out of free-speech note text by string +matching is guesswork, and the review sheet shows the note before it is saved. + +Files: `backend/src/modules/voice/extraction.prompt.ts`, +`backend/src/modules/voice/extraction.wire.ts`, `docs/specs/voice-treatment-entry/spec.md`. +Verified: `npx eslint src/modules/voice/**/*.ts` clean, `npm test -- extraction` 74/74. +No unit test — both are prompt text, which the extraction specs deliberately do not assert on. + ## Remaining before this is shippable - The **manual pass in §12** — none of it has been run. Safari and iPad especially, since that is diff --git a/docs/specs/voice-treatment-entry/spec.md b/docs/specs/voice-treatment-entry/spec.md index 7825c9d..110d0c7 100644 --- a/docs/specs/voice-treatment-entry/spec.md +++ b/docs/specs/voice-treatment-entry/spec.md @@ -535,7 +535,21 @@ what was captured, so nothing is hidden by leaving it out. Per-locale trigger vocabulary lives in the prompt's locale notes, exactly like the tooth vocabulary. The resolver only checks that a trigger was reported, so it needs no per-locale -knowledge and stays locale-neutral by construction (§6). +knowledge and stays locale-neutral by construction (§6). The vocabulary covers two shapes: a +verb that asks («بنویس که», "noteer") and the *name of the field* the words go into («یادداشت», +«یادداشت‌ها», «توضیحات», "notes", "comments"). Naming the field is how a clinician usually asks, +and the first version listed only the verbs, so «توی یادداشت‌ها بنویس» produced no trigger and +the note was discarded. + +Where the dictated note ends is a second question, and the model answered it inconsistently: for +«توی توضیحات بنویس سلام چطوری و بفرست برای لابراتوآر سانشاین» the note sometimes ran to the end +of the sentence and swallowed the lab dispatch. The rule is that the note ends where the next +instruction begins, stated as a no-overlap invariant: a clause reported as a lab, a tooth, a +prosthesis or a deadline is that instruction and must not also appear in `comment`. It lives in +both the prompt and the JSON-schema field description, because the schema description is what the +model reads while it fills the field. This is a prompt-side rule with no code enforcement — +stripping an already-extracted clause out of the note text by string matching would be guesswork +on free speech, and the review sheet shows the note before it is saved. ### Prosthesis work implies the treatment type