All Python Scripts Merged
All Python Scripts Merged
py
2 """
3 ■■■■ ■■■■ ■■■■ ■■■■■■ ■■■■■■■ (Enhanced Conflict Network Builder)
4 ■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■■ ■■■■■ ■■■■ ■■■■ ■■■■ ■■■■■■■■
5 ■■ ■■■■■■ ■■■■■■■ ■■■■■■■■ ■■ ■■■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■.
6 """
7 import sys
8 import os
9 import json
10 import time
11 import pickle
12 import warnings
13 import asyncio
14 import copy
15 import logging
16 from datetime import datetime
17 from typing import Dict, List, Any, Optional, Tuple, Union, Callable
18 from functools import partial
19 from [Link] import ThreadPoolExecutor
20
21 [Link]([Link]([Link]([Link](__file__), '..', '..')))
22
23 from core.base_entities import (
24 ConflictNetwork, Conflict, ConflictPhase, ConflictSubject, ConflictScope,
25 Character, Relationship, RelationshipType, RelationshipNature, RelationshipDirection
26 )
27 from utils.cache_utils import memory
28 from utils.perf_utils import PerfTracker
29 # Import the multi-model API caller
30 from utils.api_utils import generate_conceptual_element_multi_model
31 # Import specific model IDs to try for inference
32 from utils.api_utils import (
33 GEM_MODEL_PRO_EXP, OR_PHI_4_REASONING_PLUS, TGR_LLAMA_3_3_70B_TURBO,
34 OAI_MODEL_DEFAULT, OR_NVIDIA_LLAMA_3_3_NEMOTRON_SUPER, OR_QWEN_3_235B
35 )
36
37
38 # ■■■■■ ■■■■■■■ ■■ ■■■■■ ■■■■■■ ■■■■■
39 logger = [Link](__name__)
40 if not [Link]:
41 handler = [Link]()
42 formatter = [Link](
43 '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
44 )
45 [Link](formatter)
46 [Link](handler)
47 [Link]([Link])
48
49 # ■■■■■ ■■■■■■■■■ ■■■■■■■ ■■ ■■■■ ■■■■
50 ORIGINAL_PROSE_PATH = [Link]("PROSE_FILE_PATH", "")
51 PROJECT_PATH = [Link]("PROJECT_PATH", "")
52 PROJECT_NAME = [Link]("PROJECT_NAME", "DefaultProject_S3Builder")
53 APP_CONFIG_PATH = [Link]("APP_CONFIG_PATH")
54
55 # ■■■■■ ■■■ ■■■■■■■ ■■■■■■ ■■■■ ■■■■■
56 APP_CONFIG_S3B = {}
57 if APP_CONFIG_PATH and [Link](APP_CONFIG_PATH):
58 import yaml
59 try:
60 with open(APP_CONFIG_PATH, 'r', encoding='utf-8') as f_conf:
61 APP_CONFIG_S3B = yaml.safe_load(f_conf)
62 [Link](f"S3Builder loaded config from {APP_CONFIG_PATH}")
63 except Exception as e_conf:
64 [Link](f"S3Builder failed to load config: {e_conf}")
65 [Link]("Using default configuration settings")
66
67
68 # ■■■■■ ■■■■■ ■■■■■■ ■■■■■■■/■■■■■■■ ■■ ■■■■■■■ ■■■■■ ■■■■ ■■■■■■
69 def _configure_io_paths():
70 """■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■■■ ■■■■ ■■■■■■■■"""
71 paths = {}
72
73 if PROJECT_PATH and ORIGINAL_PROSE_PATH:
74 prose_basename = [Link]([Link](ORIGINAL_PROSE_PATH))[0].replace(" ", "_")
75 output_dir = [Link](PROJECT_PATH, "station3_output")
76
77 [Link]({
78 "prose_basename": prose_basename,
79 "output_dir": output_dir,
80 "station3_output_pkl": [Link](output_dir, f"{prose_basename}_station3_output.pkl"),
81 "station3_output_json": [Link](output_dir, f"{prose_basename}_station3_output.json"
82 "station1_input_dir": [Link](PROJECT_PATH, "station1_output"),
83 "station1_input_pkl": [Link](PROJECT_PATH, "station1_output", f"{prose_basename}_st
84 "station2_input_dir": [Link](PROJECT_PATH, "station2_output"),
85 "station2_input_pkl": [Link](PROJECT_PATH, "station2_output", f"{prose_basename}_st
86 })
87 else:
88 # ■■■■■ ■■■■■■■■■■ ■■■ ■■ ■■■ ■■■■■ ■■■■■■■■■ ■■■■■■■
89 [Link]({
90 "prose_basename": "unknown_prose_s3b",
91 "output_dir": "station3_output",
92 "station3_output_pkl": "station3_output.pkl",
93 "station3_output_json": "station3_output.json",
94 "station1_input_dir": "station1_output",
95 "station1_input_pkl": "station1_output.pkl",
96 "station2_input_dir": "station2_output",
97 "station2_input_pkl": "station2_output.pkl",
98 })
99 [Link]("S3Builder: PROJECT_PATH or ORIGINAL_PROSE_PATH not fully set. Using default I
100
101 # ■■■■■ ■■■■ ■■■■■■■ ■■■ ■■ ■■■ ■■■■■■■
102 [Link](paths["output_dir"], exist_ok=True)
103
104 return paths
105
106 # ■■■■■ ■■■■■■ ■■■■■■■/■■■■■■■ ■■■ ■■■■■ ■■■■■■
107 IO_PATHS = _configure_io_paths()
108
109
110 class AIResultSelector:
111 """
112 ■■■ ■■■■■■ ■■ ■■■■■■ ■■■■ ■■■■■ ■■ ■■■■■ ■■■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■■.
113 ■■■■ ■■■■■■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■ ■■■■■ ■■■■■.
114 """
115 def __init__(self, preferred_models=None, selection_strategy="first_valid"):
116 """
117 ■■■■■ ■■■■ ■■■■■ ■■■■■■ ■■■■■■■■■
118
119 Args:
120 preferred_models (List[str], optional): ■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■■ ■■■■■■ ■■■■
121 selection_strategy (str, optional): ■■■■■■■■■■ ■■■■■■ ■■■■■■■
122 ("first_valid", "quality_score", "consensus")
123 """
124 self.preferred_models = preferred_models or [
125 OR_PHI_4_REASONING_PLUS,
126 GEM_MODEL_PRO_EXP,
127 TGR_LLAMA_3_3_70B_TURBO,
128 OR_NVIDIA_LLAMA_3_3_NEMOTRON_SUPER,
129 OAI_MODEL_DEFAULT
130 ]
131 self.selection_strategy = selection_strategy
132
133 def select_result(self, multi_model_output: Dict[str, Any], expected_key: Optional[str] = None) -
134 """
135 ■■■■■■ ■■■■ ■■■■■ ■■ ■■■■■ ■■■■■■■ ■■■■■■■■
136
137 Args:
138 multi_model_output: ■■■■■ ■■■■■■■ ■■■■■■■■
139 expected_key: ■■■■■■■ ■■■■■■■ ■■ ■■■■■■■
140
141 Returns:
142 tuple: (■■■■■■■ ■■■■■■■■, ■■■ ■■■■■■■ ■■■■■■■■)
143 """
144 if not isinstance(multi_model_output, dict) or not multi_model_output:
145 return None, None
146
147 # ■■■■■■■■■■ 1: ■■■■■■ ■■ ■■■■■■■ ■■■■■■■ ■■■■■
148 for preferred_model in self.preferred_models:
149 for model_name, result_data in multi_model_output.items():
150 if preferred_model in model_name and isinstance(result_data, dict) and "error" not in
151 if expected_key:
152 if expected_key in result_data and result_data[expected_key]:
153 [Link](f"AIResultSelector selected result for '{expected_key}' from
154 return result_data[expected_key], model_name
155 else:
156 [Link](f"AIResultSelector selected general result from preferred model {
157 return result_data, model_name
158
159 # ■■■■■■■■■■ 2: ■■■■■■ ■■ ■■■■■ ■■■■■
160 for model_name, result_data in multi_model_output.items():
161 if isinstance(result_data, dict) and "error" not in result_data:
162 if expected_key:
163 if expected_key in result_data and result_data[expected_key]:
164 [Link](f"AIResultSelector selected result for '{expected_key}' from {mod
165 return result_data[expected_key], model_name
166 else:
167 [Link](f"AIResultSelector selected general result from {model_name}")
168 return result_data, model_name
169
170 [Link](f"AIResultSelector: No valid result found for key '{expected_key if expected_k
171 return None, None
172
173
174 class DataExtractor:
175 """
176 ■■■ ■■■■■■ ■■ ■■■■■■■ ■■■■■■■■ ■■■■■■■■■ ■■ ■■■■■ ■■■■■
177 ■■■■■■ ■■■■■■■■ ■■■■■■■■■■ ■■■■■■■ ■■■■■ ■■■■ ■■■■■■.
178 """
179
180 @staticmethod
181 def extract_s1_character_data(s1_data: Dict[str, Any]) -> List[Character]:
182 """
183 ■■■■■■■ ■■■■■■ ■■■■■■■■ ■■ ■■■■■■ ■■■■■■
184
185 Args:
186 s1_data: ■■■■■■ ■■■■■■ ■■■■■■
187
188 Returns:
189 ■■■■■ ■■■■■■ ■■■■■■■■
190 """
191 characters = []
192 s1_analysis = s1_data.get("analysis_outputs", {})
193 major_chars_info = s1_analysis.get("major_characters_identified", {})
194 major_char_names = major_chars_info.get("major_characters", [])
195 char_analysis_dict = s1_analysis.get("character_analysis", {})
196
197 processed_names = set()
198 for char_name_s1 in major_char_names:
199 if char_name_s1 in processed_names:
200 continue
201
202 processed_names.add(char_name_s1)
203 analysis = char_analysis_dict.get(char_name_s1, {})
204
205 if "error" in analysis:
206 continue
207
208 char_obj = Character(
209 name=char_name_s1,
210 description=[Link]("Narrative_Function", "N/A"),
211 profile={
212 "personality_traits": [Link]("Personality_Traits", "N/A"),
213 "motivations_goals": [Link]("Motivations_Goals", "N/A"),
214 "potential_arc": [Link]("Potential_Arc_Observation", "N/A")
215 },
216 metadata={
217 "s1_key_relationships_brief": [Link]("Key_Relationships_Brief", "N/A"),
218 "source": "Station1_Analysis",
219 "extraction_timestamp": [Link]().isoformat()
220 }
221 )
222 [Link](char_obj)
223
224 [Link](f"Extracted {len(characters)} characters from S1 data")
225 return characters
226
227 @staticmethod
228 def extract_s2_conceptual_data(s2_data: Dict[str, Any]) -> Dict[str, Any]:
229 """
230 ■■■■■■■ ■■■■■■■■ ■■■■■■■■■■ ■■ ■■■■■■ ■■■■■■■
231
232 Args:
233 s2_data: ■■■■■■ ■■■■■■ ■■■■■■■
234
235 Returns:
236 ■■■■■ ■■■■■■■■ ■■■■■■■■■■
237 """
238 s2_outputs = s2_data.get("conceptual_outputs", {}).get("user_selections", {})
239
240 # ■■■■ ■■ ■■■■■■ ■■■■■■ ■■■■■■ ■■■ ■■■■■■■■
241 conceptual_data = {
242 "story_statement": s2_outputs.get("final_story_statement", ""),
243 "hybrid_genre": s2_outputs.get("final_hybrid_genre", ""),
244 "elevator_pitch": s2_outputs.get("final_elevator_pitch", ""),
245 "three_d_map": s2_outputs.get("selected_3d_map_details", {}),
246 "dynamic_tone": s2_outputs.get("selected_dynamic_tone", {}),
247 "artistic_references": s2_outputs.get("selected_artistic_references", {}),
248 "extraction_timestamp": [Link]().isoformat()
249 }
250
251 # ■■■■■ ■■ ■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■ ■■ ■■■■■■■■■ ■■■■■■■■
252 if "final_thematic_statement" in s2_outputs:
253 conceptual_data["thematic_statement"] = s2_outputs["final_thematic_statement"]
254
255 if "final_world_elements" in s2_outputs:
256 conceptual_data["world_elements"] = s2_outputs["final_world_elements"]
257
258 return conceptual_data
259
260
261 class RelationshipInferenceEngine:
262 """
263 ■■■■ ■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■■■ ■■ ■■■■■■■ ■■■ ■■■■■■■ ■■■■■.
264 ■■■■ ■■■ ■■■■■■■ ■■■■■■ ■■■■■■■ ■■■■■ ■■ ■■■■■.
265 """
266
267 def __init__(self, network: ConflictNetwork, ai_selector: AIResultSelector = None):
268 """
269 ■■■■■ ■■■■ ■■■■■■■ ■■■■■■■■
270
271 Args:
272 network: ■■■■ ■■■■■■ ■■■■ ■■■■ ■■■■■ ■■■■■■■■ ■■■■■
273 ai_selector: ■■■■ ■■■■■ ■■■■■■ ■■■■■■■■■
274 """
275 [Link] = network
276 self.ai_selector = ai_selector or AIResultSelector()
277 self.relationship_counter = 0
278
279 async def infer_relationships_from_ai(self, s1_data: Dict[str, Any], s2_conceptual: Dict[str, Any
280 """
281 ■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■■■
282
283 Args:
284 s1_data: ■■■■■■ ■■■■■■ ■■■■■■
285 s2_conceptual: ■■■■■■■■ ■■■■■■■■■■ ■■ ■■■■■■ ■■■■■■■
286 full_script_text: ■■■■ ■■■■■■ ■■■■■■■■■ (■■■■■■■)
287
288 Returns:
289 ■■■ ■■■■■■■■ ■■■■ ■■ ■■■■■■■■■
290 """
291 [Link]("Starting AI-driven relationship inference...")
292 if not [Link].get_all_characters():
293 [Link]("No characters in network for AI relationship inference.")
294 return 0
295
296 # ■■■■■ ■■■■ ■■■■■■ ■■■■■■■■■
297 char_list_str = ", ".join([f"'{[Link]}' (ID: {[Link]})" for char in [Link].get_all_
298 s1_rel_summary = s1_data.get("analysis_outputs", {}).get("relationship_analysis", {}).get("ke
299
300 context_for_ai = {
301 "■■■■■■": "■■■■■■■ ■■ ■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■
302 "■■■■■■■■_■■■■■■■": char_list_str,
303 "■■■■_■■■■■■■■_■■_■■■■■■1": s1_rel_summary,
304 "■■■■_■■■■■_■■_■■■■■■2": s2_conceptual.get("story_statement", "■■■ ■■■■■"),
305 "■■■■■■■_■■■■■■■_■■■■■■■■": [[Link] for rt in RelationshipType],
306 "■■■■■■■_■■■■■■■_■■■■■■■■": [[Link] for rn in RelationshipNature]
307 }
308
309 # ■■■■■ ■■■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■
310 prompt = f"""
311 ■■■■■■■■ ■■■ ■■■■■■ ■■■■■■ (■■■■■ ■■■■■■■■■ ■■■■ ■■■■■■ ■■■■■ ■■■■ ■■■■■)■ ■■ ■■■■■
312 ■■■ ■■■■■ ■■■■■■ ■■■■■ ■■■■■■■:
313 1. ■■■ ■■■■■■■■■ (■■■■ ■■■■■■■■ ■■ ID ■■■ ■■■ ■■■■■ ■■■■■■).
314 2. ■■■■■ ■■■ ■■■■■■■ (■■ ■■■■■ `■■■■■■■_■■■■■■■_■■■■■■■■`). ■■■■ ■■■■■ ■■■■■■.
315 3. ■■■■■ ■■■■■ ■■■■■■■ (■■ ■■■■■ `■■■■■■■_■■■■■■■_■■■■■■■■`).
316 4. ■■■ ■■■■■ ■■■■■■ (■■■■ ■■ ■■■■■■) ■■■■ ■■■■■■■ ■■■■■■■ ■■ ■■■■■ ■■■■■■■■■ ■■ ■■■
317 5. ■■■■ ■■■ ■■■■■■■ (1-10■ ■■■ 10 ■■ ■■■■■■).
318 6. ■■■■■ ■■■■■ ■■■■■■■ (DIRECTED ■■■ ■■■■ ■■ ■■■■■ ■■■■ ■■ ■■■■■ ■■■■■■ BIDIRECTIONAL ■
319 7. ■■■■ ■■■■■■ ■■■■■ ■■■■ ■■■ ■■■ ■■■■■■■■ ■■ ■■■■.
320
321 ■■■ ■■■■■■■ **■■■■■■** ■■■■■■ JSON ■■■■■ ■■■ ■■■■■ `"inferred_relationships"`. ■■■■ ■■■ ■■
322 `"character1_name_or_id"`, `"character2_name_or_id"`, `"relationship_type"`, `"relationship_nature"`,
323
324 ■■■■ ■■■■■■ ■■■■■ ■■■■■:
325 {{
326 "character1_name_or_id": "■■■",
327 "character2_name_or_id": "■■■■",
328 "relationship_type": "RIVALRY",
329 "relationship_nature": "NEGATIVE",
330 "description_rationale": "■■■■■■ ■■■ ■■■■■ ■■■■■■■■ ■■■ ■■■■ ■■■■■■■ ■■■■■■■ ■■■■■ ■■■
331 "strength": 7,
332 "direction": "BIDIRECTIONAL",
333 "triggers": ["■■■■ ■■■■ ■■■■■ ■■■■■", "■■■■■■■■ ■■■■■■ ■■■■■■■"]
334 }}
335 ■■■ ■■■ ■■■ 3-7 ■■■■■■■ ■■■■■ ■■■■ ■■■ ■■■■■ ■■■■■ ■■■■ ■■ ■■■■ ■■■■■.
336 """
337 # ■■■■■ ■■■■■■■■■ ■■ ■■■■■■■ ■■■■■
338 rel_inference_models = [
339 {"provider": "openrouter", "model_id": OR_PHI_4_REASONING_PLUS, "priority": 1},
340 {"provider": "together", "model_id": TGR_LLAMA_3_3_70B_TURBO, "priority": 2},
341 {"provider": "google", "model_id": GEM_MODEL_PRO_EXP, "priority": 3},
342 {"provider": "openrouter", "model_id": OR_NVIDIA_LLAMA_3_3_NEMOTRON_SUPER, "priority": 4}
343 ]
344
345 # ■■■■■■■ ■■■■■■ ■■■■■■■■■ ■■ ■■■■ ■■■■■■■ ■■■■
346 ai_results = await generate_conceptual_element_multi_model(
347 [Link](context_for_ai, ensure_ascii=False),
348 full_script_text, prompt, "AI Relationship Inference Engine",
349 preferred_models=rel_inference_models
350 )
351
352 # ■■■■■■ ■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■ ■■■■■■■
353 inferred_rels_list, winning_model = self.ai_selector.select_result(ai_results, "inferred_rela
354
355 if not inferred_rels_list or not isinstance(inferred_rels_list, list):
356 [Link]("AI relationship inference did not yield valid results. Falling back to S1
357 return await self.infer_relationships_from_s1_summary(s1_data)
358
359 # ■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■
360 return self._process_inferred_relationships(inferred_rels_list, winning_model)
361
362 def _process_inferred_relationships(self, inferred_relationships: List[Dict[str, Any]], model_nam
363 """
364 ■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■
365
366 Args:
367 inferred_relationships: ■■■■■ ■■■■■■■■ ■■■■■■■■
368 model_name: ■■■ ■■■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■■
369
370 Returns:
371 ■■■ ■■■■■■■■ ■■■■■■■
372 """
373 char_name_to_id_map = {[Link]: [Link] for char in [Link].get_all_characters()}
374 char_id_to_name_map = {[Link]: [Link] for char in [Link].get_all_characters()}
375 added_count = 0
376
377 for rel_data in inferred_relationships:
378 if not isinstance(rel_data, dict):
379 continue
380
381 # ■■■■■■■ ■■■■■■ ■■■■■■■■
382 c1_ref = rel_data.get("character1_name_or_id")
383 c2_ref = rel_data.get("character2_name_or_id")
384
385 c1_id = char_name_to_id_map.get(c1_ref) or (c1_ref if c1_ref in char_id_to_name_map else
386 c2_id = char_name_to_id_map.get(c2_ref) or (c2_ref if c2_ref in char_id_to_name_map else
387
388 if not (c1_id and c2_id and c1_id != c2_id):
389 [Link](f"Skipping invalid/incomplete character refs: {c1_ref}, {c2_ref}")
390 continue
391
392 # ■■■■■■ ■■■ ■■■■■■ ■■■■■■■
393 try:
394 rel_type_str = rel_data.get("relationship_type", "OTHER").upper()
395 rel_nature_str = rel_data.get("relationship_nature", "NEUTRAL").upper()
396 rel_direction_str = rel_data.get("direction", "BIDIRECTIONAL").upper()
397
398 rel_type = RelationshipType[rel_type_str] if rel_type_str in RelationshipType.__membe
399 rel_nature = RelationshipNature[rel_nature_str] if rel_nature_str in RelationshipNatu
400 rel_direction = RelationshipDirection[rel_direction_str] if rel_direction_str in Rela
401 except KeyError:
402 [Link](f"Invalid type/nature/direction from AI: {rel_data.get('relationship_t
403 rel_type = [Link]
404 rel_nature = [Link]
405 rel_direction = [Link]
406
407 # ■■■■■ ■■■■■■■
408 if not [Link].get_relationship_between_characters(c1_id, c2_id):
409 # ■■■■■ ■■■■■■■ ■■ ■■■■■■ ■■■■■■
410 new_relationship = Relationship(
411 source=c1_id,
412 target=c2_id,
413 type=rel_type,
414 nature=rel_nature,
415 direction=rel_direction,
416 strength=int(rel_data.get("strength", 5)),
417 description=rel_data.get("description_rationale", ""),
418 metadata={
419 "source": "Enhanced_AI_Inference_Engine",
420 "model_used": model_name,
421 "inference_timestamp": [Link]().isoformat(),
422 "triggers": rel_data.get("triggers", []),
423 "confidence_score": 0.8, # ■■■■ ■■■■■■■■■ ■■■■ ■■■■■■ ■■■■■■ ■■■■ ■
424 }
425 )
426
427 [Link].add_relationship(new_relationship)
428 added_count += 1
429 self.relationship_counter += 1
430 [Link](f"Added relationship: {char_id_to_name_map.get(c1_id)} - {char_id_to_name
431
432 return added_count
433
434 async def infer_relationships_from_s1_summary(self, s1_data: Dict[str, Any]) -> int:
435 """
436 ■■■■■■■ ■■■■■■■■ ■■ ■■■■ ■■■■■■ ■■■■■■ ■■■■■ ■■■■■■■
437
438 Args:
439 s1_data: ■■■■■■ ■■■■■■ ■■■■■■
440
441 Returns:
442 ■■■ ■■■■■■■■ ■■■■ ■■ ■■■■■■■■■
443 """
444 [Link]("Using S1 summary for relationship inference fallback.")
445 s1_rels_analysis = s1_data.get("analysis_outputs", {}).get("relationship_analysis", {})
446 s1_key_rels = s1_rels_analysis.get("key_relationships", [])
447 char_name_to_id = {[Link]: [Link] for char in [Link].get_all_characters()}
448 added_count = 0
449
450 for rel_info in s1_key_rels:
451 char_names = rel_info.get("characters", [])
452 if len(char_names) == 2:
453 s_name, t_name = char_names[0], char_names[1]
454 s_id, t_id = char_name_to_id.get(s_name), char_name_to_id.get(t_name)
455
456 if s_id and t_id and s_id != t_id:
457 desc = rel_info.get("dynamic", "").lower()
458
459 # ■■■■■ ■■■■■ ■■■■■■■ ■■■■ ■■■■ ■■■
460 nature = [Link]
461 if any(kw in desc for kw in ["■■", "■■■■■", "■■■■■", "■■■", "■■■■■■", "■■■"
462 nature = [Link]
463 elif any(kw in desc for kw in ["■■■■", "■■■■■■", "■■■■", "■■■■", "■■■■■", "
464 nature = [Link]
465 elif any(kw in desc for kw in ["■■■■", "■■■■", "■■■■", "■■■■"]):
466 nature = [Link]
467
468 # ■■■■■ ■■■ ■■■■■■■ ■■■■ ■■■■ ■■■
469 rel_type = [Link]
470 if "■■" in desc or "■■■■" in desc:
471 rel_type = [Link]
472 elif "■■■■■" in desc or "■■■■" in desc:
473 rel_type = [Link]
474 elif "■■■■" in desc or "■■■■" in desc:
475 rel_type = [Link]
476 elif "■■■■" in desc or "■■■■" in desc:
477 rel_type = [Link]
478 elif "■■■■" in desc or "■■■■" in desc:
479 rel_type = [Link]
480 elif "■■■■" in desc or "■■■■■" in desc or "■■■■" in desc:
481 rel_type = [Link]
482
483 # ■■■■■■■ ■■■■■■■ ■■■■■■ ■■ ■■■■■ ■■■■■■■
484 narrative_importance = rel_info.get("narrative_importance", "")
485 strength = 5 # ■■■■ ■■■■■■■■ ■■■■■■
486
487 # ■■■■■■ ■■■■■ ■■■■ ■■■■■ ■■ ■■■ ■■■■■■■
488 if narrative_importance:
489 if any(term in narrative_importance.lower() for term in ["■■■■■", "■■■■■",
490 strength = 9
491 elif any(term in narrative_importance.lower() for term in ["■■■", "■■■■■",
492 strength = 7
493 elif any(term in narrative_importance.lower() for term in ["■■■■■", "■■■■■
494 strength = 3
495
496 if not [Link].get_relationship_between_characters(s_id, t_id):
497 new_relationship = Relationship(
498 source=s_id,
499 target=t_id,
500 type=rel_type,
501 nature=nature,
502 strength=strength,
503 description=rel_info.get("dynamic", ""),
504 metadata={
505 "source": "S1_Summary_Enhanced_Inference",
506 "narrative_importance": narrative_importance,
507 "inference_timestamp": [Link]().isoformat()
508 }
509 )
510
511 [Link].add_relationship(new_relationship)
512 added_count += 1
513 self.relationship_counter += 1
514
515 [Link](f"Inferred {added_count} relationships from S1 summary.")
516 return added_count
517
518
519 class ConflictInferenceEngine:
520 """
521 ■■■■ ■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■■■ ■■ ■■■■■■■ ■■■ ■■■■■■ ■■■■■■ ■■
522 ■■■■ ■■■ ■■■■■■■ ■■■■■■ ■■■■■■■■ ■■■■■■ ■■■ ■■■■■■■.
523 """
524
525 def __init__(self, network: ConflictNetwork, ai_selector: AIResultSelector = None):
526 """
527 ■■■■■ ■■■■ ■■■■■■■ ■■■■■■■■
528
529 Args:
530 network: ■■■■ ■■■■■■ ■■■■ ■■■■ ■■■■■ ■■■■■■■■ ■■■■■
531 ai_selector: ■■■■ ■■■■■ ■■■■■■ ■■■■■■■■■
532 """
533 [Link] = network
534 self.ai_selector = ai_selector or AIResultSelector()
535 self.conflicts_counter = 0
536
537 async def infer_conflicts_from_ai(self, s2_conceptual: Dict[str, Any], full_script_text: Optional
538 """
539 ■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■■■
540
541 Args:
542 s2_conceptual: ■■■■■■■■ ■■■■■■■■■■ ■■ ■■■■■■ ■■■■■■■
543 full_script_text: ■■■■ ■■■■■■ ■■■■■■■■■ (■■■■■■■)
544
545 Returns:
546 ■■■ ■■■■■■■■ ■■■■ ■■ ■■■■■■■■■
547 """
548 [Link]("Starting AI-driven conflict inference...")
549 if not [Link].get_all_characters():
550 [Link]("No characters in network for AI conflict inference.")
551 return 0
552
553 # ■■■■■ ■■■■ ■■■■■■ ■■■■■■■■■
554 char_list_for_ai = [
555 {
556 "id": [Link],
557 "name": [Link],
558 "description": [Link],
559 "profile_excerpt": ", ".join(f"{k}: {v}" for k, v in [Link]() if v != "N/
560 } for char in [Link].get_all_characters()
561 ]
562
563 # ■■■■■■■ ■■■■■■■ ■■■■■■■■ ■■■■■■■ ■■■■■■ ■■■■ ■■■■■
564 relationship_summary = []
565 for rel in [Link].get_all_relationships():
566 source_char = [Link].get_character([Link])
567 target_char = [Link].get_character([Link])
568 if source_char and target_char:
569 relationship_summary.append({
570 "character1": source_char.name,
571 "character2": target_char.name,
572 "type": [Link],
573 "nature": [Link],
574 "description": [Link][:100] + "..." if len([Link]) > 100 else r
575 })
576
577 context_for_ai = {
578 "■■■■■■": "■■■■■■■ ■■ ■■■■■ ■■■■■■■■ ■■■■■■■■ ■■ ■■■■■ ■■■■■ ■■■ ■■■■■■ ■■
579 "■■■■■■■■_■■■■■■■_■■_■■■■■■■": char_list_for_ai,
580 "■■■■■■■■_■■■■■■■": relationship_summary[:5], # ■■■ 5 ■■■■■■ ■■■■■■
581 "■■■■_■■■■■_■■_■■■■■■2": s2_conceptual.get("story_statement", "■■■ ■■■■■"),
582 "■■■■_■■■■■_■■■■■■■_■■■■■■■■_■■_■■■■■■2": s2_conceptual.get("three_d_map", {}).ge
583 "■■■■■■■_■■■■■■■_■■■■■■_■■■■■■": [[Link] for cs in ConflictSubject],
584 "■■■■■■■_■■■■■■■_■■■■■_■■■■■■": [[Link] for csc in ConflictScope],
585 "■■■■■■■_■■■■■■■_■■■■■■": [[Link] for cp in ConflictPhase]
586 }
587
588 # ■■■■■ ■■■■■■ ■■■■ ■■■■■■■
589 prompt = f"""
590 ■■■■■■■■ ■■■ ■■■■■■ ■■■■■■ (■■■■■ ■■■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■ ■■■■■■ ■■■■ ■■■
591 ■■■ ■■■■ ■■■■■ (■■■ ■■■■ 3-5 ■■■■■■):
592 1. ■■■■■ ■■■■■ ■■■■■■ ■■■■■■ (■■■■ ■■ ■■■■■ ■■■■■■).
593 2. ■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■■■ (■■■■ ■■■■■■■ ■■ IDs).
594 3. ■■■■■ ■■■■■ ■■■■■■ (■■ ■■■■■ `■■■■■■■_■■■■■■■_■■■■■■_■■■■■■`).
595 4. ■■■■■ ■■■■ ■■■■■■ (■■ ■■■■■ `■■■■■■■_■■■■■■■_■■■■■_■■■■■■`).
596 5. ■■■■■ ■■■■■■■ ■■■■■■■ ■■■■■■ (■■ ■■■■■ `■■■■■■■_■■■■■■■_■■■■■■`■ ■■■■ `LATENT` ■■
597 6. ■■■ ■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■ ■■ ■■■■■ ■■■■■■■■■ ■■ ■■■■■■.
598 7. ■■■■ ■■■/■■■■■ ■■■■■■ (1-10■ ■■■ 10 ■■ ■■■■■■).
599 8. ■■■ ■■■■■■■■ ■■■ ■■■■■ ■■■■■■■ ■■ ■■■■.
600 9. ■■■■■ ■■■■ ■■■■■■ ■■ ■■■■ ■■■■■■ ■■ ■■■■ ■■■ ■■■■■■.
601
602 ■■■ ■■■■■■■ **■■■■■■** ■■■■■■ JSON ■■■■■ ■■■ ■■■■■ `"inferred_conflicts"`. ■■■■ ■■■ ■■■■■
603 `"conflict_name"`, `"involved_character_names_or_ids"` (list of strings), `"subject"` (string), `"sco
604
605 ■■■■ ■■■■■■ ■■■■ ■■■■:
606 {{
607 "conflict_name": "■■■■ ■■■ ■■■■■",
608 "involved_character_names_or_ids": ["■■■", "■■■■■■", "■■■■"],
609 "subject": "MATERIAL",
610 "scope": "GROUP",
611 "initial_phase": "EMERGING",
612 "description_rationale": "■■■ ■■■■ ■■■■■ ■■■■ ■■■■ ■■■ ■■■ ■■■■■ ■■■ ■■■■■ ■■■■■■■■ ■■
613 "strength": 8,
614 "related_relationships": ["■■■■■■■ ■■■■■■■■ ■■■ ■■■ ■■■■■", "■■■■■ ■■■■■■ ■■■■■■ ■■■ ■■
615 "pivot_points": ["■■■■■■ ■■■■ ■■■■ ■■■■", "■■■■■■ ■■■■■ ■■ ■■■■■■ ■■■■■■■", "■■■■■■ ■
616 }}
617 """
618 # ■■■■■ ■■■■ ■■■■■■■ ■■ ■■■■■■■ ■■■■■
619 conflict_inference_models = [
620 {"provider": "openrouter", "model_id": OR_NVIDIA_LLAMA_3_3_NEMOTRON_SUPER, "priority": 1}
621 {"provider": "google", "model_id": GEM_MODEL_PRO_EXP, "priority": 2},
622 {"provider": "together", "model_id": TGR_LLAMA_3_3_70B_TURBO, "priority": 3},
623 {"provider": "openrouter", "model_id": OR_PHI_4_REASONING_PLUS, "priority": 4}
624 ]
625
626 # ■■■■■■■ ■■■■■■ ■■■■■■■■■ ■■ ■■■■ ■■■■■■■ ■■■■
627 ai_results = await generate_conceptual_element_multi_model(
628 [Link](context_for_ai, ensure_ascii=False),
629 full_script_text, prompt, "AI Conflict Inference Engine",
630 preferred_models=conflict_inference_models
631 )
632
633 # ■■■■■■ ■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■ ■■■■■■■
634 inferred_conflicts_list, winning_model = self.ai_selector.select_result(ai_results, "inferred
635
636 if not inferred_conflicts_list or not isinstance(inferred_conflicts_list, list):
637 [Link]("AI conflict inference did not yield valid results. Using basic S2 inferen
638 return self.infer_conflicts_from_s2_summary(s2_conceptual)
639
640 # ■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■
641 return self._process_inferred_conflicts(inferred_conflicts_list, winning_model)
642
643 def _process_inferred_conflicts(self, inferred_conflicts: List[Dict[str, Any]], model_name: str)
644 """
645 ■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■
646
647 Args:
648 inferred_conflicts: ■■■■■ ■■■■■■■■ ■■■■■■■■
649 model_name: ■■■ ■■■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■■
650
651 Returns:
652 ■■■ ■■■■■■■■ ■■■■■■■
653 """
654 char_name_to_id_map = {[Link]: [Link] for char in [Link].get_all_characters()}
655 char_id_set = set(char_name_to_id_map.values())
656 added_count = 0
657
658 for conflict_data in inferred_conflicts:
659 if not isinstance(conflict_data, dict):
660 continue
661
662 # ■■■■■■■ ■■■■■■■■ ■■■■■■■■ ■■ ■■■■■■
663 char_refs = conflict_data.get("involved_character_names_or_ids", [])
664 involved_ids = []
665
666 for ref in char_refs:
667 char_id = char_name_to_id_map.get(ref) or (ref if ref in char_id_set else None)
668 if char_id:
669 involved_ids.append(char_id)
670
671 if not involved_ids:
672 [Link](f"Skipping conflict '{conflict_data.get('conflict_name')}' due to no v
673 continue
674
675 # ■■■■■■ ■■■■■ ■■■■■ ■■■■■■ ■■■■■■
676 try:
677 subject_str = conflict_data.get("subject", "OTHER").upper()
678 scope_str = conflict_data.get("scope", "PERSONAL").upper()
679 phase_str = conflict_data.get("initial_phase", "EMERGING").upper()
680
681 subject = ConflictSubject[subject_str] if subject_str in ConflictSubject.__members__
682 scope = ConflictScope[scope_str] if scope_str in ConflictScope.__members__ else Confl
683 phase = ConflictPhase[phase_str] if phase_str in ConflictPhase.__members__ else Confl
684 except KeyError:
685 [Link](f"Invalid subject/scope/phase from AI for '{conflict_data.get('conflic
686 subject, scope, phase = [Link], [Link], ConflictPhase.
687
688 # ■■■■■ ■■■■■■ ■■■ ■■■■■■ ■■ ■■■■■■ ■■■■■■
689 new_conflict = Conflict(
690 name=conflict_data.get("conflict_name", "Unnamed AI Conflict"),
691 involved_characters=list(set(involved_ids)), # ■■■■ ■■■ ■■■■■ ■■■■■■■■
692 subject=subject,
693 scope=scope,
694 phase=phase,
695 strength=int(conflict_data.get("strength", 5)),
696 description=conflict_data.get("description_rationale", ""),
697 metadata={
698 "source": "Enhanced_AI_Inference_Engine",
699 "model_used": model_name,
700 "inference_timestamp": [Link]().isoformat(),
701 "pivot_points": conflict_data.get("pivot_points", []),
702 "confidence_score": 0.75, # ■■■■ ■■■■■■■■■ ■■■■ ■■■■■■ ■■■■■■ ■■■■ ■■■■
703 "potential_resolutions": [] # ■■■■ ■■■■■ ■■ ■■■■■ ■■■■■ ■■ ■■■■■■ ■■■■■
704 }
705 )
706
707 # ■■■■■ ■■■■■■■■ ■■■ ■■■■■ ■■ ■■■■
708 related_relationships = conflict_data.get("related_relationships", [])
709 if related_relationships:
710 # ■■■■■■ ■■■■■■ ■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■ ■■ ■■■■■■
711 related_rel_ids = []
712 for rel_desc in related_relationships:
713 # ■■■■ ■■■■■ ■■■■■■■■ ■■■■■■ ■■■■ ■■■
714 # ■■■■■■■■ ■■■■■■ ■■■■■ ■■■■■
715 for rel in [Link].get_all_relationships():
716 if rel_desc.lower() in [Link]():
717 related_rel_ids.append([Link])
718 break
719
720 new_conflict.related_relationships = related_rel_ids
721
722 [Link].add_conflict(new_conflict)
723 added_count += 1
724 self.conflicts_counter += 1
725 [Link](f"Added conflict: {new_conflict.name}")
726
727 return added_count
728
729 def infer_conflicts_from_s2_summary(self, s2_conceptual: Dict[str, Any]) -> int:
730 """
731 ■■■■■■■ ■■■■■■■■ ■■ ■■■■ ■■■■■■ ■■■■■■■ ■■■■■ ■■■■■■■
732
733 Args:
734 s2_conceptual: ■■■■■■■■ ■■■■■■■■■■ ■■ ■■■■■■ ■■■■■■■
735
736 Returns:
737 ■■■ ■■■■■■■■ ■■■■ ■■ ■■■■■■■■■
738 """
739 [Link]("Using S2 summary for conflict inference fallback.")
740 all_char_ids = [[Link] for char in [Link].get_all_characters()]
741 if not all_char_ids:
742 return 0
743
744 added_count = 0
745
746 # ■■■■■■■ ■■ ■■■■ ■■■■■
747 ss = s2_conceptual.get("story_statement", "")
748 if ss:
749 # ■■■■■■ ■■■ ■■■■■■ ■■■■■■ ■■■■■■■■ ■■ ■■■■
750 num_chars = min(len(all_char_ids), 3)
751 involved = all_char_ids[:num_chars]
752
753 if involved:
754 # ■■■■■ ■■■■ ■■■■■■ ■■■■■ ■■■ ■■■ ■■■■■■■■ ■■■■■■■■
755 scope = [Link] if len(involved) == 1 else (
756 [Link] if len(involved) == 2 else [Link]
757 )
758
759 # ■■■■■■ ■■■■■ ■■■■■ ■■■■■■ ■■ ■■■■
760 subject = [Link] # ■■■■■■■
761 if any(kw in [Link]() for kw in ["■■■", "■■■■", "■■■", "■■■", "■■■■■■■"]):
762 subject = [Link]
763 elif any(kw in [Link]() for kw in ["■■■■", "■■■■", "■■■■■", "■■■"]):
764 subject = [Link]
765 elif any(kw in [Link]() for kw in ["■■■■", "■■■■■", "■■■", "■■■■"]):
766 subject = [Link]
767
768 # ■■■■■ ■■■■■■ ■■■■■■
769 central_conflict = Conflict(
770 name=f"■■■■■■ ■■■■■■■: {ss[:30]}..." if len(ss) > 30 else ss,
771 description=f"■■■■■■■■ ■■■ ■■■■ ■■■■■: {ss}",
772 involved_characters=involved,
773 subject=subject,
774 scope=scope,
775 strength=8, # ■■■■■■■■■■ ■■■■■■ ■■■■■■■ ■■■
776 metadata={
777 "source": "S2_StoryStatement_Enhanced_Inference",
778 "inference_timestamp": [Link]().isoformat(),
779 "is_central_conflict": True
780 }
781 )
782
783 [Link].add_conflict(central_conflict)
784 added_count += 1
785 self.conflicts_counter += 1
786 [Link](f"Added central conflict from story statement: {central_conflict.name}")
787
788 # ■■■■■■■ ■■ ■■■■■ ■■■■■■■ ■■■■■■ ■■■■■■■
789 three_d_map = s2_conceptual.get("three_d_map", {})
790 events = three_d_map.get("■■■■_■■■■■■■_■■■■■■", [])
791
792 # ■■■■■■ ■■■ ■■■ ■■■■■■■■ ■■■■■■■■■■ ■■ ■■■■■■■ ■■■■■■■■
793 max_conflicts_s2_fb = min(
794 len(events),
795 APP_CONFIG_S3B.get("station_settings", {}).get("station3_network_builder", {}).get("max_c
796 )
797
798 for i, event_data in enumerate(events[:max_conflicts_s2_fb]):
799 if isinstance(event_data, dict) and "event" in event_data:
800 event_text = event_data["event"]
801
802 # ■■■■■■■ ■■■■■ ■■■■■■■■ ■■■■■■■■
803 # ■■■■■■ ■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■■■ ■■■■■ ■■ ■■■■■ ■■■ ■■■■■
804 char_index_offset = (i * 2) % len(all_char_ids)
805 involved = [all_char_ids[(char_index_offset + j) % len(all_char_ids)] for j in range(
806
807 if involved:
808 # ■■■■■ ■■■■ ■■■■■■ ■■■■■■ ■■■■■ ■■■ ■■■■
809 scope = [Link] if len(involved) > 2 else (
810 [Link] if len(involved) == 2 else [Link]
811 )
812
813 subject = [Link] # ■■■■■■■ ■■■■■■ ■■■■■
814 if any(kw in event_text.lower() for kw in ["■■■", "■■■■■", "■■■■■", "■■■■", "
815 subject = [Link]
816 elif any(kw in event_text.lower() for kw in ["■■■■", "■■■■", "■■■■■", "■■■■"]
817 subject = [Link]
818 elif any(kw in event_text.lower() for kw in ["■■■■", "■■■■", "■■■■■", "■■■■",
819 subject = [Link]
820
821 # ■■■■■ ■■■ ■■■■■■ ■■■■■ ■■■ ■■■■ ■■■■■ ■■ ■■■■■■ ■■■■■■
822 # ■■■■■■■ ■■■■■■■■ ■■■■■ ■■■■■■■■ ■■■■■■■ ■■■■
823 position_factor = 1 - abs((i - max_conflicts_s2_fb/2) / (max_conflicts_s2_fb/2))
824 strength = int(5 + position_factor * 4) # ■■■■ ■■■ 5 ■9
825
826 # ■■■■■ ■■■■■■■ ■■■■■ ■■■ ■■■■■■
827 phase = [Link] if i == 0 else (
828 [Link] if i < max_conflicts_s2_fb//3 else (
829 [Link] if i < 2*max_conflicts_s2_fb//3 else ConflictPha
830 )
831 )
832
833 # ■■■■■ ■■■■■■ ■■■■■■
834 event_conflict = Conflict(
835 name=f"■■■■ ■■■■■: {event_text[:30]}..." if len(event_text) > 30 else event
836 description=f"■■ ■■■■■ ■■■■■■■ ■■■■■■ ■■■■■■■: {event_text}",
837 involved_characters=involved,
838 subject=subject,
839 scope=scope,
840 phase=phase,
841 strength=strength,
842 metadata={
843 "source": "S2_3DMap_Enhanced_Inference",
844 "event_index": i,
845 "inference_timestamp": [Link]().isoformat(),
846 "symbolic_layer": event_data.get("symbolic_layer", "")
847 }
848 )
849
850 [Link].add_conflict(event_conflict)
851 added_count += 1
852 self.conflicts_counter += 1
853 [Link](f"Added event conflict: {event_conflict.name}")
854
855 [Link](f"Inferred {added_count} conflicts from S2 summary.")
856 return added_count
857
858
859 class NetworkBuilder:
860 """
861 ■■■ ■■■■■■ ■■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■ ■■■■■■■■ ■■ ■■■■■■■■ ■■■■■
862 ■■■ ■■■■ ■■■■ ■■■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■■.
863 """
864
865 def __init__(self):
866 """■■■■■ ■■■■ ■■■■■■"""
867 self.ai_selector = AIResultSelector()
868 self.data_extractor = DataExtractor()
869
870 async def build_network(self, s1_path: str, s2_path: str, script_text_path: Optional[str] = None)
871 """
872 ■■■■ ■■■■ ■■■■■■ ■■■■■■■ ■■ ■■■■■■ ■■■■■■■ ■■■■■■■
873
874 Args:
875 s1_path: ■■■■ ■■■ ■■■■■■ ■■■■■■ ■■■■■■
876 s2_path: ■■■■ ■■■ ■■■■■■ ■■■■■■ ■■■■■■■
877 script_text_path: ■■■■ ■■■ ■■■■ ■■■■■■ ■■■■■■■■■ (■■■■■■■)
878
879 Returns:
880 ■■■■■ ■■■■■ ■■■ ■■■■■ ■■■■■ ■■■■■■ ■■■ ■■ ■■■ ■■■■ ■■■■■■
881 """
882 [Link]("=== Starting Enhanced Network Builder ===")
883
884 # ■■■■■■ ■■ ■■■■ ■■■■■■■ ■■■■■■■■
885 full_script_text_content = None
886 if script_text_path and [Link](script_text_path):
887 from utils.file_utils import read_text_file
888 full_script_text_content = read_text_file(script_text_path)
889 if full_script_text_content:
890 [Link](f"Loaded full script text ({len(full_script_text_content)} chars) for con
891 else:
892 [Link](f"Script text file read was empty: {script_text_path}")
893
894 if not ([Link](s1_path) and [Link](s2_path)):
895 err_msg = f"Missing S1 ({s1_path} exist: {[Link](s1_path)}) or S2 ({s2_path} exis
896 [Link](err_msg)
897 return {"error": err_msg, "network_object": ConflictNetwork(f"ErrorStateNet_{PROJECT_NAME
898
899 # ■■■■■ ■■■■■■■■ ■■ ■■■■■■■ ■■■■■■■
900 try:
901 with open(s1_path, 'rb') as f: s1_data = [Link](f)
902 with open(s2_path, 'rb') as f: s2_data = [Link](f)
903 except Exception as e:
904 [Link](f"Error loading S1/S2 data: {e}", exc_info=True)
905 return {"error": f"Failed to load S1/S2 data: {e}", "network_object": ConflictNetwork(f"L
906
907 # ■■■■■ ■■■■■■ ■■■■■■
908 network = ConflictNetwork(f"Enhanced_Network_{PROJECT_NAME}")
909 [Link]({
910 "builder_station": "Enhanced Network Builder",
911 "s1_source": [Link](s1_path),
912 "s2_source": [Link](s2_path),
913 "build_timestamp": [Link]().isoformat(),
914 "script_basename": [Link](script_text_path) if script_text_path else "Unknown"
915 })
916
917 # ■■■■■■■ ■■■■■■ ■■■■■■■■
918 s1_chars = self.data_extractor.extract_s1_character_data(s1_data)
919 for char in s1_chars:
920 network.add_character(char)
921
922 # ■■■■■■■ ■■■■■■ ■■■■■■■■ ■■■■■■■■■■
923 s2_concepts = self.data_extractor.extract_s2_conceptual_data(s2_data)
924 [Link]["conceptual_summary"] = s2_concepts
925
926 # ■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■■■
927 rel_engine = RelationshipInferenceEngine(network, self.ai_selector)
928 conflict_engine = ConflictInferenceEngine(network, self.ai_selector)
929
930 try:
931 # ■■■■■■■ ■■■■■■■■ ■■■■■■■■■ ■■■■ ■■■■■■ ■■■■■■ ■■■■■■
932 rel_task = rel_engine.infer_relationships_from_ai(s1_data, s2_concepts, full_script_text_
933 conflict_task = conflict_engine.infer_conflicts_from_ai(s2_concepts, full_script_text_con
934
935 rel_count, conflict_count = await [Link](rel_task, conflict_task)
936
937 # ■■■■■ ■■■■ (snapshot) ■■■■■ ■■■ ■■■■■■■■■
938 network.create_snapshot(description="Initial network state after enhanced inference")
939
940 # ■■■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■■ ■■■■■■■
941 [Link]({
942 "inferred_relationships_count": rel_count,
943 "inferred_conflicts_count": conflict_count,
944 "timestamp_completed": [Link]().isoformat(),
945 "network_density": len(network.get_all_relationships()) / (len(network.get_all_charac
946 })
947
948 except Exception as e:
949 [Link](f"Error during inference process: {e}", exc_info=True)
950 [Link]["error_during_inference"] = str(e)
951
952 [Link](f"Enhanced Network Builder finished. Network: {len(network.get_all_characters())}
953
954 return {
955 "message": "Enhanced Conflict Network construction complete.",
956 "network_summary": {
957 "name": [Link],
958 "characters": len(network.get_all_characters()),
959 "relationships": len(network.get_all_relationships()),
960 "conflicts": len(network.get_all_conflicts()),
961 "snapshots": len([Link])
962 },
963 "network_object": network
964 }
965
966
967 @[Link]
968 async def run_s3_network_builder_main_async(s1_path: str, s2_path: str, script_text_path: Optional[st
969 """
970 ■■■■■■ ■■■■■■■■ ■■■■■ ■■■■ ■■■■■■ ■■■■■■■■ ■■■■ ■■■■■■■ ■■■■■■■ ■■■■■■■
971
972 Args:
973 s1_path: ■■■■ ■■■ ■■■■■■ ■■■■■■ ■■■■■■
974 s2_path: ■■■■ ■■■ ■■■■■■ ■■■■■■ ■■■■■■■
975 script_text_path: ■■■■ ■■■ ■■■■ ■■■■■■ ■■■■■■■■■ (■■■■■■■)
976
977 Returns:
978 ■■■■■ ■■■■■ ■■■ ■■■■■ ■■■■■ ■■■■■■
979 """
980 network_builder = NetworkBuilder()
981 return await network_builder.build_network(s1_path, s2_path, script_text_path)
982
983
984 async def main_entry_s3_builder():
985 """■■■■ ■■■■■■ ■■■■■■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■"""
986 [Link]("--- Enhanced Network Builder: Main Entry Point ---")
987
988 try:
989 async with PerfTracker(label="Enhanced_NetworkBuilder_Run") as perf:
990 results = await run_s3_network_builder_main_async(
991 IO_PATHS["station1_input_pkl"],
992 IO_PATHS["station2_input_pkl"],
993 ORIGINAL_PROSE_PATH
994 )
995 except Exception as e:
996 [Link](f"Unexpected error in main execution: {e}", exc_info=True)
997 results = {
998 "error": f"Unhandled exception: {str(e)}",
999 "network_object": ConflictNetwork(f"ErrorStateNet_{PROJECT_NAME}")
1000 }
1001 perf = {"stats": {"error": "Performance tracking failed due to exception"}}
1002
1003 # ■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■■■
1004 output_payload = {
1005 "metadata": {
1006 "project_name": PROJECT_NAME,
1007 "prose_file_basename": IO_PATHS["prose_basename"],
1008 "station_name": "Enhanced Conflict Network Builder",
1009 "timestamp": [Link]().isoformat(),
1010 "input_files": {
1011 "s1_output": IO_PATHS["station1_input_pkl"],
1012 "s2_output": IO_PATHS["station2_input_pkl"],
1013 "script_text": ORIGINAL_PROSE_PATH
1014 },
1015 "status": "Success" if "error" not in results else "Failed",
1016 "error_message": [Link]("error"),
1017 "version": "2.0.0"
1018 },
1019 "results": results,
1020 "performance_stats": [Link]
1021 }
1022
1023 # ■■■ ■■■■■■■
1024 try:
1025 # ■■■ ■■■■■■ pickle (■■ ■■■■ ■■■■■■ ■■■■■■)
1026 with open(IO_PATHS["station3_output_pkl"], 'wb') as f:
1027 [Link](output_payload, f)
1028 [Link](f"Network Builder output (PKL with Network) saved to: {IO_PATHS['station3_output_
1029
1030 # ■■■ ■■■■■■ JSON (■■■ ■■■■ ■■■■■■ ■■■■ ■■■ ■■■■ ■■■■■■■ ■■■■ ■■■■■)
1031 json_safe_payload = [Link](output_payload)
1032 if "results" in json_safe_payload and isinstance(json_safe_payload["results"], dict) and "net
1033 net = json_safe_payload["results"]["network_object"]
1034 # ■■■■■ ■■■■ ■■■■■■ ■■■■■■ ■■■■■ ■■ ■■■■■■ ■■■■■■
1035 json_safe_payload["results"]["network_summary_for_json"] = {
1036 "name": [Link],
1037 "characters": len(net.get_all_characters()),
1038 "character_names": [[Link] for char in net.get_all_characters()[:10]] + (["..."] i
1039 "relationships": len(net.get_all_relationships()),
1040 "conflicts": len(net.get_all_conflicts()),
1041 "conflict_names": [[Link] for conflict in net.get_all_conflicts()[:10]] + ([".
1042 "snapshots": len([Link]),
1043 "metadata": {k: v for k, v in [Link]() if not isinstance(v, (dict, list,
1044 }
1045 del json_safe_payload["results"]["network_object"]
1046
1047 with open(IO_PATHS["station3_output_json"], 'w', encoding='utf-8') as f:
1048 [Link](json_safe_payload, f, ensure_ascii=False, indent=2, default=str)
1049 [Link](f"Network Builder output (JSON summary) saved to: {IO_PATHS['station3_output_json
1050
1051 except Exception as e:
1052 [Link](f"Error saving Network Builder output: {e}", exc_info=True)
1053
1054 [Link]("--- Enhanced Network Builder: Processing Completed ---")
1055
1056
1057 if __name__ == "__main__":
1058 if not (ORIGINAL_PROSE_PATH and PROJECT_PATH):
1059 [Link]("PROSE_FILE_PATH or PROJECT_PATH env vars not set. Exiting.")
1060 [Link](1)
1061
1062 if [Link] == "win32" and sys.version_info >= (3,8):
1063 asyncio.set_event_loop_policy([Link]())
1064
1065 station1_input_path = IO_PATHS["station1_input_pkl"]
1066 station2_input_path = IO_PATHS["station2_input_pkl"]
1067
1068 if not ([Link](station1_input_path) and [Link](station2_input_path)):
1069 [Link](f"Required inputs missing. S1 ({station1_input_path}) or S2 ({station2_input_pat
1070 [Link](1)
1071
1072 [Link](main_entry_s3_builder())
1 # backend/stations/station_conflict_network.py
2 """
3 ■■■■ ■■■■■ ■■■■ ■■■■■■ ■■■■■■■ (Enhanced Conflict Network Analysis Station)
4
5 ■■■ ■■■■■ ■■■■ ■■■■■ ■■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■ ■■ ■■■ station3_netw
6 ■■■ ■■■■■■ ■■■■■■■ ■■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■■ ■■ ■■■■■ ■■■■■■ ■■■■■■■ ■■■■■
7 """
8
9 import sys
10 import os
11 import json
12 import pickle
13 import logging
14 import asyncio
15 import networkx as nx
16 import [Link] as plt
17 import numpy as np
18 from datetime import datetime
19 from typing import Dict, List, Any, Optional, Tuple, Union, Callable
20
21 [Link]([Link]([Link]([Link](__file__), '..')))
22
23 from core.base_entities import (
24 ConflictNetwork, Conflict, ConflictPhase, ConflictSubject, ConflictScope,
25 Character, Relationship, RelationshipType, RelationshipNature, RelationshipDirection
26 )
27 from utils.cache_utils import memory
28 from utils.perf_utils import PerfTracker
29
30 logger = [Link](__name__)
31 if not [Link]:
32 handler = [Link]()
33 formatter = [Link](
34 '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
35 )
36 [Link](formatter)
37 [Link](handler)
38 [Link]([Link])
39
40 # ■■■■■ ■■■■■■■■■ ■■■■■■■ ■■ ■■■■ ■■■■
41 PROJECT_NAME = [Link]("PROJECT_NAME", "ConflictNetworkAnalysis")
42 PROJECT_PATH = [Link]("PROJECT_PATH", ".")
43 ORIGINAL_PROSE_PATH = [Link]("PROSE_FILE_PATH", "[Link]")
44
45 # ■■■■■ ■■■■■■ ■■■■■■■/■■■■■■■
46 def _configure_io_paths():
47 """■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■■■ ■■■■ ■■■■■■■■"""
48 paths = {}
49
50 if PROJECT_PATH and ORIGINAL_PROSE_PATH:
51 prose_basename = [Link]([Link](ORIGINAL_PROSE_PATH))[0].replace(" ", "_")
52
53 # ■■■■■■ ■■■■■■■ ■■■■■■ ■■■■■■
54 output_dir = [Link](PROJECT_PATH, "network_analysis_output")
55 visualization_dir = [Link](output_dir, "visualizations")
56
57 # ■■■■■■ ■■■■■■■ ■■ ■■■■■■ 3
58 station3_input_dir = [Link](PROJECT_PATH, "station3_output")
59 station3_input_pkl = [Link](station3_input_dir, f"{prose_basename}_station3_output.pkl"
60
61 [Link]({
62 "prose_basename": prose_basename,
63 "output_dir": output_dir,
64 "visualization_dir": visualization_dir,
65 "analysis_output_pkl": [Link](output_dir, f"{prose_basename}_network_analysis_outpu
66 "analysis_output_json": [Link](output_dir, f"{prose_basename}_network_analysis_outp
67 "station3_input_dir": station3_input_dir,
68 "station3_input_pkl": station3_input_pkl
69 })
70 else:
71 # ■■■■■ ■■■■■■■■■■ ■■■ ■■ ■■■ ■■■■■ ■■■■■■■■■ ■■■■■■■
72 [Link]({
73 "prose_basename": "unknown_prose",
74 "output_dir": "network_analysis_output",
75 "visualization_dir": "network_analysis_output/visualizations",
76 "analysis_output_pkl": "network_analysis_output.pkl",
77 "analysis_output_json": "network_analysis_output.json",
78 "station3_input_dir": "station3_output",
79 "station3_input_pkl": "station3_output.pkl"
80 })
81 [Link]("PROJECT_PATH or ORIGINAL_PROSE_PATH not fully set. Using default I/O names.")
82
83 # ■■■■■ ■■■■■■ ■■■■■■■ ■■■ ■■ ■■■ ■■■■■■
84 [Link](paths["output_dir"], exist_ok=True)
85 [Link](paths["visualization_dir"], exist_ok=True)
86
87 return paths
88
89 # ■■■■■ ■■■■■■ ■■■■■■■/■■■■■■■ ■■■ ■■■■■ ■■■■■■
90 IO_PATHS = _configure_io_paths()
91
92
93 class NetworkAnalyzer:
94 """
95 ■■■■ ■■■■■ ■■■■■■ ■■■■■■ ■■■■ ■■■■■■ ■■■■■■■■ ■■■■■■
96 ■■■■ ■■■■■■■■■■ ■■■■ ■■■■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■■.
97 """
98
99 def __init__(self, network: ConflictNetwork):
100 """
101 ■■■■■ ■■■■ ■■■■■■
102
103 Args:
104 network: ■■■■ ■■■■ ■■■■■■ ■■■■■■■
105 """
106 [Link] = network
107 self.character_metrics = {}
108 self.conflict_metrics = {}
109 self.relationship_metrics = {}
110 self.global_metrics = {}
111
112 def analyze_network(self) -> Dict[str, Any]:
113 """
114 ■■■■■ ■■■■■ ■■■■ ■■■■■■■ ■■■ ■■ ■■■ ■■■■■■ ■■■■■■■■ ■■■■■■■■■ ■■■■■■■■■
115
116 Returns:
117 ■■■■■ ■■■■■ ■■■ ■■■■■ ■■■■■■■ ■■■■■■
118 """
119 [Link]("Starting comprehensive network analysis...")
120
121 self._analyze_characters()
122 self._analyze_relationships()
123 self._analyze_conflicts()
124 self._calculate_global_metrics()
125
126 # ■■■■■ ■■■■■■■
127 analysis_results = {
128 "network_name": [Link],
129 "analysis_timestamp": [Link]().isoformat(),
130 "global_metrics": self.global_metrics,
131 "character_metrics": self.character_metrics,
132 "relationship_metrics": self.relationship_metrics,
133 "conflict_metrics": self.conflict_metrics,
134 "structural_patterns": self._identify_structural_patterns(),
135 "narrative_insights": self._generate_narrative_insights(),
136 "improvement_suggestions": self._generate_improvement_suggestions()
137 }
138
139 [Link]("Comprehensive network analysis completed.")
140 return analysis_results
141
142 def _analyze_characters(self):
143 """■■■■■ ■■■■■■ ■■■■■■■■ ■■ ■■■■■■"""
144 [Link]("Analyzing character metrics...")
145
146 # ■■■■■ ■■■■■■ ■■■ ■■■■ NetworkX ■■■■■■■ ■■ ■■■■■■
147 G = [Link]()
148
149 # ■■■■■ ■■■■■■■■ ■■■■
150 for char in [Link].get_all_characters():
151 G.add_node([Link], name=[Link], type='character')
152
153 # ■■■■■ ■■■■■■■■ ■■■■■
154 for rel in [Link].get_all_relationships():
155 if G.has_node([Link]) and G.has_node([Link]):
156 G.add_edge([Link], [Link], weight=[Link])
157
158 # ■■■■ ■■■■■■ ■■■■■■■ ■■■■■■■■ ■■■ ■■■■■
159 centrality_degree = nx.degree_centrality(G) if [Link]() else {}
160 centrality_betweenness = nx.betweenness_centrality(G) if [Link]() else {}
161 centrality_closeness = nx.closeness_centrality(G) if [Link]() else {}
162
163 # ■■■■■ ■■■■■■ ■■■■■■■■ ■■ ■■■■■■■■
164 conflict_involvement = {}
165 for char_id in [Link]():
166 if [Link][char_id].get('type') == 'character':
167 # ■■■■ ■■■ ■■■■■■■■ ■■■■ ■■■■■ ■■■■ ■■■■■■■
168 involved_conflicts = [c for c in [Link].get_all_conflicts() if char_id in [Link]
169 conflict_involvement[char_id] = len(involved_conflicts)
170
171 # ■■■ ■■■■■■■■ ■■■ ■■■■■
172 for char in [Link].get_all_characters():
173 char_id = [Link]
174 self.character_metrics[char_id] = {
175 "name": [Link],
176 "centrality_degree": centrality_degree.get(char_id, 0),
177 "centrality_betweenness": centrality_betweenness.get(char_id, 0),
178 "centrality_closeness": centrality_closeness.get(char_id, 0),
179 "conflict_involvement_count": conflict_involvement.get(char_id, 0),
180 "relationship_count": len([Link].get_character_relationships(char_id)),
181 "narrative_influence_score": (
182 centrality_degree.get(char_id, 0) * 0.4 +
183 centrality_betweenness.get(char_id, 0) * 0.3 +
184 (conflict_involvement.get(char_id, 0) / max(conflict_involvement.values()) if con
185 )
186 }
187
188 # ■■■■■ ■■■■■■■■ ■■■■■■■
189 self.global_metrics["influential_characters"] = sorted(
190 self.character_metrics.items(),
191 key=lambda x: x[1]["narrative_influence_score"],
192 reverse=True
193 )[:5] # ■■■■ 5 ■■■■■■ ■■■■■■■
194
195 def _analyze_relationships(self):
196 """■■■■■ ■■■■■■ ■■■■■■■■ ■■ ■■■■■■"""
197 [Link]("Analyzing relationship metrics...")
198
199 relationships = [Link].get_all_relationships()
200
201 # ■■■■■ ■■■■■ ■■■■■ ■■■■■■■■
202 rel_types_distribution = {}
203 rel_natures_distribution = {}
204
205 for rel in relationships:
206 rel_type = [Link]
207 rel_nature = [Link]
208
209 rel_types_distribution[rel_type] = rel_types_distribution.get(rel_type, 0) + 1
210 rel_natures_distribution[rel_nature] = rel_natures_distribution.get(rel_nature, 0) + 1
211
212 # ■■■■ ■■■■■ ■■■■■■ ■■■■■ ■■■■■ ■■ ■■■■ ■■■■■■
213 network_density = 0
214 character_count = len([Link].get_all_characters())
215 if character_count > 1:
216 max_possible_relationships = character_count * (character_count - 1) / 2
217 network_density = len(relationships) / max_possible_relationships * 100
218
219 # ■■■■■■ ■■■■■ ■■■■■■■■
220 avg_strength = sum([Link] for rel in relationships) / len(relationships) if relationshi
221
222 # ■■■■■ ■■■■■■■■ ■■■■■■■
223 for rel in relationships:
224 source_char = [Link].get_character([Link])
225 target_char = [Link].get_character([Link])
226 if source_char and target_char:
227 self.relationship_metrics[[Link]] = {
228 "source_name": source_char.name,
229 "target_name": target_char.name,
230 "type": [Link],
231 "nature": [Link],
232 "strength": [Link],
233 "normalized_strength": [Link] / 10, # ■■■ ■■■■■ 0-1
234 "description_length": len([Link]),
235 "is_reciprocal": [Link] == [Link],
236 "involved_conflicts_count": len([c for c in [Link].get_all_conflicts()
237 if [Link] in c.related_relationships])
238 }
239
240 # ■■■■■■ ■■■■■■■■ ■■■■■■
241 self.global_metrics.update({
242 "relationship_count": len(relationships),
243 "relationship_types_distribution": rel_types_distribution,
244 "relationship_natures_distribution": rel_natures_distribution,
245 "network_density_percentage": network_density,
246 "average_relationship_strength": avg_strength,
247 "strongest_relationship": max(relationships, key=lambda r: [Link]).id if relationship
248 "weakest_relationship": min(relationships, key=lambda r: [Link]).id if relationships
249 })
250
251 def _analyze_conflicts(self):
252 """■■■■■ ■■■■■■ ■■■■■■■■ ■■ ■■■■■■"""
253 [Link]("Analyzing conflict metrics...")
254
255 conflicts = [Link].get_all_conflicts()
256
257 # ■■■■■ ■■■■■ ■■■■■■ ■■■■■■■■
258 conflict_subjects_distribution = {}
259 conflict_scopes_distribution = {}
260 conflict_phases_distribution = {}
261
262 for conf in conflicts:
263 subject = [Link]
264 scope = [Link]
265 phase = [Link]
266
267 conflict_subjects_distribution[subject] = conflict_subjects_distribution.get(subject, 0)
268 conflict_scopes_distribution[scope] = conflict_scopes_distribution.get(scope, 0) + 1
269 conflict_phases_distribution[phase] = conflict_phases_distribution.get(phase, 0) + 1
270
271 # ■■■■■■ ■■■■■ ■■■■■■■■
272 avg_strength = sum([Link] for conf in conflicts) / len(conflicts) if conflicts else 0
273
274 # ■■■■■■ ■■■■■■■■ ■■■■■■■■
275 avg_involved_chars = sum(len(conf.involved_characters) for conf in conflicts) / len(conflicts
276
277 # ■■■■■ ■■■■■■■■ ■■■■■■■
278 for conf in conflicts:
279 # ■■■■■■■ ■■■■■ ■■■■■■■■ ■■■■■■■■
280 involved_char_names = []
281 for char_id in conf.involved_characters:
282 char = [Link].get_character(char_id)
283 if char:
284 involved_char_names.append([Link])
285
286 self.conflict_metrics[[Link]] = {
287 "name": [Link],
288 "subject": [Link],
289 "scope": [Link],
290 "phase": [Link],
291 "strength": [Link],
292 "normalized_strength": [Link] / 10, # ■■■ ■■■■■ 0-1
293 "involved_characters_count": len(conf.involved_characters),
294 "involved_character_names": involved_char_names,
295 "related_relationships_count": len(conf.related_relationships),
296 "description_length": len([Link]),
297 "narrative_impact_score": [Link] * len(conf.involved_characters) / 10 # ■■■■
298 }
299
300 # ■■■■■■ ■■■■■■■■ ■■■■■■
301 self.global_metrics.update({
302 "conflict_count": len(conflicts),
303 "conflict_subjects_distribution": conflict_subjects_distribution,
304 "conflict_scopes_distribution": conflict_scopes_distribution,
305 "conflict_phases_distribution": conflict_phases_distribution,
306 "average_conflict_strength": avg_strength,
307 "average_involved_characters": avg_involved_chars,
308 "strongest_conflict": max(conflicts, key=lambda c: [Link]).id if conflicts else None,
309 "most_central_conflict": max(conflicts, key=lambda c: len(c.involved_characters)) if conf
310 "most_initial_phase_count": conflict_phases_distribution.get("LATENT", 0) + conflict_phas
311 })
312
313 def _calculate_global_metrics(self):
314 """■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■"""
315 [Link]("Calculating global network metrics...")
316
317 # ■■■■ ■■■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■■ ■■■■■■■■
318 positive_rels = sum(1 for rel in [Link].get_all_relationships() if [Link] == Relati
319 negative_rels = sum(1 for rel in [Link].get_all_relationships() if [Link] == Relati
320 total_rels = len([Link].get_all_relationships())
321
322 if total_rels > 0:
323 pos_neg_ratio = positive_rels / negative_rels if negative_rels > 0 else float('inf')
324 balance_score = abs(0.5 - (positive_rels / total_rels)) * 2 # 0 = ■■■■■■ ■■■■■■■ 1 =
325 else:
326 pos_neg_ratio = 0
327 balance_score = 1
328
329 # ■■■■ ■■■■ ■■■■■ ■■■■■ ■■■■■■■■
330 subject_diversity = len(self.global_metrics.get("conflict_subjects_distribution", {})) / len(
331 scope_diversity = len(self.global_metrics.get("conflict_scopes_distribution", {})) / len(Conf
332 phase_diversity = len(self.global_metrics.get("conflict_phases_distribution", {})) / len(Conf
333
334 # ■■■■■ "■■■■ ■■■■■" ■■■■■■
335 # ■■ ■■ ■■■■ ■■■■■■■ ■■■■■■ ■■■ ■■ ■■■■■■ ■■■■■■ ■■■■■■■
336 influential_chars = self.global_metrics.get("influential_characters", [])
337 central_char_ids = [item[0] for item in influential_chars[:2]] if influential_chars else []
338 central_char_names = [[Link].get_character(char_id).name for char_id in central_char_id
339
340 # ■■■■ ■■■■■ ■■■■■■ (■■■■ ■■ ■■■ ■■■■■■)
341 character_count = len([Link].get_all_characters())
342 relationship_count = len([Link].get_all_relationships())
343 conflict_count = len([Link].get_all_conflicts())
344
345 complexity_score = 0
346 if character_count > 0:
347 complexity_score = (
348 0.3 * min(relationship_count / character_count / 5, 1) + # ■■■■ ■■■■■■■■ ■■■■■■
349 0.3 * min(conflict_count / character_count / 2, 1) + # ■■■■ ■■■■■■■■ ■■■■■■
350 0.2 * (1 - balance_score) + # ■■■■■■■ ■■■ ■■■■■■■■
351 0.1 * subject_diversity + # ■■■■ ■■■■■■ ■■■■■■■■
352 0.1 * phase_diversity # ■■■■ ■■■■■ ■■■■■■■■
353 )
354
355 # ■■■ ■■■ ■■■■■■ (■■ ■■■■ ■■■■■■■■ ■■■■■■■■■)
356 plot_momentum = 0
357 if conflict_count > 0:
358 # ■■■■ ■■■■■■■■ ■■ ■■■■■ ■■■■■■ (■■■■■■■■ ■■■■)
359 advanced_conflicts = self.global_metrics.get("conflict_phases_distribution", {}).get("ESC
360 self.global_metrics.get("conflict_phases_distribution", {}).get("CLIM
361
362 # ■■■■ ■■■■■■■■ ■■■■■■■ (■■■■■ ■■ ■■■■■■)
363 plot_momentum = (
364 0.4 * (advanced_conflicts / conflict_count) +
365 0.3 * min(1, negative_rels / total_rels * 2) if total_rels > 0 else 0 + # ■■■■■■■■
366 0.3 * min(1, self.global_metrics.get("average_conflict_strength", 0) / 7) # ■■■ ■■■
367 )
368
369 # ■■■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■■■
370 self.global_metrics.update({
371 "character_count": character_count,
372 "positive_negative_ratio": pos_neg_ratio,
373 "relationship_balance_score": balance_score,
374 "narrative_center_of_gravity": {
375 "central_characters": central_char_names,
376 "primary_conflict": [Link].get_conflict(self.global_metrics.get("strongest_conf
377 if self.global_metrics.get("strongest_conflict") and [Link].get_conflict(se
378 },
379 "network_complexity_score": complexity_score,
380 "plot_momentum_score": plot_momentum,
381 "subject_diversity": subject_diversity,
382 "scope_diversity": scope_diversity,
383 "phase_diversity": phase_diversity,
384 "network_analysis_timestamp": [Link]().isoformat()
385 })
386
387 def _identify_structural_patterns(self) -> Dict[str, Any]:
388 """■■■■■ ■■■■■■■ ■■■■■■■■ ■■ ■■■■■■"""
389 [Link]("Identifying structural patterns...")
390
391 # ■■■■■ ■■■■■■ ■■■ NetworkX
392 G = [Link]()
393 for char in [Link].get_all_characters():
394 G.add_node([Link], name=[Link], type='character')
395
396 for rel in [Link].get_all_relationships():
397 if G.has_node([Link]) and G.has_node([Link]):
398 G.add_edge([Link], [Link], weight=[Link], nature=[Link])
399
400 # ■■■■■ ■■ ■■■■■■■■■ (■■■■■■■■■)
401 communities = []
402 if len([Link]()) > 3:
403 try:
404 # ■■■■■■■ ■■■■■■■■ Louvain ■■■■■ ■■ ■■■■■■■■■
405 from community import best_partition
406 partition = best_partition(G)
407 communities_dict = {}
408 for node, community_id in [Link]():
409 if community_id not in communities_dict:
410 communities_dict[community_id] = []
411 char = [Link].get_character(node)
412 if char:
413 communities_dict[community_id].append([Link])
414
415 communities = list(communities_dict.values())
416 except (ImportError, ModuleNotFoundError):
417 # ■■■ ■■ ■■■ ■■■■■ community ■■■■■■ ■■■■■■ ■■■■■ ■■■■
418 try:
419 # ■■■■■■■ ■■■■■■■■ ■■■■■■■
420 components = list(nx.connected_components(G))
421 communities = []
422 for comp in components:
423 community = []
424 for node_id in comp:
425 char = [Link].get_character(node_id)
426 if char:
427 [Link]([Link])
428 if community:
429 [Link](community)
430 except Exception as e:
431 [Link](f"Error detecting communities: {e}")
432
433 # ■■■■■ ■■■■■■■■ ■■■■■■■■/■■■■■■■
434 bridge_characters = []
435 try:
436 if len([Link]()) > 3:
437 # ■■■■ ■■■■■■ ■■■■■■■
438 betweenness = nx.betweenness_centrality(G)
439 # ■■■■■■ ■■■■■■■■ ■■■ ■■■■■■■■ ■■■■■■■
440 threshold = max([Link]()) * 0.7 if betweenness else 0
441 bridge_chars = {node_id: score for node_id, score in [Link]() if score > t
442
443 for node_id, score in bridge_chars.items():
444 char = [Link].get_character(node_id)
445 if char:
446 bridge_characters.append({
447 "name": [Link],
448 "id": [Link],
449 "betweenness_score": score
450 })
451 except Exception as e:
452 [Link](f"Error identifying bridge characters: {e}")
453
454 # ■■■■■ ■■■■■■■■■ ■■■■■■■ (■■■■■■■ ■■■■■■■)
455 balanced_triads = 0
456 unbalanced_triads = 0
457 try:
458 if len([Link]()) > 2:
459 # ■■■■■ ■■ ■■■■■■■■■
460 for triad in nx.cycle_basis(G):
461 if len(triad) == 3:
462 # ■■■■ ■■■ ■■■■■■■ ■■■■■■■ ■■ ■■■■■■■■
463 negative_edges = 0
464 for i in range(3):
465 u, v = triad[i], triad[(i+1)%3]
466 if G.has_edge(u, v) and G[u][v].get('nature') == "NEGATIVE":
467 negative_edges += 1
468
469 # ■■■■■■ ■■■■■■■ ■■■ ■■■ ■■■ ■■■■■■■ ■■■■■■■ ■■■■■■ (0 ■■ 2)
470 if negative_edges % 2 == 0:
471 balanced_triads += 1
472 else:
473 unbalanced_triads += 1
474 except Exception as e:
475 [Link](f"Error analyzing triads: {e}")
476
477 # ■■■■■ ■■■■■■■ ■■■■■■■■
478 return {
479 "communities": communities,
480 "community_count": len(communities),
481 "bridge_characters": bridge_characters,
482 "balanced_triads": balanced_triads,
483 "unbalanced_triads": unbalanced_triads,
484 "structural_balance_ratio": balanced_triads / (balanced_triads + unbalanced_triads) if (b
485 "largest_community_size": max([len(c) for c in communities]) if communities else 0
486 }
487
488 def _generate_narrative_insights(self) -> Dict[str, Any]:
489 """■■■■■ ■■■ ■■■■■ ■■ ■■■■■ ■■■■■■"""
490 [Link]("Generating narrative insights...")
491
492 # ■■■■■■■■ ■■■■■■■■ (■■■ ■■■■■■■ ■■■■■ ■■■■■■■)
493 isolated_chars = []
494 for char in [Link].get_all_characters():
495 if len([Link].get_character_relationships([Link])) == 0:
496 isolated_chars.append([Link])
497
498 # ■■■■■■ ■■■■■■■ ■■■■■■■■ (■■■■■■ ■■■■ ■■■■■■ ■■■■■)
499 blind_spots = []
500 for conflict in [Link].get_all_conflicts():
501 if not conflict.related_relationships and len(conflict.involved_characters) > 1:
502 conf_info = {
503 "conflict_name": [Link],
504 "involved_characters": [[Link].get_character(c_id).name for c_id in conflic
505 if [Link].get_character(c_id)]
506 }
507 blind_spots.append(conf_info)
508
509 # ■■■■■ ■■■■■ ■■■■■■■ (■■ ■■■■ ■■■■ ■■ ■■■■■■■■■)
510 phase_distribution = self.global_metrics.get("conflict_phases_distribution", {})
511 early_phases = phase_distribution.get("LATENT", 0) + phase_distribution.get("EMERGING", 0)
512 late_phases = phase_distribution.get("ESCALATING", 0) + phase_distribution.get("CLIMAX", 0) +
513
514 phase_progression_balanced = True
515 phase_progression_note = "■■■■■ ■■■■■■ ■■■■■■■ ■■■■■."
516
517 if early_phases > late_phases * 2:
518 phase_progression_balanced = False
519 phase_progression_note = "■■■■ ■■■■■■■■ ■■ ■■■■■ ■■■■■■ ■■ ■■■■■ ■■■ ■■■■■ ■■■
520 elif late_phases > early_phases * 2:
521 phase_progression_balanced = False
522 phase_progression_note = "■■■■ ■■■■■■■■ ■■ ■■■■■ ■■■■■■■ ■■ ■■■■■ ■■■ ■■■■ ■■
523
524 # ■■■■■■ ■■■■■■ ■■■■■■
525 narrative_diversity_score = (
526 self.global_metrics.get("subject_diversity", 0) * 0.4 +
527 self.global_metrics.get("scope_diversity", 0) * 0.3 +
528 self.global_metrics.get("phase_diversity", 0) * 0.3
529 )
530
531 # ■■■■■ ■■■■■ ■■■■■■■■ (■■ ■■■■ ■■■■ ■■■■■ ■■■■ ■■ ■■■■■■ ■■ ■■■■ ■■■■■■■■■)
532 dominant_conflict = None
533 conflict_strengths = [([Link], [Link]) for c in [Link].get_all_conflicts()]
534 if conflict_strengths:
535 conflict_strengths.sort(key=lambda x: x[1], reverse=True)
536 if len(conflict_strengths) > 1 and conflict_strengths[0][1] > conflict_strengths[1][1] *
537 dominant_conf = [Link].get_conflict(conflict_strengths[0][0])
538 if dominant_conf:
539 dominant_conflict = {
540 "name": dominant_conf.name,
541 "strength": dominant_conf.strength,
542 "note": "■■■ ■■■■■■ ■■■■■ ■■■■ ■■■ ■■■■■■ ■■■ ■■ ■■■ ■■ ■■■■■■ ■■
543 }
544
545 # ■■■■■ ■■■■■ ■■■■■■■
546 return {
547 "isolated_characters": isolated_chars,
548 "blind_spots": blind_spots,
549 "phase_progression_balanced": phase_progression_balanced,
550 "phase_progression_note": phase_progression_note,
551 "narrative_diversity_score": narrative_diversity_score,
552 "dominant_conflict": dominant_conflict,
553 "narrative_complexity_assessment": self._assess_narrative_complexity()
554 }
555
556 def _assess_narrative_complexity(self) -> Dict[str, Any]:
557 """■■■■■ ■■■■■ ■■■■■ ■■■■■■ ■■■■■■■■ ■■■ ■■■■ ■■■■■■"""
558 complexity_score = self.global_metrics.get("network_complexity_score", 0)
559
560 # ■■■■■ ■■■■■ ■■■■■■■
561 complexity_level = "■■■■■"
562 if complexity_score < 0.3:
563 complexity_level = "■■■■"
564 elif complexity_score > 0.7:
565 complexity_level = "■■■■"
566
567 # ■■■■■ ■■■■■■■ ■■■■■■■■ ■■■■■■■
568 complexity_factors = []
569
570 if self.global_metrics.get("character_count", 0) > 7:
571 complexity_factors.append("■■■ ■■■■ ■■ ■■■■■■■■")
572
573 rel_per_char = self.global_metrics.get("relationship_count", 0) / self.global_metrics.get("ch
574 if rel_per_char > 3:
575 complexity_factors.append("■■■■ ■■■■■■ ■■■■■")
576
577 conf_per_char = self.global_metrics.get("conflict_count", 0) / self.global_metrics.get("chara
578 if conf_per_char > 1.5:
579 complexity_factors.append("■■■■ ■■■■■■■■ ■■■ ■■■■■")
580
581 subject_diversity = self.global_metrics.get("subject_diversity", 0)
582 if subject_diversity > 0.7:
583 complexity_factors.append("■■■■ ■■■■ ■■ ■■■■■■ ■■■■■■")
584
585 # ■■■■■■■■ ■■■■■ ■■■ ■■■■■ ■■■■■■■
586 recommendations = []
587 if complexity_level == "■■■■" and complexity_score < 0.2:
588 [Link]("■■■■■ ■■■■■ ■■■■■■ ■■■ ■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■■")
589 [Link]("■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■■")
590 elif complexity_level == "■■■■":
591 [Link]("■■■■■ ■■■■■■■■ ■■■■■■■■■ ■■■■■■■")
592 [Link]("■■■■■ ■■■■■■■ ■■ ■■■■■■ ■■■■■■■■ ■■■■■■■■")
593 elif complexity_level == "■■■■■":
594 [Link]("■■■■■■ ■■■■■ ■■■■■■ ■■■■■■■ ■■ ■■■■ ■■■■■ ■■■■■■■■")
595 elif complexity_level == "■■■■" and complexity_score > 0.85:
596 [Link]("■■■■■ ■■■ ■■■■■ ■■■■■■ ■■■■■ ■■■■■ ■■■■■■/■■■■■■■")
597 [Link]("■■■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■■ ■■■■■■ ■■■■■")
598 elif complexity_level == "■■■■":
599 [Link]("■■■■■■ ■■ ■■■■■ ■■■■■■■ ■■■■■■■ ■■■■ ■■■■■ ■■■■■■■")
600
601 return {
602 "complexity_level": complexity_level,
603 "complexity_score": complexity_score,
604 "primary_complexity_factors": complexity_factors,
605 "recommendations": recommendations
606 }
607
608 def _generate_improvement_suggestions(self) -> List[Dict[str, Any]]:
609 """■■■■■ ■■■■■■■■ ■■■■■■ ■■■■ ■■■■■■"""
610 [Link]("Generating improvement suggestions...")
611
612 suggestions = []
613
614 # ■■■■■■ ■■ ■■■■ ■■■■■■ ■■■■■■
615 isolated_chars = self._generate_narrative_insights().get("isolated_characters", [])
616 if isolated_chars:
617 [Link]({
618 "type": "underdeveloped_characters",
619 "importance": "high",
620 "description": f"■■■■ {len(isolated_chars)} ■■■■■■ ■■■■■■ ■■■■ ■■■■■■: {', '.j
621 "suggestion": "■■■■■ ■■■■■■ ■■■■ ■■■■■■■■ ■■■■■■ ■■ ■■■■ ■■■■■ ■■■■■■■■
622 })
623
624 # ■■■■■■ ■■ ■■■■■ ■■■■■■■■ ■■■■■■■■■ ■■■■■■■■
625 relationship_balance = self.global_metrics.get("relationship_balance_score", 0)
626 if relationship_balance > 0.7:
627 rel_natures = self.global_metrics.get("relationship_natures_distribution", {})
628 if rel_natures.get("POSITIVE", 0) > rel_natures.get("NEGATIVE", 0) * 2:
629 [Link]({
630 "type": "relationship_imbalance",
631 "importance": "medium",
632 "description": "■■■ ■■■■■ ■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■■.",
633 "suggestion": "■■■■■ ■■■■ ■■ ■■■■■■ ■■■■■■■ ■■■ ■■■ ■■■■■■■■ ■■■■ ■■■
634 })
635 elif rel_natures.get("NEGATIVE", 0) > rel_natures.get("POSITIVE", 0) * 2:
636 [Link]({
637 "type": "relationship_imbalance",
638 "importance": "medium",
639 "description": "■■■■■ ■■■■■■■■ ■■■■■■■ ■■■ ■■■■■■.",
640 "suggestion": "■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■■ ■■ ■■■■■■■ ■■■■■■■■■ ■■ ■
641 })
642
643 # ■■■■■■ ■■ ■■■■■ ■■■■■ ■■■■■■
644 phase_distribution = self.global_metrics.get("conflict_phases_distribution", {})
645 if phase_distribution.get("LATENT", 0) + phase_distribution.get("EMERGING", 0) > 0.8 * self.g
646 [Link]({
647 "type": "stagnant_conflicts",
648 "importance": "high",
649 "description": "■■■■ ■■■■■■■■ ■■ ■■■■■ ■■■■■ (■■■■■ ■■ ■■■■■).",
650 "suggestion": "■■■■■ ■■■■ ■■■ ■■■■■■■■ ■■■■■■■■ ■■■ ■■■■■ ■■■■■■ ■■■■■■
651 })
652 elif phase_distribution.get("CLIMAX", 0) + phase_distribution.get("RESOLUTION", 0) > 0.7 * se
653 [Link]({
654 "type": "premature_resolution",
655 "importance": "high",
656 "description": "■■■■ ■■■■ ■■■■■■■■ ■■■ ■■■■■■ ■■ ■■■■ ■■■■ ■■■■.",
657 "suggestion": "■■■■■ ■■■■■■ ■■■■■ ■■ ■■■■■ ■■■■■■ ■■ ■■■■■ ■■ ■■■ ■■■■■■■
658 })
659
660 # ■■■■■■ ■■ ■■■■ ■■■■■■ ■■■■■■
661 subject_diversity = self.global_metrics.get("subject_diversity", 0)
662 if subject_diversity < 0.4 and self.global_metrics.get("conflict_count", 0) > 2:
663 # ■■■■■ ■■■■■■■ ■■■■■■■
664 subjects_dist = self.global_metrics.get("conflict_subjects_distribution", {})
665 dominant_subject = max(subjects_dist.items(), key=lambda x: x[1])[0] if subjects_dist els
666
667 [Link]({
668 "type": "subject_monotony",
669 "importance": "medium",
670 "description": f"■■■■■ ■■■■■ ■■■■ ■■■■ ({dominant_subject}) ■■■ ■■■■■■.",
671 "suggestion": "■■■■■ ■■■■■■ ■■■■■■ ■■■ ■■■■■ ■■■■■■ ■■■■■■ ■■■■ ■■ ■■■■■
672 })
673
674 # ■■■■■■ ■■ ■■■■■■ ■■■■■■
675 scope_distribution = self.global_metrics.get("conflict_scopes_distribution", {})
676 if scope_distribution.get("PERSONAL", 0) > 0.8 * self.global_metrics.get("conflict_count", 0)
677 [Link]({
678 "type": "limited_scope",
679 "importance": "low",
680 "description": "■■■■ ■■■■■■■■ ■■■■■■ ■■ ■■■ ■■ ■■■■■■■■ ■■■ ■■■■■■ ■■■■."
681 "suggestion": "■■■■■ ■■■■■■ ■■■ ■■■■■ ■■■■■■■■■ ■■ ■■■■■■■■ ■■■■■■■■ ■■■
682 })
683
684 # ■■■■■■ ■■ ■■■■■ ■■■■■■
685 network_density = self.global_metrics.get("network_density_percentage", 0)
686 if network_density < 20 and self.global_metrics.get("character_count", 0) > 4:
687 [Link]({
688 "type": "sparse_network",
689 "importance": "medium",
690 "description": "■■■■ ■■■■■■ ■■■■■■ ■■■■■■■ ■■ ■■■ ■■■■■ ■■■■■■■■■ ■■■ ■■■
691 "suggestion": "■■■■■ ■■■■ ■■ ■■■■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■ ■■■■■■ ■■■■
692 })
693 elif network_density > 80 and self.global_metrics.get("character_count", 0) > 5:
694 [Link]({
695 "type": "overly_dense_network",
696 "importance": "low",
697 "description": "■■■■ ■■■■■ ■■■■■■■ ■■ ■■■■ ■■■■■■ ■■■ ■■■■ ■■■■■■■■.",
698 "suggestion": "■■■■■ ■■■■ ■■■ ■■■■■■■■ ■■■■■■ ■■■■■ ■■■■■■■■ ■■■■■ ■■■■.
699 })
700
701 return suggestions
702
703
704 class NetworkVisualizer:
705 """
706 ■■■ ■■■■■■ ■■ ■■■■■ ■■■■■■ ■■■■■■ ■■■■■ ■■■■■■.
707 ■■■■ ■■■■■ ■■■■■■■■ ■■■■ ■■■■■■ ■■■■■■■■ ■■■■■■.
708 """
709
710 def __init__(self, network: ConflictNetwork):
711 """
712 ■■■■■ ■■■■ ■■■■■■ ■■■ ■■■■■■
713
714 Args:
715 network: ■■■■ ■■■■ ■■■■■■ ■■■■■■■ ■■■■■■
716 """
717 [Link] = network
718
719 # ■■■■■ ■■■■■ ■■■■■■ ■■■■■■■■
720 self.color_scheme = {
721 "character_node": "#4287f5",
722 "conflict_node": "#f54242",
723 "POSITIVE": "#42d442",
724 "NEGATIVE": "#f54242",
725 "NEUTRAL": "#d9d9d9",
726 "DYNAMIC": "#a142f5",
727 "VALUE": "#008080",
728 "POWER": "#800080",
729 "MATERIAL": "#ffa500",
730 "PSYCHOLOGICAL": "#4b0082",
731 "OTHER": "#808080",
732 "LATENT": "#add8e6",
733 "EMERGING": "#ffff99",
734 "ESCALATING": "#ffa07a",
735 "CLIMAX": "#ff6347",
736 "RESOLUTION": "#98fb98"
737 }
738
739 self.visualization_dir = IO_PATHS["visualization_dir"]
740 [Link](self.visualization_dir, exist_ok=True)
741
742 def create_character_network_visualization(self, title: str = "■■■■ ■■■■■■ ■■■■■■■■", filena
743 """
744 ■■■■■ ■■■■ ■■■■■ ■■■■■■■■ ■■■ ■■■■■■■■
745
746 Args:
747 title: ■■■■■ ■■■■■■
748 filename: ■■■ ■■■ ■■■■■■
749
750 Returns:
751 ■■■■ ■■■■■ ■■■■■■■
752 """
753 [Link](f"Creating character network visualization: {filename}")
754
755 # ■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■■
756 [Link]['[Link]'] = 'Arial'
757
758 # ■■■■■ ■■■ ■■■■■ NetworkX
759 G = [Link]()
760
761 # ■■■■■ ■■■■■■■■ ■■■■
762 characters = [Link].get_all_characters()
763 for char in characters:
764 G.add_node([Link], name=[Link], type='character')
765
766 # ■■■■■ ■■■■■■■■ ■■■■■
767 edge_colors = []
768 edge_widths = []
769 relationship_types = []
770
771 for rel in [Link].get_all_relationships():
772 if G.has_node([Link]) and G.has_node([Link]):
773 G.add_edge([Link], [Link], weight=[Link], type=[Link], nature=re
774 edge_colors.append(self.color_scheme.get([Link], "#d9d9d9"))
775 edge_widths.append(1 + [Link] / 3)
776 relationship_types.append([Link])
777
778 # ■■■■ ■■■■■ ■■■■■■
779 try:
780 if len([Link]()) > 2:
781 pos = nx.spring_layout(G, k=1.5/[Link](len([Link]())), seed=42)
782 else:
783 pos = nx.random_layout(G)
784 except Exception as e:
785 [Link](f"Error in creating layout: {e}. Using random layout.")
786 pos = nx.random_layout(G)
787
788 # ■■■■■ ■■■■■ ■■■■■■■
789 [Link](figsize=(14, 10))
790
791 # ■■■ ■■■■■ (■■■■■■■■)
792 nx.draw_networkx_nodes(
793 G, pos,
794 node_color=[self.color_scheme["character_node"]] * len([Link]()),
795 node_size=[300] * len([Link]()),
796 alpha=0.8,
797 edgecolors='black',
798 linewidths=0.5
799 )
800
801 # ■■■ ■■■■■■ (■■■■■■■■)
802 nx.draw_networkx_edges(
803 G, pos,
804 width=edge_widths,
805 edge_color=edge_colors,
806 alpha=0.7
807 )
808
809 # ■■■■■ ■■■■■■■■ (■■■■■ ■■■■■■■■)
810 labels = {node: [Link][node]['name'] for node in [Link]()}
811 nx.draw_networkx_labels(G, pos, labels=labels, font_size=10, font_weight='bold')
812
813 # ■■■■■ ■■■■■■■ ■■■■■■■■■
814 [Link](title, fontsize=16)
815 [Link]('off')
816
817 # ■■■■■ ■■■■■ ■■■■■■■
818 legend_elements = [
819 plt.Line2D([0], [0], color=self.color_scheme["POSITIVE"], lw=4, label='■■■■■ ■■■■■■■')
820 plt.Line2D([0], [0], color=self.color_scheme["NEGATIVE"], lw=4, label='■■■■■ ■■■■■'),
821 plt.Line2D([0], [0], color=self.color_scheme["NEUTRAL"], lw=4, label='■■■■■ ■■■■■■'),
822 plt.Line2D([0], [0], color=self.color_scheme["DYNAMIC"], lw=4, label='■■■■■ ■■■■■■')
823 ]
824 [Link](handles=legend_elements, title="■■■■■ ■■■■■■■■", loc="upper right")
825
826 # ■■■ ■■■■■ ■■■■■■■
827 output_path = [Link](self.visualization_dir, filename)
828 plt.tight_layout()
829 [Link](output_path, dpi=300, bbox_inches='tight')
830 [Link]()
831
832 [Link](f"Character network visualization saved to: {output_path}")
833 return output_path
834
835 def create_conflict_network_visualization(self, title: str = "■■■■ ■■■■■■■■", filename: str =
836 """
837 ■■■■■ ■■■■ ■■■■■ ■■■■■■■■ ■■ ■■■■■■■■ ■■■■■■■■
838
839 Args:
840 title: ■■■■■ ■■■■■■
841 filename: ■■■ ■■■ ■■■■■■
842
843 Returns:
844 ■■■■ ■■■■■ ■■■■■■■
845 """
846 [Link](f"Creating conflict network visualization: {filename}")
847
848 # ■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■■
849 [Link]['[Link]'] = 'Arial'
850
851 # ■■■■■ ■■■ ■■■■■ ■■■■■ ■■■■■■■ NetworkX
852 G = [Link]()
853
854 # ■■■■■ ■■■■■■■■ ■■■■ ■■ ■■■ ■■■■
855 characters = [Link].get_all_characters()
856 for char in characters:
857 G.add_node([Link], name=[Link], type='character')
858
859 # ■■■■■ ■■■■■■■■ ■■■■ ■■ ■■■ ■■■
860 conflicts = [Link].get_all_conflicts()
861 for conflict in conflicts:
862 G.add_node(f"conflict_{[Link]}", name=[Link], type='conflict',
863 subject=[Link], phase=[Link])
864
865 # ■■■ ■■■■■■ ■■■■■■■■■ ■■■■■■■■ ■■■
866 for char_id in conflict.involved_characters:
867 if G.has_node(char_id):
868 G.add_edge(char_id, f"conflict_{[Link]}")
869
870 # ■■■■ ■■■■■ ■■■■■■ (■■■■■ ■■■■ ■■■■■■ ■■■■■■■■ ■■■■■■ ■■■■■■■)
871 try:
872 # ■■■■■ ■■■■■■■ ■■■■■
873 character_nodes = [node for node in [Link]() if [Link][node]['type'] == 'character']
874 conflict_nodes = [node for node in [Link]() if [Link][node]['type'] == 'conflict']
875
876 # ■■■■■■■ ■■■■■■■ ■■■■■ ■■■■■■■ ■■■ ■■■■ ■■■■ ■■■ ■■ ■■■ ■■■■■■■
877 if character_nodes and conflict_nodes:
878 pos = nx.bipartite_layout(G, character_nodes)
879 else:
880 pos = nx.spring_layout(G, k=1.5/[Link](len([Link]())), seed=42)
881 except Exception as e:
882 [Link](f"Error in creating layout: {e}. Using spring layout.")
883 pos = nx.spring_layout(G, k=1.5/[Link](len([Link]())), seed=42)
884
885 # ■■■■■ ■■■■■ ■■■■■■■
886 [Link](figsize=(16, 12))
887
888 # ■■■ ■■■■■ (■■■■■■■■ ■■■■■■■■■) ■■ ■■■■■ ■■■■■■■
889 node_colors = []
890 node_sizes = []
891 node_shapes = []
892
893 for node in [Link]():
894 node_type = [Link][node]['type']
895 if node_type == 'character':
896 node_colors.append(self.color_scheme["character_node"])
897 node_sizes.append(300)
898 node_shapes.append('o') # ■■■■■ ■■■■■■■■
899 else: # conflict
900 subject = [Link][node]['subject']
901 node_colors.append(self.color_scheme.get(subject, self.color_scheme["OTHER"]))
902 node_sizes.append(500)
903 node_shapes.append('s') # ■■■■ ■■■■■■■■
904
905 # ■■■ ■■■■■ ■■■■■■■■ ■■■■■■■ ■■■■■■■
906 for shape, size, color, node in zip(node_shapes, node_sizes, node_colors, [Link]()):
907 if shape == 'o': # ■■■■■
908 nx.draw_networkx_nodes(
909 G, pos,
910 nodelist=[node],
911 node_color=[color],
912 node_size=size,
913 alpha=0.8,
914 edgecolors='black',
915 linewidths=0.5,
916 node_shape=shape
917 )
918 else: # ■■■■
919 nx.draw_networkx_nodes(
920 G, pos,
921 nodelist=[node],
922 node_color=[color],
923 node_size=size,
924 alpha=0.8,
925 edgecolors='black',
926 linewidths=0.5,
927 node_shape=shape
928 )
929
930 # ■■■ ■■■■■■ (■■■■■■■ ■■■ ■■■■■■■■ ■■■■■■■■■)
931 nx.draw_networkx_edges(
932 G, pos,
933 width=1.0,
934 edge_color="#d9d9d9",
935 alpha=0.5
936 )
937
938 # ■■■■■ ■■■■■■■■
939 labels = {node: [Link][node]['name'] for node in [Link]()}
940 nx.draw_networkx_labels(G, pos, labels=labels, font_size=9, font_weight='bold')
941
942 # ■■■■■ ■■■■■■■ ■■■■■■■■■
943 [Link](title, fontsize=16)
944 [Link]('off')
945
946 # ■■■■■ ■■■■■ ■■■■■■■
947 legend_elements = [
948 plt.Line2D([0], [0], markerfacecolor=self.color_scheme["character_node"], marker='o', col
949 plt.Line2D([0], [0], markerfacecolor=self.color_scheme["VALUE"], marker='s', color='white
950 plt.Line2D([0], [0], markerfacecolor=self.color_scheme["POWER"], marker='s', color='white
951 plt.Line2D([0], [0], markerfacecolor=self.color_scheme["MATERIAL"], marker='s', color='wh
952 plt.Line2D([0], [0], markerfacecolor=self.color_scheme["PSYCHOLOGICAL"], marker='s', colo
953 ]
954 [Link](handles=legend_elements, title="■■■■■ ■■■■■", loc="upper right")
955
956 # ■■■ ■■■■■ ■■■■■■■
957 output_path = [Link](self.visualization_dir, filename)
958 plt.tight_layout()
959 [Link](output_path, dpi=300, bbox_inches='tight')
960 [Link]()
961
962 [Link](f"Conflict network visualization saved to: {output_path}")
963 return output_path
964
965 def create_conflict_phases_visualization(self, title: str = "■■■■■ ■■■■■■■■", filename: str =
966 """
967 ■■■■■ ■■■■ ■■■■■■ ■■■■■ ■■■■■■■■
968
969 Args:
970 title: ■■■■■ ■■■■■■
971 filename: ■■■ ■■■ ■■■■■■
972
973 Returns:
974 ■■■■ ■■■■■ ■■■■■■■
975 """
976 [Link](f"Creating conflict phases visualization: {filename}")
977
978 # ■■■ ■■■■■ ■■■■ ■■■■ ■■■■■■■
979 [Link]['[Link]'] = 'Arial'
980
981 # ■■■ ■■■■■■ ■■■■■ ■■■■■■■■
982 conflicts = [Link].get_all_conflicts()
983
984 phases = {
985 "LATENT": {"label": "■■■■", "color": self.color_scheme["LATENT"], "count": 0},
986 "EMERGING": {"label": "■■■■", "color": self.color_scheme["EMERGING"], "count": 0},
987 "ESCALATING": {"label": "■■■■■■", "color": self.color_scheme["ESCALATING"], "count": 0},
988 "CLIMAX": {"label": "■■■■", "color": self.color_scheme["CLIMAX"], "count": 0},
989 "RESOLUTION": {"label": "■■", "color": self.color_scheme["RESOLUTION"], "count": 0}
990 }
991
992 # ■■■■ ■■■ ■■■■■■■■ ■■ ■■ ■■■■■
993 for conflict in conflicts:
994 phase_name = [Link]
995 if phase_name in phases:
996 phases[phase_name]["count"] += 1
997
998 # ■■■■■ ■■■■■ ■■■■■■■
999 [Link](figsize=(12, 8))
1000
1001 # ■■■■ ■■■■■■ ■■■■■■■
1002 phase_labels = [info["label"] for name, info in [Link]() if info["count"] > 0]
1003 phase_counts = [info["count"] for name, info in [Link]() if info["count"] > 0]
1004 phase_colors = [info["color"] for name, info in [Link]() if info["count"] > 0]
1005
1006 if not phase_counts: # ■■■ ■■ ■■■ ■■■■ ■■■■■■
1007 [Link](0.5, 0.5, "■■ ■■■■ ■■■■■■ ■■■■■■", ha='center', va='center', fontsize=14)
1008 [Link]('off')
1009 else:
1010 wedges, texts, autotexts = [Link](
1011 phase_counts,
1012 labels=phase_labels,
1013 colors=phase_colors,
1014 autopct='%1.1f%%',
1015 startangle=90,
1016 wedgeprops={'edgecolor': 'black', 'linewidth': 1}
1017 )
1018
1019 # ■■■■■ ■■■ ■■■■
1020 for text in texts:
1021 text.set_fontsize(12)
1022 text.set_fontweight('bold')
1023
1024 for autotext in autotexts:
1025 autotext.set_fontsize(10)
1026 autotext.set_fontweight('bold')
1027 autotext.set_color('black')
1028
1029 # ■■■■■ ■■■■■■■
1030 [Link](title, fontsize=16)
1031
1032 # ■■■■■ ■■■■■■■ ■■■■■■
1033 total_conflicts = len(conflicts)
1034 info_text = f"■■■■■■ ■■■■■■■■: {total_conflicts}"
1035 [Link](0.5, 0.01, info_text, ha='center', fontsize=12)
1036
1037 # ■■■ ■■■■■ ■■■■■■■
1038 output_path = [Link](self.visualization_dir, filename)
1039 plt.tight_layout()
1040 [Link](output_path, dpi=300, bbox_inches='tight')
1041 [Link]()
1042
1043 [Link](f"Conflict phases visualization saved to: {output_path}")
1044 return output_path
1045
1046 def create_all_visualizations(self) -> Dict[str, str]:
1047 """
1048 ■■■■■ ■■■■ ■■■■■■■■ ■■■■■■■ ■■■■■■
1049
1050 Returns:
1051 ■■■■■ ■■■■■ ■■■ ■■■■■ ■■■■■■■■ ■■■■■■■■■
1052 """
1053 [Link]("Creating all network visualizations...")
1054
1055 outputs = {}
1056
1057 # ■■■■■ ■■■■ ■■■■ ■■■■■■■■ ■■■ ■■■■■■■■
1058 outputs["character_network"] = self.create_character_network_visualization(
1059 title=f"■■■■ ■■■■■■ ■■■■■■■■ - {[Link]}",
1060 filename=f"{IO_PATHS['prose_basename']}_character_network.png"
1061 )
1062
1063 # ■■■■■ ■■■■ ■■■■ ■■■■■■■■
1064 outputs["conflict_network"] = self.create_conflict_network_visualization(
1065 title=f"■■■■ ■■■■■■■■ - {[Link]}",
1066 filename=f"{IO_PATHS['prose_basename']}_conflict_network.png"
1067 )
1068
1069 # ■■■■■ ■■■■ ■■■■■ ■■■■■■■■
1070 outputs["conflict_phases"] = self.create_conflict_phases_visualization(
1071 title=f"■■■■■ ■■■■■ ■■■■■■■■ - {[Link]}",
1072 filename=f"{IO_PATHS['prose_basename']}_conflict_phases.png"
1073 )
1074
1075 [Link](f"All visualizations complete. Created {len(outputs)} visualizations.")
1076 return outputs
1077
1078
1079 class ConflictNetworkService:
1080 """
1081 ■■■■ ■■■■■■■ ■■■■■■ ■■■■■■ ■■■■ ■■■■■■ ■■■■■■.
1082 ■■■■ ■■■■■ ■■■■■ ■■■■■■■■■ ■■ ■■■■■ ■■■■ ■■■■■■ ■■■■■ ■■■■■■ ■■■ ■■■■■■.
1083 """
1084
1085 def __init__(self, network: Optional[ConflictNetwork] = None):
1086 """
1087 ■■■■■ ■■■■ ■■■■ ■■■■■■
1088
1089 Args:
1090 network: ■■■■ ■■■■ ■■■■■■ (■■■■■■■)
1091 """
1092 [Link] = network
1093 [Link] = None
1094 [Link] = None
1095
1096 if network:
1097 self.setup_with_network(network)
1098
1099 def setup_with_network(self, network: ConflictNetwork) -> None:
1100 """
1101 ■■■■■ ■■■■■■ ■■■■■■■■ ■■■■ ■■■■ ■■■■■
1102
1103 Args:
1104 network: ■■■■ ■■■■ ■■■■■■
1105 """
1106 [Link] = network
1107 [Link] = NetworkAnalyzer(network)
1108 [Link] = NetworkVisualizer(network)
1109
1110 def load_network_from_station3(self, station3_output_path: str) -> bool:
1111 """
1112 ■■■■■ ■■■■ ■■■■■■ ■■ ■■■■■■ ■■■■■■ 3
1113
1114 Args:
1115 station3_output_path: ■■■■ ■■■ ■■■■■■ ■■■■■■ 3
1116
1117 Returns:
1118 ■■■■/■■■ ■■■■■■■
1119 """
1120 [Link](f"Loading network from Station 3 output: {station3_output_path}")
1121
1122 try:
1123 with open(station3_output_path, 'rb') as f:
1124 station3_data = [Link](f)
1125
1126 # ■■■■■■ ■■■■■■■ ■■■■ ■■■■■■ ■■ ■■■■■ ■■■■■■■■■ ■■■■■■■■
1127 network = None
1128
1129 if isinstance(station3_data, ConflictNetwork):
1130 network = station3_data
1131 [Link]("Network object found directly at the top level.")
1132 elif isinstance(station3_data, dict):
1133 if "results" in station3_data and isinstance(station3_data["results"], dict):
1134 if "network_object" in station3_data["results"]:
1135 network = station3_data["results"]["network_object"]
1136 [Link]("Network object found in results.network_object.")
1137
1138 if not network:
1139 [Link]("Could not find a valid ConflictNetwork object in the Station 3 output."
1140 return False
1141
1142 # ■■■■■ ■■■■■■ ■■ ■■■■■■ ■■■■■■■
1143 self.setup_with_network(network)
1144 [Link](f"Successfully loaded network '{[Link]}' with {len(network.get_all_char
1145 f"{len(network.get_all_relationships())} relationships, and {len(network.get_
1146 return True
1147
1148 except Exception as e:
1149 [Link](f"Error loading network from Station 3 output: {e}", exc_info=True)
1150 return False
1151
1152 def run_full_analysis_and_visualization(self) -> Dict[str, Any]:
1153 """
1154 ■■■■■ ■■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■■ ■■■■■■
1155
1156 Returns:
1157 ■■■■■ ■■■■■■■ ■■■■■■■■■
1158 """
1159 [Link]("Running full network analysis and visualization...")
1160
1161 if not [Link]:
1162 [Link]("No network available. Please load a network first.")
1163 return {"error": "No network available for analysis."}
1164
1165 if not [Link] or not [Link]:
1166 self.setup_with_network([Link])
1167
1168 try:
1169 # ■■■■■ ■■■■■■
1170 analysis_results = [Link].analyze_network()
1171
1172 # ■■■■■ ■■■■■■■■
1173 visualization_outputs = [Link].create_all_visualizations()
1174
1175 # ■■■■■ ■■■■■■■
1176 results = {
1177 "network_info": {
1178 "name": [Link],
1179 "character_count": len([Link].get_all_characters()),
1180 "relationship_count": len([Link].get_all_relationships()),
1181 "conflict_count": len([Link].get_all_conflicts())
1182 },
1183 "analysis_results": analysis_results,
1184 "visualization_outputs": visualization_outputs,
1185 "analysis_timestamp": [Link]().isoformat()
1186 }
1187
1188 [Link]("Full analysis and visualization completed successfully.")
1189 return results
1190
1191 except Exception as e:
1192 [Link](f"Error during full analysis and visualization: {e}", exc_info=True)
1193 return {"error": f"Analysis failed: {str(e)}"}
1194
1195
1196 @[Link]
1197 async def run_station_conflict_network_analysis(station3_output_path: str) -> Dict[str, Any]:
1198 """
1199 ■■■■■ ■■■■ ■■■■■ ■■■■ ■■■■■■
1200
1201 Args:
1202 station3_output_path: ■■■■ ■■■ ■■■■■■ ■■■■■■ 3
1203
1204 Returns:
1205 ■■■■■ ■■■■■ ■■■■■■ ■■■■ ■■■■■■
1206 """
1207 [Link]("=== Starting Conflict Network Analysis Station ===")
1208
1209 # ■■■■■ ■■■■ ■■■■ ■■■■■■
1210 network_service = ConflictNetworkService()
1211
1212 # ■■■■■ ■■■■ ■■■■■■ ■■ ■■■■■■ ■■■■■■ 3
1213 if not network_service.load_network_from_station3(station3_output_path):
1214 error_msg = "Failed to load network from Station 3 output."
1215 [Link](error_msg)
1216 return {"error": error_msg}
1217
1218 # ■■■■■ ■■■■■■■ ■■■■■■ ■■■■■■ ■■■■■■■■
1219 results = network_service.run_full_analysis_and_visualization()
1220
1221 [Link]("=== Conflict Network Analysis Station completed ===")
1222 return results
1223
1224
1225 async def main_conflict_network_station():
1226 """■■■■ ■■■■■■ ■■■■■■■■ ■■■■■ ■■■■■ ■■■■ ■■■■■■"""
1227 [Link]("--- Starting Enhanced Conflict Network Analysis Station ---")
1228
1229 # ■■■■■■ ■■ ■■■■ ■■■■■■ ■■■■■■ 3
1230 if not [Link](IO_PATHS["station3_input_pkl"]):
1231 [Link](f"Station 3 output not found at: {IO_PATHS['station3_input_pkl']}")
1232 [Link]("Please run Station 3 first to generate the conflict network.")
1233 return
1234
1235 try:
1236 async with PerfTracker(label="ConflictNetworkAnalysis_Run") as perf:
1237 results = await run_station_conflict_network_analysis(IO_PATHS["station3_input_pkl"])
1238 except Exception as e:
1239 [Link](f"Unexpected error in main execution: {e}", exc_info=True)
1240 results = {"error": f"Unhandled exception: {str(e)}"}
1241 perf = {"stats": {"error": "Performance tracking failed due to exception"}}
1242
1243 # ■■■■■ ■■■■■■ ■■■■■■■ ■■■■■■■■
1244 output_payload = {
1245 "metadata": {
1246 "project_name": PROJECT_NAME,
1247 "prose_file_basename": IO_PATHS["prose_basename"],
1248 "station_name": "Enhanced Conflict Network Analysis",
1249 "timestamp": [Link]().isoformat(),
1250 "input_files": {"station3_output": IO_PATHS["station3_input_pkl"]},
1251 "status": "Success" if "error" not in results else "Failed",
1252 "error_message": [Link]("error"),
1253 "version": "2.0.0"
1254 },
1255 "results": results,
1256 "performance_stats": [Link]
1257 }
1258
1259 # ■■■ ■■■■■■■
1260 try:
1261 with open(IO_PATHS["analysis_output_pkl"], 'wb') as f:
1262 [Link](output_payload, f)
1263 [Link](f"Analysis output (PKL) saved to: {IO_PATHS['analysis_output_pkl']}")
1264
1265 # ■■■■■ ■■■■■■■ ■■■ JSON (■■ ■■■■■■■ ■■ ■■■■■■■■ ■■■ ■■■■■■■ ■■■■■■■)
1266 def json_serializable(obj):
1267 if isinstance(obj, datetime):
1268 return [Link]()
1269 return str(obj)
1270
1271 with open(IO_PATHS["analysis_output_json"], 'w', encoding='utf-8') as f:
1272 [Link](output_payload, f, ensure_ascii=False, indent=2, default=json_serializable)
1273 [Link](f"Analysis output (JSON) saved to: {IO_PATHS['analysis_output_json']}")
1274
1275 except Exception as e:
1276 [Link](f"Error saving analysis output: {e}", exc_info=True)
1277
1278 [Link]("--- Enhanced Conflict Network Analysis Completed ---")
1279
1280
1281 if __name__ == "__main__":
1282 if not ORIGINAL_PROSE_PATH:
1283 [Link]("PROSE_FILE_PATH environment variable not set. Exiting.")
1284 [Link](1)
1285 if not PROJECT_PATH:
1286 [Link]("PROJECT_PATH environment variable not set. Using default output directory.")
1287
1288 if [Link] == "win32" and sys.version_info >= (3,8):
1289 asyncio.set_event_loop_policy([Link]())
1290
1291 [Link]("Running Enhanced Conflict Network Analysis as a standalone module.")
1292 [Link](main_conflict_network_station())