Files
dyolink/backend/src/modules/voice/extraction.wire.ts

274 lines
8.1 KiB
TypeScript
Raw Normal View History

import { normalizeFdiCode } from '../../common/fdi';
feat(backend): OpenRouter voice providers and per-locale registry ASR and extraction are separate, independently swappable roles resolved per locale from config. All three locales point at the same OpenRouter models today (whisper-1, gemini-3.7-flash); the indirection stays because Persian ASR is the weakest link and repointing only `fa` must not be a code change. The model emits a deliberately flat wire shape rather than the internal discriminated unions — strict json_schema mode has poor union support — and toVoiceIntent narrows it. That normalizer is total: a missing or malformed payload yields a shape the resolvers report as unresolved rather than one that throws. The prompt supplies catalog codes with labels in the actor's locale, so the model matches spoken words rather than translating, and carries per-locale tooth vocabulary. English gets an explicit warning that a bare two-digit number is ambiguous under Universal numbering, and must not be treated as FDI unless the speaker said so. From review of this commit: - only an actually FDI-shaped code takes the explicit branch; fdi:"6" alongside valid arch/side/position used to lose the tooth entirely - an unrecognised due kind passes through to be flagged, instead of collapsing to null and looking like no deadline was ever spoken - vendor error bodies stay out of the thrown message and the default log level; a 4xx can echo the request back, transcript included - the chat call sets provider.require_parameters so OpenRouter only routes to endpoints that honour the JSON schema, rather than ones treating it as a hint - an unknown locale in VOICE_ENABLED_LOCALES now fails at boot like an unknown provider id, instead of silently disabling the microphone everywhere Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 18:00:53 +03:30
import type {
ConnectedSpanIntent,
DueIntent,
ProsthesisIntent,
ToothIntent,
VoiceIntent,
Weekday,
} from './voice.types';
import { WEEKDAYS } from './voice.types';
/**
* Deliberately flat: strict `json_schema` mode has poor support for discriminated unions,
docs: cut the comments that were not earning their place I wrote 731 comment lines on this branch against 4,530 lines of code — 14%, where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads like its surroundings, and this did not. Removed by genre rather than by taste: - restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the map that literally shows it, and a docblock on startOfWeek explaining that it returns the start of the week; - narrating history — "this used to rebuild the whole map", "left the bar recording forever" — which the commit message and git blame already carry; - saying the same thing in several places: the "cannot record is not a denied microphone" reason appeared three times in one file, and the "aborting stops a per-minute metered call" reason across three files. Each now lives once, where the behaviour it explains lives; - defending decisions nobody would question, like why toLatinDigits is its own module; - over-explaining defensive branches, three separate comments to distinguish null from missing-kind from unrecognised-kind. What stays is what the code cannot say: the patient-right convention in toFdi, whose failure mode is a valid code for the wrong tooth; the "this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari accepting no mimeType hint; and the invariants whose violation already cost a bug — the body parser's middleware ordering and the dispatch panel's auto-fill rules. Comments only. The diff contains no non-comment line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
* so every variant field is present and nullable. `toVoiceIntent` narrows it into the
* internal union and is total anything it cannot classify becomes a shape the resolvers
* report as unresolved rather than something that throws here.
feat(backend): OpenRouter voice providers and per-locale registry ASR and extraction are separate, independently swappable roles resolved per locale from config. All three locales point at the same OpenRouter models today (whisper-1, gemini-3.7-flash); the indirection stays because Persian ASR is the weakest link and repointing only `fa` must not be a code change. The model emits a deliberately flat wire shape rather than the internal discriminated unions — strict json_schema mode has poor union support — and toVoiceIntent narrows it. That normalizer is total: a missing or malformed payload yields a shape the resolvers report as unresolved rather than one that throws. The prompt supplies catalog codes with labels in the actor's locale, so the model matches spoken words rather than translating, and carries per-locale tooth vocabulary. English gets an explicit warning that a bare two-digit number is ambiguous under Universal numbering, and must not be treated as FDI unless the speaker said so. From review of this commit: - only an actually FDI-shaped code takes the explicit branch; fdi:"6" alongside valid arch/side/position used to lose the tooth entirely - an unrecognised due kind passes through to be flagged, instead of collapsing to null and looking like no deadline was ever spoken - vendor error bodies stay out of the thrown message and the default log level; a 4xx can echo the request back, transcript included - the chat call sets provider.require_parameters so OpenRouter only routes to endpoints that honour the JSON schema, rather than ones treating it as a hint - an unknown locale in VOICE_ENABLED_LOCALES now fails at boot like an unknown provider id, instead of silently disabling the microphone everywhere Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 18:00:53 +03:30
*/
export type WireToothIntent = {
spoken: string;
/** The two-digit FDI code the clinician spoke; null when the tooth was described. */
feat(backend): OpenRouter voice providers and per-locale registry ASR and extraction are separate, independently swappable roles resolved per locale from config. All three locales point at the same OpenRouter models today (whisper-1, gemini-3.7-flash); the indirection stays because Persian ASR is the weakest link and repointing only `fa` must not be a code change. The model emits a deliberately flat wire shape rather than the internal discriminated unions — strict json_schema mode has poor union support — and toVoiceIntent narrows it. That normalizer is total: a missing or malformed payload yields a shape the resolvers report as unresolved rather than one that throws. The prompt supplies catalog codes with labels in the actor's locale, so the model matches spoken words rather than translating, and carries per-locale tooth vocabulary. English gets an explicit warning that a bare two-digit number is ambiguous under Universal numbering, and must not be treated as FDI unless the speaker said so. From review of this commit: - only an actually FDI-shaped code takes the explicit branch; fdi:"6" alongside valid arch/side/position used to lose the tooth entirely - an unrecognised due kind passes through to be flagged, instead of collapsing to null and looking like no deadline was ever spoken - vendor error bodies stay out of the thrown message and the default log level; a 4xx can echo the request back, transcript included - the chat call sets provider.require_parameters so OpenRouter only routes to endpoints that honour the JSON schema, rather than ones treating it as a hint - an unknown locale in VOICE_ENABLED_LOCALES now fails at boot like an unknown provider id, instead of silently disabling the microphone everywhere Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 18:00:53 +03:30
fdi: string | null;
arch: 'upper' | 'lower' | null;
side: 'patient_right' | 'patient_left' | null;
position: number | null;
};
export type WireDue = {
kind: 'weekday' | 'offset' | 'jalali' | 'gregorian' | 'none';
weekday: Weekday | null;
which: 'this' | 'next' | null;
unit: 'day' | 'week' | 'month' | null;
amount: number | null;
jy: number | null;
jm: number | null;
jd: number | null;
y: number | null;
m: number | null;
d: number | null;
};
export type WireVoiceIntent = {
treatmentType: string | null;
teeth: WireToothIntent[];
connectedSpans: { from: WireToothIntent; to: WireToothIntent }[];
comment: string | null;
prosthesisDefaultType: string | null;
prosthesisOverrides: { tooth: WireToothIntent; type: string }[];
labId: string | null;
labMatchExact: boolean;
due: WireDue;
};
const TOOTH_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['spoken', 'fdi', 'arch', 'side', 'position'],
properties: {
spoken: {
type: 'string',
description: 'The exact transcript words for this tooth.',
},
fdi: {
type: ['string', 'null'],
description:
'The two-digit FDI code the clinician said for this tooth, e.g. "26". Null only ' +
'when the tooth was described in words instead of numbered.',
feat(backend): OpenRouter voice providers and per-locale registry ASR and extraction are separate, independently swappable roles resolved per locale from config. All three locales point at the same OpenRouter models today (whisper-1, gemini-3.7-flash); the indirection stays because Persian ASR is the weakest link and repointing only `fa` must not be a code change. The model emits a deliberately flat wire shape rather than the internal discriminated unions — strict json_schema mode has poor union support — and toVoiceIntent narrows it. That normalizer is total: a missing or malformed payload yields a shape the resolvers report as unresolved rather than one that throws. The prompt supplies catalog codes with labels in the actor's locale, so the model matches spoken words rather than translating, and carries per-locale tooth vocabulary. English gets an explicit warning that a bare two-digit number is ambiguous under Universal numbering, and must not be treated as FDI unless the speaker said so. From review of this commit: - only an actually FDI-shaped code takes the explicit branch; fdi:"6" alongside valid arch/side/position used to lose the tooth entirely - an unrecognised due kind passes through to be flagged, instead of collapsing to null and looking like no deadline was ever spoken - vendor error bodies stay out of the thrown message and the default log level; a 4xx can echo the request back, transcript included - the chat call sets provider.require_parameters so OpenRouter only routes to endpoints that honour the JSON schema, rather than ones treating it as a hint - an unknown locale in VOICE_ENABLED_LOCALES now fails at boot like an unknown provider id, instead of silently disabling the microphone everywhere Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 18:00:53 +03:30
},
arch: { type: ['string', 'null'], enum: ['upper', 'lower', null] },
side: {
type: ['string', 'null'],
enum: ['patient_right', 'patient_left', null],
description: "The PATIENT's side, never the viewer's.",
},
position: {
type: ['integer', 'null'],
description:
'Position from the midline: 1 = central incisor … 8 = third molar. Never an FDI code.',
feat(backend): OpenRouter voice providers and per-locale registry ASR and extraction are separate, independently swappable roles resolved per locale from config. All three locales point at the same OpenRouter models today (whisper-1, gemini-3.7-flash); the indirection stays because Persian ASR is the weakest link and repointing only `fa` must not be a code change. The model emits a deliberately flat wire shape rather than the internal discriminated unions — strict json_schema mode has poor union support — and toVoiceIntent narrows it. That normalizer is total: a missing or malformed payload yields a shape the resolvers report as unresolved rather than one that throws. The prompt supplies catalog codes with labels in the actor's locale, so the model matches spoken words rather than translating, and carries per-locale tooth vocabulary. English gets an explicit warning that a bare two-digit number is ambiguous under Universal numbering, and must not be treated as FDI unless the speaker said so. From review of this commit: - only an actually FDI-shaped code takes the explicit branch; fdi:"6" alongside valid arch/side/position used to lose the tooth entirely - an unrecognised due kind passes through to be flagged, instead of collapsing to null and looking like no deadline was ever spoken - vendor error bodies stay out of the thrown message and the default log level; a 4xx can echo the request back, transcript included - the chat call sets provider.require_parameters so OpenRouter only routes to endpoints that honour the JSON schema, rather than ones treating it as a hint - an unknown locale in VOICE_ENABLED_LOCALES now fails at boot like an unknown provider id, instead of silently disabling the microphone everywhere Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 18:00:53 +03:30
},
},
} as const;
export const VOICE_INTENT_JSON_SCHEMA = {
type: 'object',
additionalProperties: false,
required: [
'treatmentType',
'teeth',
'connectedSpans',
'comment',
'prosthesisDefaultType',
'prosthesisOverrides',
'labId',
'labMatchExact',
'due',
],
properties: {
treatmentType: {
type: ['string', 'null'],
description: 'A treatment type CODE from the supplied list, or null.',
},
teeth: { type: 'array', items: TOOTH_SCHEMA },
connectedSpans: {
type: 'array',
description: 'Bridges / splinted units. Endpoints inclusive.',
items: {
type: 'object',
additionalProperties: false,
required: ['from', 'to'],
properties: { from: TOOTH_SCHEMA, to: TOOTH_SCHEMA },
},
},
comment: {
type: ['string', 'null'],
description: 'Clinical notes, in the spoken language.',
},
prosthesisDefaultType: {
type: ['string', 'null'],
description:
'A prosthesis type CODE applied to every tooth unless overridden.',
},
prosthesisOverrides: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['tooth', 'type'],
properties: { tooth: TOOTH_SCHEMA, type: { type: 'string' } },
},
},
labId: {
type: ['string', 'null'],
description: 'An id from the supplied lab list. Never invent one.',
},
labMatchExact: {
type: 'boolean',
description: 'True only when the spoken name matched a lab name exactly.',
},
due: {
type: 'object',
additionalProperties: false,
required: [
'kind',
'weekday',
'which',
'unit',
'amount',
'jy',
'jm',
'jd',
'y',
'm',
'd',
],
properties: {
kind: {
type: 'string',
enum: ['weekday', 'offset', 'jalali', 'gregorian', 'none'],
},
weekday: { type: ['string', 'null'], enum: [...WEEKDAYS, null] },
which: { type: ['string', 'null'], enum: ['this', 'next', null] },
unit: {
type: ['string', 'null'],
enum: ['day', 'week', 'month', null],
},
amount: { type: ['integer', 'null'] },
jy: { type: ['integer', 'null'] },
jm: { type: ['integer', 'null'] },
jd: { type: ['integer', 'null'] },
y: { type: ['integer', 'null'] },
m: { type: ['integer', 'null'] },
d: { type: ['integer', 'null'] },
},
},
},
} as const;
/** Two digits, quadrant 1-8, position 1-8 — the only thing that can be an FDI code. */
const FDI_SHAPE = /^[1-8][1-8]$/;
function toToothIntent(wire: WireToothIntent | undefined | null): ToothIntent {
const spoken = typeof wire?.spoken === 'string' ? wire.spoken : '';
docs: cut the comments that were not earning their place I wrote 731 comment lines on this branch against 4,530 lines of code — 14%, where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads like its surroundings, and this did not. Removed by genre rather than by taste: - restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the map that literally shows it, and a docblock on startOfWeek explaining that it returns the start of the week; - narrating history — "this used to rebuild the whole map", "left the bar recording forever" — which the commit message and git blame already carry; - saying the same thing in several places: the "cannot record is not a denied microphone" reason appeared three times in one file, and the "aborting stops a per-minute metered call" reason across three files. Each now lives once, where the behaviour it explains lives; - defending decisions nobody would question, like why toLatinDigits is its own module; - over-explaining defensive branches, three separate comments to distinguish null from missing-kind from unrecognised-kind. What stays is what the code cannot say: the patient-right convention in toFdi, whose failure mode is a valid code for the wrong tooth; the "this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari accepting no mimeType hint; and the invariants whose violation already cost a bug — the body parser's middleware ordering and the dispatch panel's auto-fill rules. Comments only. The diff contains no non-comment line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
// "۲۶" and "2 6" are FDI codes that do not match literally; unnormalised they fall
// through to the positional branch with no quadrant and read as unresolved.
const fdi = normalizeFdiCode(wire?.fdi);
feat(backend): OpenRouter voice providers and per-locale registry ASR and extraction are separate, independently swappable roles resolved per locale from config. All three locales point at the same OpenRouter models today (whisper-1, gemini-3.7-flash); the indirection stays because Persian ASR is the weakest link and repointing only `fa` must not be a code change. The model emits a deliberately flat wire shape rather than the internal discriminated unions — strict json_schema mode has poor union support — and toVoiceIntent narrows it. That normalizer is total: a missing or malformed payload yields a shape the resolvers report as unresolved rather than one that throws. The prompt supplies catalog codes with labels in the actor's locale, so the model matches spoken words rather than translating, and carries per-locale tooth vocabulary. English gets an explicit warning that a bare two-digit number is ambiguous under Universal numbering, and must not be treated as FDI unless the speaker said so. From review of this commit: - only an actually FDI-shaped code takes the explicit branch; fdi:"6" alongside valid arch/side/position used to lose the tooth entirely - an unrecognised due kind passes through to be flagged, instead of collapsing to null and looking like no deadline was ever spoken - vendor error bodies stay out of the thrown message and the default log level; a 4xx can echo the request back, transcript included - the chat call sets provider.require_parameters so OpenRouter only routes to endpoints that honour the JSON schema, rather than ones treating it as a hint - an unknown locale in VOICE_ENABLED_LOCALES now fails at boot like an unknown provider id, instead of silently disabling the microphone everywhere Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 18:00:53 +03:30
// Only take the explicit branch for something actually FDI-shaped. A model that emits
// fdi:"6" alongside correct arch/side/position would otherwise lose the tooth entirely.
if (FDI_SHAPE.test(fdi)) {
return { kind: 'explicit', fdi, spoken };
}
return {
kind: 'positional',
arch: wire?.arch as 'upper' | 'lower',
side: wire?.side as 'patient_right' | 'patient_left',
position: typeof wire?.position === 'number' ? wire.position : Number.NaN,
spoken,
};
}
function toDueIntent(wire: WireDue | undefined | null): DueIntent | null {
switch (wire?.kind) {
case 'weekday':
return {
kind: 'weekday',
weekday: wire.weekday as Weekday,
which: wire.which as 'this',
};
case 'offset':
return {
kind: 'offset',
unit: wire.unit as 'day',
amount: typeof wire.amount === 'number' ? wire.amount : Number.NaN,
};
case 'jalali':
return {
kind: 'jalali',
jy: wire.jy as number,
jm: wire.jm as number,
jd: wire.jd as number,
};
case 'gregorian':
return {
kind: 'gregorian',
y: wire.y as number,
m: wire.m as number,
d: wire.d as number,
};
case 'none':
case undefined:
return null;
default:
// An unrecognised kind means a deadline WAS spoken and we failed to classify it.
// Passing it through lets the resolver flag it; collapsing it to null would make a
// misunderstood deadline indistinguishable from no deadline at all.
return { kind: wire?.kind } as unknown as DueIntent;
}
}
export function toVoiceIntent(wire: WireVoiceIntent): VoiceIntent {
const teeth = Array.isArray(wire?.teeth) ? wire.teeth : [];
const spans = Array.isArray(wire?.connectedSpans) ? wire.connectedSpans : [];
const overrides = Array.isArray(wire?.prosthesisOverrides)
? wire.prosthesisOverrides
: [];
const connectedSpans: ConnectedSpanIntent[] = spans.map((span) => ({
from: toToothIntent(span?.from),
to: toToothIntent(span?.to),
}));
const hasProsthesis =
wire?.prosthesisDefaultType != null || overrides.length > 0;
const prosthesis: ProsthesisIntent | null = hasProsthesis
? {
defaultType: wire?.prosthesisDefaultType ?? null,
overrides: overrides.map((o) => ({
tooth: toToothIntent(o?.tooth),
type: o?.type,
})),
}
: null;
return {
treatmentType: wire?.treatmentType ?? null,
teeth: teeth.map(toToothIntent),
connectedSpans,
comment: wire?.comment ?? null,
prosthesis,
labId: wire?.labId ?? null,
labMatchExact: wire?.labMatchExact === true,
due: toDueIntent(wire?.due),
};
}