
    CPj:                        d Z ddlmZ ddlZddlZddlmZ ddlmZm	Z	m
Z
mZmZ ddlmZ  ej                  e      ZdZdZ G d	 d
e      Z	 	 	 	 	 	 ddZ	 	 	 	 	 	 ddZddZy)u  BackgroundPlannerStep — 에피소드 단위 배경 생성 플랜 (Phase 5 / Task 3).

mode=floor_plan_anchored 시에 단일 LLM 호출로
  - frequency 분석(저빈도 location 제외, ≥3 shot만 floor_plan)
  - building group 그루핑
  - floor_plan_order / chain_bg_order 결정
  - prev_shot_only fallback 분류
을 한꺼번에 결정한다. set_design / location-loop chain_bg_planning을 대체.

흐름:
  1. 의존 체크포인트 6개 로드 (shot_validator/shot_selection/scene_director/
     entity_merge/entity_detail/visual_world_rules)
  2. selected shot 추출 (per scene)
  3. location_lines + 전체 short_id 수집 (entity_merge가 source-of-truth, kind는
     entity_detail에서 보강 가능. 누락 시 'unknown'.)
  4. scene → primary_location 매핑
  5. all_shot_ids 수집 (S{scene}_Shot{idx})
  6. build_planner_user_prompt → run_background_planner (3회 retry)

토글:
  - settings.background_mode == 'floor_plan_anchored' → 활성
  - 그 외(off / chain_only) → applicable_count=0 즉시 반환

체크포인트 data 구조:
  - rationale_summary, floor_plans, floor_plan_order, chain_bg_groups,
    chain_bg_order, prev_shot_only (run_background_planner 결과 그대로)
    )annotationsN)Path)AnyDictListOptionalTuple)
StepRunner   3c                  *    e Zd ZdZddZddZdd	dZy)
BackgroundPlannerStepuC   단일 LLM 호출로 episode 단위 배경 생성 플랜을 결정.c                    ddl }ddl}ddlm} |j                  |j
                  t        t        d}|j                  |j                  |d      j                  d            j                         dd S )	uT   SCHEMA_VERSION + PROMPT_VERSION 포함한 config_hash. resume 시 stale plan 회피.r   Nsettings)background_modemodelschema_versionprompt_versionT)	sort_keysutf-8   )hashlibjsonapp.core.configr   r   openai_modelSCHEMA_VERSIONPROMPT_VERSIONsha256dumpsencode	hexdigest)selfr   _jsonr   payloads        \/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/background_planner_step.py_config_hashz"BackgroundPlannerStep._config_hash0   se    %,'77**,,	
 ~~KK4K077@

)+cr 	    c                L   ddl m} t        |j                        | j                  z  dz  dz  | j
                  z  |z  dz  }|j                         r&	 t        j                  |j                  d            S y # t        $ r!}t        j                  d||       Y d }~y d }~ww xY w)	Nr   r   checkpointsepisodeszmanifest.jsonr   )encodingz2background_planner: %s checkpoint parse failed: %s)r   r   r   projects_dir
project_id
episode_idexistsr   loads	read_text	Exceptionloggerwarning)r#   step_idr   cpexcs        r&   _load_prev_checkpointz+BackgroundPlannerStep._load_prev_checkpoint@   s    ,&&'$//9()+/??;'( 	
 99;zz",,,"@AA   SU\^abs   $A9 9	B#BB#c                2   ddl m} |j                  dk7  r;t        j	                  d|j                         dddt
        | j                         i dS | j                  d      }| j                  d      }| j                  d      }| j                  d	      }| j                  d
      }| j                  d      }| j                  d      }	t        ||      }
t        ||      \  }}i }|rg|j                  di       xs i j                  dg       xs g D ]:  }|j                  d      }|j                  dd      xs d}|-||t        |      <   < g }t        |
j                               D ]5  }|
|   D ]+  }|j                  dd      }|j                  d| d|        - 7 |s9t        j	                  d       dg g g g g d}dddt
        | j                         |dS d}|rM|j                  di       xs i }|j                  dd      xs# |j                  dd      xs t        |      xs d}ddlm}m}m} ddlm} g }|	r5|	j                  di       xs i }t-        |j                  dg       xs g       } ||
||||      }	  || j.                  |||| j1                         |      }dddt
        | j                         |dS # |$ rL} t        j3                  d |        dddt
        | j                         t5        |       dg g g g g d!dcY d } ~ S d } ~ ww xY w)"Nr   r   floor_plan_anchoreduL   background_planner: skipped — background_mode=%s (not floor_plan_anchored))applicable_countcompleted_countfailed_countr   config_hashdatashot_validatorshot_selectionscene_directorentity_mergeentity_detailvisual_world_rules
scene_saver@   scenesscene_indexprimary_location 
shot_indexS_Shotu>   background_planner: no selected shots — returning empty planu>   선택된 shot이 없어 배경 생성 플랜을 비웁니다.)rationale_summaryfloor_plansfloor_plan_orderchain_bg_groupschain_bg_orderprev_shot_only   
rules_texttext)build_planner_user_promptrun_background_plannerPlannerError)call_structuredsegments)selected_shots_by_scenelocation_linesrF   scene_primary_locationsscene_segments)project_configuser_promptlocation_short_idsshot_idsopik_metadatacall_structured_fnz1background_planner: planner exhausted retries: %s)errorrO   rP   rQ   rR   rS   rT   )r   r   r   r4   infor   r'   r9   _extract_selected_shots_build_location_linesgetintsortedkeysappend_summarize_rules_data'app.modules.pipeline.background_plannerrX   rY   rZ   app.modules.llm.llm_clientr[   listra   build_opik_metadatarg   str)!r#   moder   shot_validator_cpshot_selection_cpscene_director_cpentity_merge_cpentity_detail_cprules_cpscene_save_cpr]   r^   all_location_idsscene_primaryscsiprimaryall_shot_idssc_idxshshi
empty_planrV   r@   rX   rY   rZ   r[   r`   ss_datarb   planr8   s!                                    r&   _executezBackgroundPlannerStep._executeQ   s   ,
 ##'<<KK^((
 %&#$ !"0#002  !667GH 667GH 667GH44^D55oF--.BC 22<@ #:0#

 ,A-,
((
 )+(,,VR8>BCCHbQWUWWVVM*&&!3R8>B>-4M#b'*	 X #%499;<F-f5ff\1-##axuSE$:; 6 = KKXY%e!$&#%"$"$*J %&#$ !"0#002"  
<<+1rDr* 88FB'(. 	 	
 	
 	? 02#''39rG!'++j""="CDN/$;))$1)
	)#22'#3%"668#2D8 !" ,,,.
 	
'  	LLLcR$%#$ !"0#002 X)+#%(*')&(&( 	s   &K L
ALLLN)returnru   )r6   ru   r   Optional[Dict[str, Any]])resume)rv   ru   r   Dict[str, Any])__name__
__module____qualname____doc__r'   r9   r    r(   r&   r   r   -   s    M "R
r(   r   c                   | r|si S i }|j                  di       xs i j                  dg       xs g D ]A  }|j                  d      }|t        |j                  dg       xs g       |t        |      <   C i }| j                  di       xs i j                  dg       xs g D ]  }|j                  d      }|t        |      }|j                  |t                     }|s?|j                  dg       xs g D ]m  }|j                  d      }	|	|	|vr|	|j                  dd      xs dd	}
|j                  d
d      }|r||
d
<   |j                  |g       j	                  |
       o ||v s||   j                  d         |S )u  shot_validator + shot_selection → {scene_index: [shot_dict, ...]}.

    각 shot_dict에는 shot_index + description 포함 (가능하면 location_id도).
    의존 체크포인트가 누락되면 빈 dict를 반환 — caller가 빈 plan으로 graceful 처리.
    r@   rH   rI   selected_shot_indicesshotsrL   descriptionrK   )rL   r   location_idc                &    | j                  dd      S )NrL   r   )rk   )xs    r&   <lambda>z)_extract_selected_shots.<locals>.<lambda>  s    lA0Fr(   key)rk   setrl   
setdefaultro   sort)rw   rx   sel_mapsr   selectedsi_intselr   r   entryloc_ids               r&   ri   ri      s    $5	 G##FB/52::8RHNBNUU=!:quu%<bAGRHB	 O 13H##FB/52::8RHNBNUU=!:Rkk&#%(%%$**B&&&C{cn!!vvmR8>B%E VVM2.F'-m$+2259 + XV!!&F!G/ O2 Or(   c                ,   | sg g fS | j                  di       xs i j                  dg       xs g }i }|r|j                  di       xs i }g }dD ]5  }|j                  |      }t        |t              s%|j                  |       7 |D ]  }t        |t              s|j                  d      xs d}	|	r|	j                  d      s=|j                  d      xs |j                  d      xs dxs d|j                  d	d      xs dd
||	<    g }
g }|D ]  }|j                  d      xs d}	|j                  d      xs |	}|	s0|j                  |	       |j                  |	i       j                  dd      j                         xs$ |j                  dd      j                         xs d}|
j                  |	 d| d|         t        t        ||
      d       }|D cg c]  \  }}|	 }}}|D cg c]  \  }}|	 }}}||fS c c}}w c c}}w )u  entity_merge.locations[] → planner LLM에 줄 location_lines + 전체 short_id 리스트.

    entity_merge는 source-of-truth (short_id, name).
    entity_detail이 있으면 short_id 매칭으로 kind/description을 보강.
    kind 누락 시 'unknown' (LLM은 spec상 unknown을 outdoor로 처리).

    Returns:
        (location_lines, all_location_ids) — 둘 다 short_id 알파벳 정렬.
    r@   	locations)r   entitiesitemsallshort_idrK   Lkindlocation_kindr   )r   r   nameunknownz (z): c                    | d   S )Nr   r   )ts    r&   r   z'_build_location_lines.<locals>.<lambda>R  s    1r(   r   )
rk   
isinstancers   extenddict
startswithro   striprm   zip)rz   r{   r   detail_by_shorted
candidatesr   valitemsidlines	short_idslocr   r   pairedr   _short_ids_sortedllines_sorteds                        r&   rj   rj     s'    2v $$VR06B;;KLRPRI 24O!!&"-3+-
<C&&+C#t$!!#& = DdD)((:&,"CcnnS1&)LTXXo-FL"SQS#xxr:@b$OC   EIggj!'Rwwv%#R(,,VR8>>@ wwvr"((* 	
 	uBtfCv./  C	5)~>F&,-fdaf-"()&$!QA&L)))) .)s   #H
7Hc                   | j                  d      xs g }|syg }| j                  dd      }| j                  dd      }|s|r|j                  d| d|        |D ]l  }t        |t              s|j                  dd      }|j                  dd      }|j                  d	d      }d
| d| }	|r|	d| z  }	|j                  |	       n dj	                  |      S )u7  visual_world_rules 결과 dict에서 사용 가능한 텍스트 발췌.

    upstream 모듈은 `{rules: [...], era, region}` 형태로 저장.
    rules_text 필드 없는 환경 호환을 위해 직접 합성한다.
    CLAUDE.md no-truncation: 모든 rule 항목 전체 본문 포함 (자르지 않음).
    rulesrK   eraregionzera=z
 | region=	rule_typer   visual_guidelinez- [z] z / 
)rk   ro   r   r   join)
r@   r   r   r   r   rrtdescguidebullets
             r&   rp   rp   X  s     HHW#EE
((5"
CXXh#F
ftC5
6(34!T"UU;#uu]B'("-rd"TF#E7m#FV  99Ur(   )rw   r   rx   r   r   zDict[int, List[Dict[str, Any]]])rz   r   r{   r   r   zTuple[List[str], List[str]])r@   r   r   ru   )r   
__future__r   r   loggingpathlibr   typingr   r   r   r   r	   app.core.step_runnerr
   	getLoggerr   r4   r   r   r   ri   rj   rp   r   r(   r&   <module>r      s   6 #    3 3 +			8	$ v
J v
|-/-/- %-`:*-:*.:* !:*zr(   