From 18f8f97a43b61121b022fde9a91726ec305c70b8 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 16 Jul 2026 10:47:38 -0500 Subject: [PATCH] fixes --- bayarea-ai-server/docker-compose.yml | 2 +- vision_runner.ts | 40 +++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/bayarea-ai-server/docker-compose.yml b/bayarea-ai-server/docker-compose.yml index 29fa60b..e47c4ba 100644 --- a/bayarea-ai-server/docker-compose.yml +++ b/bayarea-ai-server/docker-compose.yml @@ -107,7 +107,7 @@ services: - -fa - "on" - -c - - "40960" + - "65536" - --parallel - "1" # KV-Cache quantisieren — 32GB VRAM, 35B-A3B + mmproj + Bildkontext: diff --git a/vision_runner.ts b/vision_runner.ts index d4b7d6e..a2576fe 100644 --- a/vision_runner.ts +++ b/vision_runner.ts @@ -184,8 +184,10 @@ function isoDate(y: number, mo: number, d: number): string | null { /** US-Formate: 6/25/26, 06-25-2026, "June 25, 2026" → YYYY-MM-DD. */ function normDate(raw: string | null): string | null { - const c = cleanStr(raw); + let c = cleanStr(raw); if (!c) return null; + // Leerzeichen um Trenner entfernen: "09 / 13 / 2021" -> "09/13/2021" + c = c.replace(/\s*([\/\-.])\s*/g, "$1").trim(); let m = c.match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2}|\d{4})$/); if (m) return isoDate(parseInt(m[3], 10), parseInt(m[1], 10), parseInt(m[2], 10)); m = c.match(/^([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{2}|\d{4})$/); @@ -344,7 +346,7 @@ STEP 3 — Transcribe VERBATIM (exactly as written, do not normalize or expand a - total_purchase_price: "Total Purchase Price" as written - down_payment_raw: "Down Payment" as written (e.g. "$350,000", "1.5M", "TBD") 6. If doc_type is "ca_only": transcribe from the CA page — the prospective buyer's printed/signed name into prospective_buyer, plus address/phone/email if present. Everything else null. -7. date_of_introduction_raw: the date next to the buyer's signature on the CA page, verbatim (e.g. "6/25/26"), else null. +7. date_of_introduction_raw: the "Date of Introduction" on the CA page (usually near the buyer's signature), transcribed EXACTLY as written including any spaces or separators (e.g. "09 / 13 / 2021", "6/25/26", "Sept 13 2021"). If a date is visible anywhere labeled "Date of Introduction", always return it verbatim — never leave it null just because the format looks unusual. Only null if truly no such date is present. 8. From the NOTES page (only if notes_page is set): - notes_business_raw: the business name(s) written in the "Business Interested In" TABLE of the notes page, verbatim (join multiple with a comma). Take ONLY the table entries — do NOT include the free-text "Notes" area at the bottom of the page. If the table is empty, null. The notes page and the info sheet are INDEPENDENT sources; never copy content from one into the other's field. @@ -426,12 +428,41 @@ async function callVision(api: string, images: string[], timeoutMs: number): Pro // Merge: deterministischer Datensatz + Vision-Rohwerte → Zielstruktur // --------------------------------------------------------------------------- +/** + * Zieht den Namen deterministisch aus dem Dateinamen (verlaessliche Quelle, + * unabhaengig von der Vision-Transkription). Schema: "Nachname, Vorname [Zusatz].pdf". + * Gibt "Nachname, Vorname" zurueck, oder null wenn nicht erkennbar. + * Beeinflusst die Bilderkennung NICHT — reine String-Operation. + */ +function nameFromFilename(fileName: string): string | null { + let s = fileName.replace(/\.pdf$/i, ""); + // Klammer-Zusaetze wie "(Gordon Greve)" / "(JF Lehman)" entfernen + s = s.replace(/\([^)]*\)/g, " ").replace(/\s+/g, " ").trim(); + // Schema ist "Nachname, Vorname ...". Nimm den Nachnamen (vor dem Komma) + // und aus dem Rest nur das erste Wort als Vorname - alles Weitere + // (Zweitnamen, Datum, Notes/Oilfield-Zusaetze) faellt weg. + const comma = s.indexOf(","); + if (comma < 0) { + // kein Komma: erstes Wort als Ganzes nehmen, ohne Datum/Zusatz + const first = s.replace(/\b\d{4,8}\b.*$/, "").trim(); + return first.length >= 2 ? first : null; + } + const last = s.slice(0, comma).trim(); + const rest = s.slice(comma + 1).trim(); + // erstes Token des Rests = Vorname (stoppt vor Datum/Zusatzwort) + const firstName = (rest.match(/^[A-Za-zÀ-ÿ.'-]+/) || [""])[0]; + if (!last || !firstName) return null; + return `${last}, ${firstName}`; +} + + function mergeRecord(det: BuyerRecord, vis: VisionRaw, model: string): BuyerRecord { const base: BuyerRecord = { ...det, _parser: "vision", _vision_model: model, _vision_ts: new Date().toISOString(), + name_from_filename: nameFromFilename(det.file_name), _vision_error: undefined, }; delete (base as Record)["_vision_error"]; @@ -464,7 +495,10 @@ function mergeRecord(det: BuyerRecord, vis: VisionRaw, model: string): BuyerReco down_payment: dp.value, down_payment_raw: dp.raw, // Vision-Datum bevorzugt; Fallback: deterministischer Wert (z.B. aus Dateinamen) - date_of_introduction: normDate(vis.date_of_introduction_raw) ?? (det.date_of_introduction as string | null) ?? null, + // Datum: ISO wenn parsebar, sonst ROHWERT behalten (Mitarbeiter korrigiert + // spaeter). Nie verwerfen, nur weil das Format ungewohnt ist. + date_of_introduction: normDate(vis.date_of_introduction_raw) ?? cleanStr(vis.date_of_introduction_raw) ?? (det.date_of_introduction as string | null) ?? null, + date_of_introduction_raw: cleanStr(vis.date_of_introduction_raw), _info_page: vis.info_page, _ca_page: vis.ca_page, _notes_page: vis.notes_page,