fix(voice): say the quadrant is missing instead of "could not be read"
"ترمیم برای دندون دو" set the treatment type but reported the tooth as
unreadable. Nothing was misheard: position 2 arrived intact, with no
quadrant, because none was spoken — four teeth carry position 2 and the
resolver correctly refused to pick one. Only the label was wrong, and it
sent the clinician looking for a transcription fault.
Adds a tooth_missing_quadrant reason that names what is missing and shows
how to say it ("دو بالا راست"), and tells the model explicitly to report a
quadrant-less number with arch and side null rather than guessing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ const LOCALE_NOTES: Record<string, string> = {
|
||||
'"شش بالا راست" = upper right six -> arch "upper", side "patient_right", position 6.',
|
||||
'Digits may appear in Persian or Latin script. Two-digit FDI notation ("یک چهار") does',
|
||||
'occur — use the "fdi" field only for that.',
|
||||
'A bare "دندون دو" carries no quadrant: report position 2 with arch and side null.',
|
||||
].join(' '),
|
||||
nl: [
|
||||
'The clinician is speaking Dutch and uses FDI notation, which is standard in the',
|
||||
@@ -50,6 +51,9 @@ export function buildExtractionPrompt(
|
||||
' what was heard.',
|
||||
'6. If you are unsure about a value, use null. A missing field is recoverable; a wrong',
|
||||
' one is not.',
|
||||
'7. A tooth number spoken WITHOUT a quadrant ("دندون دو", "tooth two") does not identify',
|
||||
' a tooth — four teeth carry that position. Still report it: set "position" and leave',
|
||||
' "arch" and "side" null. Never pick a quadrant that was not said.',
|
||||
'',
|
||||
localeNote,
|
||||
'',
|
||||
|
||||
@@ -150,6 +150,57 @@ describe('resolveToothIntents', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('says the quadrant is missing rather than blaming the words', () => {
|
||||
// Regression: "ترمیم برای دندون دو" reported "could not be read", sending the
|
||||
// clinician to look for a transcription fault. Position 2 was understood fine —
|
||||
// what is missing is the quadrant, and four teeth carry position 2.
|
||||
const bare = {
|
||||
kind: 'positional',
|
||||
arch: null,
|
||||
side: null,
|
||||
position: 2,
|
||||
spoken: 'دندون دو',
|
||||
} as unknown as ToothIntent;
|
||||
|
||||
const result = resolveToothIntents([bare]);
|
||||
expect(result.teeth).toEqual([]);
|
||||
expect(result.unresolved).toEqual([
|
||||
{ spoken: 'دندون دو', reason: 'tooth_missing_quadrant' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports a missing quadrant for a half-specified tooth too', () => {
|
||||
// "دو بالا" narrows it to 12 or 22 — still not one tooth, and still not our guess.
|
||||
for (const half of [
|
||||
{ arch: 'upper', side: null },
|
||||
{ arch: null, side: 'patient_right' },
|
||||
]) {
|
||||
const result = resolveToothIntents([
|
||||
{
|
||||
kind: 'positional',
|
||||
...half,
|
||||
position: 2,
|
||||
spoken: 'دو',
|
||||
} as unknown as ToothIntent,
|
||||
]);
|
||||
expect(result.unresolved[0].reason).toBe('tooth_missing_quadrant');
|
||||
}
|
||||
});
|
||||
|
||||
it('still calls an out-of-range position out of range when the quadrant is missing', () => {
|
||||
// Position wins: "nine" is wrong however completely it was said.
|
||||
const result = resolveToothIntents([
|
||||
{
|
||||
kind: 'positional',
|
||||
arch: null,
|
||||
side: null,
|
||||
position: 9,
|
||||
spoken: 'نه',
|
||||
} as unknown as ToothIntent,
|
||||
]);
|
||||
expect(result.unresolved[0].reason).toBe('position_out_of_range');
|
||||
});
|
||||
|
||||
it('keeps unresolved items separate when the model omits the spoken span', () => {
|
||||
// Without `spoken` these are indistinguishable; collapsing them would hide a lost tooth.
|
||||
const result = resolveToothIntents([
|
||||
|
||||
@@ -55,7 +55,14 @@ function unresolvedReason(intent: ToothIntent): UnresolvedItem['reason'] {
|
||||
!Number.isInteger(intent.position) ||
|
||||
intent.position < 1 ||
|
||||
intent.position > 8;
|
||||
return positionBad ? 'position_out_of_range' : 'malformed';
|
||||
if (positionBad) return 'position_out_of_range';
|
||||
// The position was understood, so the words were not the problem: the speaker never
|
||||
// said which quadrant. "دندون دو" names four teeth at once, and telling the
|
||||
// clinician it "could not be read" would send them looking for the wrong fault.
|
||||
const archMissing = intent.arch !== 'upper' && intent.arch !== 'lower';
|
||||
const sideMissing =
|
||||
intent.side !== 'patient_right' && intent.side !== 'patient_left';
|
||||
return archMissing || sideMissing ? 'tooth_missing_quadrant' : 'malformed';
|
||||
}
|
||||
|
||||
return 'malformed';
|
||||
|
||||
@@ -65,6 +65,8 @@ export type VoiceIntent = {
|
||||
export type UnresolvedReason =
|
||||
| 'not_permanent_tooth'
|
||||
| 'position_out_of_range'
|
||||
/** A position was understood but no quadrant was spoken — four teeth match. */
|
||||
| 'tooth_missing_quadrant'
|
||||
| 'malformed'
|
||||
| 'span_not_same_arch'
|
||||
| 'unknown_catalog_code'
|
||||
|
||||
@@ -909,6 +909,7 @@
|
||||
"voiceUnresolved": {
|
||||
"not_permanent_tooth": "not a permanent tooth",
|
||||
"position_out_of_range": "not a valid tooth position",
|
||||
"tooth_missing_quadrant": "quadrant not said — e.g. “upper right two”",
|
||||
"malformed": "could not be read",
|
||||
"span_not_same_arch": "a bridge cannot span both jaws",
|
||||
"unknown_catalog_code": "not in this clinic’s list",
|
||||
|
||||
@@ -910,6 +910,7 @@
|
||||
"voiceUnresolved": {
|
||||
"not_permanent_tooth": "دندان دائمی نیست",
|
||||
"position_out_of_range": "شماره دندان معتبر نیست",
|
||||
"tooth_missing_quadrant": "بالا/پایین و چپ/راست گفته نشد — مثلاً «دو بالا راست»",
|
||||
"malformed": "قابل خواندن نبود",
|
||||
"span_not_same_arch": "بریج نمیتواند بین دو فک باشد",
|
||||
"unknown_catalog_code": "در فهرست این مطب نیست",
|
||||
|
||||
@@ -909,6 +909,7 @@
|
||||
"voiceUnresolved": {
|
||||
"not_permanent_tooth": "geen blijvend element",
|
||||
"position_out_of_range": "geen geldige elementpositie",
|
||||
"tooth_missing_quadrant": "kwadrant niet genoemd — bijv. “rechtsboven twee”",
|
||||
"malformed": "kon niet worden gelezen",
|
||||
"span_not_same_arch": "een brug kan niet over beide kaken lopen",
|
||||
"unknown_catalog_code": "staat niet in de lijst van deze praktijk",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FdiToothId, ToothSelectionGroup } from '@/types/treatment';
|
||||
export type VoiceUnresolvedReason =
|
||||
| 'not_permanent_tooth'
|
||||
| 'position_out_of_range'
|
||||
| 'tooth_missing_quadrant'
|
||||
| 'malformed'
|
||||
| 'span_not_same_arch'
|
||||
| 'unknown_catalog_code'
|
||||
|
||||
Reference in New Issue
Block a user