• 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
Name
Last commit
Last update
..
schemas_builtin Loading commit data...
workers Loading commit data...
__init__.py Loading commit data...
base.py Loading commit data...
budget.py Loading commit data...
chandra.py Loading commit data...
detect.py Loading commit data...
doctr.py Loading commit data...
engine_configs.py Loading commit data...
extract.py Loading commit data...
gateway.py Loading commit data...
loopguard.py Loading commit data...
manager.py Loading commit data...
olmocr.py Loading commit data...
paddle.py Loading commit data...
rasterize.py Loading commit data...
resume.py Loading commit data...
schemas.py Loading commit data...
served_model.py Loading commit data...
subprocess_engine.py Loading commit data...
surya.py Loading commit data...
venv_build.py Loading commit data...