
    jj"                       U d Z ddlmZ ddlZddlZddlmZmZ ddlm	Z	 ddl
mZ  ej                  e      Z ed       G d	 d
             Z ed       G d d             ZddddddZdaded<   daded<   ddZ	 	 	 	 	 	 ddZddZy)u  ECG-FM numerical verifier (Phase 4).

The interpretation LLM produces measurements (heart rate, intervals, axis,
ST deviation) along with its qualitative read. Multimodal LLMs are
demonstrably weak at reading numbers off rendered EKG strips — they
confabulate plausible-looking values. This module runs a numerical model
on the underlying WFDB signal to produce ground-truth measurements, then
the caller reconciles: if the LLM and verifier disagree by more than a
configurable threshold, the verifier wins and the corrected number is
flagged on the case.

This module is intentionally lazy — the model loads on first call so the
service can boot even on a host that hasn't fetched the weights yet. If
torch or the model files are missing, every call returns measurements_skipped
with a clear reason rather than failing hard.

Setup (one-time, on the droplet):

    cd apps/ekg-tutor/interpret
    .venv/bin/pip install torch torchvision wfdb
    .venv/bin/python -c "from huggingface_hub import snapshot_download;         snapshot_download('bowang-lab/ECG-FM', local_dir='./models/ecg-fm')"

Then set EKG_TUTOR_ECG_FM_PATH=./models/ecg-fm in /root/secrets.env.
    )annotationsN)	dataclassasdict)Path)AnyT)frozenc                  ^    e Zd ZU ded<   ded<   ded<   ded<   ded<   ded<   d	ed
<   d	ed<   y)MeasurementVerificationstrfieldfloat	llm_valueverifier_valueunitdelta	thresholdbool	agreementcorrection_appliedN)__name__
__module____qualname____annotations__     O/var/www/adampowell.pro/apps/ekg-tutor/interpret/src/pipelines/ecg_fm_verify.pyr
   r
   &   s.    J
ILOr   r
   c                  >    e Zd ZU ded<   ded<   ded<   ded<   d
dZy	)VerifierResultr   	available
str | Noneskipped_reasonmodel_versionzlist[MeasurementVerification]verificationsc           	         | j                   | j                  | j                  | j                  D cg c]  }t	        |       c}dS c c}w )Nr   r!   r"   r#   )r   r!   r"   r#   r   )selfvs     r   to_dictzVerifierResult.to_dict9   sI    "11!//151C1CD1CAfQi1CD	
 	
 Es   A
N)returnzdict[str, Any])r   r   r   r   r(   r   r   r   r   r   2   s    O00
r   r   g      $@      )ratezintervals.przintervals.qrszintervals.qtcaxisr   _modelr    _load_errorc                    t         t        t         t        fS t        j                  j	                  dd      j                         } | s
dadt        fS t        |       j                         sd|  adt        fS 	 ddl}	 t        j                  d       d	adt        fS # t        $ r dadt        fcY S w xY w# t        $ r}d
| adt        fcY d}~S d}~ww xY w)zReturns (model, error_reason). Caches the result so the load only runs
    once per process. On missing torch / weights / etc., returns (None, reason).NEKG_TUTOR_ECG_FM_PATH zEKG_TUTOR_ECG_FM_PATH not setz$ECG-FM weights directory not found: r   z6torch is not installed; run: uv pip install torch wfdbu   ECG-FM model loader not implemented — Phase 4 scaffolding only. Wire up bowang-lab/ECG-FM in pipelines/ecg_fm_verify.py to enable verification.z8ECG-FM loader not implemented (Phase 4 scaffolding only)zECG-FM load failed: )r.   r/   osenvirongetstripr   existstorchImportErrorloggerwarning	Exception)ecg_fm_pathr8   es      r   _load_model_lazyr?   P   s     [4{""**..!8"=CCEK5[  ##%<[MJ[  !
! 	^	
 Q[  !  !N[  !"  !,QC0[  !s0   :B ?B7 B43B47	C CCCc                \    t               \  }}|t        d|xs ddg       S t        d      )zRuns ECG-FM on the WFDB signal and reconciles with LLM measurements.

    `llm_measurements` is a flat dict of the LLM's numeric reads, keyed by
    the same field strings used in THRESHOLDS. Missing fields are ignored.
    NFunavailabler%   uu   ECG-FM forward pass not implemented; Phase 4 scaffolding only. Loader returned non-None unexpectedly — investigate.)r?   r   NotImplementedError)signal_pathllm_measurementsmodelerrs       r   verify_measurementsrG   w   sH     "#JE3}/-	
 	
" 	A r   c                 d    t         j                  j                  dd      j                         dk7  S )zACheap check for /health endpoints. Doesn't trigger the lazy load.r1   r2   )r3   r4   r5   r6   r   r   r   is_availablerI      s&    ::>>126<<>"DDr   )r)   ztuple[Any, str | None])rC   z
str | PathrD   zdict[str, float]r)   r   )r)   r   )__doc__
__future__r   loggingr3   dataclassesr   r   pathlibr   typingr   	getLoggerr   r:   r
   r   
THRESHOLDSr.   r   r/   r?   rG   rI   r   r   r   <module>rR      s   4 #  	 )  			8	$ $   $
 
 
" 
  Z $!N& DEr   