Ë
    ÍþÍj‹/  ã                   óŒ  — d Z ddlZddlZddlZddlmZ ddlmZ ddlZddl	m
Z
 ddlmZ ddlmZmZ ddlmZ  ej$                  e«      Z ej*                  e
j,                  «      Zd	dd
dœZdddddœZddddddddddddœZd
edeedœZddd œd!d"d#d$dœd%d&d&d'd(d)d*d+d,œd-œZh d.£h d/£h d0£d1œZd2Zd3ed4efd5„Z d6ee!   d7e!fd8„Z"d9ed7e!fd:„Z#d9ed6e!d7e!fd;„Z$d<e!d7e!fd=„Z%d<e!d7efd>„Z&d?ed<e!d7e'eef   fd@„Z(dAedBedCed7dfdD„Z)d<e!d7e!fdE„Z*dFe+dGedHed<e!d7e+f
dI„Z,dPd?edHed<e!d9ed7ef
dJ„Z-dKedLedMe.d7dfdN„Z/dQd?ed9edMe.d<ee!   d7ee   f
dO„Z0y)Ru÷  
protection_service.py â€” protecciÃ³n de caras en servidor (port de hec-photo-app).

Dos ejes independientes y combinables, como en HEC (ambos apagables por separado):

  - `protection_heads_enabled`: el archivo que SE SERVIRÃ� lleva las caras difuminadas, horneadas en
    servidor con StoryFace KIS (`POST /api/v2/kis/apply_effect`, 1 crÃ©dito por llamada).
  - `protection_overlay` (`none` | `lens` | `puzzle`): capa animada en el NAVEGADOR sobre lo que se
    sirva, que estropea las capturas de pantalla. AquÃ­ solo se guarda y valida su configuraciÃ³n; la
    dibujan los componentes de `frontend/src/components/protection/`.

No se precalcula nada al ingerir la foto: proteger por adelantado CADA foto de CADA familia gastarÃ­a un
crÃ©dito aunque esa familia no abra nunca el portal. Se protege bajo demanda, la primera vez que se pide
la vista previa, y el resultado queda en disco (`.protect/` junto al original; las visitas siguientes
son gratis e instantÃ¡neas).

âš ï¸� Diferencia deliberada con HEC: allÃ­, si KIS falla, esa foto se sirve SIN proteger (fail-open, para no
bloquear la venta). AquÃ­ el respaldo es la vista previa reducida, sin texto encima (`media_service`):
la familia sigue viendo y comprando la foto, pero nunca un original sin proteger.
é    N)ÚPath)ÚOptional)Úconfig)Úget_db)Ú	ai_healthÚphoto_service)Úwith_retriesç      ð?F)Ú	intensityÚ	max_headsÚinverté   éZ   g      @ç       @)ÚnumBallsÚradiusÚzoomÚspeedÚmixÚclassicé   é   é   gÍÌÌÌÌÌì?gÍÌÌÌÌÌÜ?é
   Té*   )ÚshapeÚ	cut_styleÚ
num_piecesÚmissingÚswapsÚcircle_sizeÚ	mix_ratioÚborder_widthÚanimateÚseedÚgapÚnone©Úprotection_heads_enabledÚprotection_headsÚprotection_overlayÚprotection_lensÚprotection_puzzle)ç        r   )r   é2   )r   r   )r   r   )é   é    )r
   ç      @)g      à?r2   )r   é   )r   r   )g333333ã?r
   )r.   r
   )r   é   )r   é   )r   i@B )r   r   r    r!   r"   r#   r&   r%   )ÚheadsÚlensÚpuzzle>   r7   r'   r8   >   r   Úcircler8   >   ÚwavyÚsharpr   Ústraight)Úoverlayr   r   Úportal_protectionÚsectionÚkeyc           	      ó4  — t         |    |   \  }}	 t        |«      }||cxk  r|k  sn t        | › d|› d|› d|› �«      ‚t	        |t
        «      rt	        |t        «      st        |«      S t        |d«      S # t        t        f$ r t        | › d|› d�«      ‚w xY w)Nú.z: invalid numberz: debe estar entre z y r   )Ú_RANGESÚfloatÚ	TypeErrorÚ
ValueErrorÚ
isinstanceÚintÚboolÚround)r?   r@   ÚvalueÚdefaultÚloÚhiÚnums          ú6/var/www/photos/backend/services/protection_service.pyÚ_numberrQ   E   s­   € Ü�WÑ˜cÑ"�F€Bˆð=Ü�E‹lˆð �Œ?˜Œ?Ü˜G˜9 A c UÐ*=¸b¸TÀÀRÀDÐIÓJÐJÜ! '¬3Ô/¼
À7ÌDÔ8QŒ3ˆs‹8ÐdÔW\Ð]`ÐbcÓWdÐdøô	 ”zÐ"ò =Ü˜G˜9 A c UÐ*:Ð;Ó<Ð<ð=ús   ‘A6 Á6!BÚrawÚreturnc                 ó’  — | xs i } t        | j                  dd«      «      t        t        «      | j                  dd«      t        t        «      t        t
        «      dœ}|d   t        d   vrt        d«      ‚| j                  d«      xs i j                  «       D ]C  \  }}|d	k(  rt        |«      |d   d	<   Œ|t        d
   v sŒ)t        d
||t        |   «      |d   |<   ŒE | j                  d«      xs i j                  «       D ],  \  }}|t        d   v sŒt        d||t        |   «      |d   |<   Œ. | j                  d«      xs i j                  «       D ]n  \  }}|t        v r#|t        |   vrt        d|› d�«      ‚||d   |<   Œ1|dk(  rt        |«      |d   d<   ŒH|t        d   v sŒTt        d||t
        |   «      |d   |<   Œp |S )u[   Mezcla `raw` con los valores por defecto y valida rangos. ValueError si algo no es vÃ¡lido.r)   Fr+   r'   r(   r=   z'protection_overlay: none, lens o puzzler*   r   r6   r,   r7   r-   zpuzzle.z: invalid valuer$   r8   )rI   ÚgetÚdictÚDEFAULT_HEADS_CONFIGÚDEFAULT_LENS_CONFIGÚDEFAULT_PUZZLE_CONFIGÚ_ENUMSrF   ÚitemsrC   rQ   )rR   ÚoutÚkÚvs       rP   Únormalize_configr_   P   sü  € à
Š)�€Cä$(¨¯©Ð1KÈUÓ)SÓ$TÜ Ô!5Ó6Ø!Ÿg™gÐ&:¸FÓCÜÔ 3Ó4Ü!Ô"7Ó8ñ€Cð ÐÑ ¬¨yÑ(9Ñ9ÜÐBÓCÐCØ—‘Ð+Ó,Ò2°×9Ñ9Ó;ò Y‰ˆˆ1Ø�Š=Ü04°Q³ˆCÐ"Ñ# HÒ-Ø”'˜'Ñ"Ò"Ü)0°¸!¸QÔ@TÐUVÑ@WÓ)XˆCÐ"Ñ# AÒ&ð	Yð
 —‘Ð*Ó+Ò1¨r×8Ñ8Ó:ò V‰ˆˆ1Ø”˜‘ÒÜ(/°¸¸1Ô>QÐRSÑ>TÓ(UˆCÐ!Ñ" 1Ò%ðVð —‘Ð,Ó-Ò3°×:Ñ:Ó<ò \‰ˆˆ1Ø”‰;Øœ˜q™	Ñ!Ü  7¨1¨#¨_Ð!=Ó>Ð>Ø*+ˆCÐ#Ñ$ QÒ'Ø�)Š^Ü26°q³'ˆCÐ#Ñ$ YÒ/Ø”'˜(Ñ#Ò#Ü*1°(¸A¸qÔBWÐXYÑBZÓ*[ˆCÐ#Ñ$ QÒ'ð\ð €Jó    Ú	school_idc                 óZ  — dd l }t        «       5 }|j                  d| t        f«      j	                  «       }d d d «       	 t        r|d   r|j                  |d   «      «      S i «      S # 1 sw Y   Œ4xY w# t        $ r,}t        j                  d| |«       t        i «      cY d }~S d }~ww xY w)Nr   z=SELECT value FROM school_settings WHERE school_id=? AND key=?rK   uP   [%s] ConfiguraciÃ³n de protecciÃ³n ilegible, se usan los valores por defecto: %s)
Újsonr   ÚexecuteÚ
_STORE_KEYÚfetchoner_   ÚloadsÚ	ExceptionÚloggerÚwarning)ra   rc   ÚconnÚrowÚexcs        rP   Úget_protectionrn   p   s£   € ÛÜ	‹ð @�TØ�l‰lÐZÐ]fÔhrÐ\sÓt×}Ñ}Óˆ÷@ð$Ü¹CÀCÈÂL §
¡
¨3¨w©<Ó 8ÓYÐYÐVXÓYÐY÷@ð @ûô ò $Ü�‰ÐiÐktÐvyÔzÜ Ó#Õ#ûð$ús.   �'A)¿#A5 Á#A5 Á)A2Á5	B*Á>!B%ÂB*Â%B*c           	      óÔ   — dd l }t        |«      }t        «       5 }|j                  d| t        |j                  |«      f«       |j                  «        d d d «       |S # 1 sw Y   |S xY w)Nr   zMINSERT OR REPLACE INTO school_settings (school_id, key, value) VALUES (?,?,?))rc   r_   r   rd   re   ÚdumpsÚcommit)ra   rR   rc   Úcfgrk   s        rP   Úset_protectionrs   {   s^   € ÛÜ
˜3Ó
€CÜ	‹ð �TØ�‰ÐdØ¤¨T¯Z©Z¸«_Ð=ô	?à�‰Œ÷ð €J÷	ð €Jús   š9AÁA'rr   c                 ó(   — | d   | d   | d   | d   dœS )ud   Lo que ve el navegador del portal: el overlay y su configuraciÃ³n. Lo de caras es cosa del servidor.r+   r,   r-   r)   )r=   r7   r8   r6   © ©rr   s    rP   Úpublic_viewrw   …   s2   € ð Ð+Ñ,ØÐ%Ñ&ØÐ)Ñ*ØÐ/Ñ0ñ	ð r`   c           	      óZ  — t        t        | j                  dt        d   «      «      d«      t	        | j                  dt        d   «      «      t        | j                  dt        d   «      «      f}t        j                  t        |«      j                  «       «      j                  «       dd S )uf   Huella de los parÃ¡metros que cambian el resultado visual (normaliza tipos: `1` y `1.0` son lo mismo).r   é   r   r   Nr   )rJ   rD   rU   rW   rH   rI   ÚhashlibÚsha1ÚreprÚencodeÚ	hexdigest)rr   r@   s     rP   Úconfig_fingerprintr   ‘   s‹   € ô 	Œe�C—G‘G˜KÔ)=¸kÑ)JÓKÓLÈaÓPÜˆC�G‰G�KÔ!5°kÑ!BÓCÓDÜˆS�W‰W�XÔ3°HÑ=Ó>Ó?ð€Cô
 �<‰<œ˜S›	×(Ñ(Ó*Ó+×5Ñ5Ó7¸¸Ð<Ð<r`   Ú
photo_pathc                 ó`   — | j                   dz  }||| j                  › dt        |«      › d�z  fS )uÊ   (carpeta de cachÃ©, archivo protegido). El nombre lleva la huella de `cfg` a propÃ³sito: sin ella,
    cambiar los ajustes no tenÃ­a efecto, porque se servÃ­a el archivo cacheado con el ajuste ANTERIOR.z.protectÚ_heads_z.jpg)ÚparentÚstemr   )r€   rr   Úprotect_dirs      rP   Úprotect_pathsr†   ›   s?   € ð ×#Ñ# jÑ0€KØ˜¨¯©Ð(9¸ÔASÐTWÓAXÐ@YÐY]Ð&^Ñ^Ð^Ð^r`   r…   r„   Úkeepc                 óÂ   — 	 | j                  |› d�«      D ]  }||k7  sŒ	|j                  d¬«       Œ y# t        $ r!}t        j	                  d||«       Y d}~yd}~ww xY w)zSBorra las variantes cacheadas de OTRAS configuraciones. Best-effort: nunca propaga.z_heads_*T)Ú
missing_oku3   heads: no se pudo limpiar la cachÃ© vieja de %s: %sN)ÚglobÚunlinkrh   ri   rj   )r…   r„   r‡   Ústalerm   s        rP   Ú_forget_stale_variantsr�   ¢   sg   € ðYØ ×%Ñ%¨¨¨hÐ&7Ó8ò 	.ˆEØ˜‹}Ø—‘¨�Õ-ñ	.øô ò YÜ�‰ÐLÈdÐTW×XÑXûðYús   ‚4 Ÿ4 ´	A½AÁAc           	      óº   — dt        | j                  dt        d   «      «      t        | j                  dt        d   «      «      | j                  d«      rddœS ddœS )uÏ  Campos del formulario de `apply_effect`.

    âš ï¸� El `protect_invert` de KIS estÃ¡ al revÃ©s de lo que promete nuestro interruptor: con `false` (su
    valor por defecto) el efecto se aplica AL FONDO y la cara queda nÃ­tida; con `true` se aplica A LA
    CARA. Nuestro `invert` significa lo contrario (sin marcar: protege las caras), asÃ­ que se manda
    invertido. Bug real en producciÃ³n de HEC: sin esto el interruptor hacÃ­a lo opuesto a su etiqueta.
    Úblurr   r   r   ÚfalseÚtrue)Úprotect_effectÚprotect_intensityÚprotect_max_headsÚprotect_invert)ÚstrrU   rW   rv   s    rP   Úkis_form_datar—   ¬   sa   € ð !Ü  §¡¨Ô6JÈ;Ñ6WÓ!XÓYÜ  §¡¨Ô6JÈ;Ñ6WÓ!XÓYØ%(§W¡W¨XÔ%6˜'ñ	ð ð =Cñ	ð r`   Úphoto_bytesÚfilenameÚ	api_tokenc           	   ƒ   ó¼  K  — t        j                  d¬«      4 ƒd {  –—† }|j                  t        j                  d|| dfit        |«      d|i¬«      ƒ d {  –—† }|j                  dk7  rVt        j                  d|j                  |j                  d d	 «       t        d
|j                  › d|j                  d d › �«      ‚|j                  «       j                  d«      }|st        d«      ‚|j                  |«      ƒ d {  –—† }|j                  dk7  rt        d«      ‚|j                  cd d d «      ƒd {  –—†  S 7 �Œ!7 Œè7 Œ@7 Œ# 1 ƒd {  –—†7  sw Y   y xY w­w)Ng      ^@)ÚtimeoutÚimagez
image/jpegz	X-API-Key)ÚfilesÚdataÚheaderséÈ   zKIS apply_effect error %s: %siô  zKIS apply_effect failed (HTTP z): Úurlz#KIS apply_effect: respuesta sin urlz1KIS apply_effect: error al descargar el resultado)ÚhttpxÚAsyncClientÚpostr   ÚKIS_API_URLr—   Ústatus_coderi   ÚerrorÚtextrh   rc   rU   Úcontent)r˜   r™   rš   rr   ÚclientÚresponser¢   Údownloads           rP   Ú	_call_kisr®   ¼   sR  è ø€ Ü× Ñ ¨Ô/÷  ð  °6ØŸ™Ü×ÑØ˜X {°LÐAÐBÜ˜sÓ#Ø  )Ð,ð	 %ó 
÷ 
ˆð ×Ñ 3Ò&Ü�L‰LÐ8¸(×:NÑ:NÐPX×P]ÑP]Ð^bÐ_bÐPcÔdÜÐ<¸X×=QÑ=QÐ<RÐRUÐV^×VcÑVcÐdhÐehÐViÐUjÐkÓlÐlØ�m‰m‹o×!Ñ! %Ó(ˆÙÜÐAÓBÐBØŸ™ C›×(ˆØ×Ñ 3Ò&ÜÐOÓPÐPØ×Ñ÷! ÷  ò  ùð
øð )øð ø÷  ÷  ñ  üsi   ‚E�D>žE¡7EÁEÁB)EÄEÄ)EÄ,EÄ8EÄ9EÅEÅEÅEÅEÅEÅEÅEc              ƒ   ó¦  ‡ ‡‡‡	K  — t        ‰ ‰«      \  }}|j                  «       r|S t        4 ƒd{  –—†  |j                  «       r|cddd«      ƒd{  –—†  S |j                  d¬«       t	        j
                  ‰ j                  «      ƒ d{  –—† Š		 t        ˆˆˆ	ˆ fd„d‰ j                  › �¬«      ƒ d{  –—† }t        j                  |«       |j                  d«      }t	        j
                  |j                  |«      ƒ d{  –—†  t	        j
                  |j                   |«      ƒ d{  –—†  t	        j
                  t"        |‰ j$                  |«      ƒ d{  –—†  ddd«      ƒd{  –—†  |S 7 �Œ@7 �Œ!7 Œé7 ŒÂ# t        $ r+}t        j                  |t        j                  |«       ‚ d}~ww xY w7 Œ­7 Œ‡7 Œ[7 ŒM# 1 ƒd{  –—†7  sw Y   |S xY w­w)u¬   VersiÃ³n con caras protegidas de una foto, cacheada en disco. Idempotente por foto Y por configuraciÃ³n.
    Propaga la excepciÃ³n si falla: el llamante decide quÃ© servir.NT)Úexist_okc                  ó4   •— t        ‰‰j                  ‰ ‰«      S ©N)r®   Úname)rš   rr   r˜   r€   s   €€€€rP   ú<lambda>zensure_heads.<locals>.<lambda>Ü   s   ø€ ¬Y°{ÀJÇOÁOÐU^Ð`cÓ-d€ r`   zkis-protect )Úlabelú.tmp)r†   ÚexistsÚ_semÚmkdirÚasyncioÚ	to_threadÚ
read_bytesr	   r³   rh   r   Úrecord_failureÚ
TASK_HEADSÚrecord_successÚwith_suffixÚwrite_bytesÚreplacer�   r„   )
r€   rš   rr   ra   r…   r\   rR   rm   Útmpr˜   s
   ```      @rP   Úensure_headsrÄ   Ð   s¢  ûè ø€ ô % Z°Ó5Ñ€K�Ø
‡z�z„|Øˆ
Ü÷ [ñ [Ø�:‰:Œ<Ø÷[÷ [ñ [ð 	×Ñ 4ÐÔ(Ü#×-Ñ-¨j×.CÑ.CÓD×Dˆð	Ü$Ö%dØ-9¸*¿/¹/Ð9JÐ+KôM÷ MˆCô
 	× Ñ  Ô+Ø�o‰o˜fÓ%ˆÜ×Ñ §¡°Ó5×5Ð5Ü×Ñ §¡¨SÓ1×1Ð1Ü×ÑÔ 6¸ÀZÇ_Á_ÐVYÓZ×ZÐZ÷[÷ [ð  €Jð![ùð [ùð EøðMùäò 	Ü×$Ñ$ Y´	×0DÑ0DÀcÔJØûð	úð
 	6øØ1øØZøð[ø÷ [÷ [ð [ð  €JüsÛ   †,G²E2³G¶F;ÁGÁE5ÁGÁ4F;ÂE8ÂF;Â#E<Â7E:Â8E<Â<A	F;ÄF3Ä'F;Ä-F5Ä.-F;ÅF7ÅF;Å GÅ+F9Å,GÅ5GÅ8F;Å:E<Å<	F0Æ&F+Æ+F0Æ0F;Æ5F;Æ7F;Æ9GÆ;GÇGÇGÇ	GÚsrcÚdstÚmax_pxc                 ó¤  — ddl m}m} |j                  j	                  dd¬«       |j                  | «      5 }|j                  |«      }|j                  ||f|j                  «       |j                  dvr|j                  d«      }|j                  d«      }|j                  |dd	d
¬«       |j                  |«       d d d «       y # 1 sw Y   y xY w)Nr   )ÚImageÚImageOpsT)Úparentsr°   )ÚRGBÚRGBArÌ   r¶   ÚWEBPéP   r   )ÚformatÚqualityÚmethod)ÚPILrÉ   rÊ   rƒ   r¹   ÚopenÚexif_transposeÚ	thumbnailÚLANCZOSÚmodeÚconvertrÀ   ÚsaverÂ   )rÅ   rÆ   rÇ   rÉ   rÊ   ÚimgrÃ   s          rP   Ú_resize_webprÜ   é   s­   € ß#Ø‡J�J×Ñ˜T¨DÐÔ1Ø	�‰�C‹ð ˜CØ×%Ñ% cÓ*ˆØ�‰�v˜vÐ&¨¯©Ô6Ø�8‰8˜?Ñ*Ø—+‘+˜eÓ$ˆCØ�o‰o˜fÓ%ˆØ�‰�˜V¨R¸ˆÔ:Ø�‰�CÔ÷÷ ñ ús   ·BCÃCc              ƒ   óÔ  K  — |xs t        |«      }|d   rt        j                  sy|d   }t        | t        j                  ||«      ƒ d{  –—† }t	        |«      }t        j                  | d|› d|› d�«      }|j                  «       r5|j                  «       j                  |j                  «       j                  k  r$t        j                  t        |||«      ƒ d{  –—†  |S 7 Œ—7 Œ­w)uª   Vista previa de la foto con las caras protegidas, o None si la protecciÃ³n estÃ¡ apagada / sin clave.
    Lanza si KIS falla (el llamante cae a la vista previa reducida).r)   Nr*   z.hrB   z.webp)rn   r   ÚSTORYFACE_API_KEYrÄ   r   r   Úmirror_pathr·   ÚstatÚst_mtimerº   r»   rÜ   )r€   ra   rÇ   rr   Ú	heads_cfgÚkis_outÚfprÆ   s           rP   Úprotected_previewrå   ö   sÏ   è ø€ ð Ò
*” 	Ó*€CØÐ)Ò*´&×2JÒ2JØØÐ&Ñ'€IÜ  ¬V×-EÑ-EÀyÐR[Ó\×\€GÜ	˜IÓ	&€BÜ
×
#Ñ
# J°"°R°D¸¸&¸ÀÐ0GÓ
H€CØ�:‰:Œ<˜3Ÿ8™8›:×.Ñ.°·±³×1HÑ1HÒHÜ×Ñ¤¨g°s¸FÓC×CÐCØ€Jð ]øð 	Dús%   ‚A
C(ÁC$ÁBC(ÃC&ÃC(Ã&C()Ú r²   )1Ú__doc__rº   rz   ÚloggingÚpathlibr   Útypingr   r£   Úcorer   Úcore.dbr   Úservicesr   r   Úutils.retryr	   Ú	getLoggerÚ__name__ri   Ú	SemaphoreÚKIS_PARALLEL_MAXr¸   rW   rX   rY   ÚDEFAULT_PROTECTIONrC   rZ   re   r–   rQ   rV   r_   rn   rs   rw   r   Útupler†   r�   r—   Úbytesr®   rÄ   rH   rÜ   rå   ru   r`   rP   ú<module>rö      s†  ðñó( Û Û Ý Ý ã å Ý ß -Ý $à	ˆ×	Ñ	˜8Ó	$€à€w×Ñ˜×0Ñ0Ó1€à%(°qÀEÑJÐ Ø#$°¸CÈ#ÑNÐ à ¸!ÈÐTUÐfjØ r°dÀBÈqñÐ ð
 !&Ø,Ø Ø*Ø.ñÐ ð &°GÑ<Ø ¨I¸zÐT^Ñ_Ø$°À&ÐYcØ&¸ÈÐYgñiñ€ò *Ú(Ú9ñ
€ð
 !€
ð
e�Sð e˜só eð˜( 4™.ð ¨Tó ð@$˜cð $ dó $ð˜cð ¨ð °ó ð�Tð ˜dó ð=˜Dð = Só =ð_˜dð _¨ð _°%¸¸d¸
Ñ2Có _ðY¨ð Y°Cð Y¸tð YÈó Yð�tð  ó ð   ð  °#ð  À#ð  ÈDð  ÐUZó  ñ( 4ð °Cð ¸dð Èsð Ð\`ó ð2
�dð 
 ð 
¨sð 
°tó 
ñ¨ð ¸ð Àcð ÐPXÐY]ÑP^ð ÐjrÐswÑjxô r`   