
    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 ddl	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dZ	 	 	 	 	 	 	 	 ddZy)uv  BackgroundClassifyStep — Phase 7 Step 1.

Spec D1: building group clustering + chain_bg/prev_shot_ref classification — single LLM call.

흐름:
  1. shot_validator/shot_selection/entity_merge/entity_detail/visual_world_rules 로드
  2. shot_count(loc_id별 출현 횟수) 집계
  3. _build_locations_from_entities — entity_merge.locations + entity_detail.kind/summary
     를 flat list로 만듦 (pre-grouping 없음 — LLM이 cluster 결정)
  4. build_classify_user_prompt → run_background_classify (3회 retry)

체크포인트 data:
  data.building_groups[] = [{group_id, members[], anchor_loc, kind, rationale}]
    )annotationsN)defaultdict)Path)AnyDictListOptional)
StepRunner   4c                  &    e Zd ZddZddZdddZy)	BackgroundClassifyStepc                    dd l }dd l}ddlm} |j                  |j
                  t        t        d}|j                  |j                  |d      j                  d            j                         d d S )Nr   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_classify_step.py_config_hashz#BackgroundClassifyStep._config_hash#   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   )encodingz(background_classify: %s 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,BackgroundClassifyStep._load_prev_checkpoint0   s    ,&&'$//9()+/??;'( 	
 99;Yzz",,,"@AA   YI7TWXXYs   $A9 9	B#BB#c                   ddl m} |j                  dv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      }i }	|rj|j                  di       xs i j                  dg       xs g D ]=  }
|
j                  d      }|
j                  dd      xs d}|-|s0||	t        |      <   ? t        |||	      }t        |||      }|s2t        j	                  d       dddt
        | j                         dg idS d}|r@|j                  di       xs i }|j                  dd      xs |j                  dd      xs d}ddlm}m}m} ddlm} t%        |D ch c]  }|d   	 c}      } |||      }	  |||d ||| j&                  | j)                               }|D ci c]  }|d   |j                  dd       }}g }|j                  dg       D ]  }|d   }g }|j                  dg       xs g D ]x  }|d   } |j/                  | |j                  dd      xs dt        |j                  d d      xs d      t1        |j                  d!d"            |j                  | d      d#       z |j/                  |||d$   |d%   |j                  d&d      d'        dddt
        | j                         d|idS c c}w # |$ rG}t        j+                  d|       dddt
        | j                         t-        |      g ddcY d }~S d }~ww xY wc c}w )(Nr   r   >   onfloor_plan_anchoredu3   background_classify: skipped — background_mode=%s)applicable_countcompleted_countfailed_countr   config_hashdatashot_validatorshot_selectionentity_mergeentity_detailvisual_world_rulesscene_directorrA   scenesscene_indexprimary_location u0   background_classify: no locations — empty planr   building_groups
rules_texttext)build_classify_user_promptrun_background_classifyClassifyError)call_structuredloc_id)user_promptall_loc_idsindoor_loc_idsshot_countscall_structured_fnproject_configopik_metadataz"background_classify: exhausted: %s)errorrL   summarygroup_idmemberslabel
shot_count	is_indoorFrS   r_   r`   ra   r\   
anchor_lockind	rationale)r]   r^   rc   rd   re   )r   r   r   r4   infor   r'   r9   getint_count_selected_shots_by_loc_build_locations_from_entities(app.modules.pipeline.background_classifyrO   rP   rQ   app.modules.llm.llm_clientrR   sortedrY   build_opik_metadatar[   strappendbool)!r#   moder   shot_validator_cpshot_selection_cpentity_merge_cpentity_detail_cprules_cpscene_director_cpscene_primaryscsiprimaryrW   	locationsrM   rA   rO   rP   rQ   rR   locrU   rT   resultr8   summary_by_loc
out_groupsclsgidmembers_outmrS   s!                                    r&   _executezBackgroundClassifyStep._execute>   s   ,##+HHKKE((
 %&#$ !"0#002  !667GH 667GH44^D55oF--.BC 667GH )+(,,VR8>BCCHbQWUWWVVM*&&!3R8>B>g-4M#b'*	 X 30-
 3-{
	 KKJK$%#$ !"0#002*B/  
<<+1rD,3Qtxx7KQrJ	
 	
 	?yAyc(myAB 1JG	,''#'#2#22"668F. LUU9C#h-B)??9U+-
::/4Cj/C02KWWY+1r18""$UU7B/52"%aeeL!&<&A"B!%aeeK&?!@-11&"=$  2 &!,/F WW["5  5* !" ,,,.&
3
 	
i B   		LL=sC$%#$ !"0#002"%c(rB 		 Vs*   L1,'L6 N6N;<M=7N=NN)returnro   )r6   ro   r   Optional[Dict[str, Any]])resume)rr   ro   r   zDict[str, Any])__name__
__module____qualname__r'   r9   r    r(   r&   r   r   "   s    x
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 |xs i }t        t              }| j                  di       xs i j                  dg       xs g D ]  }|j                  d      }||j                  t        |      t                     }|j                  dg       xs g D ]\  }	|	j                  d      }
|
|
|vr|	j                  d      xs |j                  t        |      d      xs d}|sP||xx   d	z  cc<   ^  t	        |      S )
u   Selected shots를 loc_id별로 집계.

    shot에 location_id가 있으면 우선, 없으면 scene_primary[scene_index]로 fallback
    (Phase 5 background_planner와 동일 패턴). 둘 다 없으면 카운트하지 않음.
    rA   rH   rI   selected_shot_indicesshots
shot_indexlocation_idrK   r   )rg   setrh   r   dict)rs   rt   ry   sel_mapsr{   primary_mapcountsselshshirS   s               r&   ri   ri      sq    $5	 G##FB/52::8RHNBNUU=!:quu%<bAGRHB	 O  %2K(-F##FB/52::8RHNBNUU=!:kk#b'35)%%$**B&&&C{cnVVM*Pkooc"gr.JPbFv!# + O <r(   c                6   | sg S | j                  di       xs i j                  dg       xs g }g }|D ]  }|j                  d      xs d}|s|j                  dd      xs d}|j                  dg       xs g }t        |t              rdj                  d |D              }	nt	        |      }	|}
|	r|r| d	|	 d
nd|	 }
|j                  ||j                  d      xs |t        |j                  |d            d|
d        |j                  d        |S )u   entity_merge.locations + entity_detail.kind/summary을 flat list로 변환.

    LLM이 직접 cluster + classify할 수 있도록 pre-grouping 없이 raw list 반환.

    Returns:
        [{loc_id, label, shot_count, is_indoor, summary}, ...]
    rA   r}   short_idrK   descriptionvisual_traitsz; c              3  2   K   | ]  }t        |        y wN)ro   ).0vs     r&   	<genexpr>z1_build_locations_from_entities.<locals>.<genexpr>   s     41As   u
    (특징: )u   특징: namer   Frb   c                    | d   S )NrS   r   )xs    r&   <lambda>z0_build_locations_from_entities.<locals>.<lambda>
  s    1X;r(   )key)rg   
isinstancelistjoinro   rp   rh   sort)ru   rv   rW   r}   outr~   siddescvtsvt_textr\   s              r&   rj   rj      s)    	 $$VR06B;;KLRPRI !#Cggj!'Rww}b)/Rggor*0bc4 ii444G#hG7;j	38G9AUG

WWV_+kooc156
 	 , HH&H'Jr(   r   )rs   r   rt   r   ry   zOptional[Dict[int, str]]r   Dict[str, int])ru   r   rv   r   rW   r   r   zList[Dict[str, Any]])__doc__
__future__r   r   loggingcollectionsr   pathlibr   typingr   r   r   r	   app.core.step_runnerr
   	getLoggerr   r4   r   r   r   ri   rj   r   r(   r&   <module>r      s    #   #  , , +			8	$ T
Z T
t /3!/!/! ,! 	!H.-...  . 	.r(   