• Stefy Lanza (nextime / spora )'s avatar
    pod-read pages lost every box, and a 500 could not say why · e0a90d44
    Stefy Lanza (nextime / spora ) authored
    Five faults, found by one client asking where the OCR boxes had gone.
    
    **The boxes.** `_page_from_pod` rebuilt a pod's page key by key -- index, text,
    width, height, and a named list of diagnostics -- and never copied `lines`,
    `regions` or `tables`. Every page read on a pod therefore arrived with no
    geometry at all, for every engine, and no request parameter could change it: the
    pod puts all of it on the wire (it runs the same `OcrPage.to_dict`) and this
    boundary dropped it. `confidence` survived only by being one of the names.
    
    From outside it looked exactly like a missing engine feature, which is the
    expensive part: the client saw `lines=0` with a populated `conf`, concluded the
    engines were not emitting boxes, and went hunting for the parameter that turns
    line detection on. There is none -- `detect` is stamp/signature detection
    (off|layout|detector|both) and has nothing to do with text lines.
    
    The reassembly now lives once, in `codai.ocr.base.page_from_dict`, shared with
    the worker-subprocess boundary that had the same job and the better
    implementation of it. Diagnostics ride by exclusion rather than by name, which
    is what that boundary already learned after losing `accents_repaired` and then
    `model_attempts` the same way.
    
    A test asserted that "confidence" and "confidence_measured" appeared in the
    SOURCE of the pod reassembly. They did -- in the key-by-key list that was
    discarding the geometry -- so it passed throughout. It now runs the boundary.
    
    **geoclip answered 500 for text.** Reproduced: "una frase" loses its space, and
    "unafrase" is eight characters of the base64 alphabet, so it decodes to six
    bytes and only PIL can tell it is not a picture. PIL raises
    UnidentifiedImageError, which is NOT a ValueError, so it escaped the caller's
    `except ValueError -> 400` and became "the server broke" for bad input. Fixed at
    both ends: `_decode_image` raises ValueError quoting the input, and geoclip's
    IMAGE half joins vpr/salad in the pre-load refusal, because its contract puts
    the image in `input` -- so a text `input` is answerable from the name, before
    1.9 GB is loaded. Its LOCATION half is deliberately excluded: it takes
    coordinates there and already answers a clean 400 naming the format.
    
    **The 5xx logging I added this morning caught nothing.** Starlette lifts a
    handler registered for status `500` out of the status table and gives it to
    ServerErrorMiddleware, which fires only for an UNHANDLED exception -- so
    `raise HTTPException(status_code=500, ...)`, which is how nearly every 500 here
    is raised, went straight past it. Confirmed live: two geoclip 500s, no `[http]`
    line, and the cause had to be found by replaying the request. Now registered on
    the exception class, logging at >= 500, with the status-keyed 400 handler still
    winning for its own richer line. Exercised with a TestClient rather than
    asserted from the source, because the first version passed every source-level
    check and caught nothing.
    
    **A model whose package is absent is no longer advertised as fine.** vosk and
    geoclip were both listed for as long as they existed while answering every
    request with an import error, because nothing asked. `codai/models/deps.py`
    answers it with a memoised `find_spec` -- never an import, never a model load --
    and is used in two places: the listing annotates `unavailable_reason` (the model
    stays listed; hiding one that works is the worse mistake) and the embeddings
    path refuses with a 503 naming the package and the two files it has to be in.
    Only MAIN-venv dependencies are declared, because a package that lives in an
    isolated worker venv is absent from this one by design.
    
    **A fully-GPU-resident GGUF reported itself as offloaded.** "21.47 GB total ...
    of which 19.52 GB was offloaded to host RAM; n_gpu_layers=46", for a model with
    46 of 60 layers on the card and ~15 GB resident there. That number is the
    process's RSS growth, and llama.cpp mmaps the whole file, so a GGUF grows RSS by
    about its file size whatever the split. The arithmetic is untouched -- the TOTAL
    is load-bearing for sizing, it was right, and under-reporting a footprint is how
    eviction stops freeing room and the forward OOMs -- but the line now says which
    of the two numbers it is holding, and the Tasks page no longer prints a delta as
    an offload size.
    Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
    e0a90d44
test_ocr_surya_confidence.py 5.53 KB