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,230 @@
1
+ #!/usr/bin/env python3
2
+ """Run a multi-turn encounter with multiple NPCs.
3
+
4
+ This script demonstrates the map node feature for parallel NPC processing.
5
+ Each turn, ALL NPCs perceive, decide, and act in response to DM input.
6
+
7
+ Usage:
8
+ python examples/npc/run_encounter.py
9
+
10
+ # With pre-created NPCs
11
+ python examples/npc/run_encounter.py --npc-dir npcs/
12
+ """
13
+
14
+ import argparse
15
+ import uuid
16
+ from pathlib import Path
17
+
18
+ import yaml
19
+ from langgraph.types import Command
20
+
21
+ from yamlgraph.graph_loader import (
22
+ compile_graph,
23
+ get_checkpointer_for_graph,
24
+ load_graph_config,
25
+ )
26
+
27
+
28
+ # ANSI colors
29
+ class C:
30
+ BOLD = "\033[1m"
31
+ DIM = "\033[2m"
32
+ CYAN = "\033[36m"
33
+ GREEN = "\033[32m"
34
+ YELLOW = "\033[33m"
35
+ MAGENTA = "\033[35m"
36
+ RED = "\033[31m"
37
+ RESET = "\033[0m"
38
+
39
+
40
+ def load_npcs(npc_dir: str | None) -> list[dict]:
41
+ """Load NPC data from directory or use defaults."""
42
+ if npc_dir:
43
+ npc_path = Path(npc_dir)
44
+ if npc_path.exists():
45
+ npcs = []
46
+ for f in npc_path.glob("*.yaml"):
47
+ with open(f) as fp:
48
+ data = yaml.safe_load(fp)
49
+ identity = data.get("identity", {})
50
+ personality = data.get("personality", {})
51
+ behavior = data.get("behavior", {})
52
+ # Build structured NPC dict
53
+ npc = {
54
+ "name": identity.get("name", f.stem),
55
+ "appearance": identity.get("appearance", ""),
56
+ "voice": identity.get("voice", ""),
57
+ "race": identity.get("race", ""),
58
+ "character_class": identity.get("character_class", ""),
59
+ "personality": personality, # Keep as dict
60
+ "behavior": behavior, # Keep as dict
61
+ "goals": behavior.get("goals", []), # Keep as list
62
+ }
63
+ npcs.append(npc)
64
+ if npcs:
65
+ print(f"{C.GREEN}✓ Loaded {len(npcs)} NPCs from {npc_dir}{C.RESET}")
66
+ return npcs
67
+
68
+ # Default NPCs for demo
69
+ return [
70
+ {
71
+ "name": "Thorin Ironfoot",
72
+ "appearance": "A stocky dwarf with burn scars and a copper beard",
73
+ "voice": "Gruff, low rumble",
74
+ "personality": {
75
+ "traits": ["Stoic", "Hardworking", "Practical"],
76
+ "ideals": ["Quality craftsmanship", "Honest trade"],
77
+ "flaws": ["Distrustful of magic", "Stubborn"],
78
+ "disposition": "neutral",
79
+ },
80
+ "behavior": {
81
+ "combat_style": "defensive",
82
+ "goals": ["Run a successful smithy", "Find an apprentice"],
83
+ },
84
+ "goals": ["Run a successful smithy", "Find an apprentice"],
85
+ "race": "Dwarf",
86
+ "character_class": "Blacksmith",
87
+ },
88
+ {
89
+ "name": "Lyra Whisperwind",
90
+ "appearance": "A lithe elf with silver hair and piercing blue eyes",
91
+ "voice": "Melodic, speaks in riddles",
92
+ "personality": {
93
+ "traits": ["Curious", "Mischievous", "Observant"],
94
+ "ideals": ["Truth", "Knowledge"],
95
+ "flaws": ["Overly secretive", "Distrustful"],
96
+ "disposition": "curious",
97
+ },
98
+ "behavior": {
99
+ "combat_style": "ranged",
100
+ "goals": ["Uncover hidden truths", "Protect the ancient grove"],
101
+ },
102
+ "goals": ["Uncover hidden truths", "Protect the ancient grove"],
103
+ "race": "Elf",
104
+ "character_class": "Ranger",
105
+ },
106
+ {
107
+ "name": "Marcus the Bold",
108
+ "appearance": "A burly human with a thick black beard and battle scars",
109
+ "voice": "Booming laugh, speaks loudly",
110
+ "personality": {
111
+ "traits": ["Jovial", "Brave", "Loyal"],
112
+ "ideals": ["Glory", "Protecting the weak"],
113
+ "flaws": ["Reckless", "Overconfident"],
114
+ "disposition": "friendly",
115
+ },
116
+ "behavior": {
117
+ "combat_style": "aggressive",
118
+ "goals": ["Find glory in battle", "Earn gold for his family"],
119
+ },
120
+ "goals": ["Find glory in battle", "Earn gold for his family"],
121
+ "race": "Human",
122
+ "character_class": "Fighter",
123
+ },
124
+ ]
125
+
126
+
127
+ def run_encounter():
128
+ """Run the interactive multi-NPC encounter."""
129
+ parser = argparse.ArgumentParser(description="Multi-NPC Encounter")
130
+ parser.add_argument("--npc-dir", "-n", help="Directory with NPC YAML files")
131
+ parser.add_argument(
132
+ "--location", "-l", default="The Rusty Anchor tavern", help="Location name"
133
+ )
134
+ args = parser.parse_args()
135
+
136
+ print(f"\n{C.BOLD}{'=' * 60}{C.RESET}")
137
+ print(f"{C.BOLD}⚔️ YAMLGraph Multi-NPC Encounter{C.RESET}")
138
+ print(f"{C.BOLD}{'=' * 60}{C.RESET}\n")
139
+
140
+ # Load graph
141
+ config = load_graph_config("examples/npc/encounter-multi.yaml")
142
+ graph = compile_graph(config)
143
+ checkpointer = get_checkpointer_for_graph(config)
144
+ app = graph.compile(checkpointer=checkpointer)
145
+
146
+ # Session setup
147
+ thread_id = str(uuid.uuid4())
148
+ run_config = {"configurable": {"thread_id": thread_id}}
149
+
150
+ # Initial state
151
+ npcs = load_npcs(args.npc_dir)
152
+ initial_state = {
153
+ "npcs": npcs,
154
+ "location": args.location,
155
+ "location_description": "A dimly lit tavern with rough wooden tables and the smell of ale",
156
+ "turn_number": 1,
157
+ "encounter_history": [],
158
+ "perceptions": [],
159
+ "decisions": [],
160
+ "narrations": [],
161
+ }
162
+
163
+ print(f"{C.CYAN}📍 Location: {args.location}{C.RESET}")
164
+ print(f"{C.CYAN}🎭 NPCs present:{C.RESET}")
165
+ for npc in npcs:
166
+ print(
167
+ f" - {C.BOLD}{npc['name']}{C.RESET} ({npc['race']} {npc['character_class']})"
168
+ )
169
+ print(f"\n{C.DIM}Type 'end' to finish the encounter, 'quit' to exit.{C.RESET}\n")
170
+
171
+ # Start the graph
172
+ result = app.invoke(initial_state, run_config)
173
+ turn = 1
174
+
175
+ while True:
176
+ # Get interrupt message
177
+ state = app.get_state(run_config)
178
+ next_nodes = state.next if hasattr(state, "next") else []
179
+
180
+ if not next_nodes:
181
+ print(f"\n{C.GREEN}✓ Encounter complete!{C.RESET}")
182
+ break
183
+
184
+ # Show prompt
185
+ print(f"\n{C.MAGENTA}{'─' * 50}{C.RESET}")
186
+ print(f"{C.YELLOW}🎲 Turn {turn} - What happens?{C.RESET}")
187
+
188
+ # Get user input
189
+ try:
190
+ dm_input = input(f"{C.CYAN}DM> {C.RESET}").strip()
191
+ except (EOFError, KeyboardInterrupt):
192
+ print(f"\n{C.YELLOW}Encounter ended by user.{C.RESET}")
193
+ break
194
+
195
+ if dm_input.lower() == "quit":
196
+ print(f"\n{C.YELLOW}Farewell, Dungeon Master!{C.RESET}")
197
+ break
198
+
199
+ if not dm_input:
200
+ print(f"{C.DIM}(Describe what happens in the scene){C.RESET}")
201
+ continue
202
+
203
+ # Resume with DM input
204
+ print(f"\n{C.DIM}Processing all NPCs...{C.RESET}")
205
+ result = app.invoke(Command(resume=dm_input), run_config)
206
+
207
+ # Show results
208
+ if result.get("turn_summary"):
209
+ print(f"\n{C.GREEN}📜 Turn Summary:{C.RESET}")
210
+ print(result["turn_summary"])
211
+
212
+ if result.get("scene_image"):
213
+ print(f"\n{C.CYAN}🖼️ Image saved: {result['scene_image']}{C.RESET}")
214
+
215
+ if dm_input.lower() == "end":
216
+ print(f"\n{C.GREEN}✓ Encounter ended!{C.RESET}")
217
+ # Show history
218
+ history = result.get("encounter_history", [])
219
+ if history:
220
+ print(f"\n{C.CYAN}📚 Encounter History:{C.RESET}")
221
+ for i, summary in enumerate(history, 1):
222
+ print(f"\n{C.DIM}Turn {i}:{C.RESET}")
223
+ print(summary[:300] + "..." if len(summary) > 300 else summary)
224
+ break
225
+
226
+ turn += 1
227
+
228
+
229
+ if __name__ == "__main__":
230
+ run_encounter()
File without changes
@@ -0,0 +1,238 @@
1
+ """Replicate image generation tool for storyboard workflow.
2
+
3
+ Supports multiple models:
4
+ - z-image: Fast, good for realistic/photographic (default)
5
+ - hidream: Better for cartoons, illustrations, stylized art
6
+ - p-image-edit: Image-to-image editing for character consistency
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ import httpx
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Model configurations
21
+ MODELS = {
22
+ "z-image": {
23
+ "id": "prunaai/z-image-turbo",
24
+ "width": 1344,
25
+ "height": 768,
26
+ "params": {
27
+ "guidance_scale": 0,
28
+ "num_inference_steps": 8,
29
+ },
30
+ },
31
+ "hidream": {
32
+ "id": "prunaai/hidream-l1-fast:f67f0ec7ef9fe91b74e8a68d34efaa9145bec28675cb190cbff8a70f0490256e",
33
+ "resolution": "1360 \u00d7 768 (Landscape)",
34
+ "params": {
35
+ "model_type": "fast",
36
+ "speed_mode": "Juiced \U0001f525 (more speed)",
37
+ },
38
+ },
39
+ }
40
+
41
+ DEFAULT_MODEL = "z-image"
42
+
43
+ # Check if replicate is available
44
+ try:
45
+ import replicate
46
+
47
+ REPLICATE_AVAILABLE = True
48
+ except ImportError:
49
+ REPLICATE_AVAILABLE = False
50
+ logger.warning("replicate package not installed. Run: pip install replicate")
51
+
52
+
53
+ @dataclass
54
+ class ImageResult:
55
+ """Result from image generation."""
56
+
57
+ success: bool
58
+ path: str | None = None
59
+ error: str | None = None
60
+
61
+
62
+ def generate_image(
63
+ prompt: str,
64
+ output_path: str | Path,
65
+ model_name: str = DEFAULT_MODEL,
66
+ ) -> ImageResult:
67
+ """Generate an image using Replicate API.
68
+
69
+ Args:
70
+ prompt: Text prompt for image generation
71
+ output_path: Path to save the generated image
72
+ model_name: Model to use ('z-image' or 'hidream')
73
+
74
+ Returns:
75
+ ImageResult with success status and path or error
76
+ """
77
+ if not REPLICATE_AVAILABLE:
78
+ return ImageResult(success=False, error="replicate package not installed")
79
+
80
+ api_token = os.environ.get("REPLICATE_API_TOKEN")
81
+ if not api_token:
82
+ return ImageResult(
83
+ success=False, error="REPLICATE_API_TOKEN not set in environment"
84
+ )
85
+
86
+ # Get model config
87
+ model_config = MODELS.get(model_name, MODELS[DEFAULT_MODEL])
88
+ model_id = model_config["id"]
89
+
90
+ output_path = Path(output_path)
91
+ output_path.parent.mkdir(parents=True, exist_ok=True)
92
+
93
+ try:
94
+ logger.info(f"🎨 Generating image with {model_name}: {prompt[:50]}...")
95
+
96
+ # Build input params based on model
97
+ if model_name == "hidream":
98
+ input_params = {
99
+ "prompt": prompt,
100
+ "seed": -1,
101
+ "resolution": model_config["resolution"],
102
+ "output_format": "png",
103
+ "output_quality": 80,
104
+ "disable_safety_checker": True,
105
+ **model_config["params"],
106
+ }
107
+ else:
108
+ # z-image and default
109
+ input_params = {
110
+ "prompt": prompt,
111
+ "width": model_config.get("width", 1344),
112
+ "height": model_config.get("height", 768),
113
+ "output_format": "png",
114
+ "output_quality": 80,
115
+ "disable_safety_checker": True,
116
+ **model_config.get("params", {}),
117
+ }
118
+
119
+ # Run the model
120
+ client = replicate.Client(api_token=api_token)
121
+ output = client.run(model_id, input=input_params)
122
+
123
+ # output is typically a URL or file-like object
124
+ image_url = output if isinstance(output, str) else str(output)
125
+
126
+ # Download the image
127
+ logger.info(f"📥 Downloading image to {output_path}")
128
+ response = httpx.get(image_url, timeout=60.0)
129
+ response.raise_for_status()
130
+
131
+ output_path.write_bytes(response.content)
132
+ logger.info(f"✓ Image saved: {output_path}")
133
+
134
+ return ImageResult(success=True, path=str(output_path))
135
+
136
+ except Exception as e:
137
+ logger.error(f"Image generation failed: {e}")
138
+ return ImageResult(success=False, error=str(e))
139
+
140
+
141
+ def edit_image(
142
+ input_image: str | Path,
143
+ prompt: str,
144
+ output_path: str | Path,
145
+ aspect_ratio: str = "16:9",
146
+ turbo: bool = True,
147
+ magic: float | None = None,
148
+ ) -> ImageResult:
149
+ """Edit an image using Replicate p-image-edit model.
150
+
151
+ Uses the input image as base and applies the prompt as modifications.
152
+ Great for maintaining character consistency across panels.
153
+
154
+ Args:
155
+ input_image: Path to the source image
156
+ prompt: Edit instructions (what to change/add)
157
+ output_path: Path to save the edited image
158
+ aspect_ratio: Output aspect ratio (default 16:9)
159
+ turbo: Use turbo mode for faster generation
160
+ magic: Prompt strength 0-1 (lower = more original, higher = more prompt)
161
+
162
+ Returns:
163
+ ImageResult with success status and path or error
164
+ """
165
+ if not REPLICATE_AVAILABLE:
166
+ return ImageResult(success=False, error="replicate package not installed")
167
+
168
+ api_token = os.environ.get("REPLICATE_API_TOKEN")
169
+ if not api_token:
170
+ return ImageResult(
171
+ success=False, error="REPLICATE_API_TOKEN not set in environment"
172
+ )
173
+
174
+ input_image = Path(input_image)
175
+ if not input_image.exists():
176
+ return ImageResult(success=False, error=f"Input image not found: {input_image}")
177
+
178
+ output_path = Path(output_path)
179
+ output_path.parent.mkdir(parents=True, exist_ok=True)
180
+
181
+ try:
182
+ logger.info(f"✏️ Editing image: {prompt[:50]}...")
183
+
184
+ client = replicate.Client(api_token=api_token)
185
+
186
+ with open(input_image, "rb") as f:
187
+ input_params = {
188
+ "turbo": turbo,
189
+ "images": [f],
190
+ "prompt": prompt,
191
+ "aspect_ratio": aspect_ratio,
192
+ "disable_safety_checker": True,
193
+ }
194
+ if magic is not None:
195
+ input_params["magic"] = magic
196
+
197
+ output = client.run(
198
+ "prunaai/p-image-edit",
199
+ input=input_params,
200
+ )
201
+
202
+ # Save the output
203
+ with open(output_path, "wb") as out:
204
+ out.write(output.read())
205
+
206
+ logger.info(f"✓ Edited image saved: {output_path}")
207
+ return ImageResult(success=True, path=str(output_path))
208
+
209
+ except Exception as e:
210
+ logger.error(f"Image editing failed: {e}")
211
+ return ImageResult(success=False, error=str(e))
212
+
213
+
214
+ def generate_storyboard_images(
215
+ panel_prompts: list[str],
216
+ output_dir: str | Path,
217
+ prefix: str = "panel",
218
+ ) -> list[ImageResult]:
219
+ """Generate multiple images for a storyboard.
220
+
221
+ Args:
222
+ panel_prompts: List of prompts for each panel
223
+ output_dir: Directory to save images
224
+ prefix: Filename prefix
225
+
226
+ Returns:
227
+ List of ImageResult for each panel
228
+ """
229
+ output_dir = Path(output_dir)
230
+ output_dir.mkdir(parents=True, exist_ok=True)
231
+
232
+ results = []
233
+ for i, prompt in enumerate(panel_prompts, 1):
234
+ output_path = output_dir / f"{prefix}_{i}.png"
235
+ result = generate_image(prompt, output_path)
236
+ results.append(result)
237
+
238
+ return results
@@ -0,0 +1 @@
1
+ """Storyboard example - demonstrates Python node for image generation."""