Field Media Ingest — Content Classifier (TB-26, 2026-07-29)

Category-less uploads through /field-media/ingest and /upload used to default silently to PHOTOS, so pier logs sent without a category cue never landed in PIERLOGS and task_parse_pierlog_vision OCR never fired (the trigger was the label, not the content). Bit us 2026-07-29: Darius’s pier logs rode a general upload and Dustin re-classified them by hand.

Shipped in entirely-api a0228fc:

  • classify_document_type(path) in services/field_photo_analyzer.py — one narrow Vision call (config.MODEL_VISION_OCR, same client as the SOW proposal path): PIERLOG / PHOTO / PERMIT / PLAN / OTHER + confidence. PDFs classify page 1 only. Never raises — any failure returns PHOTO/low so filing behaves exactly as before.
  • Sentinel contract: category default on /ingest + /upload is now empty = “caller didn’t choose” → defaults to PHOTOS but the classifier may re-route. An explicit category from any caller always wins — Kilroy’s verbal-cue category=PIERLOGS path is untouched.
  • Only promotes INTO PIERLOG (medium+ confidence), re-filed before save_file so the existing OCR-queue block fires naturally. PERMIT/PLAN/OTHER verdicts are logged for tuning, never auto-filed (v1).
  • Burst guard: field_media:classify_tries:<project> — max 8 Vision classify calls / project / 10 min; past the cap, spillover routes to PIERLOG by inference only if the burst already detected one (field_media:classified_pierlog:<project>). Fails open on Redis errors.
  • Response surfaces auto_classified + classifier_confidence so Kilroy can say “filed 6 to pier logs (auto-detected)“. Dedup guards unchanged.
  • Side fix: PyMuPDF 1.28 Pixmap.tobytes(jpg_quality=) kwarg — the old jpeg_quality= crashed the PDF rasterize path (same latent bug fixed in permit_parser._pdf_to_image_blocks).

Verified live in-container against real files: pier-log JPG → PIERLOG/high, pier-log PDF → PIERLOG/high, site photo → PHOTO/high. 19 tests in tests/test_tb26_ingest_classifier.py. /ingest-batch deliberately not wired in v1 (doesn’t share _file_and_register).

Deploy pending: container rebuild/restart left deliberate per brief.

Related: field-media-dedup-transparency, field-photo-analyzer, pier-log-ocr