Ë
    íÖjVž  ã                   óÖ  — d Z ddlZej                  j                  dd¬«       ddlZddlZddlZddlZddlm	Z	 ddl
m
Z
mZ ddlmZ  ee«      j                  «       j                   j                   Zej$                  j'                  d e ee«      j                  «       j                   «      «       dd	lmZmZmZ dd
lmZmZmZ ddlmZmZ dZ ddddœZ!edz  dz  Z"dZ#ddddddœZ$ e%h d£«       e%h d£«       e%ddh«       e%ddh«       e%h d£«      fZ& ejN                  d«      Z(ddddœZ)d Z*d!Z+d"„ Z,d#„ Z-d$„ Z.d%„ Z/dLd&„Z0d'„ Z1d(„ Z2d)„ Z3dMd*„Z4d+„ Z5d,„ Z6d-„ Z7e"d.fd/„Z8d0„ Z9d1„ Z:d2Z; ejN                  d3ejx                  «      Z=d4„ Z>d5„ Z?d6„ Z@dNd7„ZAdOd8„ZBd9„ ZCd:„ ZDd;„ ZEd<„ ZFd=„ ZGd>„ ZHedz  d?z  ZIedz  d@z  ZJdAZKdBZLdCZMdD„ ZNdE„ ZOdF„ ZPddGde"eIeJdfdH„ZQdI„ ZRdNdJ„ZSeTdKk(  r eU eS«       «      ‚y)Paf  
danbooru_harvest: cosecha de metadatos de la API de Danbooru.

Contraparte de wd14_harvest.py: en vez de pasar imagenes por WD14, reutiliza el
`tag_string` que Danbooru ya trae (tags hechos por la comunidad + rating) y su
coocurrencia para expandir los pools por nivel y construir la biblioteca de
temas. NUNCA descarga imagenes.

Este fichero crece por tareas (plan danbooru-metadata-expansion):
    T2  load_dictionary / load_seeds / aggregate / bucketize / strip_scene_tags
    T3  builders + writers (pools, snapshot, validacion)
    T4  temas (themes.json)
    T5  CLI (--refresh / --dry-run / --posts-file)
é    Núutf-8Úbackslashreplace)ÚencodingÚerrors)ÚCounter)ÚdatetimeÚtimezone)ÚPath)ÚBUCKETSÚclassifyÚload_danbooru)Úapply_weightÚseverityÚstrip_weights_of)ÚDanbooruClientÚDanbooruError)Ú	sugerenteÚspicyÚ	explicitozg+szs+qzq+eÚArchivosÚdanbooru)ÚposeÚsceneÚstateÚ
expressionÚcameraé   é   >   ÚlyingÚon_backÚon_sideÚrunningÚsittingÚwalkingÚcrawlingÚkneelingÚstandingÚ	all_foursÚ	crouchingÚ	recliningÚ	squattingÚ
on_stomachÚ
straddlingÚon_all_fours>   Ú
open_mouthÚparted_lipsÚclosed_mouthÚindoorsÚoutdoorsÚdayÚnight>   ÚnudeÚclothedÚtoplessÚ
bottomlessÚcompletely_nudezR^(holding|holding_weapon|full_body|letterboxed|multiple_views|cumulonimbus_cloud)$Úsfwr   Úexplicit)r;   r   r<   é   c                  ó   — t        «       S )zHDiccionario Danbooru -> (tags de personaje, tags de copyright, aliases).)r   © ó    úEngine/danbooru_harvest.pyÚload_dictionaryrB   I   s
   € ä‹?Ðr@   c                 ó  — t         D �ci c]  }|g “Œ }}d}t        | «      j                  d¬«      }|j                  «       D ]º  }|j	                  dd«      d   j                  «       }|sŒ)|j                  d«      r\|j                  d«      rK|dd	 j                  «       j                  «       }||v r|}n"t        d
|dd	 j                  «       › d�«       d}Œ–|€t        d|› �«       Œ§||   j                  |«       Œ¼ |S c c}w )a  Lee un fichero de seeds por nivel -> {nivel: [tags]}.

    Formato: una seccion `[sugerente]`/`[spicy]`/`[explicito]` seguida de un tag
    por linea. Se saltan comentarios (`#`) y lineas en blanco. Una cabecera de
    seccion desconocida se ignora con un aviso (y no aporta tags).
    Nr   ©r   Ú#r   r   Ú[Ú]éÿÿÿÿú#[!] seccion desconocida ignorada: [z[!] tag sin seccion ignorado: )ÚLEVELSr
   Ú	read_textÚ
splitlinesÚsplitÚstripÚ
startswithÚendswithÚlowerÚprintÚappend)ÚpathÚlevelÚseedsÚcurrentÚtextÚrawÚlineÚnames           rA   Ú
load_seedsr\   N   s  € õ %+Ó+¡F˜5ˆU�B‰Y F€EÐ+Ø€GÜ�‹:×Ñ¨ÐÓ1€DØ�‰Ö ˆØ�y‰y˜˜aÓ  Ñ#×)Ñ)Ó+ˆÙØØ�?‰?˜3Ô D§M¡M°#Ô$6Ø˜˜"�:×#Ñ#Ó%×+Ñ+Ó-ˆDØ�u‰}Ø‘äÐ;¸DÀÀ2¸J×<LÑ<LÓ<NÐ;OÈqÐQÔRØ�ØØˆ?ÜÐ2°4°&Ð9Ô:ØØˆg‰×Ñ˜dÕ#ð !ð  €Lùò' ,s   ‰
Dc                 ó  — t        «       }| D ]b  }	 t        |j                  d«      xs d«      }t        |d«      }|j                  d«      xs dj                  «       D ]  }||xx   |z  cc<   Œ Œd |S # t        t        f$ r d}Y ŒXw xY w)zÔCoocurrencia de tags, ponderada por max(score, 1) de cada post.

    `posts` es una lista de dicts de Danbooru; solo se lee `score` y `tag_string`
    (separado por espacios). Campos desconocidos se ignoran.
    Úscorer   r   Ú
tag_stringÚ )r   ÚintÚgetÚ	TypeErrorÚ
ValueErrorÚmaxrM   )ÚpostsÚcounterÚpostr^   ÚweightÚtags         rA   Ú	aggregaterk   k   s�   € ô ‹i€GÛˆð	Ü˜Ÿ™ Ó)Ò.¨QÓ/ˆEô �U˜A“ˆØ—H‘H˜\Ó*Ò0¨b×7Ñ7Ö9ˆCØ�C‹L˜FÑ"ŒLñ :ð ð €Nøô œ:Ð&ò 	ØŠEð	ús   ‘A4Á4BÂBc                 óì   — t         D �ci c]  }|t        «       “Œ }}g }| j                  «       D ];  \  }}t        |||«      \  }}	|€|j	                  ||	f«       Œ,||   |xx   |z  cc<   Œ= ||fS c c}w )zÛReparte un Counter tag->peso en los buckets de classify().

    -> (buckets, dropped). `buckets` tiene las BUCKETS de wd14_harvest (incluye
    `otros`); `dropped` es [(tag, motivo)] para lo que classify() rechaza.
    )r   r   Úitemsr   rS   )
rg   ÚcharsÚcopiesr[   ÚbucketsÚdroppedrj   ri   ÚbucketÚreasons
             rA   Ú	bucketizert   }   s„   € õ ,3Ó3©7 4ˆt”W“Y‰¨7€GÐ3Ø€GØ—}‘}–‰ˆˆVÜ! # u¨fÓ5‰ˆ�Øˆ>Ø�N‰N˜C ˜=Õ)à�F‰O˜CÓ  FÑ*Ô ð 'ð �GÐÐùò 4s   ‰A1c                 óÎ   — | syg }| j                  d«      D ]<  }|j                  «       }|sŒt        |||«      \  }}|dk(  rŒ,|j                  |«       Œ> dj	                  |«      S )a  Quita de `block` los tags cuyo bucket classify() es `scene`.

    Conserva el resto y el orden original. `chars`/`copies` por defecto vacios
    (Ruling 1): las pool lines ya llegan sin tags de personaje/copyright, asi
    que Task 6 puede llamarla sin cargar el diccionario.
    r`   Ú,r   ú, )rM   rN   r   rS   Újoin)Úblockrn   ro   ÚkeptÚtokenrj   rr   Ú_s           rA   Ústrip_scene_tagsr}   Ž   sl   € ñ ØØ€DØ—‘˜SÖ!ˆØ�k‰k‹mˆÙØÜ˜S %¨Ó0‰	ˆ�Ø�WÒØØ�‰�CÕð "ð �9‰9�T‹?Ðr@   c                 ó�   — | xs dj                  d«      D �cg c]#  }|j                  «       sŒ|j                  «       ‘Œ% c}S c c}w )z;Separa una linea de bloque en tags (por comas), sin vacios.r`   rv   )rM   rN   )rZ   Úts     rA   Ú_tokensr€   ¨   s9   € à $¢
¨×1Ñ1°#Ô6ÓDÑ6˜!¸!¿'¹'½)ˆA�G‰G�IÐ6ÑDÐDùÒDs
   ˜A®Ac                 óz   — | €g S t        | d«      r| j                  «       S t        | j                  «       d„ ¬«      S )z<[(tag, peso)] de mayor a menor. Acepta Counter o dict plano.Úmost_commonc                 ó   — | d    | d   fS )Nr   r   r?   )Úkvs    rA   Ú<lambda>z_top_pairs.<locals>.<lambda>³   s   € °2°a±5°&¸"¸Q¹%±r@   ©Úkey)Úhasattrr‚   Úsortedrm   )rr   s    rA   Ú
_top_pairsrŠ   ­   s9   € à€~Øˆ	Üˆv�}Ô%Ø×!Ñ!Ó#Ð#Ü�&—,‘,“.Ñ&@ÔAÐAr@   c                 ó@   — | xs dj                  «       }||v ry||v ryy)a,  Categoria Danbooru si la conocemos por el diccionario, si no None.

    El snapshot guarda `category` desde `DanbooruClient.tag()`, pero este
    builder no recibe cliente (no debe tocar la red): usa el diccionario
    offline como aproximacion (4=personaje, 3=copyright) y `null` para el resto.
    r`   r=   é   N©rQ   )rj   rn   ro   r   s       rA   Ú_category_ofrŽ   ¶   s-   € ð 
Š�×ÑÓ€AØˆE�zØØˆF�{ØØr@   c                 ób  — dg}dh}| xs dj                  «       }|r&||vr"|j                  |«       |j                  |«       t        t        «      D ��ch c]  \  }}|j                  «       |v r|’Œ }}}t        D �]
  }	t        j                  |	d«      }
d}t        |j                  |	«      «      D ]Ò  \  }}||
k\  st        |«      |k\  r ŒQ|xs dj                  «       }|r'||v s#t        j                  |j                  «       «      rŒYt        t        «      D ��cg c]  \  }}|j                  «       |v r|‘Œ }}}|r|d   |v rŒ–|r|j                  |d   «       |j                  |«       |j                  |«       |dz  }ŒÔ �Œ dj                  d„ |d| D «       «      S c c}}w c c}}w )	a›  Una linea de prompt a partir de la coocurrencia de UN seed.

    Orden: `1girl` + `seed` (garantizado) + top `pose` + top `scene` +
    top `state` + top `expression` + top `camera`. Cada tag pasa por
    `apply_weight` (NO se reimplementa el peso). Se recorta a `max_tokens`
    tags separados por comas. `buckets` es `{bucket: Counter|dict}`.

    Coherencia (`BLOCK_CAPS`/`BLOCK_CONFLICT_GROUPS`/`BLOCK_STOPLIST`): tope
    por categoria, un solo tag por grupo excluyente (posture/mouth/place/time/
    nudity) y fuera los omnipresentes (`holding`, `full_body`...). Sin esto la
    agregacion mezcla opuestos ("standing, sitting", "open_mouth, closed_mouth").
    Ú1girlr`   r   r   r   rw   c              3   ó2   K  — | ]  }t        |«      –— Œ y ­w©N)r   )Ú.0r   s     rA   Ú	<genexpr>zbuild_block.<locals>.<genexpr>ô   s   è ø€ Ð@Ñ.?¨”\ !—_Ñ.?ùs   ‚N)rN   rS   ÚaddÚ	enumerateÚBLOCK_CONFLICT_GROUPSrQ   ÚBLOCK_ORDERÚ
BLOCK_CAPSrb   rŠ   ÚlenÚBLOCK_STOPLISTÚmatchrx   )Úseedrp   Ú
max_tokensrZ   ÚseenÚ	seed_normÚidxÚgroupÚused_groupsÚcatÚcapÚtakenrj   r|   Úgroupss                  rA   Úbuild_blockr¨   Å   s®  € ð ˆ9€DØˆ9€DØ’˜×"Ñ"Ó$€IÙ�Y dÑ*Ø�‰�IÔØ�‰�Ôô $Ô$9Ô:ôá:‰JˆC�Ø�?‰?Ó Ñ%ò 	Ø:ð ñ ÷
 ˆ{ˆÜ�n‰n˜S !Ó$ˆØˆÜ  §¡¨SÓ!1Ö2‰FˆC�Ø˜Š|œs 4›y¨JÒ6ÙØ’9˜"×#Ñ#Ó%ˆCÙ˜# ™+¬×)=Ñ)=¸c¿i¹i»kÔ)JØô #,Ô,AÔ"Bôá"B‘J�C˜Ø—9‘9“; %Ñ'ò Ø"Bð ñ ñ
 ˜& ™) {Ñ2ØÙØ—‘  q¡	Ô*Ø�H‰H�SŒMØ�K‰K˜ÔØ�Q‰J‰Eò# 3ð ð, �9‰9Ñ@¨d°;°JÑ.?Ó@Ó@Ð@ùó7ùós   ÁF%ÄF+
c           
      óB  ‡— t         D �ci c]  }|g “Œ }}t        D ]û  }t        |   }|j                  |«      xs i }t	        t        |«      t        t        «      z  «      }	t        ||   «      }
| j                  |g «      D ]”  }|	r|n|j                  |«      }|sŒt        ||«      }|xs dj                  «       Št        ˆfd„t        t        |«      «      D «       «      }|t        k  rŒk||
v rŒp|
j                  |«       ||   j                  |«       Œ– Œý |S c c}w )a<  `{nivel: {seed: {bucket: Counter}}}` -> `{severity: [lineas]}`.

    El nivel del seed manda sobre la severidad de la linea (mapeo fijo
    `LEVEL_TO_SEVERITY`). Se descartan los seeds que rinden menos de
    `MIN_POOL_TOKENS` tags utiles (mismo suelo que `wd14_blocks --min-tokens`);
    "utiles" = los que vienen de los buckets, SIN contar el `1girl` de cabecera
    ni el `seed` (ambos garantizados). Tambien se descartan lineas repetidas.
    `chars`/`copies` se aceptan para simetria con el resto del motor;
    `bucketize()` ya los aplico, aqui no re-clasifica.
    r`   c              3   óR   •K  — | ]  }|j                  «       d k7  r	|‰k7  rd–— Œ  y­w)r�   r   Nr�   )r“   r   r    s     €rA   r”   zbuild_pools.<locals>.<genexpr>  s-   øè ø€ ð Ù:�aØ—7‘7“9 Ò'¨A°ªNô Ñ:ùs   ƒ$')Ú
SEVERITIESrJ   ÚLEVEL_TO_SEVERITYrb   ÚboolÚsetr   r¨   rN   Úsumr€   r   ÚMIN_POOL_TOKENSr•   rS   )rV   Úper_level_bucketsrn   ro   ÚsevÚpoolsrU   ÚtargetÚseed_bucketsÚsharedÚ
seen_linesr�   rp   rZ   Úusefulr    s                  @rA   Úbuild_poolsr¹   ÷   s!  ø€ õ !+Ó+¡
˜ˆS�"‰W 
€EÐ+ßˆÜ" 5Ñ)ˆØ(×,Ñ,¨UÓ3Ò9°rˆô ”c˜,Ó'¬#¬g«,Ñ6Ó7ˆÜ˜˜v™Ó'ˆ
Ø—I‘I˜e RÖ(ˆDÙ&,‘l°,×2BÑ2BÀ4Ó2HˆGÙØÜ˜t WÓ-ˆDð š ×*Ñ*Ó,ˆIÜó Ü"Ô#3°DÓ#9Ô:óó ˆFð œÒ'ØØ�zÑ!ØØ�N‰N˜4Ô Ø�&‰M× Ñ  Õ&ñ# )ð ð2 €Lùò5 ,s   Š
Dc                 ó  — t        | «      } | j                  j                  dd¬«       | j                  | j                  dz   z  }	 |j	                  ddd¬«      5 }|j                  |«       ddd«       t        j                  || «       |j                  «       r	 t        j                  |«       yy# 1 sw Y   ŒGxY w# t        $ r Y yw xY w# |j                  «       r&	 t        j                  |«       w # t        $ r Y w w xY ww xY w)	zkEscribe `text` en `path` via `<name>.tmp` + `os.replace`. Deja intacto
    el fichero previo si algo falla.T©ÚparentsÚexist_okú.tmpÚwr   Ú
©r   ÚnewlineN)r
   ÚparentÚmkdirr[   ÚopenÚwriteÚosÚreplaceÚexistsÚunlinkÚOSError)rT   rX   ÚtmpÚfs       rA   Ú_write_text_atomicrÎ     sß   € ô �‹:€DØ‡K�K×Ñ˜d¨TÐÔ2Ø
�+‰+˜Ÿ™ VÑ+Ñ
,€Cð	Ø�X‰X�c G°TˆXÔ:¸aØ�G‰G�DŒM÷ ;ä
�
‰
�3˜Ôà�:‰:Œ<ðÜ—	‘	˜#•ð ÷	 ;Ð:ûô ò Ùðûð �:‰:Œ<ðÜ—	‘	˜#•øÜò Ùðúð úsZ   ÁC ÁB2Á,C ÂB> Â2B;Â7C Â>	C
Ã	C
ÃDÃC5Ã4DÃ5	DÃ>DÄ DÄDc                 óB  — t        | «      } | j                  j                  dd¬«       | j                  | j                  dz   z  }	 |j	                  ddd¬«      5 }t        j                  ||dd	¬
«       |j                  d«       ddd«       t        j                  || «       |j                  «       r	 t        j                  |«       yy# 1 sw Y   ŒGxY w# t        $ r Y yw xY w# |j                  «       r&	 t        j                  |«       w # t        $ r Y w w xY ww xY w)z7Volcado atomico de `obj` a JSON (`tmp` + `os.replace`).Tr»   r¾   r¿   r   rÀ   rÁ   Fr   )Úensure_asciiÚindentN)r
   rÃ   rÄ   r[   rÅ   ÚjsonÚdumprÆ   rÇ   rÈ   rÉ   rÊ   rË   )rT   ÚobjrÌ   rÍ   s       rA   Úwrite_json_atomicrÕ   1  sñ   € ä�‹:€DØ‡K�K×Ñ˜d¨TÐÔ2Ø
�+‰+˜Ÿ™ VÑ+Ñ
,€Cð
Ø�X‰X�c G°TˆXÔ:¸aÜ�I‰I�c˜1¨5¸Õ;Ø�G‰G�DŒM÷ ;ô 	�
‰
�3˜Ôà�:‰:Œ<ðÜ—	‘	˜#•ð ÷ ;Ð:ûô ò Ùðûð �:‰:Œ<ðÜ—	‘	˜#•øÜò Ùðúð úsZ   ÁC& Á+CÂC& Â4C ÃCÃC& Ã	C#Ã"C#Ã&DÃ8DÄDÄ	DÄDÄDÄDTc                 óT  ‡	— t        |«      }|j                  dd¬«       i }t        D ]ø  }| j                  |«      xs g D �cg c]&  }|sŒ|j	                  «       sŒ|j	                  «       ‘Œ( }}|d|› d�z  }|s$d||<   |rt        d|› d|j                  › d�«       Œvt        d	„ |D «       «      Š	d
|j                  «       › dt        |«      › d�dj                  ˆ	fd„t        D «       «      z   dz   }t        ||dj                  |«      z   dz   «       t        |«      ||<   Œú |S c c}w )zâEscribe `bloques_{sfw,spicy,explicit}.txt` -> `{severity: n_lineas}`.

    Un pool vacio **no** pisa el fichero previo (keep_on_empty=True) y avisa;
    con keep_on_empty=False escribe solo la cabecera. Escritura atomica.
    Tr»   Úbloques_z.txtr   z	[!] pool z vacio: se conserva z
 sin tocarc              3   óV   K  — | ]!  }t        t        t        |«      «      «      –— Œ# y ­wr’   )r   r€   r   )r“   Úls     rA   r”   zwrite_pools.<locals>.<genexpr>U  s$   è ø€ ð 
Ù<A°qŒH”WÔ-¨aÓ0Ó1×2¹Eùs   ‚')z# danbooru bloques ú: z# lineas
# contenido por severidad: rw   c              3   óL   •K  — | ]  }|› d ‰j                  |d«      › �–— Œ y­w©Ú=r   N©rb   )r“   ÚkÚdists     €rA   r”   zwrite_pools.<locals>.<genexpr>[  s(   øè ø€ ÐD¹°A˜1˜#˜Q˜tŸx™x¨¨1›~Ð.Ô/¹ùó   ƒ!$z

rÀ   )r
   rÄ   r«   rb   rN   rR   r[   r   Úupperrš   rx   rÎ   )
r³   Úout_dirÚkeep_on_emptyÚresultr²   rÙ   ÚlinesrT   Úheaderrà   s
            @rA   Úwrite_poolsrè   C  s=  ø€ ô �7‹m€GØ‡M�M˜$¨€MÔ.Ø€FßˆØ%*§Y¡Y¨s£^Ò%9°rÐ%9ÓNÑ%9˜qºaÀAÇGÁGÅI�—‘•Ð%9ˆÐNØ˜8 C 5¨Ð-Ñ-ˆÙØˆF�3‰KÙÜ˜	 # Ð&:¸4¿9¹9¸+ÀZÐPÔQØäñ 
Ù<Aó
ó 
ˆð " #§)¡)£+ ¨b´°U³°ð =*ð *à�i‰iÓD½ÓDÓDñEàGMñNð 	ô
 	˜4 ¨$¯)©)°EÓ*:Ñ!:¸TÑ!AÔBÜ˜%“jˆˆsŠð% ð& €Mùò% Os   ÁD%Á
D%ÁD%c                 ó  — t        «       }t        «       }i }t        D ]Ö  }|j                  |«      xs i j                  «       D ]®  \  }	}
|
xs g }
t	        |
«      }|j                  |	i «      }|j                  «       D ]&  \  }}|j                  |d«      t        |«      z   ||<   Œ( |
D ]7  }|j                  d«      xs dj                  «       D ]  }||xx   dz  cc<   Œ Œ9 |j                  |«       Œ° ŒØ |j                  «       D ��ci c]9  \  }}|t        |«      t        |||«      t        |j                  |d«      «      dœ“Œ; }}}t        j                  t        j                  «      j                  «       |||dœS c c}}w )a$  Snapshot local: `{fecha, params, tags, cooc}` (ver plan Task 3).

    `posts_by_level` es `{nivel: {seed: [posts]}}`. `tags` agrega TODOS los
    posts (`count` = apariciones crudas, `score` = peso sumado, `category` del
    diccionario o null). `cooc` es `{seed: {tag: peso}}` por seed.
    r   r_   r`   r   )ÚcountÚcategoryr^   )ÚfechaÚparamsÚtagsÚcooc)r   rJ   rb   rm   rk   Ú
setdefaultra   rM   ÚupdaterŽ   r   Únowr	   ÚutcÚ	isoformat)rV   Úposts_by_levelrn   ro   rí   Ú	tag_countÚ	tag_scorerï   rU   r�   rf   rg   Úmergedrj   ri   rh   rê   rî   s                     rA   Úbuild_snapshotrù   b  s{  € ô “	€IÜ“	€IØ€DßˆØ*×.Ñ.¨uÓ5Ò;¸×BÑBÖD‰KˆD�%Ø’K˜RˆEÜ Ó&ˆGØ—_‘_ T¨2Ó.ˆFØ&Ÿ}™}ž‘��VØ$Ÿj™j¨¨aÓ0´3°v³;Ñ>��s’ð  /ã�Ø ŸH™H \Ó2Ò8°b×?Ñ?ÖA�CØ˜c“N aÑ'”Nñ Bð ð ×Ñ˜WÕ%ñ Eð ð$ $Ÿ/™/Ô+ôñ ,‰JˆC�ð 	Ü˜“ZÜ$ S¨%°Ó8Ü˜Ÿ™ s¨AÓ.Ó/ñ
ñ 	
ð
 ,ð 	ñ ô —‘œhŸl™lÓ+×5Ñ5Ó7ØØØñ	ð ùós   Ä	>F c                 ó\  — t        | «      } |xs i }|xs g }|xs i }|xs g D �cg c]&  }|sŒ|j                  «       sŒ|j                  «       ‘Œ( }}ddt        j                  t        j
                  «      j                  «       › �dt        |«      › dt        |«      › dt        |«      › dt        |«      › �ddg}t        |t        «      r1t        |t        ¬	«      D ]  }|j                  d
|› d||   › �«       Œ n*t        |t        ¬	«      D ]  }|j                  d
|› �«       Œ |j                  d«       |D ]X  }t        |t        t        f«      r,t        |«      dk\  r|j                  d
|d   › d|d   › �«       ŒE|j                  d
|› �«       ŒZ |j                  d«       t        |t        «      r1t        |t        ¬	«      D ]  }	|j                  d
|	› d||	   › �«       Œ n|D ]  }	|j                  d
|	› �«       Œ |j                  d«       |D ]  }|j                  d
|› �«       Œ t        | dj!                  |«      dz   «       |rÚ| j"                  dz  dz  }
g }t%        «       }|
j'                  «       r_|
j)                  d¬«      j+                  «       D ]<  }|j                  «       }|sŒ||vsŒ|j-                  |«       |j                  |«       Œ> |D ])  }||vsŒ|j-                  |«       |j                  |«       Œ+ t        |
dj!                  |«      dz   «       yyc c}w )zŽEscribe `validacion.txt` (atomico) y anade los tags `revisar` a
    `revisar/tags.txt` (uno por linea, deduplicado). Nunca descarta nada.
    z# validacion danbooruz	# fecha: z# aceptados: z | rechazados: z | sinonimos: z | a revisar: r`   z[ACEPTADOS]r†   z  Ú	z[RECHAZADOS]r   r   r   z[CANONICALIZADOS]z	->	z[A REVISAR (otros)]rÀ   Úrevisarztags.txtr   rD   N)r
   rN   r   rò   r	   ró   rô   rš   Ú
isinstanceÚdictr‰   ÚstrrS   ÚtupleÚlistrÎ   rx   rÃ   r®   rÉ   rK   rL   r•   )rT   Úacceptedrq   Úcanonicalizedrü   r   Úoutrj   ÚitemrY   ÚrpathÚexistingrŸ   rZ   s                 rA   Úwrite_validation_reportr  ˆ  sî  € ô �‹:€DØŠ~˜2€HØŠm˜€GØ!Ò' R€MØ#*¢=¨b =ÓE¡=˜Q²Q¸1¿7¹7½9ˆq�w‰w�y =€GÐEð 	 Ø
”H—L‘L¤§¡Ó.×8Ñ8Ó:Ð;Ð<Ø
œ˜H›� o´c¸'³l°^ð DÜ˜MÓ*Ð+¨>¼#¸g»,¸ð	Ià
Øð€Cô �(œDÔ!Ü˜(¬×,ˆCØ�J‰J˜˜C˜5  8¨C¡= /Ð2Õ3ñ -ô ˜(¬×,ˆCØ�J‰J˜˜C˜5�zÕ"ð -ð ‡J�Jˆ~ÔÛˆÜ�dœU¤D˜MÔ*¬s°4«y¸Aª~Ø�J‰J˜˜D ™G˜9 B t¨A¡w iÐ0Õ1à�J‰J˜˜D˜6�{Õ#ð	 ð ‡J�JÐ"Ô#Ü�-¤Ô&Ü˜-¬S×1ˆCØ�J‰J˜˜C˜5  }°SÑ'9Ð&:Ð;Õ<ñ 2ó !ˆCØ�J‰J˜˜C˜5�zÕ"ð !ð ‡J�JÐ$Ô%ÛˆØ�
‰
�R˜�u�:Õð ô �t˜TŸY™Y s›^¨dÑ2Ô3áØ—‘˜iÑ'¨*Ñ4ˆØˆÜ‹uˆØ�<‰<Œ>ØŸ™°˜Ó9×DÑDÖF�Ø—j‘j“l�Ú˜3 dš?Ø—H‘H˜S”MØ—O‘O CÕ(ð	 Gó
 ˆCØ˜$ŠØ—‘˜”Ø—‘ Õ$ð ô
 	˜5 $§)¡)¨HÓ"5¸Ñ"<Õ=ð ùòM Fs   ¦L)®L)¿L))Úoutfitr   ÚlightingÚpropsa0  sunset|sunrise|golden_hour|backlight|backlighting|rim_light|cinematic_lighting|lighting|^light$|^lights$|candle|firelight|moonlight|^neon$|neon_lights|window_light|dappled_sunlight|sunlight|^sun$|soft_light|hard_light|^glow$|glowing|^dark$|shadow|silhouette|spotlight|^night$|^day$|^dusk$|^dawn$|^bright$c                 óF   — d„ }t        | xs i j                  «       |¬«      S )zA[(tag, peso)] de mayor a menor; tolera pesos no numericos (-> 0).c                 óp   — 	 t        | d   «      }| t        | d   «      fS # t        t        f$ r d}Y Œ$w xY w)Nr   ç        r   )Úfloatrc   rd   rÿ   )r  ri   s     rA   r‡   z_sorted_pairs.<locals>.keyÞ  sF   € ð	Ü˜4 ™7“^ˆFð �œ˜T !™W›Ð&Ð&øô œ:Ð&ò 	ØŠFð	ús   ‚! ¡5´5r†   )r‰   rm   )Úpairsr‡   s     rA   Ú_sorted_pairsr  Ü  s#   € ò'ô �5’;˜B×%Ñ%Ó'¨SÔ1Ð1r@   c                 ó„   — d}| xs i j                  «       D ]  }	 |t        |«      z  }Œ |S # t        t        f$ r Y Œ'w xY w)zBPeso total de un `{tag: peso}` (tolerante a valores no numericos).r  )Úvaluesr  rc   rd   )r  ÚtotalÚvalues      rA   Ú_total_weightr  ç  sQ   € à€EØ’+˜2×%Ñ%Ö'ˆð	Ø”U˜5“\Ñ!‰Eð (ð
 €Løô œ:Ð&ò 	Ùð	ús   ›-­?¾?c                 ó   — | g i i i i dœS )u;   Tema vacio con el schema de Â§6 (todos los ejes presentes).)ÚsourceÚ	seed_tagsr	  r   r
  r  r?   )r  s    rA   Ú_empty_themer  ò  s    € ð ØØØØØñð r@   c                 ó€  — g }t        «       }| D ]¦  }|xs dj                  «       j                  «       }|rt        j	                  |«      rŒ=t        t        «      D ��cg c]  \  }}||v r|‘Œ }}}|r|d   |v rŒl|r|j                  |d   «       |j                  |«       |sŒ–t        |«      |k\  sŒ¥ |S  |S c c}}w )zæFiltra una lista de tags con los grupos excluyentes y la stoplist.

    Mantiene el orden de entrada (mayor peso primero) y se queda con el primer
    tag de cada grupo (`BLOCK_CONFLICT_GROUPS`). `limit` recorta el resultado.
    r`   r   )
r®   rN   rQ   r›   rœ   r–   r—   r•   rS   rš   )	rî   Úlimitr  Úusedrj   Úlowr¡   r¢   r§   s	            rA   Ú_coherent_tagsr  þ  sÎ   € ð €CÜ‹5€DÛˆØŠy�b×ÑÓ!×'Ñ'Ó)ˆÙ”n×*Ñ*¨3Ô/Øô (Ô(=Ô>ô
á>‘
��UØ�e‰|ò Ø>ð 	ñ 
ñ
 �f˜Q‘i 4Ñ'ØÙØ�H‰H�V˜A‘YÔØ�
‰
�3ŒÚ”S˜“X Ó&ØØ€Jð! ð  €Jùó
s   ÁB:c                 ó^  — |xs d}|xs d}g }| xs i j                  «       D ]A  \  }}t        |||«      \  }}|dk7  rŒ|j                  t        |«      t	        |«      |f«       ŒC |j                  d„ ¬«       |r|d| }i }	|D �]  \  }
}}| j                  |«      xs i }g g g }}}t        |«      D ]„  \  }}|r||k(  s|j                  «       dv rŒ t        |||«      \  }}|dk(  r9|j                  |«       t        j                  |«      sŒ\|j                  |«       Œn|dk(  sŒt|j                  |«       Œ† |rt        ||«      }|d| }t        ||«      }t        d«      }|g|d	<   ||g|D �cg c]
  }||k7  sŒ	|‘Œ c}z   i|d<   |r	d
d|ii|d<   |r
|d   |i|d<   ||	|<   �Œ" |	S c c}w )a4  Temas auto: uno por cada top scene-seed de la coocurrencia.

    `cooc` es `{seed: {tag: peso}}` (como el `cooc` del snapshot). Se consideran
    "seed de escena" los que `classify()` mete en el bucket `scene`; se ordenan
    por peso total y se toman `top`. Para cada uno:
        seed_tags = [seed]
        scene     = {seed: [seed, *top tags de escena]}
        lighting  = {top_tag_de_luz: [*tags de luz]}   si los hay
        outfit    = {"sugerente": {"theme": [*top tags de ropa]}}  si los hay
        props     = {}
    El resultado es `{seed: tema}`.
    r?   r   c                 ó   — | d    | d   fS )Nr   r   r?   )r  s    rA   r…   z!discover_themes.<locals>.<lambda>0  s   € ¨¨Q© x°°a±Ñ&9r@   r†   N)r�   Úsolor	  Úautor  r   Úthemer   r
  )rm   r   rS   r  rÿ   Úsortrb   r  rQ   ÚLIGHTINGÚsearchr  r  )rï   rn   ro   ÚtopÚscene_seedsr�   r  rr   r|   ÚthemesÚ_totalÚ_keyÚ
scene_tagsÚoutfit_tagsÚ
light_tagsrj   Ú_weightr$  r   s                      rA   Údiscover_themesr1    sù  € ð ŠK�R€EØŠ\�r€Fà€KØš
 ×)Ñ)Ö+‰ˆˆeÜ˜T 5¨&Ó1‰	ˆ�Ø�WÒØØ×ÑœM¨%Ó0´#°d³)¸TÐBÕCð	 ,ð ×ÑÑ9ÐÔ:Ù
Ø! $ 3Ð'ˆà€FÜ)Ñˆ��dØ—‘˜“Ò$ "ˆØ.0°"°b �Kˆ
Ü)¨%Ö0‰LˆC�Ù˜# š+¨¯©«Ð8IÑ)IØÜ   e¨VÓ4‰IˆF�AØ˜Ò Ø×!Ñ! #Ô&Ü—?‘? 3Õ'Ø×%Ñ% cÕ*Ø˜8Ó#Ø×"Ñ" 3Õ'ð 1ñ Ü'¨
°CÓ8ˆJØ% d sÐ+ˆKÜ'¨
°CÓ8ˆJä˜VÓ$ˆØ"˜VˆˆkÑØ  ±ZÓ)M±Z°À1ÈÃ9ª!°ZÑ)MÑ MÐNˆˆg‰ÙØ*¨W°kÐ,BÐCˆE�(‰OÙØ!+¨A¡°
Ð ;ˆE�*ÑØˆˆt‹ð3 *ð4 €Mùò *Ns   Å1
F*
Å<F*
c                 óÂ  — |dk(  r$|D ]  }|| d   vsŒ| d   j                  |«       Œ  y|j                  d«      }|d   }|dk(  rOt        |«      dk  rt        d|› �«       y|d	   }dj	                  |d
d «      }|| d   j                  |i «      |<   y|dv r:t        |«      d
k  rt        d|› �«       ydj	                  |d	d «      }|| |   |<   yt        d|› �«       y)z@Vuelca una linea `clave = tags` sobre un tema curado (in-place).r�   r  NÚ.r   r	  rŒ   z)[!] linea de outfit incompleta ignorada: r   r   )r   r
  r  z[!] linea incompleta ignorada: z'[!] clave curada desconocida ignorada: )rS   rM   rš   rR   rx   rð   )r$  r‡   rî   rj   ÚpartsÚaxisrU   Úvariants           rA   Ú_apply_curated_liner7  R  s  € à
ˆf‚}ÛˆCØ˜% Ñ,Ò,Ø�kÑ"×)Ñ)¨#Õ.ð ð 	Ø�I‰I�c‹N€EØ�‰8€DØˆxÒÜˆu‹:˜Š>ÜÐ=¸c¸UÐCÔDØØ�a‘ˆØ—(‘(˜5  ˜9Ó%ˆØ9=ˆˆh‰×"Ñ" 5¨"Ó-¨gÒ6Ø	Ð/Ñ	/Üˆu‹:˜Š>ÜÐ3°C°5Ð9Ô:ØØ—(‘(˜5  ˜9Ó%ˆØ#ˆˆd‰�GÒäÐ7¸°uÐ=Õ>r@   c                 ó¤  — t        | «      } | j                  «       si S i }d}| j                  d¬«      j                  «       D �]…  }|j	                  dd«      d   j                  «       }|sŒ*|j                  d«      r¦|j                  d«      r•|dd	 j                  «       }|j                  «       j                  d
«      rQ|j	                  dd«      d   j                  «       }|st        d«       d}Œ±|}|j                  |t        d«      «       nt        d|› d�«       d}Œá|€t        d|› �«       Œòd|vrt        d|› �«       �Œ|j	                  dd«      \  }}|j                  «       j                  «       }|j	                  d«      D �	cg c]#  }	|	j                  «       sŒ|	j                  «       ‘Œ% }
}	t        ||   ||
«       �Œˆ |S c c}	w )aæ  Parsea `temas_curados.txt` -> `{nombre: tema}`; `{}` si no existe.

    Formato (spec Task 4):
        [tema:playa]
        seed = beach
        outfit.sugerente.bikini = bikini, swimsuit
        scene.shore = beach, ocean, horizon
        lighting.sunset = sunset, backlight, golden hour
        props.towel = beach towel, beach umbrella
    Se saltan comentarios (`#`) y lineas en blanco. Los valores conservan su
    forma exacta (espacios incluidos); solo se recortan extremos.
    Nr   rD   rE   r   r   rF   rG   rH   ztema:Ú:z#[!] cabecera de tema vacia ignoradaÚcuradorI   z[!] linea sin tema ignorada: rÝ   z[!] linea sin '=' ignorada: rv   )r
   rÉ   rK   rL   rM   rN   rO   rP   rQ   rR   rð   r  r7  )rT   r*  rW   rY   rZ   rç   r[   r‡   r  r   rî   s              rA   Úload_curatedr;  l  s°  € ô �‹:€DØ�;‰;Œ=Øˆ	à€FØ€GØ�~‰~ wˆ~Ó/×:Ñ:×<ˆØ�y‰y˜˜aÓ  Ñ#×)Ñ)Ó+ˆÙØØ�?‰?˜3Ô D§M¡M°#Ô$6Ø˜!˜B�Z×%Ñ%Ó'ˆFØ�|‰|‹~×(Ñ(¨Ô1Ø—|‘| C¨Ó+¨AÑ.×4Ñ4Ó6�ÙÜÐ?Ô@Ø"�GØØ�Ø×!Ñ! $¬°XÓ(>Õ?äÐ;¸F¸8À1ÐEÔFØ�ØØˆ?ÜÐ1°$°Ð8Ô9ØØ�d‰?ÜÐ0°°Ð7Ô8ÙØ—Z‘Z  QÓ'‰
ˆˆUØ�i‰i‹k×ÑÓ!ˆØ#(§;¡;¨sÔ#3ÓAÑ#3˜a°q·w±wµy�—‘•	Ð#3ˆÐAÜ˜F 7™O¨S°$Ö7ð7 =ð8 €Mùò Bs   ÆGÆ$Gc           	      óÜ   — t        |t        «      s|j                  d| › d|› d�«       y|D ]>  }t        |t        «      r|j	                  «       rŒ$|j                  d| › d|› d|›�«        y y)z<Anade a `errors` si `tags` no es lista de strings no vacios.útema 'z': 'z' debe ser una lista de tagsNz' contiene un tag invalido: )rý   r  rS   rÿ   rN   )r[   rT   rî   r   rj   s        rA   Ú_check_tagsr>  ž  so   € ä�dœDÔ!Ø�‰˜˜t˜f D¨¨Ð.JÐKÔLØÛˆÜ˜#œsÔ#¨3¯9©9­;Ø�M‰MØ˜˜˜d 4 &Ð(DÀSÀGÐLôñ ñ r@   c                 óL  — g }t        |t        «      sd| › d�gS |j                  d«      }t        |t        «      r|s|j	                  d| › d�«       d}t
        D �](  }|j                  |«      }|�|i k(  rŒt        |t        «      s|j	                  d| › d|› d�«       ŒF|xs t        |«      }|dk(  r¤|j                  «       D ]�  \  }}|t        vr.|j	                  d| › d	|› d
dj                  t        «      › d�«       t        |t        «      s|j	                  d| › d|› d�«       Œe|j                  «       D ]  \  }	}
t        | d|› d|	› �|
|«       Œ Œ’ Œþ|j                  «       D ]  \  }	}
t        | |› d|	› �|
|«       Œ �Œ+ |s|j	                  d| › dt
        › d�«       |S )u  Valida un tema contra el schema de Â§6 -> lista de errores ([] == valido).

    Reglas: `seed_tags` lista no vacia; al menos un eje de `THEME_AXES` con
    variantes; toda clave de `outfit` en `LEVELS`; todo valor de variante, lista
    de strings no vacios.
    r=  z': no es un objeto (dict)r  z*': 'seed_tags' debe ser una lista no vaciaFz': eje 'z' debe ser un objetor	  z': nivel de outfit invalido 'z' (validos: rw   Ú)z': 'outfit.z!' debe ser un objeto de varianteszoutfit.r3  z': al menos uno de z debe tener variantes)rý   rþ   rb   r  rS   Ú
THEME_AXESr­   rm   rJ   rx   r>  )r[   r$  r   rV   Únon_empty_axisr5  ÚvalrU   Úvariantsr6  rî   s              rA   Úvalidate_themerE  «  sÒ  € ð €FÜ�eœTÔ"Ø˜˜Ð7Ð8Ð9Ð9à�I‰I�kÓ"€EÜ�eœTÔ"©%Ø�‰˜˜t˜fÐ$NÐOÔPà€Nß�
ˆØ�i‰i˜‹oˆØˆ;˜# š)ØÜ˜#œtÔ$Ø�M‰M˜F 4 &¨°°Ð6JÐKÔLØØ'Ò4¬4°«9ˆØ�8ÒØ#&§9¡9¦;‘��xØ¤Ñ&Ø—M‘MØ   Ð&CÀEÀ7ð K%Ø%)§Y¡Y¬vÓ%6Ð$7°qð:ôô " (¬DÔ1Ø—M‘MØ    k°%°ð 9'ð 'ôð Ø%-§^¡^Ö%5‘M�G˜TÜ ¨°¨w°a¸°yÐ&AÀ4ÈÕPñ &6ñ $/ð "%§¡¦‘�˜Ü˜D T F¨!¨G¨9Ð"5°t¸VÕDò "-ð1 ñ6 Ø�‰Ø�T�FÐ-¬j¨\Ð9NÐOô	
ð €Mr@   c                 óv  — | xs i } |xs i }i }| j                  «       D ]
  \  }}|||<   Œ |j                  «       D ]
  \  }}|||<   Œ t        |«      }i }|j                  «       D ]P  \  }}||v rt        |t        «      rd|d<   |||<   Œ%t	        ||«      }	|	r|	D ]  }
t        d|› d|
› �«       Œ ŒL|||<   ŒR |S )aK  Fusiona auto + curados (el curado pisa al auto por nombre) y valida.

    Los temas **curados son SIEMPRE del usuario**: se conservan aunque sean
    invalidos (marcados `source="curado"`), de modo que el unico muro duro,
    `write_themes`, frene la escritura con un error claro que hay que arreglar;
    un bloque roto de `temas_curados.txt` NO desaparece en silencio. Los temas
    **auto** que no validan si se descartan (ruido de maquina) con aviso.
    `chars`/`copies` se aceptan por simetria con el resto del motor (aqui no se
    reclasifica: `discover_themes` ya lo hizo).
    r:  r  z[!] tema auto 'z' descartado: )rm   r®   rý   rþ   rE  rR   )r#  Úcuratedrn   ro   rø   r[   r$  Úcurated_namesrå   ÚerrsÚerrs              rA   Úbuild_themesrK  Ý  sá   € ð Š:�2€DØŠm˜€Gà€FØ—z‘z–|‰ˆˆeØˆˆtŠð $à—}‘}–‰ˆˆeØˆˆtŠð 'ô ˜“L€MØ€FØ—|‘|–~‰ˆˆeØ�=Ñ Ü˜%¤Ô&Ø"*��h‘Ø ˆF�4‰LØÜ˜d EÓ*ˆÙÛ�Ü˜¨ v¨^¸C¸5ÐAÕBð àØˆˆtŠð &ð €Mr@   c                 ó  — t        |«      }| xs i } g }t        | t        ¬«      D ]   }|j                  t	        || |   «      «       Œ" |r/|D ]  }t        d|› �«       Œ t        dt        |«      › d|› �«      ‚t        || «       y)a  Valida TODOS los temas y escribe `themes.json` (atomico) si son validos.

    Si algun tema falla, imprime cada error y lanza `ValueError` **sin escribir**
    (el fichero destino queda intacto o ausente). Si todos son validos, delega en
    `write_json_atomic`.
    r†   ú[!] zthemes invalidos (z error/es); no se escribio N)	r
   r‰   rÿ   ÚextendrE  rR   rd   rš   rÕ   )r*  rT   Ú
all_errorsr[   rJ  s        rA   Úwrite_themesrP    s�   € ô �‹:€DØŠ\�r€FØ€JÜ�v¤3×'ˆØ×Ñœ.¨¨v°d©|Ó<Õ=ð (áÛˆCÜ�D˜˜�,Õð äØ ¤ Z£Ð 1Ð1LÈTÈFÐSó
ð 	
ô �d˜FÕ#r@   zdanbooru_seeds.txtztemas_curados.txté   éÈ   )ÚidNc                 ó¾  — t        | «      } | j                  «       st        d| › �«      ‚t        j                  | j                  d¬«      «      }t        |t        «      st        d«      ‚t        D �ci c]  }|i “Œ }}|j                  «       D ]G  \  }}||vst        |t        «      sŒ|j                  «       D ]  \  }}t        |xs g «      ||   |<   Œ ŒI |S c c}w )zÑLee un fixture `{nivel: {seed: [posts]}}` para `--dry-run`/`--posts-file`.

    Normaliza a `{nivel: {seed: [posts]}}` para TODOS los niveles (los que falten
    quedan como dicts vacios). No toca la red.
    zposts-file no existe: z	utf-8-sigrD   z@posts-file debe ser un objeto JSON {nivel: {seed: [posts, ...]}})r
   rÉ   ÚFileNotFoundErrorrÒ   ÚloadsrK   rý   rþ   rd   rJ   rm   r  )rT   ÚdatarU   rõ   Úby_seedr�   rf   s          rA   Ú_read_posts_filerY  &  sÙ   € ô �‹:€DØ�;‰;Œ=ÜÐ"8¸¸Ð ?Ó@Ð@ô �:‰:�d—n‘n¨k�nÓ:Ó;€DÜ�dœDÔ!ÜØNó
ð 	
õ .4Ó4©V E�e˜R‘i¨V€NÐ4ØŸ*™*ž,‰ˆˆwØ˜Ñ&¬j¸Ä$Ô.GØØ"Ÿ=™=ž?‰KˆD�%Ü*.¨uª{¸Ó*;ˆN˜5Ñ! $Ò'ñ +ð 'ð
 Ðùò 5s   Á2
Cc                 ój  — |rt        |«      nt        }t        D �ci c]  }|i “Œ }}g }d}t        D ]T  }|j                  |g «      D ]=  }	 | j	                  |t
        |   t        |¬«      }t        |xs g «      ||   |<   |dz  }Œ? ŒV |||fS c c}w # t        $ r–}	d}t        D ]P  }
	 | j	                  |t
        |   t        ||
¬«      }n# t        $ r Y Œ1w xY wt        d|› d|› d|	› d|
xs d	› �«        n |€/|› d|› d|	› �}|j                  |«       t        d|› d
�«       Y d}	~	ŒáY d}	~	ŒÂd}	~	ww xY w)aU  Descarga posts por seed -> `(posts_by_level, skipped, fetched)`.

    `client.posts(seed, rating=RATING_MAP[level], min_score=20, limit=limit or 200)`.
    **Fallback de orden:** si `order:score` falla (HTTP 500 `QueryCanceled` en
    tags ultra-comunes), se reintenta con `order:id` antes de descartar el seed.
    **Aislamiento por seed (T7):** un `DanbooruError` de UN seed que falla en
    ambos ordenes NO aborta la corrida: se anota `"{nivel}:{seed}: {err}"` en
    `skipped`, se avisa y se continua con el resto. `fetched` = n de llamadas con
    exito (0 == todos los seeds fallaron).
    r   )ÚratingÚ	min_scorer  N)r[  r\  r  ÚorderrM  r9  rÚ   z; recuperado con order:Údefaultz$ (se continua con el resto de seeds)r   )ra   ÚDANBOORU_DEFAULT_LIMITrJ   rb   rf   Ú
RATING_MAPÚDANBOORU_MIN_SCOREr   ÚFALLBACK_ORDERSrR   rS   r  )ÚclientrV   r  rU   rõ   ÚskippedÚfetchedr�   rf   ÚexcÚfallbackÚnotes               rA   Ú_fetch_postsri  ?  s�  € ñ  ŒC�ŒJÔ%;€EÝ-3Ó4©V E�e˜R‘i¨V€NÐ4Ø€GØ€GßˆØ—I‘I˜e RÖ(ˆDðØŸ™ØÜ% eÑ,Ü0Øð	 %ó �ô: +/¨uª{¸Ó*;ˆN˜5Ñ! $Ñ'Ø�q‰L‰GñA )ð ðD ˜7 GÐ+Ð+ùòK 5øô !ò Ø�ß /�Hð	!Ø &§¡Ø Ü#-¨eÑ#4Ü&8Ø"'Ø"*ð !-ó !™øô )ò !Ù ð!úäØ˜u˜g Q t f¨B¨s¨eð 4!Ø!)Ò!6¨YÐ 7ð9ôñ ð !0ð  �=Ø#˜W A d V¨2¨c¨UÐ3�DØ—N‘N 4Ô(Ü˜D  Ð&JÐKÔLÜô	 !ûð%úsB   œ
BÁ BÂ	D2ÂD-Â(!C
Ã	D-Ã
	CÃD-ÃCÃAD-Ä-D2c                 ó¨  — t         D �ci c]  }|i “Œ }}i }i }g }t         D ]¥  }| j                  |«      xs i j                  «       D ]}  \  }}	t        t	        |	«      ||«      \  }
}|
||   |<   |D ]  \  }}|j                  ||«       Œ |
j                  «       D ](  \  }}|D ]  }|dk(  r|j                  |«       Œ|||<   Œ  Œ* Œ Œ§ ||||fS c c}w )aO  `{nivel: {seed: [posts]}}` -> `(per_level_buckets, accepted, dropped, revisar)`.

    `per_level_buckets` = `{nivel: {seed: {bucket: Counter}}}` (una linea por seed).
    `accepted` = `{tag: bucket}` (solo los clasificados); `dropped` = `{tag: motivo}`;
    `revisar` = tags que `classify()` manda a `otros` (sin categoria clara).
    Úotros)rJ   rb   rm   rt   rk   rð   rS   )rõ   rn   ro   rU   r±   r  rq   rü   r�   rf   rp   Údroprj   rs   r[   rg   s                   rA   Ú_bucketize_levelsrm  s  só   € õ 17Ó7± u˜ ™°ÐÐ7Ø€HØ€GØ€GßˆØ*×.Ñ.¨uÓ5Ò;¸×BÑBÖD‰KˆD�%Ü%¤i°Ó&6¸¸vÓF‰MˆG�TØ-4Ð˜eÑ$ TÑ*Û#‘��VØ×"Ñ" 3¨Õ/ð  $à!(§¡¦‘��gÛ"�CØ˜w’ØŸ™ sÕ+à(,˜ šñ	 #ñ "1ñ Eð ð ˜h¨°Ð8Ð8ùò! 8s   ‰
CFc                 ó,  — 	 t        |«      }t        «       \  }}	}
|duxs |}g }|�t        |«      }n–|r t        d«       t        D �ci c]  }|i “Œ }}nt|€
t        «       }| rt        | «      nt        }t        d|› d�«       t        ||| «      \  }}}|dk(  r|rt        d«       i d|dœS |rt        dt        |«      › d	|› d
�«       t        d„ |j                  «       D «       «      }|dk(  rt        d«       i d|xs dgdœS |rt        |«      dz  n
t        |«      }t        |||	«      \  }}}}t        ||||	«      }t        | rt        | «      nt        dœ}t!        ||||	|«      }t#        |j%                  d«      xs i ||	«      }t'        |«      }t)        ||||	«      }t+        ||dz  «       t-        ||¬«      }t/        |dz  |«       t1        |«      t1        |«      z  D �ci c]  }||
v r|
|   |k7  r||
|   “Œ }}t3        |dz  |t5        |j7                  «       «      ||«       |st        d|› �«       |t        |«      |dœS c c}w c c}w # t8        $ r8} t        d«       t        d| › d�«       i dt;        | «      xs dgdœcY d} ~ S d} ~ ww xY w)aO  Refresco manual: red -> pools/themes/snapshot/validacion (todo atomico).

    Devuelve `{"blocks": {severity: n_lineas}, "themes": n, "skipped": [...]}`;
    `skipped` viene vacio en un refresco correcto.

    `dry_run`/`posts_file` leen los posts de un fixture (`posts_file`) en vez del
    cliente y escriben en `<out_dir>/_preview/` **sin tocar la red**.

    **Aislamiento por seed (T7):** si un seed falla (`DanbooruError`), se anota en
    `"skipped"` y se continua con el resto; basta UN seed con exito para construir
    y escribir con los datos disponibles (los niveles vacios conservan su
    `bloques_*.txt`). Si **todos** los seeds fallan (nada descargado) se mantiene
    el contrato previo: aviso `[!] Danbooru no disponible; ...` y **no se toca**
    ningun artefacto. Sin resultados (pero sin errores) tampoco se escribe nada.
    NzH[!] --dry-run sin --posts-file: sin datos de entrada (no se toca la red)z[*] descargando metadatos (z posts/seed)...r   z?[!] Danbooru no disponible; se conservan los artefactos previos)Úblocksr*  rd  rM  z# seed(s) fallaron; se continua con z seed(s) correctosc              3   ó\   K  — | ]$  }|j                  «       D ]  }t        |«      –— Œ Œ& y ­wr’   )r  rš   )r“   rX  rf   s      rA   r”   zrefresh.<locals>.<genexpr>À  s0   è ø€ ð 
á2�Ø Ÿ™Ö)�ô ��Jà)ð Ù2ùs   ‚*,z7[!] sin resultados; se conservan los artefactos previosz+sin resultados; artefactos previos intactosÚ_preview)r\  r  rï   zthemes.json)rã   zsnapshot.jsonzvalidacion.txtz[ok] artefactos en z    (r@  r   )r\   rB   rY  rR   rJ   r   ra   r_  ri  rš   r¯   r  r
   rm  r¹   ra  rù   r1  rb   r;  rK  rP  rè   rÕ   r®   r  r  rm   r   rÿ   )!r  Údry_runÚ
posts_filerã   Ú
seeds_pathÚcurated_pathrc  rV   rn   ro   ÚaliasesÚfixture_moderd  rõ   rU   Ú
limit_usedre  Útotal_postsr´   r±   r  rq   rü   r³   rí   Úsnapshotr#  rG  r*  ro  rj   r  rf  s!                                    rA   Úrefreshr{  �  s  € ð"[UÜ˜:Ó&ˆÜ!0Ó!2Ñˆˆv�wà!¨Ð-Ò8°ˆØˆàÐ!Ü-¨jÓ9‰NÙÜð&ôõ 6<Ó<±V¨E˜e R™i°VˆNÑ<àˆ~Ü'Ó)�Ù',œ˜UœÔ2HˆJÜÐ/°
¨|¸?ÐKÔLÜ/;¸FÀEÈ5Ó/QÑ,ˆN˜G WØ˜!Š|¡äðôð #%°¸gÑFÐFÙÜØœ3˜w›<˜.Ð(KØ�iÐ1ð3ôô
 ñ 
à)×0Ñ0Ô2ó
ó 
ˆð
 ˜!ÒÜÐKÔLàØØ"ÒUÐ'TÐ&Uñð ñ 2>”$�w“- *Ò,Ä4ÈÃ=ˆä8IØ˜E 6ó9
Ñ5Ð˜8 W¨gô ˜EÐ#4°e¸VÓDˆô ,Ù#(”S˜”ZÔ.Dñ
ˆô " %¨¸ÀÈÓOˆä˜xŸ|™|¨FÓ3Ò9°r¸5À&ÓIˆÜ˜|Ó,ˆÜ˜d G¨U°FÓ;ˆô 	�V˜V mÑ3Ô4Ü˜U¨FÔ3ˆÜ˜& ?Ñ2°HÔ=ô ˜8“}¤s¨7£|Ò3ó
á3�Ø�g‰~ '¨#¡,°#Ò"5ð �˜‘ÑØ3ð 	ð 
ô
 	 ØÐ%Ñ%ØÜ�—‘“Ó!ØØô	
ñ ÜÐ'¨ xÐ0Ô1Ø ¬C°«KÀGÑLÐLùòO =ùòp
øô  ò Uô 	ÐOÔPÜ��c�U˜!ˆnÔØ¨´s¸3³xÒ7RÀ?Ð6SÑTÕTûðUúsL   ‚AI Á
IÁAI Â,AI ÄC!I Ç%IÇ>A	I É
I É	JÉ-JÊJÊJc                  óV  — t        j                  dd¬«      } | j                  ddd¬«       | j                  dt        d d	¬
«       | j                  ddd¬«       | j                  dd d¬«       | j                  dd d¬«       | j                  dd d¬«       | j                  dd d¬«       | S )NÚdanbooru_harvestz;Cosecha de metadatos de Danbooru -> pools y temas (manual).)ÚprogÚdescriptionz	--refreshÚ
store_truez=descarga metadatos y regenera pools/temas/snapshot/validacion)ÚactionÚhelpz--limitzposts por seed (default 200))Útyper^  r‚  z	--dry-runz*no toca la red; escribe en <out>/_preview/z--posts-filez8fixture JSON {nivel: {seed: [posts]}} en vez del cliente)r^  r‚  z--outzdirectorio de salidaz--seedszfichero de seeds por nivelz	--curatedzfichero de temas curados)ÚargparseÚArgumentParserÚadd_argumentra   )Úparsers    rA   Ú_build_parserrˆ  ü  sÜ   € Ü×$Ñ$ØØQô€Fð ×ÑØØØLð ô ð
 ×ÑØÜØØ+ð	 ô ð ×ÑØØØ9ð ô ð
 ×ÑØØØGð ô ð
 ×Ñ˜¨Ð4JÐÔKØ
×Ñ˜	¨4Ð6RÐÔSØ
×Ñ˜¨TÐ8RÐÔSØ€Mr@   c           
      ó  ‡— t        «       }|j                  | «      }|j                  s)|j                  s|j                  s|j                  «        yt        |j                  |j                  |j                  |j                  rt        |j                  «      nt        |j                  rt        |j                  «      nt        |j                  rt        |j                  «      nt        ¬«      }|j                  d«      xs i Š‰r*t        ddj!                  ˆfd„t"        D «       «      z   «       t        d|j                  dd«      › �«       |j                  d	«      r t        d
dj!                  |d	   «      z   «       y)Nr   )r  rr  rs  rã   rt  ru  ro  z[ok] bloques por nivel: rw   c              3   óL   •K  — | ]  }|› d ‰j                  |d«      › �–— Œ y­wrÜ   rÞ   )r“   r²   ro  s     €rA   r”   zmain.<locals>.<genexpr>0  s)   øè ø€ ÐLÁ¸#˜3˜%˜q §¡¨C°Ó!3Ð 4Ô5Áùrá   z[ok] temas: r*  rd  z[!] saltado: z; )rˆ  Ú
parse_argsr{  rr  rs  Ú
print_helpr  r  r
   ÚOUT_DIRrV   Ú
SEEDS_FILErG  ÚCURATED_FILErb   rR   rx   r«   )Úargvr‡  Úargsrå   ro  s       @rA   Úmainr’    s  ø€ Ü‹_€FØ×Ñ˜TÓ"€DØ�LŠL˜DŸLšL¨D¯OªOØ×ÑÔØäØ�j‰jØ—‘Ø—?‘?Ø"&§(¢(”�T—X‘X”´Ø'+§z¢z”4˜Ÿ
™
Ô#´zØ+/¯<ª<”T˜$Ÿ,™,Ô'¼\ô€Fð �Z‰Z˜Ó!Ò' R€FÙÜØ&Ø�i‰iÓLÅÓLÓLñMô	
ô 
ˆL˜Ÿ™ H¨aÓ0Ð1Ð
2Ô3Ø‡z�z�)ÔÜˆo §	¡	¨&°Ñ*;Ó <Ñ<Ô=Ør@   Ú__main__)r?   r?   )é   r’   )é   )VÚ__doc__ÚsysÚstdoutÚreconfigurer„  rÒ   rÇ   ÚreÚcollectionsr   r   r	   Úpathlibr
   Ú__file__ÚresolverÃ   ÚBASE_DIRrT   Úinsertrÿ   Úwd14_harvestr   r   r   Úwd14_blocksr   r   r   Údanbooru_clientr   r   rJ   r`  r�  r˜   r™   Ú	frozensetr—   Úcompiler›   r¬   r«   r°   rB   r\   rk   rt   r}   r€   rŠ   rŽ   r¨   r¹   rÎ   rÕ   rè   rù   r  rA  ÚIr&  r  r  r  r  r1  r7  r;  r>  rE  rK  rP  rŽ  r�  ra  r_  rb  rY  ri  rm  r{  rˆ  r’  Ú__name__Ú
SystemExitr?   r@   rA   Ú<module>r©     sŠ  ðñó à ‡
�
× Ñ  Ð0BÐ Ô Cã Û Û 	Û 	Ý ß 'Ý á�‹>×!Ñ!Ó#×*Ñ*×1Ñ1€Ø ‡�‡��‘3‘t˜H“~×-Ñ-Ó/×6Ñ6Ó7Ô 8÷ :Ñ 9÷ AÑ @÷ :à	,€Ø ¨5¸uÑE€
Ø
�ZÑ
 *Ñ
,€ð A€ð  !¨a¸qÈAÑN€
áò ó ñ Ò;Ó<Ùˆy˜*Ð%Ó&Ùˆu�gÐÓÙÒMÓNð	Ð ð �—‘ðó€ð
 #(°'È
ÑSÐ Ø)€
à€òò
ò:ò$ó"ò4Eò
Bòó/Aòd%òPò$ð$  '°dó ò>#òL=>ðJ 6€
ð
 ˆ2�:‰:ð6ð
 ‡D�Dó€ò2òò	óó66òr?ò4/òd
ò/òd"òJ$ð2 ˜
Ñ"Ð%9Ñ9€
Ø˜*Ñ$Ð':Ñ:€ð Ð ØÐ ð €òò21,òh9ð4  °$ÀØ!°ÀTólUò^ó@ð8 ˆzÒÙ
‘T“VÓ
Ðð r@   