Skip to content

17 Stool Photo Pipeline

Anubha Parashar edited this page Oct 10, 2026 · 1 revision

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.

Responsibilities

  • 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.

Inputs and outputs

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

Pipeline

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"]
Loading

Timestamp precedence as implemented

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.

Timezone handling

  • An EXIF time with OffsetTimeOriginal is 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_date comes 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).

Duplicate detection and bulk intake

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.

Observation and review states

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.

Words the system refuses to output

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.

Security boundaries

  • Originals are encrypted with Fernet (crypto.encrypt) and written under a random secrets.token_hex(24).enc name through api.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 EncryptedText columns. Metadata holds timestamps and camera make/model only, never pixel data.
  • Photo deletion sets deleted_at and deletes the blob. Discarding an intake card deletes its blob. Image responses send no-store and nosniff.
  • Every intake, image, confirm and discard route returns 404 to other families (test_intake_is_private_to_the_family). All actions are audited.

Clinical safety rules

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.

Implementation

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.

Related pages

Daily Health and Care Calendar · Encryption and Private Storage · Security and Privacy Model · Testing and Validation · Limitations

Clone this wiki locally