
    4j7F                    h   d Z ddlmZ ddlZddlZddlmZmZmZm	Z	  ej                  e      ZddlmZ ddlmZ ddlmZ ddlmZmZmZ g d	Z	 	 	 	 	 	 	 	 	 	 	 	 dd
ZddZddZ	 ddd	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 ddZddZdZ ddZ!ddZ"	 	 d 	 	 	 	 	 	 	 	 	 	 	 d!dZ#y)"uJ  ImageService/ReferenceImageService/SceneImageService 공용 helper.

Phase 3b.2에서 ImageService의 네 private 메서드(`_image_to_dict`,
`_get_latest_world_guide`, `_auto_set_primary`, `_build_lineage_fields`)를
module-level 함수로 승격. ReferenceImageService가 독립 클래스로 이관되어도
동일 helper를 공유할 수 있게 한다. SceneImageService(Phase 3b.3)도 재사용 예정.

기존 ImageService의 instance 메서드(`self._xxx`)는 내부적으로 본 모듈의 함수로
위임하는 shim으로 유지된다 — api/v1/images.py 및 테스트 호환.
    )annotationsN)AnyDictListOptional)Session)to_relative_image_path)get_module_info)
ImageAssetProjectSettings
WorldGuide)image_to_dictget_latest_world_guideauto_set_primarybuild_lineage_fieldsload_project_llm_configfill_missing_t2i_promptspopulate_t2i_promptsc           
        d}t        |      D ]i  \  }}||   }|j                  xs d|d<   |j                  xs d|d<   	 |j                  rt	        j
                  |j                        ng |d<   |d   rhd}k |sy|D 	cg c]&  }	|	j                  d      s|	j                  d      s|	( }
}	|
syt        j                  dt        |
             d	d
lm}  ||      }|D ci c]  }|d   |j                  dd       }}|
D 	cg c]<  }	|	d   |	j                  dd      |	j                  dd      |	j                  dd      d> }}	|j                  ||      }|D ]i  }|d   r	|d   |v s||d      }t        |t               r+|j                  dd      |d<   |j                  dd      |d<   Tt        |t"              se||d<   k | j%                          y# t        j                  t        f$ r	 g |d<   Y w xY wc c}	w c c}w c c}	w )u  Populate T2I prompt fields on stills from ORM + runtime fallback.

    W5 F22 Phase B.20 (2026-04-22): scene_image_service.generate_images의
    T2I loader 블록을 이관. 처리:
      1) stills_orm에서 t2i_prompt_cinematic/closeup + t2i_variations_json(JSON decode) 로드
      2) cinematic 없고 variations도 없는 still만 T2IVisualConverter로 런타임 생성
      3) convert_scenes 결과를 still_data에 쓰고 db.commit

    stills와 stills_orm은 인덱스로 1:1 매칭. 이후 caller는
    ``fill_missing_t2i_prompts(stills)``로 최종 fallback을 적용한다.
    Mutates stills in-place. 반환값 없음.
    F t2i_prompt_cinematict2i_prompt_closeupt2i_variationsTNu;   T2I prompts missing for %d stills — generating at runtimer   )T2IVisualConverter)
llm_clientid
t2i_promptstill_frame_promptscreenplay_scene_headingvisible_entities_json[])r   r   r   r    ab)	enumerater   r   t2i_variations_jsonjsonloadsJSONDecodeError	TypeErrorgetloggerwarninglen app.modules.t2i_visual_converterr   convert_scenes
isinstancedictstrcommit)dbstills
stills_ormentitiesopenai_clienthas_missing_t2isi
still_data	still_ormsactually_missingr   	convertereentity_t2i_mapscene_dictsscene_t2i_mapt2is                     X/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/image_service_helpers.pyr   r   %   sX   & O#F+JrN	-6-K-K-Qr
)*+4+G+G+M2
'(	. 00 

9889 '( 01"O ,  auu+,QUU;K5L 	
6   
NNE D"m<I@HI1agquu\266NI " "A D'"#%%(<b"A().H"(M%&UU+BD%I		
 "   ,,[.IM
01j6F-6W
4 01C#t$58WWS"5E
12367733C
/0C%58
12  IIKS $$i0 	.+-J'(	. Js%    0G+G?#HAH	G<;G<c                   | D ]  }|j                  d      sb|j                  dg       }|rNt        |t              r>|d   j                  dd      |d<   t        |      dkD  r|d   j                  dd      |d<   |j                  d      s|j                  dd      |d<   |j                  d      r|j                  dd      |d<    y	)
u  Fill missing t2i_prompt_cinematic / t2i_prompt_closeup on stills in-place.

    W5 F22 Phase B.18 (2026-04-22): scene_image_service.generate_images의
    fallback 블록을 이관. 각 still에 대해 3단 fallback:
      1) t2i_variations[0/1].t2i_prompt (이미 파싱된 리스트)
      2) still_frame_prompt 원본
      3) 그대로 둠 (빈 문자열)

    Mutates stills in-place. 반환값 없음.
    r   r   r   r   r      r   r   N)r*   r0   listr-   )r5   r;   t2i_varss      rE   r   r   o   s     
~~45!~~&6;HJx65=a[__\SU5V
12x=1$7?{|UW7XJ34~~451;@TVX1YJ-.~~23/9~~>RTV/WJ+,     c                D   	 | j                  t              j                  t        j                  |k(        j	                         }|r+|j
                  rt        j                  |j
                        S i S # t        $ r"}t        j                  d||       Y d}~i S d}~ww xY w)u   프로젝트 LLM 설정 로드. 없으면 빈 dict.

    W5 F22 Phase B.3에서 scene_image_service._load_project_llm_config을
    module-level helper로 승격. scene_variation_service의 recommend_variations
    등에서 공유.
    z.llm_config_json load failed for project %s: %sN)queryr   filter
project_idfirstllm_config_jsonr&   r'   	Exceptionr+   r,   )r4   rN   psexcs       rE   r   r      s    ZXXo&--o.H.HJ.VW]]_"$$::b0011 I  ZGUXYYIZs   A/A4 4	B=BB)actual_refs_cachec                   d}| |j                  | j                  xs d      }n:|8| j                  r,t        || j                  | j                  | j                        }i d| j
                  d| j                  dt        | dd      dt        t        | dd            dt        | dd      d	| j                  d
| j                  d| j                  d| j                  rt        | j                        ndd| j                  d| j                  d| j                  d| j                  d| j                   d| j"                  xs dd| j$                  d| j&                  | j(                  | j*                  | j,                  | j.                  | j0                  | j2                  | j4                  t        | j6                        | j8                  | j:                  | j<                  | j>                  xs d|| j@                  | jB                  dS )uV  Convert an ImageAsset ORM object to a dict.

    Task 5 (single-vs-batch reference contract §4.4):
    - reference_image_ids = lineage (visible_entities character/prop UUID list)
    - actual_attached_refs = actual labels attached to LLM call (llm_call_log join)

    Resolution order for actual_attached_refs:
      1. actual_refs_cache 전달 시 (list endpoint, N+1 회피) → cache.get(still_id)
      2. db 전달 시 (detail endpoint) → _lookup_actual_refs single query
      3. 둘 다 None → actual_attached_refs = None (backward compat, 기존 caller)

    list endpoint 사용 패턴 (N+1 회피 의무):
        still_ids = [img.still_id for img in images if img.still_id]
        cache = _lookup_actual_refs_batch(db, project_id, episode_id, still_ids)
        return [image_to_dict(img, actual_refs_cache=cache) for img in images]
    Nr   r   
asset_typepipeline_roleis_intermediateFdisposition	entity_idstill_id
episode_id	file_pathprompt_usedgeneration_modelwidthheightstatusreview_notesvalidation_scorevalidation_resultr!   )sanitization_strategyoriginal_promptsanitization_notevariant_typeangle_appliedcolor_appliedsource_image_id
is_primaryprompt_typecode_versionprompt_file_versionreference_image_idsactual_attached_refstheme_label
created_at)"r*   r[   _lookup_actual_refsrN   r\   r   rV   getattrboolrZ   r]   r	   r^   r_   r`   ra   rb   rc   rd   re   rf   rg   rh   ri   rj   rk   rl   rm   rn   ro   rp   rq   rs   rt   )imgr4   rT   actual_refss       rE   r   r      s$   0 (,K$'++CLL,>B?	CLL)
$cff$cnn$ 	ot<	$ 	4-> FG$ 	wsM48$ 	S]]$ 	CLL$ 	cnn$ 	cmm+CMM:QS$ 	s$ 	C00$ 	$  	#**!$" 	#**#$$ 	((.B%$& 	C00'$( 	S22)$* "%!:!:.. 22((****..3>>*(("66"66>$ +nnG$ $rJ   c                (   ddl m}  |d      }	 | j                  |||d| dd      j                         }|sy	 t        j                  |d   xs d	      S # t        $ r!}t
        j                  d||       Y d}~yd}~ww xY w# t        $ r Y yw xY w)
u!  detail endpoint — 1 still 의 가장 최근 llm_call_log entry ref labels.

    Task 5 (spec §4.4): operation_type IN ('single_scene_image_gen', 'scene_image_gen')
    + metadata_json.still_id 매칭 + 가장 최근 created_at row.
    log entry 없거나 retention 지나면 None.
    r   texta!  
        SELECT reference_image_ids FROM llm_call_log
        WHERE project_id = :pid
          AND episode_id = :eid
          AND operation_type IN ('single_scene_image_gen', 'scene_image_gen')
          AND metadata_json::text LIKE :sid_pat
        ORDER BY created_at DESC LIMIT 1
    z%"still_id": "z"%)pideidsid_patz*actual_refs lookup failed for still %s: %sNr!   )	
sqlalchemyr|   executerO   rQ   r+   r,   r&   r'   )r4   rN   r\   r[   _textsqlrowrS   s           rE   ru   ru      s     )
  	CjjJ^T\S]]_C`a

%' 	 zz#a&.D))  CXsS  s(   (A B 	B!A==B	BBc                P   |si S ddl m}  |d      }	 | j                  ||||d      j                         }i }|D ]%  \  }	}
|			 t        j                  |
xs d      ||	<   ' |S # t        $ r"}t
        j                  d|       i cY d}~S d}~ww xY w# t        $ r g ||	<   Y hw xY w)u  list endpoint — episode 전체 still_id 의 latest log entry 1 query 로 lookup.

    Task 5 (spec §4.4) — N+1 회피.
    PostgreSQL DISTINCT ON 으로 still_id 별 가장 최근 created_at row 1개씩.

    Returns: {still_id: ref_labels_list}. log entry 없는 still_id 는 dict 미포함
    (caller 가 cache.get(sid) → None handling).
    Parse 실패 row 는 빈 list 로 isolated (다른 row 영향 없음).
    r   r{   a  
        SELECT DISTINCT ON ((metadata_json::jsonb)->>'still_id')
            (metadata_json::jsonb)->>'still_id' AS still_id,
            reference_image_ids
        FROM llm_call_log
        WHERE project_id = :pid
          AND episode_id = :eid
          AND operation_type IN ('single_scene_image_gen', 'scene_image_gen')
          AND (metadata_json::jsonb)->>'still_id' = ANY(:sids)
        ORDER BY (metadata_json::jsonb)->>'still_id', created_at DESC
    )r}   r~   sidsz#actual_refs batch lookup failed: %sNr!   )	r   r|   r   allrQ   r+   r,   r&   r'   )r4   rN   r\   	still_idsr   r   rowsrS   resultsidref_ids_jsons              rE   _lookup_actual_refs_batchr     s     	(
 
 
	Czz#z*iXY]]_
 $&F!\;	**\%9T:F3K	 " M  <cB	  	F3K	s/   $A& B&	B/BBBB%$B%c                   | j                  t              j                  t        j                  |k(  t        j                  |k(        j                  t        j                  j                               j                         }|r 	 t        j                  |j                        S i S # t        j                  $ r"}t        j                  d||       Y d}~i S d}~ww xY w)z8Get the latest world guide for an episode (JSON-parsed).z*guide_json parse failed for project %s: %sN)rL   r   rM   rN   r\   order_byrt   descrO   r&   r'   
guide_jsonr(   r+   r,   )r4   rN   r\   wgrS   s        rE   r   r   2  s     		
%%3Z5J5Jj5X	Y	*'',,.	/		  
	Z::bmm,, I ## 	ZNNGUXYYI	Zs   B% %C8CCuploadedc                0    | j                   xs dt        k(  S )u[   사람이 손으로 올린 자산인가 — 코드가 남긴 표식을 코드가 읽는다.r   )r^   MANUAL_UPLOAD_PROMPT)rx   s    rE   is_manual_uploadr   G  s    OO!r&:::rJ   c                B   |j                   xs d}|j                  d      xs |j                  d      }|j                  r| j                  t              j                  t        j                  |k(  t        j                  |j                  k(  t        j                  |j                  k7        j                         }|D ]C  }|j                   xs d}|j                  d      xs |j                  d      }||k(  s=d|_	        E |j                  r| j                  t              j                  t        j                  |k(  t        j                  |j                  k(  t        j                  |j                  k7        j                         }d}		 ddlm}
 t        t        |
dd            }	|	rPt!        |      sEt#        d |D              r3t$        j'                  d	|j                  |j                         d|_	        y
|D ]	  }d|_	         d|_	        y
# t        $ r Y sw xY w)u  Auto-set the newly created image as primary, unsetting others.

    Composite images (prompt_used starts with [composite:] or [outlook_id:]) are
    excluded — composite 이미지들은 generate_composite_image에서 자체적으로
    is_primary를 관리한다. Composite끼리 / 비-composite끼리만 primary 경쟁.

    ## ★사람이 올린 대표는 파이프라인이 안 내린다 (2026-09-20)

    사용자가 손으로 올린 최종본이 **걷기 한 번에 사라졌다** — 새 이미지를
    한 장도 안 사도 캐시된 산출을 다시 영속하면서 이 함수가 같은 still 의
    형제를 **전부** 내렸기 때문이다(실측: 오전에 올린 8장이 저녁 걷기에
    덮였다. 현재 이 화의 사람 대표는 16장).

    그래서 같은 still 에 **사람이 올린 대표**가 있으면 이 자산을 대표로
    승격하지 않고 그 자리를 지킨다. 새 자산은 **저장은 된다** — 지우지
    않으므로 사용자가 보고 고를 수 있다.

    ★사람이 올린 자산 자신이 들어올 때는 그대로 대표가 된다(사람이
     바꾸려는 것이다).
    ★레버: `settings.image_protect_manual_primary` 를 끄면 종전 동작.
    r   z[composite:z[outlook_id:r   T)settingsimage_protect_manual_primaryc              3  N   K   | ]  }t        |      xr |j                    y wN)r   rm   ).0r=   s     rE   	<genexpr>z#auto_set_primary.<locals>.<genexpr>  s)      +!)A )+<<!)s   #%u   auto_set_primary: still %s 에 **사람이 올린 대표**가 있다 — 새 자산 %s 는 저장하되 대표로 올리지 않는다NrG   )r^   
startswithrZ   rL   r   rM   rN   r   r   rm   r[   app.core.configr   rw   rv   rQ   r   anyr+   r,   )r4   rN   rx   promptis_compositesiblingssib
sib_promptsib_is_composite_protect	_settingss              rE   r   r   L  s   , __"F$$]3Xv7H7H7XL
}}HHZ V%%3$$5'
 SU 	 C.BJ)44]ClzG\G\]kGl//!"	  ||HHZ V%%3##s||3'
 SU 	 	=G94A BH -c2 +!)+ +NNScff& CNCCN  CN  		s   H 	HHc                   t        |      }g }|r5|D ]/  }| j                  t              j                  t        j                  |k(  t        j
                  |k(  t        j                  dk(  t        j                  dk(        j                         }|s| j                  t              j                  t        j                  |k(  t        j
                  |k(  t        j                  dk(        j                  t        j                  j                               j                         }|s|j                  |j                         2 ||d   |j                  d      t        j                   |      dS )zMBuild lineage fields for an image asset (code_version / reference_image_ids).	referencerG   versionprompt_dependency)rn   ro   rp   rq   )r
   rL   r   rM   rN   rZ   rV   rm   rO   r   rt   r   appendr   r*   r&   dumps)	r4   rN   module_nameref_entity_idsrn   inforef_image_idsr~   ref_imgs	            rE   r   r     s.    ;'D!M!C$))Z7((C/))[8))Q.	   HHZ(V"--;",,3"--<
 Xj3388:;UW  $$WZZ0/ "4 #Y#xx(;<#zz-8	 rJ   )r4   
OrmSessionr5   List[Dict[str, Any]]r6   z	List[Any]r7   r   r8   r   returnNone)r5   r   r   r   )r4   r   rN   r2   r   Dict[str, Any]r   )rx   r   r4   zOptional[OrmSession]rT   zOptional[Dict[str, List[str]]]r   r   )
r4   'OrmSession'rN   r2   r\   r2   r[   r2   r   Optional[List[str]])
r4   r   rN   r2   r\   r2   r   z	List[str]r   zDict[str, List[str]])r4   r   rN   r2   r\   r2   r   r   )rx   r   r   rw   )r4   r   rN   r2   rx   r   r   r   )NN)r4   r   rN   r2   r   r2   r   r   rn   zOptional[str]r   r   )$__doc__
__future__r   r&   loggingtypingr   r   r   r   	getLogger__name__r+   sqlalchemy.ormr   r   app.core.file_pathsr	   app.core.version_registryr
   app.models.projectr   r   r   __all__r   r   r   r   ru   r   r   r   r   r   r    rJ   rE   <module>r      s  	 #   , ,			8	$ 0 6 5 F FGG G G #	G
 G 
GTX2$  $D 9=	D	DD 6	D
 DN!!! ! 	!
 !H,,, , 	,
 ,^$ " ;
G\ +/!%))) ) (	)
 ) )rJ   