mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
fix(labs): reserve and re-check each page of a scan, and read digits from the text layer
The route reserved one read's budget and checked the wire once, and a 20-page report then ran up to 40 calls for many minutes on that single admission. Every read after the first now reserves its own share of the day's budget and asks the wire again (aiEgressRefusal) before it is sent; when either refuses, the scan stops there, keeps the pages already read and names the rest with stoppedBy budget or unavailable. The run settles against everything it reserved, with the provider's reported usage net of its cached share, and a scan that fails after several calls is charged for them instead of refunded in full. A rendered PDF page now goes out with its own text layer, read by pdfjs beside the image, so a digitally produced report's digits, decimal separators and units come from its characters rather than from a 1600 px picture. Splitting the PDF into single-page documents would need a PDF writer the project does not ship; the text layer needs none and helps every provider. A scanned page has no text layer and reads as before. Codex's cut reply keeps the text it streamed (on a non-enumerable property, so no log or event serialises it), and its closed rows are salvaged like every other provider's. The merge folds a reading only into one an earlier page listed, never two rows of the same page, and the printed range is part of the key. OCR'd text the deadline cannot split small enough for one read is refused up front as labs.ocr.tooLarge.
This commit is contained in:
1 parent
6376710254
commit
79d905f089
26 files changed
+715
-126
No files matched your search
@@ -242,6 +242,9 @@ describe("extractErrorMessage", () => {
|
||||
expect(extractErrorMessage(refusal("labs.ocr.outputLimit", 422), t)).toBe(
|
||||
"labs.ocr.outputLimit",
|
||||
);
|
||||
expect(extractErrorMessage(refusal("labs.ocr.tooLarge", 422), t)).toBe(
|
||||
"labs.ocr.tooLarge",
|
||||
);
|
||||
});
|
||||
|
||||
it("never calls an unrelated 403 a missing consent", () => {
|
||||
@@ -296,6 +299,15 @@ describe("<OcrReadNotes> — what a read could not give", () => {
|
||||
expect(render({ unreadPages: [] })).toContain("Part of the report");
|
||||
});
|
||||
|
||||
it("says why a scan stopped early when it was not the provider", () => {
|
||||
expect(render({ unreadPages: [4], stoppedBy: "budget" })).toContain(
|
||||
"AI budget for today ran out",
|
||||
);
|
||||
expect(render({ unreadPages: [4], stoppedBy: "unavailable" })).toContain(
|
||||
"AI was turned off while the report was being read",
|
||||
);
|
||||
});
|
||||
|
||||
it("says how many readings the report had when it ran past the cap", () => {
|
||||
expect(render({ rowsCapped: { kept: 400, found: 512 } })).toContain(
|
||||
"The report lists 512 readings. One scan takes 400",
|
||||
|
||||
@@ -107,6 +107,8 @@ export function extractErrorMessage(
|
||||
return t("labs.ocr.pdfNeedsAnthropic");
|
||||
case "labs.ocr.outputLimit":
|
||||
return t("labs.ocr.outputLimit");
|
||||
case "labs.ocr.tooLarge":
|
||||
return t("labs.ocr.tooLarge");
|
||||
case "consent.ai.required":
|
||||
return t("labs.ocr.consentRequired");
|
||||
case "ai.provider.none":
|
||||
@@ -272,6 +274,7 @@ export function OcrReviewDialog({
|
||||
setReadNotes({
|
||||
truncated: data.truncated,
|
||||
unreadPages: data.unreadPages,
|
||||
stoppedBy: data.stoppedBy,
|
||||
rowsCapped: data.rowsCapped,
|
||||
});
|
||||
setStage("review");
|
||||
@@ -516,7 +519,7 @@ export function OcrPageCoverageNote({
|
||||
|
||||
type OcrReadNotesValue = Pick<
|
||||
OcrExtractResponseDto,
|
||||
"truncated" | "unreadPages" | "rowsCapped"
|
||||
"truncated" | "unreadPages" | "stoppedBy" | "rowsCapped"
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -528,7 +531,7 @@ type OcrReadNotesValue = Pick<
|
||||
*/
|
||||
export function OcrReadNotes({ notes }: { notes: OcrReadNotesValue }) {
|
||||
const { t } = useTranslations();
|
||||
const { truncated, unreadPages, rowsCapped } = notes;
|
||||
const { truncated, unreadPages, stoppedBy, rowsCapped } = notes;
|
||||
if (!truncated && !unreadPages && !rowsCapped) return null;
|
||||
return (
|
||||
<div data-slot="ocr-read-notes" className="text-warning space-y-1 text-sm">
|
||||
@@ -541,6 +544,13 @@ export function OcrReadNotes({ notes }: { notes: OcrReadNotesValue }) {
|
||||
: t("labs.ocr.truncated")}
|
||||
</p>
|
||||
) : null}
|
||||
{stoppedBy ? (
|
||||
<p>
|
||||
{stoppedBy === "budget"
|
||||
? t("labs.ocr.stoppedBudget")
|
||||
: t("labs.ocr.stoppedUnavailable")}
|
||||
</p>
|
||||
) : null}
|
||||
{unreadPages ? (
|
||||
<p>
|
||||
{unreadPages.length > 0
|
||||
|
||||
Reference in new issue
Block a user