yamlgraph 0.3.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. examples/__init__.py +1 -0
  2. examples/codegen/__init__.py +5 -0
  3. examples/codegen/models/__init__.py +13 -0
  4. examples/codegen/models/schemas.py +76 -0
  5. examples/codegen/tests/__init__.py +1 -0
  6. examples/codegen/tests/test_ai_helpers.py +235 -0
  7. examples/codegen/tests/test_ast_analysis.py +174 -0
  8. examples/codegen/tests/test_code_analysis.py +134 -0
  9. examples/codegen/tests/test_code_context.py +301 -0
  10. examples/codegen/tests/test_code_nav.py +89 -0
  11. examples/codegen/tests/test_dependency_tools.py +119 -0
  12. examples/codegen/tests/test_example_tools.py +185 -0
  13. examples/codegen/tests/test_git_tools.py +112 -0
  14. examples/codegen/tests/test_impl_agent_schemas.py +193 -0
  15. examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
  16. examples/codegen/tests/test_jedi_analysis.py +226 -0
  17. examples/codegen/tests/test_meta_tools.py +250 -0
  18. examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
  19. examples/codegen/tests/test_syntax_tools.py +85 -0
  20. examples/codegen/tests/test_synthesize_prompt.py +94 -0
  21. examples/codegen/tests/test_template_tools.py +244 -0
  22. examples/codegen/tools/__init__.py +80 -0
  23. examples/codegen/tools/ai_helpers.py +420 -0
  24. examples/codegen/tools/ast_analysis.py +92 -0
  25. examples/codegen/tools/code_context.py +180 -0
  26. examples/codegen/tools/code_nav.py +52 -0
  27. examples/codegen/tools/dependency_tools.py +120 -0
  28. examples/codegen/tools/example_tools.py +188 -0
  29. examples/codegen/tools/git_tools.py +151 -0
  30. examples/codegen/tools/impl_executor.py +614 -0
  31. examples/codegen/tools/jedi_analysis.py +311 -0
  32. examples/codegen/tools/meta_tools.py +202 -0
  33. examples/codegen/tools/syntax_tools.py +26 -0
  34. examples/codegen/tools/template_tools.py +356 -0
  35. examples/fastapi_interview.py +167 -0
  36. examples/npc/api/__init__.py +1 -0
  37. examples/npc/api/app.py +100 -0
  38. examples/npc/api/routes/__init__.py +5 -0
  39. examples/npc/api/routes/encounter.py +182 -0
  40. examples/npc/api/session.py +330 -0
  41. examples/npc/demo.py +387 -0
  42. examples/npc/nodes/__init__.py +5 -0
  43. examples/npc/nodes/image_node.py +92 -0
  44. examples/npc/run_encounter.py +230 -0
  45. examples/shared/__init__.py +0 -0
  46. examples/shared/replicate_tool.py +238 -0
  47. examples/storyboard/__init__.py +1 -0
  48. examples/storyboard/generate_videos.py +335 -0
  49. examples/storyboard/nodes/__init__.py +12 -0
  50. examples/storyboard/nodes/animated_character_node.py +248 -0
  51. examples/storyboard/nodes/animated_image_node.py +138 -0
  52. examples/storyboard/nodes/character_node.py +162 -0
  53. examples/storyboard/nodes/image_node.py +118 -0
  54. examples/storyboard/nodes/replicate_tool.py +49 -0
  55. examples/storyboard/retry_images.py +118 -0
  56. scripts/demo_async_executor.py +212 -0
  57. scripts/demo_interview_e2e.py +200 -0
  58. scripts/demo_streaming.py +140 -0
  59. scripts/run_interview_demo.py +94 -0
  60. scripts/test_interrupt_fix.py +26 -0
  61. tests/__init__.py +1 -0
  62. tests/conftest.py +178 -0
  63. tests/integration/__init__.py +1 -0
  64. tests/integration/test_animated_storyboard.py +63 -0
  65. tests/integration/test_cli_commands.py +242 -0
  66. tests/integration/test_colocated_prompts.py +139 -0
  67. tests/integration/test_map_demo.py +50 -0
  68. tests/integration/test_memory_demo.py +283 -0
  69. tests/integration/test_npc_api/__init__.py +1 -0
  70. tests/integration/test_npc_api/test_routes.py +357 -0
  71. tests/integration/test_npc_api/test_session.py +216 -0
  72. tests/integration/test_pipeline_flow.py +105 -0
  73. tests/integration/test_providers.py +163 -0
  74. tests/integration/test_resume.py +75 -0
  75. tests/integration/test_subgraph_integration.py +295 -0
  76. tests/integration/test_subgraph_interrupt.py +106 -0
  77. tests/unit/__init__.py +1 -0
  78. tests/unit/test_agent_nodes.py +355 -0
  79. tests/unit/test_async_executor.py +346 -0
  80. tests/unit/test_checkpointer.py +212 -0
  81. tests/unit/test_checkpointer_factory.py +212 -0
  82. tests/unit/test_cli.py +121 -0
  83. tests/unit/test_cli_package.py +81 -0
  84. tests/unit/test_compile_graph_map.py +132 -0
  85. tests/unit/test_conditions_routing.py +253 -0
  86. tests/unit/test_config.py +93 -0
  87. tests/unit/test_conversation_memory.py +276 -0
  88. tests/unit/test_database.py +145 -0
  89. tests/unit/test_deprecation.py +104 -0
  90. tests/unit/test_executor.py +172 -0
  91. tests/unit/test_executor_async.py +179 -0
  92. tests/unit/test_export.py +149 -0
  93. tests/unit/test_expressions.py +178 -0
  94. tests/unit/test_feature_brainstorm.py +194 -0
  95. tests/unit/test_format_prompt.py +145 -0
  96. tests/unit/test_generic_report.py +200 -0
  97. tests/unit/test_graph_commands.py +327 -0
  98. tests/unit/test_graph_linter.py +627 -0
  99. tests/unit/test_graph_loader.py +357 -0
  100. tests/unit/test_graph_schema.py +193 -0
  101. tests/unit/test_inline_schema.py +151 -0
  102. tests/unit/test_interrupt_node.py +182 -0
  103. tests/unit/test_issues.py +164 -0
  104. tests/unit/test_jinja2_prompts.py +85 -0
  105. tests/unit/test_json_extract.py +134 -0
  106. tests/unit/test_langsmith.py +600 -0
  107. tests/unit/test_langsmith_tools.py +204 -0
  108. tests/unit/test_llm_factory.py +109 -0
  109. tests/unit/test_llm_factory_async.py +118 -0
  110. tests/unit/test_loops.py +403 -0
  111. tests/unit/test_map_node.py +144 -0
  112. tests/unit/test_no_backward_compat.py +56 -0
  113. tests/unit/test_node_factory.py +348 -0
  114. tests/unit/test_passthrough_node.py +126 -0
  115. tests/unit/test_prompts.py +324 -0
  116. tests/unit/test_python_nodes.py +198 -0
  117. tests/unit/test_reliability.py +298 -0
  118. tests/unit/test_result_export.py +234 -0
  119. tests/unit/test_router.py +296 -0
  120. tests/unit/test_sanitize.py +99 -0
  121. tests/unit/test_schema_loader.py +295 -0
  122. tests/unit/test_shell_tools.py +229 -0
  123. tests/unit/test_state_builder.py +331 -0
  124. tests/unit/test_state_builder_map.py +104 -0
  125. tests/unit/test_state_config.py +197 -0
  126. tests/unit/test_streaming.py +307 -0
  127. tests/unit/test_subgraph.py +596 -0
  128. tests/unit/test_template.py +190 -0
  129. tests/unit/test_tool_call_integration.py +164 -0
  130. tests/unit/test_tool_call_node.py +178 -0
  131. tests/unit/test_tool_nodes.py +129 -0
  132. tests/unit/test_websearch.py +234 -0
  133. yamlgraph/__init__.py +35 -0
  134. yamlgraph/builder.py +110 -0
  135. yamlgraph/cli/__init__.py +159 -0
  136. yamlgraph/cli/__main__.py +6 -0
  137. yamlgraph/cli/commands.py +231 -0
  138. yamlgraph/cli/deprecation.py +92 -0
  139. yamlgraph/cli/graph_commands.py +541 -0
  140. yamlgraph/cli/validators.py +37 -0
  141. yamlgraph/config.py +67 -0
  142. yamlgraph/constants.py +70 -0
  143. yamlgraph/error_handlers.py +227 -0
  144. yamlgraph/executor.py +290 -0
  145. yamlgraph/executor_async.py +288 -0
  146. yamlgraph/graph_loader.py +451 -0
  147. yamlgraph/map_compiler.py +150 -0
  148. yamlgraph/models/__init__.py +36 -0
  149. yamlgraph/models/graph_schema.py +181 -0
  150. yamlgraph/models/schemas.py +124 -0
  151. yamlgraph/models/state_builder.py +236 -0
  152. yamlgraph/node_factory.py +768 -0
  153. yamlgraph/routing.py +87 -0
  154. yamlgraph/schema_loader.py +240 -0
  155. yamlgraph/storage/__init__.py +20 -0
  156. yamlgraph/storage/checkpointer.py +72 -0
  157. yamlgraph/storage/checkpointer_factory.py +123 -0
  158. yamlgraph/storage/database.py +320 -0
  159. yamlgraph/storage/export.py +269 -0
  160. yamlgraph/tools/__init__.py +1 -0
  161. yamlgraph/tools/agent.py +320 -0
  162. yamlgraph/tools/graph_linter.py +388 -0
  163. yamlgraph/tools/langsmith_tools.py +125 -0
  164. yamlgraph/tools/nodes.py +126 -0
  165. yamlgraph/tools/python_tool.py +179 -0
  166. yamlgraph/tools/shell.py +205 -0
  167. yamlgraph/tools/websearch.py +242 -0
  168. yamlgraph/utils/__init__.py +48 -0
  169. yamlgraph/utils/conditions.py +157 -0
  170. yamlgraph/utils/expressions.py +245 -0
  171. yamlgraph/utils/json_extract.py +104 -0
  172. yamlgraph/utils/langsmith.py +416 -0
  173. yamlgraph/utils/llm_factory.py +118 -0
  174. yamlgraph/utils/llm_factory_async.py +105 -0
  175. yamlgraph/utils/logging.py +104 -0
  176. yamlgraph/utils/prompts.py +171 -0
  177. yamlgraph/utils/sanitize.py +98 -0
  178. yamlgraph/utils/template.py +102 -0
  179. yamlgraph/utils/validators.py +181 -0
  180. yamlgraph-0.3.9.dist-info/METADATA +1105 -0
  181. yamlgraph-0.3.9.dist-info/RECORD +185 -0
  182. yamlgraph-0.3.9.dist-info/WHEEL +5 -0
  183. yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
  184. yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
  185. yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
@@ -0,0 +1,138 @@
1
+ """Animated storyboard node for generating frame images.
2
+
3
+ Generates 3 images per panel: first_frame, original, last_frame.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from .replicate_tool import generate_image
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ GraphState = dict[str, Any]
19
+
20
+
21
+ def generate_animated_images_node(state: GraphState) -> dict:
22
+ """Generate images for all animated panel frames.
23
+
24
+ Reads animated_panels from state and generates 3 images per panel.
25
+ Saves to outputs/storyboard/{thread_id}/animated/ directory.
26
+
27
+ Args:
28
+ state: Graph state with 'animated_panels' list of {first_frame, original, last_frame}
29
+
30
+ Returns:
31
+ State update with 'images' list organized by panel
32
+ """
33
+ animated_panels = state.get("animated_panels", [])
34
+ if not animated_panels:
35
+ logger.error("No animated_panels in state")
36
+ return {
37
+ "current_step": "generate_animated_images",
38
+ "images": [],
39
+ "error": "No animated panels to generate",
40
+ }
41
+
42
+ # Sort by _map_index if present to maintain order
43
+ if animated_panels and isinstance(animated_panels[0], dict):
44
+ animated_panels = sorted(
45
+ animated_panels,
46
+ key=lambda x: x.get("_map_index", 0) if isinstance(x, dict) else 0,
47
+ )
48
+
49
+ # Create output directory
50
+ thread_id = state.get("thread_id", datetime.now().strftime("%Y%m%d_%H%M%S"))
51
+ output_dir = Path("outputs/storyboard") / thread_id / "animated"
52
+ output_dir.mkdir(parents=True, exist_ok=True)
53
+
54
+ total_images = len(animated_panels) * 3
55
+ logger.info(
56
+ f"šŸŽ¬ Generating {total_images} images ({len(animated_panels)} panels Ɨ 3 frames)"
57
+ )
58
+
59
+ # Get model selection from state (default: z-image)
60
+ model_name = state.get("model", "z-image")
61
+ logger.info(f"šŸ–¼ļø Using model: {model_name}")
62
+
63
+ # Generate images for each panel
64
+ all_results: list[dict] = []
65
+ frame_keys = ["first_frame", "original", "last_frame"]
66
+
67
+ for panel_idx, panel in enumerate(animated_panels, 1):
68
+ # Handle Pydantic model or dict
69
+ if hasattr(panel, "model_dump"):
70
+ panel_dict = panel.model_dump()
71
+ elif isinstance(panel, dict):
72
+ panel_dict = panel
73
+ else:
74
+ logger.warning(f"Panel {panel_idx} has unexpected type: {type(panel)}")
75
+ continue
76
+
77
+ panel_result = {"panel": panel_idx, "frames": {}}
78
+
79
+ for frame_key in frame_keys:
80
+ prompt = panel_dict.get(frame_key, "")
81
+ if not prompt:
82
+ logger.warning(f"Panel {panel_idx} missing {frame_key}")
83
+ continue
84
+
85
+ output_path = output_dir / f"panel_{panel_idx}_{frame_key}.png"
86
+ logger.info(f"šŸ“ø Panel {panel_idx} {frame_key}: {prompt[:50]}...")
87
+
88
+ result = generate_image(prompt, output_path, model_name=model_name)
89
+
90
+ if result.success and result.path:
91
+ panel_result["frames"][frame_key] = result.path
92
+ else:
93
+ logger.error(f"Panel {panel_idx} {frame_key} failed: {result.error}")
94
+ panel_result["frames"][frame_key] = None
95
+
96
+ all_results.append(panel_result)
97
+
98
+ # Save metadata
99
+ story = state.get("story", {})
100
+ if hasattr(story, "model_dump"):
101
+ story_dict = story.model_dump()
102
+ elif isinstance(story, dict):
103
+ story_dict = story
104
+ else:
105
+ story_dict = {}
106
+
107
+ metadata_path = output_dir / "animated_story.json"
108
+ metadata = {
109
+ "concept": state.get("concept", ""),
110
+ "title": story_dict.get("title", ""),
111
+ "narrative": story_dict.get("narrative", ""),
112
+ "panels": [
113
+ {
114
+ "index": r["panel"],
115
+ "frames": r["frames"],
116
+ "prompts": {
117
+ k: animated_panels[r["panel"] - 1].get(k, "")
118
+ if isinstance(animated_panels[r["panel"] - 1], dict)
119
+ else ""
120
+ for k in frame_keys
121
+ },
122
+ }
123
+ for r in all_results
124
+ ],
125
+ "generated_at": datetime.now().isoformat(),
126
+ }
127
+ metadata_path.write_text(json.dumps(metadata, indent=2))
128
+ logger.info(f"šŸ“ Metadata saved: {metadata_path}")
129
+
130
+ # Count successes
131
+ success_count = sum(1 for r in all_results for path in r["frames"].values() if path)
132
+ logger.info(f"āœ… Generated {success_count}/{total_images} images")
133
+
134
+ return {
135
+ "current_step": "generate_animated_images",
136
+ "images": all_results,
137
+ "output_dir": str(output_dir),
138
+ }
@@ -0,0 +1,162 @@
1
+ """Character-consistent storyboard node.
2
+
3
+ This node:
4
+ 1. Generates a character image from description (step 0)
5
+ 2. Uses image-to-image editing to place character in each panel scene
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from .replicate_tool import ImageResult, edit_image, generate_image
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Type alias for state
21
+ GraphState = dict[str, Any]
22
+
23
+
24
+ def generate_character_storyboard(state: GraphState) -> dict:
25
+ """Generate character-consistent storyboard images.
26
+
27
+ Step 0: Generate base character image from character_prompt
28
+ Panels 1-3: Use image-to-image to place character in each scene
29
+
30
+ Args:
31
+ state: Graph state with 'story' containing character and panel prompts
32
+
33
+ Returns:
34
+ State update with 'images' list and metadata
35
+ """
36
+ story = state.get("story")
37
+ if not story:
38
+ logger.error("No story in state")
39
+ return {
40
+ "current_step": "generate_character_storyboard",
41
+ "images": [],
42
+ "error": "No story in state",
43
+ }
44
+
45
+ # Handle Pydantic model or dict
46
+ if hasattr(story, "model_dump"):
47
+ story_dict = story.model_dump()
48
+ elif isinstance(story, dict):
49
+ story_dict = story
50
+ else:
51
+ story_dict = {}
52
+
53
+ # Extract prompts
54
+ character_prompt = story_dict.get("character_prompt", "")
55
+ panels = story_dict.get("panels", [])
56
+
57
+ if not character_prompt:
58
+ logger.error("No character_prompt in story")
59
+ return {
60
+ "current_step": "generate_character_storyboard",
61
+ "images": [],
62
+ "error": "No character_prompt provided",
63
+ }
64
+
65
+ if not panels:
66
+ logger.error("No panels in story")
67
+ return {
68
+ "current_step": "generate_character_storyboard",
69
+ "images": [],
70
+ "error": "No panel prompts provided",
71
+ }
72
+
73
+ # Create output directory
74
+ thread_id = state.get("thread_id", datetime.now().strftime("%Y%m%d_%H%M%S"))
75
+ output_dir = Path("outputs/storyboard") / thread_id
76
+ output_dir.mkdir(parents=True, exist_ok=True)
77
+
78
+ logger.info(f"šŸŽ¬ Generating character-consistent storyboard in {output_dir}")
79
+
80
+ # Get model selection from state (default: z-image for character)
81
+ model_name = state.get("model", "z-image")
82
+ logger.info(f"šŸ–¼ļø Using model for character: {model_name}")
83
+
84
+ image_paths: list[str] = []
85
+ results: list[ImageResult] = []
86
+
87
+ # Step 0: Generate base character image
88
+ character_path = output_dir / "character.png"
89
+ logger.info(f"šŸ‘¤ Step 0 - Creating character: {character_prompt[:60]}...")
90
+
91
+ character_result = generate_image(
92
+ prompt=character_prompt,
93
+ output_path=character_path,
94
+ model_name=model_name,
95
+ )
96
+ results.append(character_result)
97
+
98
+ if not character_result.success:
99
+ logger.error(f"Character generation failed: {character_result.error}")
100
+ return {
101
+ "current_step": "generate_character_storyboard",
102
+ "images": [],
103
+ "error": f"Character generation failed: {character_result.error}",
104
+ }
105
+
106
+ image_paths.append(str(character_path))
107
+ logger.info(f"āœ“ Character created: {character_path}")
108
+
109
+ # Panels 1-3: Image-to-image editing with character as base
110
+ for i, panel_prompt in enumerate(panels[:3], 1): # Max 3 panels
111
+ if not panel_prompt:
112
+ logger.warning(f"Panel {i} has no prompt, skipping")
113
+ continue
114
+
115
+ panel_path = output_dir / f"panel_{i}.png"
116
+ logger.info(f"šŸ“ø Panel {i}: {panel_prompt[:60]}...")
117
+
118
+ panel_result = edit_image(
119
+ input_image=character_path,
120
+ prompt=panel_prompt,
121
+ output_path=panel_path,
122
+ aspect_ratio="16:9",
123
+ )
124
+ results.append(panel_result)
125
+
126
+ if panel_result.success and panel_result.path:
127
+ image_paths.append(panel_result.path)
128
+ logger.info(f"āœ“ Panel {i} created")
129
+ else:
130
+ logger.error(f"Panel {i} failed: {panel_result.error}")
131
+
132
+ # Save metadata
133
+ metadata_path = output_dir / "story.json"
134
+ metadata = {
135
+ "concept": state.get("concept", ""),
136
+ "title": story_dict.get("title", ""),
137
+ "narrative": story_dict.get("narrative", ""),
138
+ "character_prompt": character_prompt,
139
+ "character_image": str(character_path),
140
+ "panels": [
141
+ {
142
+ "prompt": panels[i] if i < len(panels) else "",
143
+ "image": image_paths[i + 1] if i + 1 < len(image_paths) else None,
144
+ }
145
+ for i in range(len(panels[:3]))
146
+ ],
147
+ "generated_at": datetime.now().isoformat(),
148
+ }
149
+ metadata_path.write_text(json.dumps(metadata, indent=2))
150
+ logger.info(f"šŸ“ Metadata saved: {metadata_path}")
151
+
152
+ success_count = sum(1 for r in results if r.success)
153
+ logger.info(
154
+ f"āœ… Generated {success_count}/{len(results)} images (1 character + {len(panels[:3])} panels)"
155
+ )
156
+
157
+ return {
158
+ "current_step": "generate_character_storyboard",
159
+ "images": image_paths,
160
+ "character_image": str(character_path),
161
+ "output_dir": str(output_dir),
162
+ }
@@ -0,0 +1,118 @@
1
+ """Storyboard node for generating panel images.
2
+
3
+ This node takes story panels from the LLM and generates images via Replicate.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from .replicate_tool import ImageResult, generate_image
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Type alias for state
19
+ GraphState = dict[str, Any]
20
+
21
+
22
+ def generate_images_node(state: GraphState) -> dict:
23
+ """Generate images for each story panel.
24
+
25
+ Reads panel prompts from state.story and generates images.
26
+ Saves to outputs/storyboard/{thread_id}/ directory.
27
+
28
+ Args:
29
+ state: Graph state with 'story' containing panel prompts
30
+
31
+ Returns:
32
+ State update with 'images' list and metadata
33
+ """
34
+ story = state.get("story")
35
+ if not story:
36
+ logger.error("No story in state")
37
+ return {
38
+ "current_step": "generate_images",
39
+ "images": [],
40
+ "error": "No story panels to generate",
41
+ }
42
+
43
+ # Handle Pydantic model or dict
44
+ if hasattr(story, "model_dump"):
45
+ story_dict = story.model_dump()
46
+ elif isinstance(story, dict):
47
+ story_dict = story
48
+ else:
49
+ story_dict = {"panels": [str(story)]}
50
+
51
+ # Extract panel prompts (supports dynamic list)
52
+ panels = story_dict.get("panels", [])
53
+ if not panels:
54
+ # Fallback for legacy panel_1/2/3 format
55
+ panels = [
56
+ story_dict.get("panel_1", ""),
57
+ story_dict.get("panel_2", ""),
58
+ story_dict.get("panel_3", ""),
59
+ ]
60
+ panels = [p for p in panels if p] # Remove empty
61
+
62
+ # Create output directory
63
+ thread_id = state.get("thread_id", datetime.now().strftime("%Y%m%d_%H%M%S"))
64
+ output_dir = Path("outputs/storyboard") / thread_id
65
+ output_dir.mkdir(parents=True, exist_ok=True)
66
+
67
+ logger.info(f"šŸŽ¬ Generating {len(panels)}-panel storyboard in {output_dir}")
68
+
69
+ # Get model selection from state (default: z-image)
70
+ model_name = state.get("model", "z-image")
71
+ logger.info(f"\ud83d\uddbc\ufe0f Using model: {model_name}")
72
+
73
+ # Generate each panel image
74
+ results: list[ImageResult] = []
75
+ image_paths: list[str] = []
76
+
77
+ for i, prompt in enumerate(panels, 1):
78
+ if not prompt:
79
+ logger.warning(f"Panel {i} has no prompt, skipping")
80
+ continue
81
+
82
+ output_path = output_dir / f"panel_{i}.png"
83
+ logger.info(f"\ud83d\udcf8 Panel {i}: {prompt[:60]}...")
84
+
85
+ result = generate_image(prompt, output_path, model_name=model_name)
86
+ results.append(result)
87
+
88
+ if result.success and result.path:
89
+ image_paths.append(result.path)
90
+ else:
91
+ logger.error(f"Panel {i} failed: {result.error}")
92
+
93
+ # Save story metadata
94
+ metadata_path = output_dir / "story.json"
95
+ metadata = {
96
+ "concept": state.get("concept", ""),
97
+ "title": story_dict.get("title", ""),
98
+ "narrative": story_dict.get("narrative", ""),
99
+ "panels": [
100
+ {
101
+ "prompt": panels[i] if i < len(panels) else "",
102
+ "image": image_paths[i] if i < len(image_paths) else None,
103
+ }
104
+ for i in range(max(len(panels), len(image_paths)))
105
+ ],
106
+ "generated_at": datetime.now().isoformat(),
107
+ }
108
+ metadata_path.write_text(json.dumps(metadata, indent=2))
109
+ logger.info(f"šŸ“ Metadata saved: {metadata_path}")
110
+
111
+ success_count = sum(1 for r in results if r.success)
112
+ logger.info(f"āœ… Generated {success_count}/{len(panels)} images")
113
+
114
+ return {
115
+ "current_step": "generate_images",
116
+ "images": image_paths,
117
+ "output_dir": str(output_dir),
118
+ }
@@ -0,0 +1,49 @@
1
+ """Storyboard-specific wrappers for image generation.
2
+
3
+ Re-exports from examples.shared.replicate_tool and adds storyboard-specific functions.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from examples.shared.replicate_tool import (
11
+ ImageResult,
12
+ edit_image,
13
+ generate_image,
14
+ )
15
+
16
+ # Re-export for backward compatibility
17
+ __all__ = [
18
+ "ImageResult",
19
+ "generate_image",
20
+ "edit_image",
21
+ "generate_storyboard_images",
22
+ ]
23
+
24
+
25
+ def generate_storyboard_images(
26
+ panel_prompts: list[str],
27
+ output_dir: str | Path,
28
+ prefix: str = "panel",
29
+ ) -> list[ImageResult]:
30
+ """Generate multiple images for a storyboard.
31
+
32
+ Args:
33
+ panel_prompts: List of prompts for each panel
34
+ output_dir: Directory to save images
35
+ prefix: Filename prefix
36
+
37
+ Returns:
38
+ List of ImageResult for each panel
39
+ """
40
+ output_dir = Path(output_dir)
41
+ output_dir.mkdir(parents=True, exist_ok=True)
42
+
43
+ results = []
44
+ for i, prompt in enumerate(panel_prompts, 1):
45
+ output_path = output_dir / f"{prefix}_{i}.png"
46
+ result = generate_image(prompt, output_path)
47
+ results.append(result)
48
+
49
+ return results
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+ """Retry image generation from existing animated storyboard metadata.
3
+
4
+ Usage:
5
+ python examples/storyboard/retry_images.py outputs/storyboard/20260117_112419/animated
6
+
7
+ Options:
8
+ --model MODEL Image model to use (default: hidream)
9
+ --reference PATH Override reference image path
10
+ --magic FLOAT Magic value for img2img (default: 0.25)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # Add project root to path for imports
21
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
22
+
23
+ from examples.storyboard.nodes.animated_character_node import (
24
+ generate_animated_character_images,
25
+ )
26
+
27
+
28
+ def main():
29
+ parser = argparse.ArgumentParser(
30
+ description="Retry image generation from existing metadata"
31
+ )
32
+ parser.add_argument(
33
+ "output_dir",
34
+ type=Path,
35
+ help="Path to animated output directory (contains animated_character_story.json)",
36
+ )
37
+ parser.add_argument(
38
+ "--model",
39
+ default="hidream",
40
+ help="Image model to use (default: hidream)",
41
+ )
42
+ parser.add_argument(
43
+ "--reference",
44
+ type=Path,
45
+ help="Override reference image path",
46
+ )
47
+ parser.add_argument(
48
+ "--new-id",
49
+ help="New thread ID for output (default: adds _retry suffix)",
50
+ )
51
+ args = parser.parse_args()
52
+
53
+ # Find metadata file
54
+ output_dir = args.output_dir
55
+ if output_dir.name != "animated":
56
+ output_dir = output_dir / "animated"
57
+
58
+ metadata_path = output_dir / "animated_character_story.json"
59
+ if not metadata_path.exists():
60
+ print(f"āŒ Metadata not found: {metadata_path}")
61
+ sys.exit(1)
62
+
63
+ print(f"šŸ“‚ Loading: {metadata_path}")
64
+ metadata = json.loads(metadata_path.read_text())
65
+
66
+ # Reconstruct animated_panels from metadata
67
+ animated_panels = []
68
+ for panel in metadata["panels"]:
69
+ animated_panels.append(
70
+ {
71
+ "_map_index": panel["index"] - 1,
72
+ **panel["prompts"],
73
+ }
74
+ )
75
+
76
+ # Determine thread_id
77
+ original_thread = output_dir.parent.name
78
+ thread_id = args.new_id or f"{original_thread}_retry"
79
+
80
+ # Build state
81
+ state = {
82
+ "concept": metadata["concept"],
83
+ "model": args.model,
84
+ "thread_id": thread_id,
85
+ "story": {
86
+ "title": metadata["title"],
87
+ "narrative": metadata["narrative"],
88
+ "character_prompt": metadata["character_prompt"],
89
+ },
90
+ "animated_panels": animated_panels,
91
+ }
92
+
93
+ # Handle reference image
94
+ if args.reference:
95
+ state["reference_image"] = str(args.reference)
96
+ elif metadata.get("reference_image"):
97
+ ref_path = Path(metadata["reference_image"])
98
+ if ref_path.exists():
99
+ state["reference_image"] = str(ref_path)
100
+ print(f"šŸŽ­ Using existing reference: {ref_path}")
101
+
102
+ print("šŸŽ¬ Retrying image generation...")
103
+ print(f" Model: {args.model}")
104
+ print(f" Panels: {len(animated_panels)}")
105
+ print(f" Output: outputs/storyboard/{thread_id}/animated/")
106
+
107
+ # Run image generation
108
+ result = generate_animated_character_images(state)
109
+
110
+ if result.get("error"):
111
+ print(f"āŒ Error: {result['error']}")
112
+ sys.exit(1)
113
+
114
+ print(f"\nāœ… Generated images in: {result['output_dir']}")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()