
    bi6                       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 ddlZddlmZ dZd	ed
<   	 dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded <   dZded!<   dZded"<   d#Zded$<   d%Zded&<   d'Z ded(<   d)Z!ded*<   d+Z"ded,<   d-Z#ded.<   d/Z$ded0<   d1Z%ded2<    e&d3d4h      Z'd5ed6<   g d7g d8d9Z(d:ed;<    e&h d<      Z)d5ed=<    e&h d>      Z*d?ed@<   dAZ+dedB<    G dC dDe,      Z- G dE dFe,      Z.e G dG dH             Z/e G dI dJ             Z0 e	e1      jd                  dKz  dLz  Z3d`dMZ4dadNZ5dadOZ6dbdPZ7dcdQZ8	 	 	 	 	 	 dddRZ9dedSZ:	 	 	 	 	 	 dfdTZ;dgdUZ<dhdVZ=	 	 	 	 	 	 didWZ>	 	 	 	 	 	 djdXZ?dkdYZ@dkdZZAdldmd[ZB	 dn	 	 	 	 	 dod\ZC	 	 	 	 	 	 djd]ZDdkd^ZEdpd_ZFy)qu.  IDS Phase 0.5 — Lite Evaluator (5항목, OCR 비의존, 정적 분석).

목적:
    Phase 0 SSOT(mapping-tables.md #5-A + target-audience.md §7)를
    LayoutMeta(JSON Schema 강제)로 받아 5항목을 즉시 PASS/WARN/FAIL 평가.

mapping #4(scripts/quality_evaluator.py: PIL 회귀 게이트)와 책임 분리:
    - mapping #4 = 27 PNG 렌더 결과의 정량 회귀(silent corruption 차단).
    - mapping #5(본 모듈) = LayoutMeta + PIL Image 정적 분석으로 디자인 품질 게이트.

5항목 (외부 명명 = task-2446 task.md / 내부 알고리즘 = mapping-tables 5-A):
    L1 Contrast      — 글리프 픽셀 contrast 분포 5th/95th percentile (mapping 5-A L1)
    L2 Margin        — safe-area(SSOT 6.7%=72px) 침범 검사 (mapping 5-A L2)
    L3 Hierarchy     — 헤딩/서브헤드/본문 비율 + dq-rules absolute_min(40) (mapping 5-A L3 + dq-rules font_sizes)
    L4 Color Token   — palette 4색 이내 + AI 퍼플 검출 (mapping 5-A L5 + dq-rules color)
    L5 Typography    — Pretendard / Noto Sans KR + weight 600+ + 자간/행간 (dq-rules font_pairing)

참조:
    - SSOT: memory/plans/ids-phase4-design-system/mapping-tables.md (lines 430~620)
    - SSOT: memory/plans/ids-phase4-design-system/target-audience.md §7 (lines 238~337)
    - dq-rules: memory/specs/dq-rules.json
    )annotationsN)	dataclassfield)Path)Any)Imagezv1.0strSSOT_MAPPING_VERSIONgx&?floatSAFE_AREA_RATIO   intGRID_BASELINE)iconztuple[str, ...]GRID_SUBGRID_WHITELIST_ROLESg      @CONTRAST_BODY_MIN_AAg      @CONTRAST_BODY_RECOMMENDED      @CONTRAST_LARGE_MIN_AACTA_CONTRAST_MIN_AA(   DQ_FONT_ABSOLUTE_MINT   DQ_FONT_HEADLINE_MIN@   DQ_FONT_SUBHEAD_MINDQ_FONT_CTA_MINDQ_FONT_DISCLAIMER_MINg?DQ_HEAD_SUB_MIN_RATIO   DQ_COLOR_MAX_PALETTE   DQ_FONT_MIN_FAMILIES   DQ_FONT_MAX_FAMILIESg     p@AI_PURPLE_HUE_MINg     r@AI_PURPLE_HUE_MAX      ?AI_PURPLE_SAT_MINg?AI_PURPLE_RATIO_FAIL
PretendardzNoto Sans KRzfrozenset[str]PRIMARY_KOREAN_FAMILIES)z#FF6E2Bz#0B1E3Fz#F8F4EEz#1F1F1F#FFFFFF)z#0071E3z#1D1D1Fz#F5F5F7r-   z#86868B)ztheme-fa-fintechztheme-consumer-warmzdict[str, list[str]]PRESET_PALETTE_TOKENS>      굴림   궁서   바탕	   굴림체	   궁서체	   바탕체DQ_BANNED_FONTS>   ,  d      zfrozenset[int]DQ_BANNED_WEIGHTSg      >@GLYPH_COLOR_DELTAc                      e Zd ZdZy)SchemaValidationErroru$   LayoutMeta가 JSON Schema를 위반.N__name__
__module____qualname____doc__     M/home/jay/workspace/.worktrees/task-2446-design/scripts/ids/lite_evaluator.pyr<   r<   k   s    .rC   r<   c                      e Zd ZdZy)SSotMismatchErroruL   mappingVersion이 SSOT(SSOT_MAPPING_VERSION)와 불일치 → 진입 차단.Nr=   rB   rC   rD   rF   rF   o   s    VrC   rF   c                  d    e Zd ZU dZded<   ded<   ded<   ded<   dZd	ed
<    ee      Zded<   y)
ItemResultu   5항목 각 평가 결과.r	   codenameverdictr   scoreNz
str | Nonereasondefault_factorydict[str, Any]details)	r>   r?   r@   rA   __annotations__rM   r   dictrQ   rB   rC   rD   rH   rH   x   s2    $
I
ILJFJ#D9G^9rC   rH   c                  h    e Zd ZU dZded<   ded<   ded<    ee      Zd	ed
<    ee      Z	ded<   y)
EvalResultu   Lite Evaluator 종합 결과.r	   overallr   rL   zlist[ItemResult]itemsrN   z	list[str]fail_reasonsrP   layout_meta_summaryN)
r>   r?   r@   rA   rR   r   listrX   rS   rY   rB   rC   rD   rU   rU      s4    'LJ#D9L)9*/*EErC   rU   schemasz lite_evaluator_input.schema.jsonc                     t         j                  dd      5 } t        j                  |       cddd       S # 1 sw Y   yxY w)u1   LayoutMeta JSON Schema(Draft-07)를 로드한다.rzutf-8)encodingN)_SCHEMA_PATHopenjsonload)fs    rD   load_schemard      s9     
		3		1 Qyy|  s	   7A c                   	 ddl m} t	               } ||      }t        |j                  |       d       }|rmg }|D ]I  }dj                  d |j                  D              xs d	}|j                  d
| d|j                          K t        ddj                  |      z         y# t        $ r}t        d      |d}~ww xY w)u   jsonschema Draft7Validator로 입력 계약 강제 검증.

    실패 시 SchemaValidationError. 외부 의존성(jsonschema)은 lazy import.
    r   )Draft7ValidatorzElite_evaluator requires 'jsonschema'. Install: pip install jsonschemaNc                    | j                   S N)path)es    rD   <lambda>z"_validate_schema.<locals>.<lambda>   s
    aff rC   key/c              3  2   K   | ]  }t        |        y wrh   )r	   ).0ps     rD   	<genexpr>z#_validate_schema.<locals>.<genexpr>   s     <qCF<   z(root)z  - : z(LayoutMeta JSON Schema validation FAIL:

)
jsonschemarf   ImportErrorRuntimeErrorrd   sortediter_errorsjoinabsolute_pathappendmessager<   )	layout_metarf   excschema	validatorerrorsmsgsrj   ri   s	            rD   _validate_schemar      s    
. ]F'II))+6<LMF 	4A88<AOO<<HDKK$tfBqyyk23	4 $7$))D/I
 	
   .
 	s   B' '	C0B<<Cc                d    | j                  d      }|t        k7  rt        d| dt         d      y)uO   mappingVersion이 SSOT_MAPPING_VERSION과 일치하지 않으면 진입 차단.mappingVersionzmappingVersion mismatch: got 'z', expected 'z' (Phase 0 SSOT).N)getr
   rF   )r   mvs     rD   _check_mapping_versionr      sI     
)	*B	!!,RD 1-..?A
 	
 "rC   c                |    | j                  d      }t        |dd d      t        |dd d      t        |dd d      fS )N#r   r"      r       )lstripr   )	hex_colorhs     rD   _hex_to_rgbr      sC    Aq1vr?C!AOS1Q_<<rC   c                \    dd}| \  }}}d ||      z  d ||      z  z   d ||      z  z   S )u+   WCAG 상대 휘도 (sRGB → linear → Y).c                6    | dz  }|dk  r|dz  S |dz   dz  dz  S )N     o@g#?gףp=
)@g)\(?gzG?g333333@rB   )cvs     rD   chanz!_relative_luminance.<locals>.chan   s.    ILq5yJE	U/Bs.JJrC   gz6?g,C?g]m{?)r   r   returnr   rB   )rgbr   r]   gbs        rD   _relative_luminancer      s@    K GAq!DGftAw..$q'1AAArC   c                d    t        |       }t        |      }||kD  r||fn||f\  }}|dz   |dz   z  S )z(WCAG contrast ratio (L1+0.05)/(L2+0.05).g?)r   )fgbgl1l2lightdarks         rD   _contrast_ratior      sD    
 
R	 B	R	 B 2g2r(B8KE4DLTD[))rC   c                   d | D        \  }}}t        |||      t        |||      }}||z   dz  }||k(  rdd|fS ||z
  }|dkD  r|d|z
  |z
  z  n|||z   z  }||k(  r||z
  |z  dz  }	n||k(  r||z
  |z  dz   }	n||z
  |z  dz   }	|	dz  ||fS )	u'   RGB(0~255) → HSL(h: 0~360, s/l: 0~1).c              3  &   K   | ]	  }|d z    yw)r   NrB   )rp   r   s     rD   rr   z_rgb_to_hsl.<locals>.<genexpr>   s     &Qq5y&s   g       @        r(   r   r"   r    g      N@)maxmin)
r   r]   r   r   mxmnr   deltasathues
             rD   _rgb_to_hslr      s     '#&GAq!Aq\3q!Q<B"WOE	RxCGE%*S[%38b=
!erBw6GC	QwA!#	q1uo!1uo!:sE!!rC   c           	     `    t        j                  t        d t        | |      D                    S )zsRGB Euclidean distance.c              3  2   K   | ]  \  }}||z
  d z    yw)r"   NrB   )rp   xys      rD   rr   z"_color_distance.<locals>.<genexpr>   s     <$!Q!a%A<rs   )mathsqrtsumzip)ar   s     rD   _color_distancer      s$    
 99S<#a)<<==rC   c                    | syt        |       }t        |      }|dz
  |z  dz  }t        j                  |      }t        j                  |      }||k(  r|t        |         S ||   ||   ||   z
  ||z
  z  z   S )u6   선형 보간 percentile (numpy 미사용 시 호환).r      g      Y@)ry   lenr   floorceilr   )valuesrq   snklohis          rD   _percentiler     s     vAAA	
Q!eA	AB	1B	RxQyR5AbEAbEMa"f---rC   c                   |\  }}}}t        |      t        |      t        |      t        |      f\  }}}}| j                  d d \  }}t        d|      t        d|      }	}t        |||z         t        |||z         }}
|
|k  s||	k  r;t	        j
                  dd| j                  dk(  r| j                  d   f      S df      S | |	|||
f   S )Nr"   r   r$   r   )r   shaper   r   npemptyndim)arrbboxr   r   wr   HWx0y0x1y1s               rD   
_crop_bboxr     s    JAq!QQQQQ/JAq!Q99Ra=DAqAYAq	BAE]C1q5MB	Rx28xxAAsyy}EFF1EFFr"ube|rC   c           	        !" t        |d         }t        | |d         }|j                  dk(  rg S |j                  dd \  |dddf   j	                  t
        j                        }|j                  dd      }|t        j                  |t
        j                  	      z
  }t        j                  ||z  j                  d
            }|t        k  }| }	|j                         r|	j                         s~|j                  r%|j                  d      j	                  t              nt        j                  g d      }
t        |t        |
d         t        |
d
         t        |
d         f      }|gS |j                        }|	j                        }||   }t        j                   |d      j	                  t              }t        |d         t        |d
         t        |d         f}t#        dt%              dz        "||d   z  }|j	                  t
        j                        }t        j&                  t        j&                  |d      d
      !t        j&                  t        j&                  |d      d
       d !"fd}g }t        j(                  |      \  }}d}t+        |      |kD  r@t        j,                  dt+        |      d
z
  |      j	                  t              }||   ||   }}t/        ||      D ]s  \  }} |t        |      t        |            }||n|}t        |||df         t        |||d
f         t        |||df         f}|j1                  t        ||             u |S )u  component bbox 내 글리프 픽셀별 contrast 분포(per-glyph LOCAL bg).

    mapping #5-A L1 알고리즘 정신:
        "글리프 픽셀별 contrast 계산. text_rgb vs effective_bg.getpixel((x,y))"
    text_alpha_mask 미제공 환경에서 fill 색상 기반 글리프 분류 사용.

    배경 분포 보존(Codex high #4 해소):
        bbox-median 단일값 대신 글리프 픽셀 위치마다 인근 비-glyph 픽셀에서
        local bg 추정(이중 샘플링) → 그라데이션/photo bg 끝점 분포가 그대로
        contrast 분포에 반영됨. 광역(전체 bbox) bg와 로컬 bg의 contrast를
        모두 산출하여 두 분포의 worst를 채택(끝점 우회 차단).
    fillr   r   Nr"   .r$   r   )dtyper   )axis)   r   r   r   r   ).Nc                H   t        d| z
        }t        d|z
        }t        dz
  | z         }t        	dz
  |z         }||f   j                         }t        
||f         }|dkD  r#||dz
  |f   z  }|t        
|dz
  |f         z  }|dkD  r#|||dz
  f   z  }|t        
||dz
  f         z  }|dkD  r.|dkD  r)||dz
  |dz
  f   z  }|t        
|dz
  |dz
  f         z  }|dk(  ry t        |d   |z        t        |d   |z        t        |d   |z        fS )Nr   r   r"   )r   r   copyr   )yyxxr   r   r   r   s_rgbs_cntr   r   cum_cntcum_rgbradiuss           rD   _window_meanz,_glyph_pixel_contrasts.<locals>._window_meanO  sp   BK BK QV$QV$B$$&GBFO$6WR!VRZ((ESa,--E6WRaZ((ESR!V,--E6b1fWR!VR!V^,,ESaa011EA:E!Hu$%s58e+;'<c%(UBR>STTrC   i  )r   r   r   r   r   ztuple[int, int, int] | None)r   r   sizer   astyper   int32reshapearrayr   r   r:   anymeanr   r   medianr   r   cumsumwherer   linspacer   r}   )#img_arr	componentfill_rgbregionr   flatdiff	dist_flatglyph_mask_flatbg_mask_flatmean_bgratio
glyph_maskbg_mask	bg_pixelsbg_median_global	global_bgbg_onlybg_countr   	contrastsglyph_ysglyph_xsmax_samplesidxr   r   local_bgr   r   r   r   r   r   r   s#                                 @@@@@rD   _glyph_pixel_contrastsr	    s$     9V,-H6!23F{{a	<<DAq
bqb/
 
 
*C;;r1D"((828844D))q)12I"33O##L (8(8(:3799$)))#**3/"((?B[s71:GAJWQZI
 w ((A.J""1a(GGIyy3::3?%a()3/?/B+CSIYZ[I\E]^I C1IO$FGI&&G~~bhh'Hii		'2;Gii		(3!<GU U*  I*-HhK
8}{"kk!S]Q.<CCCH%c]HSM(h) 2BBR1!-X9#b"ai.!3s2r19~#6CB	N8KLR012 rC   c                   |d   D cg c]  }|j                  dd      r	|d   dv r| }}|st        dddd	d
ddi      S g }d}d}g }|D ]  }t        | |      }|st        |d      }	t        |d      }
|d   dk(  }|rt        nt
        }|d   dv xr |j                  dd      dk\  }|r|st        }|	|k  }| xr	 |
t        k  }|r#d}|j                  d|d    d|	dd|dd       n|rd}|j                  |d   |d   t        |	d      t        |
d      |||rdn|rdndd         |rdn|rdnd}|rd!n|rd"nd#}t        dd|||rd$j                  |      nd%d&|i      S c c}w )'u9   L1: 글리프 픽셀 contrast 분포 5th/95th percentile.
componentsisTextTrole>   ctabodycaptionsubheadheadline
disclaimerL1ContrastWARNF   u,   text component 0건 — 평가 대상 없음component_countr   rI   rJ   rK   rL   rM   rQ   F   _   r  >   r  r  
fontWeighti  X  zL1 rJ   z contrast p5=.2f < z.1fu    (절대 하한)r$   FAILPASS)rJ   r  p5p95min_requiredis_ctarK   r   K   r7   ; Nper_component)r   rH   r	  r   r   r   r   r   r}   roundr{   )r   r   r   text_componentsr(  overall_failoverall_warnrX   r  r"  r#  r%  r$  is_large	comp_fail	comp_warnrK   rL   s                     rD   evaluate_l1_contrastr0  t  s    \*554 QvY2q%q 	
O 
 A&*
 	
 +-MLL L "
*7A6	A&)R(6e#.4*:NV9 77[AEE,PS<TX[<[F0L%	"]I/H)H	Lai[bXSc8JJZ[ L&	&	BlS!} , %.6yVf
	
1"
H %f\6vGB<RSE*6tyy&D -0 us   "E&c                F   | d   }| d   }|d   |d   }}|d   |d   }}|d   |d   }}g }	| d	   D 
cg c]  }
|
j                  d
d      r	|
d   dvr|
 }}
|D ]Z  }
|
d   \  }}}}||k  }||k  }||z   ||z
  kD  }||z   ||z
  kD  }|s|s|s|s6|	j                  |
d   |
d   ||||g||||dd       \ t        ||      t        z  }t        t	        ||z
        t	        ||z
        t	        ||z
        t	        ||z
              }|dk  }| j                  di       }t        |j                  dd            }|t        k7  }g }g }|	r|j                  dt        |	       d       |s!|j                  d|dd| d| d| d| 
       |r|j                  d| d       |r2d }t        d!d"t        |	      d#z  z
        }d$j                  ||z         }n|rd%}d&}d$j                  |      }nd'}d(}d)}t        d*d+||||t        |d,      ||| |	t        |	      d-.      S c c}
w )/uI   L2: 모든 텍스트 component bbox가 safe-area 안에 있어야 한다.canvassafeAreawidthheighttopbottomleftrightr  r  Tr  >   r   logo
decorationr   rJ   )r6  r8  r9  r7  )rJ   r  r   
violationsr    gridbaseliner   u   safe-area 침범    건u!   safe-area SSOT 불일치: 기대 z.0fu   px ±4 / 실제 top=z	, bottom=z, left=z, right=zgrid baseline=u:    ≠ SSOT 8 (4px는 sub-grid 화이트리스트만 허용)r   r   <   
   r'  r  r&  r!  r7   NL2Marginr   )	safe_areassot_expected_pxssot_alignedgrid_baselinegrid_baseline_alignedr<  violation_countr  )r   r}   r   r   absr   r   r   r{   rH   r)  )r   r2  safer   r   r6  r7  r8  r9  r<  r   r*  r   r   r   r   v_topv_leftv_rightv_bottomexpectedr   rF  r=  r>  grid_violationrX   warn_reasonsrK   rL   rM   s                                  rD   evaluate_l2_marginrS    s    "Fz"D'?F8,qAu+tH~Cv,W%D')J \*554 QvY6T%T 	
O 
  vY
1aCTq5QY'Ea&j)FgfIfI1aL$ &!("*	#	
, 1ay?*HC(NFXD8OEH	E A:L ??62&D488J*+H.N L L/J/@DE/~=QRUQV WXWTF(5';	

 XJ&`a	
 ArC
Ob001<,67	<( %h 2(%)7%7$":
 Ks   "Hc                   i }g }| d   D ]<  }|j                  dd      s|j                  |d   g       j                  |d          > g }| d   D ]@  }|j                  dd      s|d   t        k  s#|j                  |d   |d   |d   d       B |r%|j                  dt         d	t	        |       d
       t
        t        t        t        d}g }|j                         D ]7  \  }}|j                  |g       D ]  }	|	|k  s	|j                  ||	|d        9 |r|j                  dt	        |       d
       d}
d}|j                  d      r\|j                  d      rKt        |d         }t        |d         }|dkD  r*||z  }|t        k  r|j                  d|ddt                |rd}d}n|
rd}d}nd}d}t        dd|||rdj                  |      nd||||d       S )!us   L3: dq-rules font_sizes(absolute_min=40, headline/subhead/cta) +
    font_ratio.min_head_sub_ratio=1.3 검증.
    r  r  Tr  fontSizerJ   )rJ   r  rU  zabsolute_min=u
   px 위반 r?  )r  r  r  r  )r  rU  r   u   role 최소값 위반 FNr  r  r   zhead/sub ratio=r  r  r      r  r&  r!  r7   L3	Hierarchyr'  )by_role_sizesabs_min_violatorsrole_violationshead_sub_ratior  )r   
setdefaultr}   r   r   r   r   r   r   rW   r   r   rH   r{   )r   by_rolerX   r   rZ  role_minr[  r  min_sizefs
ratio_warnr\  head_maxsub_maxrK   rL   s                   rD   evaluate_l3_hierarchyre  (  sU    ')G L& @uuXt$1V9b)00:?@ /1& uuXt$Z=//$$6AfI1Z=Q	 01C@Q<R;SSVW	
 )&,	 H -/O"..* h++dB' 	BH}&&!r(C	 4S5I4J#NO J#'N{{:7;;y#9wz*+gi()Q;%/N 55##%nS%9=R<ST 	*6tyy&D$!2.,	
 rC   c           	        | j                   dk7  rg S | dddf   }|j                  dd \  }}t        d|dz        }t        d|dz        }|dd|dd|f   j                  dd      j	                  t
              }d}||z  j	                  t
              }	|	dddf   d	z  |	dddf   d
z  z   |	dddf   z   }
i }i }t        |
|      D ]_  \  }}|j                  |d      dz   ||<   ||vr1|j	                  t        j                        j                         ||<   S||xx   |z  cc<   a t        |j                               }g }t        |j                         d       d| D ]a  \  }}||   |z  j	                  t
              }|j                  t        |d         t        |d         t        |d         f|r||z  ndf       c |S )u   간이 색상 클러스터링: 픽셀 다운샘플 → hue 기반 그룹화 → top-k weight.

    scikit-learn k-means 의존성 회피. 결정적 동작 (numpy seed 무관).
    r$   .Nr"   r   r   r   r   r7   rA  c                    | d    S )Nr   rB   )kvs    rD   rk   z"_cluster_palette.<locals>.<lambda>  s    A rC   rl   r   )r   r   r   r   r   r   r   r   r   int64r   r   r   ry   rW   r}   )r   r   r   r   r   step_ystep_xsamplebinsizebinskeyscountssumsrm   pxtotalpaletter   avgs                      rD   _cluster_paletterv  |  s   
 xx1}	
c2A2g,C99Ra=DAqAG_FAG_F68V8#$,,R3::3?F Gg%%c*D1:d1a4j2o-QT
:DF"$DtV$ Rjja(1,sd?		"((+002DIIOI  EG->?C 
QCyA~%%c*SVc#a&k3s1v;7"E		

 NrC   c           	         g }| D ]h  \  }}t        |      \  }}}d}|D ]3  }	t        ||	d   z
        }
|
dkD  rd|
z
  }
|
|k  s$|	dxx   |z  cc<   d} n |rR|j                  |||||d       j |S )uL   Δhue<15° 클러스터 통합 (그라데이션 카운트 부풀림 방지).Fr      ih  weightT)r   r   lry  r   )r   rJ  r}   )rt  hue_tolerancegroupsr   ry  r   r   r   mergedr   dhs              rD   _unify_huesr    s     $&F ZV%c*S% 	AS1S6\"BCx2XM!(v%	 MM#EVTWXYZ MrC   c                   t        | d      }t        |      }|D cg c]  }|d   dkD  s| }}t        |      }| dddf   j                  t              }|j
                  dd \  }}	t        d	t        ||	      d
z        }
|dd|
dd|
f   j                  dd      }d}|D ]^  }t        t	        |d         t	        |d	         t	        |d         f      \  }}}t        |cxk  r
t        k  sMn P|t        kD  sZ|d	z  }` t        |      r|t        |      z  nd}t        j                  |j                  dd      g       }|D cg c]  }t        |       }}|d   D ch c]'  }|j                  dd      r|d   j                         ) }}g }|D ]6  }t        |      |st!        fd|D              r&|j#                  |       8 g }|t$        kD  r|j#                  d| d       |t&        kD  r|j#                  d|dd       g }|r3|j#                  dt        |       d| d|j                  d       d       |rd}d}d j)                  |      }n|rd!}d"}d j)                  |      }nd#}d$}d}t+        d%d&||||t-        |d'      ||D cg c]%  }|d(   t-        |d)   d	      t-        |d   d'      d*' c}d+,      S c c}w c c}w c c}w c c}w )-uP   L4: palette 4색 이내 + AI 퍼플(hue 270~300, sat>0.5) 캔버스 10%+ 검출.r  )r   ry  g{Gz?.Nr$   r"   r      r   r   r   themePreset r  r  Tr   c              3  <   K   | ]  }t        |      d k    yw)rV  N)r   )rp   tr   s     rD   rr   z*evaluate_l4_color_token.<locals>.<genexpr>  s"      #
23OHa(2-#
s   zpalette u   색 > 4 (brand 2~3 + accent 1)u   AI 퍼플 그라디언트 z.1%z! > 10% (LR_NO_AI_PURPLE_GRADIENT)zoff-token fill u   건: z (themePreset='u   ' palette 외)r   rV  r'  r  r&  r!  r7   L4zColor Tokenr    r   r   )r   r   ry  )color_countai_purple_ratiooff_token_fillsrt  r  )rv  r  r   r   r   r   r   r   r   r&   r'   r)   r.   r   r   upperr   r}   r!   r*   r{   rH   r)  )r   r   rt  unifiedr   significantr  r   r   r   steprl  purple_countrr  r   r   _purple_ratiopreset_tokenspreset_rgbsr   
text_fills	off_tokenr   rX   rR  rK   rL   rM   r   s                                @rD   evaluate_l4_color_tokenr    sK   
 w!,G'"G%<8t);1<K<k"K #rr'

!
!#
&C99Ra=DAqq#a)s"#D44 ((Q/FL !3r!u:s2a5z3r!u:"FGS!8'88SCT=TAL 25V<#f+-#L *--koomR.PRTUM+89a;q>9K9 \*554  	
&	J 
 I #t$s #
7B#
  
 T"# !L)){m#AB	
 **(c(: ;) *	

 !Lc)n-U9+ >(__];<NL	

 <(	<(&$\15( % %uQsVQ'758VWCXY	
 C =& :ns   KKK&,K*Kc                `   g }g }t               }g }g }| d   D ]  }|j                  dd      s|j                  d      }|s0|j                  |d    d       |j                  |d   ddd	       Z|j                  |       |t        v r3|j                  |d    d
| d       |j                  |d   |dd	       n:|t
        vr2|j                  |d    d
| d       |j                  |d   |dd	       |j                  d      }|t        v r2|j                  |d    d| d       |j                  |d   |dd       |d   dv r?|=|dk  r8|j                  |d    d|d    d| d       |j                  |d   |dd       |j                  d      }	|	$|	dk  s|	dkD  r|j                  |d    d|	 d       |j                  d      }
|
|
d kD  s|
d!k  s|
d"kD  s|j                  |d    d#|
 d$        t        |      }|t        k  r|j                  d%| d&t         d'       n%|t        kD  r|j                  d%| d(t         d)       |rd*}d+}d,j                  |      }n|rd-}d.}d,j                  |      }nd/}d}d}t        d0d1|||t        |      |||d23      S )4u4   L5: 폰트 가족 / weight / 자간 / 행간 검증.r  r  T
fontFamilyrJ   u6    fontFamily 미지정 (Pretendard/Noto Sans KR 강제)Nmissing)rJ   familytypez fontFamily='u    ' (banned: 굴림/바탕/궁서)bannedu:   ' (허용: Pretendard, Noto Sans KR — task.md L5 강제)not_primaryr  z fontWeight=z (banned: 100/200/300))rJ   ry  r  r  >   r  r  r  z (z) fontWeight=u    < 600 (SemiBold 이상 권장)weak_emphasisletterSpacingir7   z letterSpacing=u
    극단값
lineHeightr   g?r   z lineHeight=u     권장 범위 0.8~3.0 벗어남z	families=r  uN    (min_families 미달 — 동일 패밀리 내 weight 대비로 충족 가능)z > u    (max_families 초과)r   rV  r'  r  r&  r!  L5
Typography)families_seenfamily_countfamily_violationsweight_violationsr  )setr   r}   addr5   r,   r9   r   r#   r%   r{   rH   ry   )r   rX   rR  r  r  r  r   r  ry  lslhr  rK   rL   rM   s                  rD   evaluate_l5_typographyr    sj    !L L!eM.0.0& 3`uuXt$|$1V9+-c de$$ai4QZ%[\&! _$V9+]6(2RS $$ai6S[%\]22V9+]6( 3K L $$6fmL
 |$&&V9+\&1GH $$ai6S[%\]V9++0BvPS|V9+R&	{-x @+ , $$6foN
 UU?#>rDyBH1V9+_RD
 KL UU< >b1f"s(b3h1V9+\"=] ^_g3`l }%L**~S)=(> ?\ ]	
 
,	,~S)=(> ?$ %	

 <(	<(#M2(!2!2	
 rC   c                   t        |        t        |        t        | d         }|j                         st	        d|       t        j                  |      j                  d      }t        j                  |      }t        ||       t        |       t        |       t        ||       t        |       g}|D cg c]E  }|j                  dk(  r4|j                   r(|j"                   d|j$                   d|j                    G }}t'        d |D              rd}nt'        d |D              rd	}nd
}t)        t+        t-        d |D              t/        |      z              }t1        ||||| d   | d   | d   t/        | d         d      S c c}w )u_  Lite Evaluator 메인 진입점.

    Steps:
        1. JSON Schema 강제 검증 (실패 → SchemaValidationError)
        2. mappingVersion 검증 (실패 → SSotMismatchError)
        3. PNG 로드 (PIL) → numpy
        4. L1~L5 각 평가
        5. EvalResult 종합

    Returns:
        EvalResult — overall verdict + 5항목 details
    
image_pathzimage_path not found: RGBr    rt   c              3  :   K   | ]  }|j                   d k(    yw)r   NrK   rp   its     rD   rr   zevaluate.<locals>.<genexpr>  s     
0B2::
0   c              3  :   K   | ]  }|j                   d k(    yw)r  Nr  r  s     rD   rr   zevaluate.<locals>.<genexpr>  s     2bRZZ6!2r  r  r!  c              3  4   K   | ]  }|j                     y wrh   )rL   r  s     rD   rr   zevaluate.<locals>.<genexpr>  s     3r"((3s   r  targetPersonar   r  )r  r  r   r  )rV   rL   rW   rX   rY   )r   r   r   existsFileNotFoundErrorr   r`   convertr   asarrayr0  rS  re  r  r  rK   rM   rI   rJ   r   r   r)  r   r   rU   )	r   img_pathimgr   rW   r  rX   rV   rL   s	            rD   evaluater    s    [!;'K-.H??"8
 CDD
**X

&
&u
-CjjoG 	Wk2;'k*5{+E ::BII 77)1RWWIR		{+L 
 
0%
00	2E2	2c3U33c%j@ABE!&}5(9)*:;";|#<=	
 s   6A
F)r   rP   )r   rP   r   None)r   r	   r   tuple[int, int, int])r   r  r   r   )r   r  r   r  r   r   )r   r  r   ztuple[float, float, float])r   r  r   r  r   r   )r   list[float]rq   r   r   r   )r   
np.ndarrayr   r  r   r  )r   r  r   rP   r   r  )r   r  r   rP   r   rH   )r   rP   r   rH   )r  )r   r  r   r   r   (list[tuple[tuple[int, int, int], float]])g      .@)rt  r  r{  r   r   zlist[dict[str, Any]])r   rP   r   rU   )GrA   
__future__r   ra   r   dataclassesr   r   pathlibr   typingr   numpyr   PILr   r
   rR   r   r   r   r   r   r   r   r   r   r   r   r   r   r!   r#   r%   r&   r'   r)   r*   	frozensetr,   r.   r5   r9   r:   	Exceptionr<   rF   rH   rU   __file__parentr_   rd   r   r   r   r   r   r   r   r   r   r	  r0  rS  re  rv  r  r  r  r  rB   rC   rD   <module>r     s  . #   (     # c " X  s 09 o 9 " e !#& 5 &" u "  U    c  c  S      " u " c  c  c  ! 5    5   5 " e " +4\>4R*S  S/ +   #,I#  %.o$> > >   5 /I /W	 W : : : F F F H~$$y03UU
4
 =
B**"6*
*"&>> 4>
>.TTT TnFF&4FF\^LLh$R  5 .WW&4WW~cV5rC   