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,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()
|