fix(voice): hear "یادداشت" as a note request, and stop the note at the next instruction
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) <noreply@anthropic.com>
This commit is contained in:
@@ -10,14 +10,17 @@ const LOCALE_NOTES: Record<string, string> = {
|
|||||||
'"شش بالا راست" = upper right six -> arch "upper", side "patient_right", position 6.',
|
'"شش بالا راست" = upper right six -> arch "upper", side "patient_right", position 6.',
|
||||||
'A jaw is spoken as "فک بالا" (upper jaw) or "فک پایین" (lower jaw), sometimes just',
|
'A jaw is spoken as "فک بالا" (upper jaw) or "فک پایین" (lower jaw), sometimes just',
|
||||||
'"بالا"/"پایین" in context, or "هر دو فک" (both jaws).',
|
'"بالا"/"پایین" in context, or "هر دو فک" (both jaws).',
|
||||||
'A note is asked for with "بنویس", "یادداشت کن", "این را یادداشت کن", "بنویس که",',
|
'A note is asked for with "بنویس", "بنویس که", "یادداشت کن", "این را یادداشت کن",',
|
||||||
'"در توضیحات بنویس". Anything else the clinician says is not a note.',
|
'or by naming the field the words go into: "یادداشت", "یادداشتها", "توضیحات",',
|
||||||
|
'"کامنت", "کامنتها" — as in "توی یادداشتها بنویس", "در توضیحات بنویس",',
|
||||||
|
'"یادداشت: ...". Anything else the clinician says is not a note.',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
nl: [
|
nl: [
|
||||||
'The clinician is speaking Dutch, where FDI is standard. "zesentwintig" and "26" are',
|
'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',
|
'tooth 26. The descriptive form is "rechtsboven zes" = upper right six. A jaw is',
|
||||||
'"bovenkaak" (upper) or "onderkaak" (lower), or "beide kaken" (both).',
|
'"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.',
|
'the clinician says is not a note.',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
en: [
|
en: [
|
||||||
@@ -25,7 +28,8 @@ const LOCALE_NOTES: Record<string, string> = {
|
|||||||
'all tooth 26. The descriptive form is "upper right six". A jaw is "upper jaw"/"lower',
|
'all tooth 26. The descriptive form is "upper right six". A jaw is "upper jaw"/"lower',
|
||||||
'jaw", or "both jaws".',
|
'jaw", or "both jaws".',
|
||||||
'A note is asked for with "write this in the notes", "note that", "add a note",',
|
'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(' '),
|
].join(' '),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,6 +180,12 @@ export function buildExtractionPrompt(
|
|||||||
' If nobody asked, BOTH are null. Never sweep up leftover speech, filler, small talk or',
|
' 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',
|
' a diagnosis nobody asked you to record. A comment without a trigger is discarded, so',
|
||||||
' guessing costs the clinician the note.',
|
' 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.',
|
'- labMatchExact: true only when the spoken name matched a lab name exactly.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|||||||
@@ -159,14 +159,17 @@ export const VOICE_INTENT_JSON_SCHEMA = {
|
|||||||
description:
|
description:
|
||||||
'The note the clinician explicitly dictated, in the spoken language, WITHOUT the ' +
|
'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 ' +
|
'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: {
|
commentTrigger: {
|
||||||
type: ['string', 'null'],
|
type: ['string', 'null'],
|
||||||
description:
|
description:
|
||||||
'The exact words that asked for a note, copied from the transcript (e.g. ' +
|
'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 ' +
|
'"بنویس که", "توی یادداشتها بنویس", "write this in the notes"). Naming the field ' +
|
||||||
'no trigger is discarded.',
|
'itself counts as asking ("یادداشت", "توضیحات", "notes", "comments"). Null when ' +
|
||||||
|
'nobody asked. A comment with no trigger is discarded.',
|
||||||
},
|
},
|
||||||
prosthesis: {
|
prosthesis: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
## Remaining before this is shippable
|
||||||
|
|
||||||
- The **manual pass in §12** — none of it has been run. Safari and iPad especially, since that is
|
- The **manual pass in §12** — none of it has been run. Safari and iPad especially, since that is
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
### Prosthesis work implies the treatment type
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user