Repository navigation
17 Stool Photo Pipeline
The stool photo pipeline turns one or many potty or diaper photographs into confirmed stool entries in Daily Health (internal code name: BabyCare). For each photo it works out when it was really taken, describes what is visibly there (colour family and coarse texture hints), checks for duplicates, and then waits for the caregiver to confirm or correct the values. Image-based observation only. Not a diagnosis. Upload time is never used as the event time.
- Detect the capture time from the image bytes, the file name and metadata, recording the source and confidence.
- Convert every candidate time into the profile's local timezone and reject times in the future or before birth.
- Produce appearance suggestions with confidences. Never name an organism, an infection or a disease.
- Hold uploads as pending intake cards (48-hour TTL). Nothing reaches the journal until the caregiver confirms.
- Detect duplicates by SHA-256, both within a batch and against stored photos.
- Store originals Fernet-encrypted and serve them only to the owning family.
| Endpoint | Input | Output |
|---|---|---|
POST /api/patients/{pid}/care/stool/intake |
up to 24 files (12 MB each; JPEG, PNG, HEIC, WebP) plus parallel client_names, client_exif, file_modified
|
intake payload: cards sorted by detected time, by_date counts, duplicates |
GET …/care/stool/intake |
— | pending and duplicate cards (expired ones are discarded first) |
GET /api/care/stool-intake/{iid}/image |
— | decrypted image, Cache-Control: no-store
|
POST /api/care/stool-intake/{iid}/confirm |
corrected at, colour, consistency, flags, create_diaper, client_key
|
the new stool entry with photo, suggestion and parent values |
DELETE /api/care/stool-intake/{iid} |
— | discards the card and deletes the encrypted blob |
POST /api/care/stool/{eid}/photos |
one file attached to an existing entry | photo id, observation, detected time |
POST /api/care/stool/{eid}/observation-review |
confirm, correct or dismiss
|
updated entry |
GET/DELETE /api/care/stool-photos/{id} |
— | image, or soft delete plus blob deletion |
GET …/care/stool/photos?start=&end= |
range of at most one year | photo history grouped by day |
flowchart TD
IMG["Image upload<br/>single or bulk up to 24"] --> HASH["SHA-256 duplicate check<br/>batch and stored photos"]
HASH --> TS["Timestamp extraction<br/>stool_intake.detect_timestamp"]
subgraph TSX["Capture time precedence"]
E1["EXIF DateTimeOriginal<br/>high"] --> E2["EXIF Digitized or DateTime<br/>high"]
E2 --> E3["Phone-reported EXIF<br/>medium"]
E3 --> F1["Filename date and time<br/>medium"]
F1 --> M1["XMP or PNG metadata<br/>medium"]
M1 --> F2["Filename date only<br/>low, needs confirmation"]
F2 --> FS["File modified time<br/>low, weak"]
FS --> NONE["None<br/>caregiver must enter time"]
end
TS --> E1
TS --> OBS["Image observation<br/>colour family and texture hints"]
OBS --> DISC["Image-based observation only.<br/>Not a diagnosis."]
DISC --> CARD["Pending intake card<br/>48 hour TTL"]
NONE --> CARD
CARD --> CONF{"Caregiver confirmation"}
CONF -->|"confirm or correct"| EV["Stool event<br/>StoolEntry plus StoolPhoto<br/>optional stool diaper"]
CONF -->|"discard"| DEL["Card discarded<br/>encrypted blob deleted"]
EV --> DH["Daily Health<br/>day summary and timeline"]
DH --> CAL["Care Calendar<br/>month week day"]
detect_timestamp() builds a list of candidates and picks the first one that is neither in the future (more than 5 minutes ahead) nor more than a day before birth. Every candidate and every rejection (in the future, before birth) is stored in metadata_json / detection_json as an audit trail.
| Order | Candidate | Stored source
|
Confidence | Needs confirmation |
|---|---|---|---|---|
| 1 | EXIF DateTimeOriginal (36867) |
exif |
high | no |
| 2 | EXIF DateTimeDigitized, else DateTime
|
exif_created |
high | no |
| 3 | EXIF date reported by the phone picker (client_exif) |
exif |
medium (label says "reported by the phone") | no |
| 4 | Date and time in the file name | filename |
medium | no |
| 5 | XMP CreateDate/DateCreated or PNG text chunk |
metadata |
medium | no |
| 6 | Date only in the file name (time is set to 12:00) | filename_date |
low |
yes (time_unknown) |
| 7 | File-system modified time sent by the client | filesystem |
low ("weak") | yes |
| — | nothing usable | none |
none |
yes, and confirm returns 422 until a time is entered |
The file-name parsers cover WhatsApp Image YYYY-MM-DD at H.MM.SS AM, PXL_YYYYMMDD_HHMMSS (treated as UTC), IMG_/VID_/Screenshot_/plain YYYYMMDD_HHMMSS, signal-YYYY-MM-DD-HHMMSS, and date-only forms such as IMG-YYYYMMDD-WA0012. The mobile picker uses the legacy gallery or the document picker so that real file names survive, because the Android photo picker renames files to ids.
- An EXIF time with
OffsetTimeOriginalis read in that offset and converted to the profile timezone. Without an offset, it is read as local time in the profile timezone. - Pixel file names are read as UTC and converted. Other file names are read as local time.
- The stored instant is UTC.
local_datecomes from the profile timezone, so a photo taken at 23:40 local time lands on that day even when it is already the next day in UTC (test_timezone_offsets_in_exif_and_utc_filenames_are_converted_to_the_childs_local_time).
Within one upload request, the same file picked twice becomes a single card. A hash that matches an active stored photo, or an intake that was already confirmed, produces a card with status=duplicate and duplicate_of (entry id plus local time). Confirming a duplicate returns 409 ("already recorded … Open that entry instead"). Cards are sorted by detected time and summarised per day. Pending cards expire after 48 hours: they are marked discarded and their blobs deleted when the list is next read or a new upload arrives.
care_vision.observe() samples the central region of a 160 px copy and maps HSV values to a colour family. It reports blood_like_colour: possible when red-like pixels exceed 4%, and states that consistency and mucus are "not assessed from a photograph". stool_intake.suggest() adds coarse texture heuristics on a 96 px copy and returns value, confidence and confidence_pct for colour, consistency, mucus-like material, blood-like colour, watery appearance and amount. consider_clinical_review is true for red, black, pale or white colour, or uncertain blood-like colour. In that case the only message is "Consider clinical review".
ai_review_status |
Set when |
|---|---|
none |
no suggestion was available |
ai_observed |
a photo was attached to an existing entry and analysed |
parent_confirmed |
the caregiver accepted the prefilled values, or chose confirm in review |
parent_corrected |
any field differs from the prefill, correct was chosen, or an ai_observed entry was edited |
dismissed |
the caregiver dismissed the observation |
On confirm, both ai_suggestion_json (what the app proposed) and parent_values_json (what the caregiver saved, plus the time source) are kept. If the caregiver moves the time by more than 60 seconds, time_source=parent_corrected and the photo's taken_at_source=manual. Corrections go through history.remember.
care_vision.FORBIDDEN_WORDS is coli, bacteria, bacterial, viral, virus, parasite, infection, disease, diagnos, rotavirus, salmonella, shigella, giardia, ibd, crohn. Both observe() and suggest() raise an error rather than return text that contains any of them. Tests check every forbidden word against the observation and suggestion payloads, and also check that day views and saved entries never contain "coli". Every payload carries the fixed disclaimer Image-based observation only — not a diagnosis.
- Originals are encrypted with Fernet (
crypto.encrypt) and written under a randomsecrets.token_hex(24).encname throughapi.storage, which never creates public URLs. The original bytes are never modified; analysis runs on downscaled copies. - File names, detection metadata, suggestions and parent values are stored in
EncryptedTextcolumns. Metadata holds timestamps and camera make/model only, never pixel data. - Photo deletion sets
deleted_atand deletes the blob. Discarding an intake card deletes its blob. Image responses sendno-storeandnosniff. - Every intake, image, confirm and discard route returns
404to other families (test_intake_is_private_to_the_family). All actions are audited.
Stool-image interpretation is observational only, not a diagnosis. AI values are suggestions until the caregiver confirms them. Parent-entered and AI-suggested values stay distinguishable. The review flag is a prompt to seek clinical review, never a finding.
api/stool_intake.py, api/care_vision.py, api/care_api.py (stool photo and intake routes), api/care.py (stool_row, photo_history), api/storage.py, mobile/StoolIntake.tsx, mobile/DailyCareScreen.tsx, web/care.js, tests/test_stool_intake.py, tests/test_care.py.
Daily Health and Care Calendar · Encryption and Private Storage · Security and Privacy Model · Testing and Validation · Limitations
Overview
Platform
Identity and access
Medical records
- 10 Document Ingestion
- 11 OCR and Human Verification
- 13 Medicines and Administration
- 14 Vaccination Model
- 15 Labs, Growth, Longitudinal
- 16 Illness and Lifetime Health
Daily care
Output and sharing
Security
Engineering
- 26 Testing and Validation
- 27 Deployment and CI/CD
- 28 Technology Stack
- 29 Design Decisions
- 30 Limitations
- 31 Roadmap