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.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- 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."""
|