@weirdfingers/baseboards 0.6.1 → 0.7.0
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.
- package/dist/index.js +54 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +2 -0
- package/templates/api/.env.example +3 -0
- package/templates/api/config/generators.yaml +58 -0
- package/templates/api/pyproject.toml +1 -1
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/api/endpoints/storage.py +85 -4
- package/templates/api/src/boards/api/endpoints/uploads.py +1 -2
- package/templates/api/src/boards/database/connection.py +98 -58
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +176 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +195 -0
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +14 -0
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +219 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +208 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +216 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +177 -0
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +178 -0
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +155 -0
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +180 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +18 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +168 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +159 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +180 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +190 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +190 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +191 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +13 -6
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +212 -0
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +208 -0
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +11 -0
- package/templates/api/src/boards/generators/implementations/kie/base.py +316 -0
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +190 -0
- package/templates/api/src/boards/generators/implementations/kie/utils.py +98 -0
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +8 -0
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +161 -0
- package/templates/api/src/boards/graphql/resolvers/upload.py +1 -1
- package/templates/web/package.json +4 -1
- package/templates/web/src/app/boards/[boardId]/page.tsx +156 -24
- package/templates/web/src/app/globals.css +3 -0
- package/templates/web/src/app/layout.tsx +15 -5
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +9 -9
- package/templates/web/src/components/boards/ArtifactPreview.tsx +34 -18
- package/templates/web/src/components/boards/GenerationGrid.tsx +101 -7
- package/templates/web/src/components/boards/GenerationInput.tsx +21 -21
- package/templates/web/src/components/boards/GeneratorSelector.tsx +232 -30
- package/templates/web/src/components/boards/UploadArtifact.tsx +385 -75
- package/templates/web/src/components/header.tsx +3 -1
- package/templates/web/src/components/theme-provider.tsx +10 -0
- package/templates/web/src/components/theme-toggle.tsx +75 -0
- package/templates/web/src/components/ui/alert-dialog.tsx +157 -0
- package/templates/web/src/components/ui/toast.tsx +128 -0
- package/templates/web/src/components/ui/toaster.tsx +35 -0
- package/templates/web/src/components/ui/use-toast.ts +186 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Veo 3.1 Fast text-to-video generator.
|
|
3
|
+
|
|
4
|
+
A faster, more cost-effective variant of Google's Veo 3.1 video generation model,
|
|
5
|
+
capable of generating high-quality videos from text prompts with optional audio synthesis.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's fal-ai/veo3.1/fast model.
|
|
8
|
+
See: https://fal.ai/models/fal-ai/veo3.1/fast
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Veo31FastInput(BaseModel):
|
|
20
|
+
"""Input schema for Google Veo 3.1 Fast text-to-video generation."""
|
|
21
|
+
|
|
22
|
+
prompt: str = Field(description="The text prompt describing the video you want to generate")
|
|
23
|
+
aspect_ratio: Literal["9:16", "16:9"] = Field(
|
|
24
|
+
default="16:9",
|
|
25
|
+
description="Aspect ratio of the generated video",
|
|
26
|
+
)
|
|
27
|
+
duration: Literal["4s", "6s", "8s"] = Field(
|
|
28
|
+
default="8s",
|
|
29
|
+
description="Duration of the generated video",
|
|
30
|
+
)
|
|
31
|
+
resolution: Literal["720p", "1080p"] = Field(
|
|
32
|
+
default="720p",
|
|
33
|
+
description="Resolution of the generated video",
|
|
34
|
+
)
|
|
35
|
+
generate_audio: bool = Field(
|
|
36
|
+
default=True,
|
|
37
|
+
description="Whether to generate audio for the video. If false, 33% less credits used",
|
|
38
|
+
)
|
|
39
|
+
enhance_prompt: bool = Field(
|
|
40
|
+
default=True,
|
|
41
|
+
description="Whether to enhance video generation",
|
|
42
|
+
)
|
|
43
|
+
auto_fix: bool = Field(
|
|
44
|
+
default=True,
|
|
45
|
+
description="Automatically attempt to fix prompts that fail content policy",
|
|
46
|
+
)
|
|
47
|
+
seed: int | None = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
description="Seed value for reproducible generation",
|
|
50
|
+
)
|
|
51
|
+
negative_prompt: str | None = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description="Guidance text to exclude from generation",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FalVeo31FastGenerator(BaseGenerator):
|
|
58
|
+
"""Generator for text-to-video using Google Veo 3.1 Fast."""
|
|
59
|
+
|
|
60
|
+
name = "fal-veo31-fast"
|
|
61
|
+
description = "Fal: Veo 3.1 Fast - Google's fast AI video generation model"
|
|
62
|
+
artifact_type = "video"
|
|
63
|
+
|
|
64
|
+
def get_input_schema(self) -> type[Veo31FastInput]:
|
|
65
|
+
"""Return the input schema for this generator."""
|
|
66
|
+
return Veo31FastInput
|
|
67
|
+
|
|
68
|
+
async def generate(
|
|
69
|
+
self, inputs: Veo31FastInput, context: GeneratorExecutionContext
|
|
70
|
+
) -> GeneratorResult:
|
|
71
|
+
"""Generate video using fal.ai veo3.1/fast."""
|
|
72
|
+
# Check for API key
|
|
73
|
+
if not os.getenv("FAL_KEY"):
|
|
74
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
75
|
+
|
|
76
|
+
# Import fal_client
|
|
77
|
+
try:
|
|
78
|
+
import fal_client
|
|
79
|
+
except ImportError as e:
|
|
80
|
+
raise ImportError(
|
|
81
|
+
"fal.ai SDK is required for FalVeo31FastGenerator. "
|
|
82
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
83
|
+
) from e
|
|
84
|
+
|
|
85
|
+
# Prepare arguments for fal.ai API
|
|
86
|
+
arguments = {
|
|
87
|
+
"prompt": inputs.prompt,
|
|
88
|
+
"aspect_ratio": inputs.aspect_ratio,
|
|
89
|
+
"duration": inputs.duration,
|
|
90
|
+
"resolution": inputs.resolution,
|
|
91
|
+
"generate_audio": inputs.generate_audio,
|
|
92
|
+
"enhance_prompt": inputs.enhance_prompt,
|
|
93
|
+
"auto_fix": inputs.auto_fix,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Add optional parameters if provided
|
|
97
|
+
if inputs.seed is not None:
|
|
98
|
+
arguments["seed"] = inputs.seed
|
|
99
|
+
if inputs.negative_prompt is not None:
|
|
100
|
+
arguments["negative_prompt"] = inputs.negative_prompt
|
|
101
|
+
|
|
102
|
+
# Submit async job
|
|
103
|
+
handler = await fal_client.submit_async(
|
|
104
|
+
"fal-ai/veo3.1/fast",
|
|
105
|
+
arguments=arguments,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Store external job ID
|
|
109
|
+
await context.set_external_job_id(handler.request_id)
|
|
110
|
+
|
|
111
|
+
# Stream progress updates
|
|
112
|
+
from .....progress.models import ProgressUpdate
|
|
113
|
+
|
|
114
|
+
event_count = 0
|
|
115
|
+
async for event in handler.iter_events(with_logs=True):
|
|
116
|
+
event_count += 1
|
|
117
|
+
# Sample every 3rd event to avoid spam
|
|
118
|
+
if event_count % 3 == 0:
|
|
119
|
+
# Extract logs if available
|
|
120
|
+
logs = getattr(event, "logs", None)
|
|
121
|
+
if logs:
|
|
122
|
+
# Join log entries into a single message
|
|
123
|
+
if isinstance(logs, list):
|
|
124
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
125
|
+
else:
|
|
126
|
+
message = str(logs)
|
|
127
|
+
|
|
128
|
+
if message:
|
|
129
|
+
await context.publish_progress(
|
|
130
|
+
ProgressUpdate(
|
|
131
|
+
job_id=handler.request_id,
|
|
132
|
+
status="processing",
|
|
133
|
+
progress=50.0,
|
|
134
|
+
phase="processing",
|
|
135
|
+
message=message,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Get final result
|
|
140
|
+
result = await handler.get()
|
|
141
|
+
|
|
142
|
+
# Extract video from result
|
|
143
|
+
# Expected structure: {"video": {"url": "...", "content_type": "...", ...}}
|
|
144
|
+
video_data = result.get("video")
|
|
145
|
+
if not video_data:
|
|
146
|
+
raise ValueError("No video returned from fal.ai API")
|
|
147
|
+
|
|
148
|
+
video_url = video_data.get("url")
|
|
149
|
+
if not video_url:
|
|
150
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
151
|
+
|
|
152
|
+
# Determine video dimensions based on resolution and aspect ratio
|
|
153
|
+
if inputs.resolution == "720p":
|
|
154
|
+
if inputs.aspect_ratio == "16:9":
|
|
155
|
+
width, height = 1280, 720
|
|
156
|
+
else: # 9:16
|
|
157
|
+
width, height = 720, 1280
|
|
158
|
+
else: # 1080p
|
|
159
|
+
if inputs.aspect_ratio == "16:9":
|
|
160
|
+
width, height = 1920, 1080
|
|
161
|
+
else: # 9:16
|
|
162
|
+
width, height = 1080, 1920
|
|
163
|
+
|
|
164
|
+
# Parse duration from "8s" format
|
|
165
|
+
duration_seconds = int(inputs.duration.rstrip("s"))
|
|
166
|
+
|
|
167
|
+
# Store video result
|
|
168
|
+
artifact = await context.store_video_result(
|
|
169
|
+
storage_url=video_url,
|
|
170
|
+
format="mp4",
|
|
171
|
+
width=width,
|
|
172
|
+
height=height,
|
|
173
|
+
duration=duration_seconds,
|
|
174
|
+
output_index=0,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return GeneratorResult(outputs=[artifact])
|
|
178
|
+
|
|
179
|
+
async def estimate_cost(self, inputs: Veo31FastInput) -> float:
|
|
180
|
+
"""Estimate cost for this generation in USD.
|
|
181
|
+
|
|
182
|
+
Note: Pricing information not available in Fal documentation.
|
|
183
|
+
Using placeholder value that should be updated with actual pricing.
|
|
184
|
+
"""
|
|
185
|
+
# TODO: Update with actual pricing from Fal when available
|
|
186
|
+
# Base cost, with 33% reduction if audio is disabled
|
|
187
|
+
base_cost = 0.10 # Placeholder estimate for fast variant
|
|
188
|
+
if not inputs.generate_audio:
|
|
189
|
+
return base_cost * 0.67 # 33% discount
|
|
190
|
+
return base_cost
|
package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Veo 3.1 Fast image-to-video generator.
|
|
3
|
+
|
|
4
|
+
Converts static images into animated videos based on text prompts using
|
|
5
|
+
Google's Veo 3.1 Fast technology via fal.ai. This is a faster version
|
|
6
|
+
with per-second pricing.
|
|
7
|
+
|
|
8
|
+
Based on Fal AI's fal-ai/veo3.1/fast/image-to-video model.
|
|
9
|
+
See: https://fal.ai/models/fal-ai/veo3.1/fast/image-to-video
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from ....artifacts import ImageArtifact
|
|
18
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Veo31FastImageToVideoInput(BaseModel):
|
|
22
|
+
"""Input schema for Veo 3.1 Fast image-to-video generation.
|
|
23
|
+
|
|
24
|
+
Artifact fields (image) are automatically detected via type introspection
|
|
25
|
+
and resolved from generation IDs to ImageArtifact objects.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
prompt: str = Field(description="Text prompt describing the desired video content and motion")
|
|
29
|
+
image: ImageArtifact = Field(
|
|
30
|
+
description="Input image to animate. Should be 720p or higher in 16:9 or 9:16 aspect ratio"
|
|
31
|
+
)
|
|
32
|
+
aspect_ratio: Literal["auto", "9:16", "16:9"] = Field(
|
|
33
|
+
default="auto",
|
|
34
|
+
description="Aspect ratio of the generated video. "
|
|
35
|
+
"'auto' automatically detects from input image",
|
|
36
|
+
)
|
|
37
|
+
duration: Literal["4s", "6s", "8s"] = Field(
|
|
38
|
+
default="8s",
|
|
39
|
+
description="Duration of the generated video in seconds",
|
|
40
|
+
)
|
|
41
|
+
generate_audio: bool = Field(
|
|
42
|
+
default=True,
|
|
43
|
+
description="Whether to generate audio for the video. Disabling reduces cost by ~33%",
|
|
44
|
+
)
|
|
45
|
+
resolution: Literal["720p", "1080p"] = Field(
|
|
46
|
+
default="720p",
|
|
47
|
+
description="Resolution of the generated video",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FalVeo31FastImageToVideoGenerator(BaseGenerator):
|
|
52
|
+
"""Generator for creating videos from static images using Google Veo 3.1 Fast."""
|
|
53
|
+
|
|
54
|
+
name = "fal-veo31-fast-image-to-video"
|
|
55
|
+
description = "Fal: Veo 3.1 Fast - Convert images to videos with text-guided animation"
|
|
56
|
+
artifact_type = "video"
|
|
57
|
+
|
|
58
|
+
def get_input_schema(self) -> type[Veo31FastImageToVideoInput]:
|
|
59
|
+
"""Return the input schema for this generator."""
|
|
60
|
+
return Veo31FastImageToVideoInput
|
|
61
|
+
|
|
62
|
+
async def generate(
|
|
63
|
+
self, inputs: Veo31FastImageToVideoInput, context: GeneratorExecutionContext
|
|
64
|
+
) -> GeneratorResult:
|
|
65
|
+
"""Generate video using fal.ai veo3.1/fast/image-to-video."""
|
|
66
|
+
# Check for API key
|
|
67
|
+
if not os.getenv("FAL_KEY"):
|
|
68
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
69
|
+
|
|
70
|
+
# Import fal_client
|
|
71
|
+
try:
|
|
72
|
+
import fal_client
|
|
73
|
+
except ImportError as e:
|
|
74
|
+
raise ImportError(
|
|
75
|
+
"fal.ai SDK is required for FalVeo31FastImageToVideoGenerator. "
|
|
76
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
77
|
+
) from e
|
|
78
|
+
|
|
79
|
+
# Upload image artifact to Fal's public storage
|
|
80
|
+
# Fal API requires publicly accessible URLs, but our storage_url might be:
|
|
81
|
+
# - Localhost URLs (not publicly accessible)
|
|
82
|
+
# - Private S3 buckets (not publicly accessible)
|
|
83
|
+
# So we upload to Fal's temporary storage first
|
|
84
|
+
from ..utils import upload_artifacts_to_fal
|
|
85
|
+
|
|
86
|
+
image_urls = await upload_artifacts_to_fal([inputs.image], context)
|
|
87
|
+
|
|
88
|
+
# Prepare arguments for fal.ai API
|
|
89
|
+
arguments = {
|
|
90
|
+
"prompt": inputs.prompt,
|
|
91
|
+
"image_url": image_urls[0],
|
|
92
|
+
"aspect_ratio": inputs.aspect_ratio,
|
|
93
|
+
"duration": inputs.duration,
|
|
94
|
+
"generate_audio": inputs.generate_audio,
|
|
95
|
+
"resolution": inputs.resolution,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Submit async job
|
|
99
|
+
handler = await fal_client.submit_async(
|
|
100
|
+
"fal-ai/veo3.1/fast/image-to-video",
|
|
101
|
+
arguments=arguments,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Store external job ID
|
|
105
|
+
await context.set_external_job_id(handler.request_id)
|
|
106
|
+
|
|
107
|
+
# Stream progress updates
|
|
108
|
+
from .....progress.models import ProgressUpdate
|
|
109
|
+
|
|
110
|
+
event_count = 0
|
|
111
|
+
async for event in handler.iter_events(with_logs=True):
|
|
112
|
+
event_count += 1
|
|
113
|
+
# Sample every 3rd event to avoid spam
|
|
114
|
+
if event_count % 3 == 0:
|
|
115
|
+
# Extract logs if available
|
|
116
|
+
logs = getattr(event, "logs", None)
|
|
117
|
+
if logs:
|
|
118
|
+
# Join log entries into a single message
|
|
119
|
+
if isinstance(logs, list):
|
|
120
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
121
|
+
else:
|
|
122
|
+
message = str(logs)
|
|
123
|
+
|
|
124
|
+
if message:
|
|
125
|
+
await context.publish_progress(
|
|
126
|
+
ProgressUpdate(
|
|
127
|
+
job_id=handler.request_id,
|
|
128
|
+
status="processing",
|
|
129
|
+
progress=50.0,
|
|
130
|
+
phase="processing",
|
|
131
|
+
message=message,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Get final result
|
|
136
|
+
result = await handler.get()
|
|
137
|
+
|
|
138
|
+
# Extract video from result
|
|
139
|
+
# Expected structure: {"video": {"url": "...", "content_type": "...", ...}}
|
|
140
|
+
video_data = result.get("video")
|
|
141
|
+
if not video_data:
|
|
142
|
+
raise ValueError("No video returned from fal.ai API")
|
|
143
|
+
|
|
144
|
+
video_url = video_data.get("url")
|
|
145
|
+
if not video_url:
|
|
146
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
147
|
+
|
|
148
|
+
# Calculate video dimensions based on resolution and aspect ratio
|
|
149
|
+
# For "auto" aspect ratio, assume 16:9 as the most common format
|
|
150
|
+
effective_aspect_ratio = inputs.aspect_ratio if inputs.aspect_ratio != "auto" else "16:9"
|
|
151
|
+
|
|
152
|
+
if inputs.resolution == "720p":
|
|
153
|
+
if effective_aspect_ratio == "16:9":
|
|
154
|
+
width, height = 1280, 720
|
|
155
|
+
else: # 9:16
|
|
156
|
+
width, height = 720, 1280
|
|
157
|
+
else: # 1080p
|
|
158
|
+
if effective_aspect_ratio == "16:9":
|
|
159
|
+
width, height = 1920, 1080
|
|
160
|
+
else: # 9:16
|
|
161
|
+
width, height = 1080, 1920
|
|
162
|
+
|
|
163
|
+
# Parse duration from "Xs" format
|
|
164
|
+
duration_seconds = int(inputs.duration.rstrip("s"))
|
|
165
|
+
|
|
166
|
+
artifact = await context.store_video_result(
|
|
167
|
+
storage_url=video_url,
|
|
168
|
+
format="mp4",
|
|
169
|
+
width=width,
|
|
170
|
+
height=height,
|
|
171
|
+
duration=duration_seconds,
|
|
172
|
+
output_index=0,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return GeneratorResult(outputs=[artifact])
|
|
176
|
+
|
|
177
|
+
async def estimate_cost(self, inputs: Veo31FastImageToVideoInput) -> float:
|
|
178
|
+
"""Estimate cost for this generation in USD.
|
|
179
|
+
|
|
180
|
+
Pricing: $0.10 per second (audio off) or $0.15 per second (audio on).
|
|
181
|
+
"""
|
|
182
|
+
# Parse duration from "Xs" format
|
|
183
|
+
duration_seconds = int(inputs.duration.rstrip("s"))
|
|
184
|
+
|
|
185
|
+
# Per-second pricing
|
|
186
|
+
if inputs.generate_audio:
|
|
187
|
+
cost_per_second = 0.15
|
|
188
|
+
else:
|
|
189
|
+
cost_per_second = 0.10
|
|
190
|
+
|
|
191
|
+
return duration_seconds * cost_per_second
|
|
@@ -27,9 +27,9 @@ class Veo31FirstLastFrameToVideoInput(BaseModel):
|
|
|
27
27
|
first_frame: ImageArtifact = Field(description="The first frame of the video (input image)")
|
|
28
28
|
last_frame: ImageArtifact = Field(description="The last frame of the video (input image)")
|
|
29
29
|
prompt: str = Field(description="Text prompt describing the desired video content and motion")
|
|
30
|
-
duration: Literal["8s"] = Field(
|
|
30
|
+
duration: Literal["4s", "6s", "8s"] = Field(
|
|
31
31
|
default="8s",
|
|
32
|
-
description="Duration of the generated video in seconds
|
|
32
|
+
description="Duration of the generated video in seconds",
|
|
33
33
|
)
|
|
34
34
|
aspect_ratio: Literal["auto", "9:16", "16:9", "1:1"] = Field(
|
|
35
35
|
default="auto",
|
|
@@ -173,8 +173,15 @@ class FalVeo31FirstLastFrameToVideoGenerator(BaseGenerator):
|
|
|
173
173
|
Using placeholder value that should be updated with actual pricing.
|
|
174
174
|
"""
|
|
175
175
|
# TODO: Update with actual pricing from Fal when available
|
|
176
|
-
#
|
|
177
|
-
|
|
176
|
+
# Parse duration from "8s" format
|
|
177
|
+
duration_seconds = int(inputs.duration.rstrip("s"))
|
|
178
|
+
|
|
179
|
+
# Base cost per 8 seconds, scaled by actual duration
|
|
180
|
+
base_cost_8s = 0.15 # Placeholder estimate for 8s
|
|
181
|
+
duration_multiplier = duration_seconds / 8.0
|
|
182
|
+
cost = base_cost_8s * duration_multiplier
|
|
183
|
+
|
|
184
|
+
# 50% reduction if audio is disabled
|
|
178
185
|
if not inputs.generate_audio:
|
|
179
|
-
return
|
|
180
|
-
return
|
|
186
|
+
return cost * 0.5
|
|
187
|
+
return cost
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WAN 2.5 Preview image-to-video generator.
|
|
3
|
+
|
|
4
|
+
An image-to-video generation model that creates dynamic video content from static
|
|
5
|
+
images using text prompts to guide motion and camera movement. Supports durations
|
|
6
|
+
of 5 or 10 seconds at 480p, 720p, or 1080p resolution.
|
|
7
|
+
|
|
8
|
+
Based on Fal AI's fal-ai/wan-25-preview/image-to-video model.
|
|
9
|
+
See: https://fal.ai/models/fal-ai/wan-25-preview/image-to-video
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from ....artifacts import ImageArtifact
|
|
18
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Wan25PreviewImageToVideoInput(BaseModel):
|
|
22
|
+
"""Input schema for WAN 2.5 Preview image-to-video generation.
|
|
23
|
+
|
|
24
|
+
Artifact fields (image) are automatically detected via type introspection
|
|
25
|
+
and resolved from generation IDs to ImageArtifact objects.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
image: ImageArtifact = Field(
|
|
29
|
+
description="The image to use as the first frame for video generation"
|
|
30
|
+
)
|
|
31
|
+
prompt: str = Field(
|
|
32
|
+
description="The text prompt describing the desired video motion. Max 800 characters.",
|
|
33
|
+
max_length=800,
|
|
34
|
+
)
|
|
35
|
+
duration: Literal["5", "10"] = Field(
|
|
36
|
+
default="5",
|
|
37
|
+
description="Duration of the generated video in seconds",
|
|
38
|
+
)
|
|
39
|
+
resolution: Literal["480p", "720p", "1080p"] = Field(
|
|
40
|
+
default="1080p",
|
|
41
|
+
description="Resolution of the generated video",
|
|
42
|
+
)
|
|
43
|
+
audio_url: str | None = Field(
|
|
44
|
+
default=None,
|
|
45
|
+
description=(
|
|
46
|
+
"URL of a WAV or MP3 audio file (3-30 seconds, max 15MB) for background music. "
|
|
47
|
+
"Audio is truncated or padded to match video duration."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
seed: int | None = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description=(
|
|
53
|
+
"Random seed for reproducibility. If not specified, a random seed will be used."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
negative_prompt: str | None = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description="Content to avoid in the generated video. Max 500 characters.",
|
|
59
|
+
max_length=500,
|
|
60
|
+
)
|
|
61
|
+
enable_prompt_expansion: bool = Field(
|
|
62
|
+
default=True,
|
|
63
|
+
description="Enable LLM-based prompt rewriting to improve results",
|
|
64
|
+
)
|
|
65
|
+
enable_safety_checker: bool = Field(
|
|
66
|
+
default=True,
|
|
67
|
+
description="Enable content safety filtering",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FalWan25PreviewImageToVideoGenerator(BaseGenerator):
|
|
72
|
+
"""Generator for creating videos from static images using WAN 2.5 Preview."""
|
|
73
|
+
|
|
74
|
+
name = "fal-wan-25-preview-image-to-video"
|
|
75
|
+
description = "Fal: WAN 2.5 Preview - Generate videos from images with motion guidance"
|
|
76
|
+
artifact_type = "video"
|
|
77
|
+
|
|
78
|
+
def get_input_schema(self) -> type[Wan25PreviewImageToVideoInput]:
|
|
79
|
+
"""Return the input schema for this generator."""
|
|
80
|
+
return Wan25PreviewImageToVideoInput
|
|
81
|
+
|
|
82
|
+
async def generate(
|
|
83
|
+
self, inputs: Wan25PreviewImageToVideoInput, context: GeneratorExecutionContext
|
|
84
|
+
) -> GeneratorResult:
|
|
85
|
+
"""Generate video using fal.ai wan-25-preview/image-to-video."""
|
|
86
|
+
# Check for API key
|
|
87
|
+
if not os.getenv("FAL_KEY"):
|
|
88
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
89
|
+
|
|
90
|
+
# Import fal_client
|
|
91
|
+
try:
|
|
92
|
+
import fal_client
|
|
93
|
+
except ImportError as e:
|
|
94
|
+
raise ImportError(
|
|
95
|
+
"fal.ai SDK is required for FalWan25PreviewImageToVideoGenerator. "
|
|
96
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
97
|
+
) from e
|
|
98
|
+
|
|
99
|
+
# Upload image artifact to Fal's public storage
|
|
100
|
+
from ..utils import upload_artifacts_to_fal
|
|
101
|
+
|
|
102
|
+
image_urls = await upload_artifacts_to_fal([inputs.image], context)
|
|
103
|
+
|
|
104
|
+
# Prepare arguments for fal.ai API
|
|
105
|
+
arguments: dict = {
|
|
106
|
+
"image_url": image_urls[0],
|
|
107
|
+
"prompt": inputs.prompt,
|
|
108
|
+
"duration": inputs.duration,
|
|
109
|
+
"resolution": inputs.resolution,
|
|
110
|
+
"enable_prompt_expansion": inputs.enable_prompt_expansion,
|
|
111
|
+
"enable_safety_checker": inputs.enable_safety_checker,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Only add optional parameters if provided
|
|
115
|
+
if inputs.seed is not None:
|
|
116
|
+
arguments["seed"] = inputs.seed
|
|
117
|
+
|
|
118
|
+
if inputs.negative_prompt is not None:
|
|
119
|
+
arguments["negative_prompt"] = inputs.negative_prompt
|
|
120
|
+
|
|
121
|
+
if inputs.audio_url is not None:
|
|
122
|
+
arguments["audio_url"] = inputs.audio_url
|
|
123
|
+
|
|
124
|
+
# Submit async job
|
|
125
|
+
handler = await fal_client.submit_async(
|
|
126
|
+
"fal-ai/wan-25-preview/image-to-video",
|
|
127
|
+
arguments=arguments,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Store external job ID
|
|
131
|
+
await context.set_external_job_id(handler.request_id)
|
|
132
|
+
|
|
133
|
+
# Stream progress updates
|
|
134
|
+
from .....progress.models import ProgressUpdate
|
|
135
|
+
|
|
136
|
+
event_count = 0
|
|
137
|
+
async for event in handler.iter_events(with_logs=True):
|
|
138
|
+
event_count += 1
|
|
139
|
+
# Sample every 3rd event to avoid spam
|
|
140
|
+
if event_count % 3 == 0:
|
|
141
|
+
# Extract logs if available
|
|
142
|
+
logs = getattr(event, "logs", None)
|
|
143
|
+
if logs:
|
|
144
|
+
# Join log entries into a single message
|
|
145
|
+
if isinstance(logs, list):
|
|
146
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
147
|
+
else:
|
|
148
|
+
message = str(logs)
|
|
149
|
+
|
|
150
|
+
if message:
|
|
151
|
+
await context.publish_progress(
|
|
152
|
+
ProgressUpdate(
|
|
153
|
+
job_id=handler.request_id,
|
|
154
|
+
status="processing",
|
|
155
|
+
progress=50.0,
|
|
156
|
+
phase="processing",
|
|
157
|
+
message=message,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Get final result
|
|
162
|
+
result = await handler.get()
|
|
163
|
+
|
|
164
|
+
# Extract video from result
|
|
165
|
+
# Expected structure: {"video": {"url": "...", "width": ..., "height": ..., ...}}
|
|
166
|
+
video_data = result.get("video")
|
|
167
|
+
if not video_data:
|
|
168
|
+
raise ValueError("No video returned from fal.ai API")
|
|
169
|
+
|
|
170
|
+
video_url = video_data.get("url")
|
|
171
|
+
if not video_url:
|
|
172
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
173
|
+
|
|
174
|
+
# Get video dimensions based on resolution setting
|
|
175
|
+
resolution_map = {
|
|
176
|
+
"480p": (854, 480),
|
|
177
|
+
"720p": (1280, 720),
|
|
178
|
+
"1080p": (1920, 1080),
|
|
179
|
+
}
|
|
180
|
+
default_width, default_height = resolution_map.get(inputs.resolution, (1920, 1080))
|
|
181
|
+
|
|
182
|
+
# Use actual dimensions from response if available, otherwise use defaults
|
|
183
|
+
width = video_data.get("width", default_width)
|
|
184
|
+
height = video_data.get("height", default_height)
|
|
185
|
+
fps = video_data.get("fps", 30)
|
|
186
|
+
duration = video_data.get("duration", int(inputs.duration))
|
|
187
|
+
|
|
188
|
+
# Store video result
|
|
189
|
+
artifact = await context.store_video_result(
|
|
190
|
+
storage_url=video_url,
|
|
191
|
+
format="mp4",
|
|
192
|
+
width=width,
|
|
193
|
+
height=height,
|
|
194
|
+
duration=duration,
|
|
195
|
+
fps=fps,
|
|
196
|
+
output_index=0,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return GeneratorResult(outputs=[artifact])
|
|
200
|
+
|
|
201
|
+
async def estimate_cost(self, inputs: Wan25PreviewImageToVideoInput) -> float:
|
|
202
|
+
"""Estimate cost for this generation in USD.
|
|
203
|
+
|
|
204
|
+
Note: Pricing information not available in Fal documentation.
|
|
205
|
+
Using placeholder value that should be updated with actual pricing.
|
|
206
|
+
"""
|
|
207
|
+
# TODO: Update with actual pricing from Fal when available
|
|
208
|
+
# Estimate based on duration - longer videos cost more
|
|
209
|
+
base_cost = 0.10
|
|
210
|
+
if inputs.duration == "10":
|
|
211
|
+
return base_cost * 2.0 # 10 second videos cost more
|
|
212
|
+
return base_cost
|