# Self-Hosted Stool Vision Decision — 2026-08-19 ## Executive verdict **Self-hosting removes the provider-shyness roadblock, but it does not remove the accuracy roadblock.** Timmy now has a working self-hosted profile, a local `llama.cpp` server path—using its OpenAI-compatible server capability[6]—provider readiness reporting, a consent-traceable training-image ingester, and a real-photo runtime receipt. The recommended bootstrap model is **SmolVLM2-2.2B-Instruct Q4_K_M plus its Q8 vision projector**. The base model is Apache-2.0 and explicitly accepts image and text inputs.[11] The official GGUF repository provides the quantized main model and projector needed by `llama.cpp`.[10] Do not ship this general-purpose VLM as the final Bristol classifier. On a real public CC-licensed Type 4 stool photograph, it did not refuse the image and emitted valid JSON, but it estimated Type 1 with confidence 0.5. Timmy's validator correctly abstained. That proves sovereign transport and content acceptance; it disproves product-grade classification accuracy. ## What was actually executed ### Host - 4 virtual CPU cores at 2.0 GHz - 7.8 GiB RAM - no GPU - no swap ### Real stool input The test image was the Wikimedia Commons file **Human Feces Bristol Stool Chart Type 4**, labeled Type 4 by its source and licensed CC BY-SA 3.0.[7] A derived test copy was resized, re-encoded, and stripped of EXIF before inference. ### SmolVLM-500M-Instruct Q8 The 500M model is Apache-2.0, is designed for image/text input, and its card says one-image inference can use 1.23 GB of GPU RAM.[1] The official GGUF files used in the spike were a 436,806,912-byte main model and a 108,783,360-byte Q8 projector.[2] Observed on this CPU host: - resident memory: **908,420 KiB** - structured request latency: **25.606 s** - detected stool: **yes** - Bristol type: **no usable value** - schema: malformed/incomplete - refusal: **none** Verdict: useful only as a cheap stool/not-stool or quality-gate experiment; too weak for the product label. ### SmolVLM2-2.2B-Instruct Q4_K_M Observed on this CPU host: - model plus projector on disk: about **1.6 GiB** - resident memory: **3,362,048 KiB** - cold structured request: **89.259 s** - subsequent Timmy end-to-end request: **15.904 s** - detected stool: **yes** - predicted color: **brown** - predicted Bristol type: **1** (source label: Type 4) - confidence: **0.5** - image quality: **poor** - Timmy result: **abstained and requested user input** - refusal: **none** Verdict: **adopt for bootstrap labeling experiments, reject as the final classifier.** The current VPS proves the path but lacks the latency and operational headroom for a smooth production service. ## Candidate decision | Candidate | License observed | Current weight footprint | Decision | |---|---|---:|---| | SmolVLM2-2.2B-Instruct | Apache-2.0[11] | 1.6 GiB GGUF bundle used | **Bootstrap choice**; no refusal, structured output, inadequate Bristol accuracy | | SmolVLM-500M-Instruct | Apache-2.0[1] | 521 MiB GGUF bundle used | Quality/admission experiments only | | Moondream 2 | Apache-2.0, but its own card calls it the previous generation[5] | 3.85 GB BF16 model file observed | Do not start new work here | | Qwen2.5-VL-3B-Instruct | Qwen Research License[3] | 7.51 GB BF16 shards observed | **Reject for product default**: the license defines use as non-commercial research/evaluation and requires a separate license for commercial use.[4] | ## Production architecture ```text phone camera -> client resize + metadata removal -> Timmy ingress: decode, MIME/magic-byte check, size cap, request ID -> self-hosted visual gate: stool / not-stool / unusable -> specialist classifier: Type 1..7 probabilities + color probabilities -> calibration + abstention policy -> strict Timmy JSON schema -> user reviews and confirms -> local journal save -> separate, explicit opt-in training contribution ``` ### Model responsibilities 1. **General VLM:** bootstrap descriptions, reject non-stool images, flag blur/occlusion, and help reviewers. It never supplies the authoritative training label. 2. **Human confirmation:** the user selects or corrects Bristol type and color. A clinical reviewer adjudicates the evaluation set and ambiguous examples. 3. **Specialist discriminative model:** train a small image classifier for `not-stool`, `unknown`, and Types 1–7, with separate color and quality heads. This is the production inference model. 4. **Timmy policy layer:** apply calibrated confidence thresholds, abstain, require confirmation, and keep symptoms entirely user-entered. This structure matters because published stool-image work exists—including a long-term automated smart-toilet feasibility study[9]—but accessible training data is scarce. One 2024 pilot analyzed 151 images from only five hospitalized patients, and the paper says its data is not publicly available except for a possible limited request.[8] We therefore need our own consented dataset rather than pretending a generic VLM already solves the domain. ## Concrete consented-data pipeline Implemented now: `scripts/ingest_training_photo.py`. For every explicitly contributed image it: - requires a consent version and human-review status; - decodes the image rather than trusting its extension; - applies EXIF orientation, then strips metadata; - resizes to a maximum 1200-pixel dimension; - hashes the derived bytes; - creates a salted pseudonymous subject key; - assigns train/validation/test by subject, not by image; - records Bristol type, broad color, quality, and review provenance in JSONL; - never writes the original source path into the manifest. Keeping every image from one subject in one split prevents the model from looking better merely because near-duplicate photos from the same person leaked into training and test sets. ## Dataset ladder ### Phase A — 100-image reality check - Collect opt-in images across all seven types and non-stool/poor-quality negatives. - Every label is user-confirmed; ambiguous items are clinician-reviewed. - Compare the VLM, a frozen image encoder plus linear head, and a small fine-tuned classifier. - Stop if the labels are too inconsistent to support the seven-way task. ### Phase B — minimum credible training corpus Target at least: - **250 independently reviewed examples per Bristol type**; - **200+ distinct contributors**; - hard negatives: empty toilets, toilet paper, urine-only, glare, blur, occlusion, diapers, and non-toilet contexts; - held-out contributors and devices; - intentionally varied lighting, bowl colors, water tint, distance, and camera quality. These are engineering gates, not a guarantee of medical validity. ### Phase C — release gates A build cannot prefill unless it demonstrates on held-out contributors: - macro-F1 of at least **0.80** across Types 1–7; - at least **0.90 precision** among non-abstained suggestions; - expected calibration error at or below **0.05**; - valid schema on **100%** of responses; - no original-image persistence in transient inference; - no training use without an explicit contribution record; - zero automatic symptom, disease, or bleeding conclusions. If confidence is below the calibrated threshold, Timmy asks the user. Coverage is allowed to fall; false certainty is not. ## Hardware gate - **Current VPS:** accepted for a single-user engineering spike only. It ran the model, but 89-second cold latency, 3.36 GiB model RSS, four saturated CPU cores, no swap, and no GPU leave inadequate production margin. - **Bootstrap inference / fine-tuning:** use a separate NVIDIA GPU worker with **16 GB VRAM recommended**. Bind inference privately; Timmy remains the public API and release authority. - **Later production classifier:** after collecting data, export the specialist model to ONNX/TensorRT. A small classifier should be much faster and cheaper than keeping a VLM in the hot path, and may ultimately run on CPU or device. ## Shipped controls - `TIMMY_VISION_PROFILE=selfhost` - loopback OpenAI-compatible default: `http://127.0.0.1:8080/v1` - self-hosted privacy status from `/api/vision-status` - upstream `/v1/models` readiness probe - API-key redaction from public status - 120-second bounded timeout for self-hosted inference - `scripts/run_selfhost_smolvlm.sh` - `scripts/ingest_training_photo.py` - fail-closed visual schema and manual fallback ## Decision **Proceed with self-hosting, but treat SmolVLM2 as the bootstrap worker—not the product brain.** It solves “the provider refuses the shit picture.” The consented dataset plus specialist classifier solves “turn the shit picture into reliable concrete data.” ## Sources [1] https://huggingface.co/HuggingFaceTB/SmolVLM-500M-Instruct — SmolVLM-500M-Instruct model card [2] https://huggingface.co/ggml-org/SmolVLM-500M-Instruct-GGUF — Official SmolVLM GGUF conversion [3] https://huggingface.co/Qwen/Qwen2.5-VL-3B-Instruct — Qwen2.5-VL-3B-Instruct model card [4] https://huggingface.co/Qwen/Qwen2.5-VL-3B-Instruct/blob/main/LICENSE — Qwen research license [5] https://huggingface.co/vikhyatk/moondream2 — Moondream 2 model card [6] https://github.com/ggml-org/llama.cpp — llama.cpp repository [7] https://commons.wikimedia.org/wiki/File:Human_Feces_Bristol_Stool_Chart_Type_4.jpg — Wikimedia Commons: Human Feces Bristol Stool Chart Type 4 [8] https://europepmc.org/articles/PMC11350077 — AI- and physician-interpreted stool image pilot study [9] https://europepmc.org/articles/PMC11686500 — Long-term automated stool monitoring smart toilet feasibility study [10] https://huggingface.co/ggml-org/SmolVLM2-2.2B-Instruct-GGUF — Official SmolVLM2 2.2B GGUF conversion [11] https://huggingface.co/HuggingFaceTB/SmolVLM2-2.2B-Instruct — SmolVLM2-2.2B-Instruct model card