@weirdfingers/baseboards 0.5.3 → 0.6.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/README.md +1 -1
- package/package.json +1 -1
- package/templates/api/alembic/env.py +9 -1
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +107 -49
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +7 -3
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +57 -1
- package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +134 -0
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +8 -5
- package/templates/api/config/generators.yaml +111 -0
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/api/app.py +2 -1
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +1 -1
- package/templates/api/src/boards/api/endpoints/uploads.py +150 -0
- package/templates/api/src/boards/auth/factory.py +1 -1
- package/templates/api/src/boards/dbmodels/__init__.py +8 -22
- package/templates/api/src/boards/generators/artifact_resolution.py +45 -12
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +16 -1
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +171 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +167 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +194 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +209 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +206 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +237 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +1 -1
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +30 -0
- package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +220 -0
- package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +173 -0
- package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +227 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +203 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +230 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +204 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +221 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +177 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +182 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +167 -0
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +299 -0
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +190 -0
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +226 -0
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +249 -0
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +244 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +42 -0
- package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +209 -0
- package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +161 -0
- package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +222 -0
- package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +152 -0
- package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +197 -0
- package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +173 -0
- package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +221 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +175 -0
- package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +153 -0
- package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +172 -0
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +175 -0
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +163 -0
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +155 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +174 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +194 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +1 -1
- package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +158 -0
- package/templates/api/src/boards/graphql/access_control.py +1 -1
- package/templates/api/src/boards/graphql/mutations/root.py +16 -4
- package/templates/api/src/boards/graphql/resolvers/board.py +0 -2
- package/templates/api/src/boards/graphql/resolvers/generation.py +10 -233
- package/templates/api/src/boards/graphql/resolvers/lineage.py +381 -0
- package/templates/api/src/boards/graphql/resolvers/upload.py +463 -0
- package/templates/api/src/boards/graphql/types/generation.py +62 -26
- package/templates/api/src/boards/middleware.py +1 -1
- package/templates/api/src/boards/storage/factory.py +2 -2
- package/templates/api/src/boards/tenant_isolation.py +9 -9
- package/templates/api/src/boards/workers/actors.py +10 -1
- package/templates/web/package.json +1 -1
- package/templates/web/src/app/boards/[boardId]/page.tsx +14 -5
- package/templates/web/src/app/lineage/[generationId]/page.tsx +233 -0
- package/templates/web/src/components/boards/ArtifactPreview.tsx +20 -1
- package/templates/web/src/components/boards/UploadArtifact.tsx +253 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fal.ai sync-lipsync v2 pro video generator.
|
|
3
|
+
|
|
4
|
+
Generate high-quality realistic lipsync animations from audio while preserving
|
|
5
|
+
unique details like natural teeth and unique facial features.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's fal-ai/sync-lipsync/v2/pro model.
|
|
8
|
+
See: https://fal.ai/models/fal-ai/sync-lipsync/v2/pro
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ....artifacts import AudioArtifact, VideoArtifact
|
|
17
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncLipsyncV2ProInput(BaseModel):
|
|
21
|
+
"""Input schema for sync-lipsync v2 pro.
|
|
22
|
+
|
|
23
|
+
Artifact fields are automatically detected via type introspection
|
|
24
|
+
and resolved from generation IDs to artifact objects.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
video: VideoArtifact = Field(description="Input video for lip-sync animation")
|
|
28
|
+
audio: AudioArtifact = Field(description="Audio to synchronize with the video")
|
|
29
|
+
sync_mode: Literal["cut_off", "loop", "bounce", "silence", "remap"] = Field(
|
|
30
|
+
default="cut_off",
|
|
31
|
+
description="Lipsync synchronization approach when media durations differ",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FalSyncLipsyncV2ProGenerator(BaseGenerator):
|
|
36
|
+
"""Generator for high-quality realistic lip-synchronization animations."""
|
|
37
|
+
|
|
38
|
+
name = "fal-sync-lipsync-v2-pro"
|
|
39
|
+
description = "Fal: sync-lipsync v2 pro - High-quality lipsync preserving facial features"
|
|
40
|
+
artifact_type = "video"
|
|
41
|
+
|
|
42
|
+
def get_input_schema(self) -> type[SyncLipsyncV2ProInput]:
|
|
43
|
+
"""Return the input schema for this generator."""
|
|
44
|
+
return SyncLipsyncV2ProInput
|
|
45
|
+
|
|
46
|
+
async def generate(
|
|
47
|
+
self, inputs: SyncLipsyncV2ProInput, context: GeneratorExecutionContext
|
|
48
|
+
) -> GeneratorResult:
|
|
49
|
+
"""Generate lip-synced video using fal.ai sync-lipsync/v2/pro."""
|
|
50
|
+
# Check for API key
|
|
51
|
+
if not os.getenv("FAL_KEY"):
|
|
52
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
53
|
+
|
|
54
|
+
# Import fal_client
|
|
55
|
+
try:
|
|
56
|
+
import fal_client
|
|
57
|
+
except ImportError as e:
|
|
58
|
+
raise ImportError(
|
|
59
|
+
"fal.ai SDK is required for FalSyncLipsyncV2ProGenerator. "
|
|
60
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
# Upload video and audio artifacts to Fal's public storage
|
|
64
|
+
# Fal API requires publicly accessible URLs
|
|
65
|
+
from ..utils import upload_artifacts_to_fal
|
|
66
|
+
|
|
67
|
+
# Upload video and audio separately
|
|
68
|
+
video_urls = await upload_artifacts_to_fal([inputs.video], context)
|
|
69
|
+
audio_urls = await upload_artifacts_to_fal([inputs.audio], context)
|
|
70
|
+
|
|
71
|
+
# Prepare arguments for fal.ai API
|
|
72
|
+
arguments = {
|
|
73
|
+
"video_url": video_urls[0],
|
|
74
|
+
"audio_url": audio_urls[0],
|
|
75
|
+
"sync_mode": inputs.sync_mode,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Submit async job
|
|
79
|
+
handler = await fal_client.submit_async(
|
|
80
|
+
"fal-ai/sync-lipsync/v2/pro",
|
|
81
|
+
arguments=arguments,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Store external job ID
|
|
85
|
+
await context.set_external_job_id(handler.request_id)
|
|
86
|
+
|
|
87
|
+
# Stream progress updates
|
|
88
|
+
from .....progress.models import ProgressUpdate
|
|
89
|
+
|
|
90
|
+
event_count = 0
|
|
91
|
+
async for event in handler.iter_events(with_logs=True):
|
|
92
|
+
event_count += 1
|
|
93
|
+
# Sample every 3rd event to avoid spam
|
|
94
|
+
if event_count % 3 == 0:
|
|
95
|
+
# Extract logs if available
|
|
96
|
+
logs = getattr(event, "logs", None)
|
|
97
|
+
if logs:
|
|
98
|
+
# Join log entries into a single message
|
|
99
|
+
if isinstance(logs, list):
|
|
100
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
101
|
+
else:
|
|
102
|
+
message = str(logs)
|
|
103
|
+
|
|
104
|
+
if message:
|
|
105
|
+
await context.publish_progress(
|
|
106
|
+
ProgressUpdate(
|
|
107
|
+
job_id=handler.request_id,
|
|
108
|
+
status="processing",
|
|
109
|
+
progress=50.0, # Approximate mid-point progress
|
|
110
|
+
phase="processing",
|
|
111
|
+
message=message,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Get final result
|
|
116
|
+
result = await handler.get()
|
|
117
|
+
|
|
118
|
+
# Extract video from result
|
|
119
|
+
# fal.ai returns: {"video": {"url": "...", "content_type": "video/mp4", ...}}
|
|
120
|
+
video_data = result.get("video")
|
|
121
|
+
|
|
122
|
+
if not video_data:
|
|
123
|
+
raise ValueError("No video returned from fal.ai API")
|
|
124
|
+
|
|
125
|
+
video_url = video_data.get("url")
|
|
126
|
+
if not video_url:
|
|
127
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
128
|
+
|
|
129
|
+
# Extract format from content_type (e.g., "video/mp4" -> "mp4")
|
|
130
|
+
content_type = video_data.get("content_type", "video/mp4")
|
|
131
|
+
video_format = content_type.split("/")[-1] if "/" in content_type else "mp4"
|
|
132
|
+
|
|
133
|
+
# Store the video result
|
|
134
|
+
# Note: The API doesn't return width/height/duration/fps, so we use input values
|
|
135
|
+
# The actual dimensions will be the same as the input video
|
|
136
|
+
artifact = await context.store_video_result(
|
|
137
|
+
storage_url=video_url,
|
|
138
|
+
format=video_format,
|
|
139
|
+
width=inputs.video.width,
|
|
140
|
+
height=inputs.video.height,
|
|
141
|
+
duration=inputs.audio.duration,
|
|
142
|
+
fps=inputs.video.fps,
|
|
143
|
+
output_index=0,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return GeneratorResult(outputs=[artifact])
|
|
147
|
+
|
|
148
|
+
async def estimate_cost(self, inputs: SyncLipsyncV2ProInput) -> float:
|
|
149
|
+
"""Estimate cost for sync-lipsync v2 pro generation in USD.
|
|
150
|
+
|
|
151
|
+
Pricing not specified in documentation, using estimate based on
|
|
152
|
+
typical video processing costs for pro models.
|
|
153
|
+
"""
|
|
154
|
+
# Estimated cost per generation for pro model
|
|
155
|
+
return 0.10
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VEED Lipsync video generator.
|
|
3
|
+
|
|
4
|
+
Generate realistic lipsync from any audio using VEED's latest model.
|
|
5
|
+
This generator synchronizes lip movements in video with provided audio.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's veed/lipsync model.
|
|
8
|
+
See: https://fal.ai/models/veed/lipsync
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from ....artifacts import AudioArtifact, VideoArtifact
|
|
16
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
17
|
+
from ..utils import upload_artifacts_to_fal
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VeedLipsyncInput(BaseModel):
|
|
21
|
+
"""Input schema for VEED lipsync.
|
|
22
|
+
|
|
23
|
+
Artifact fields are automatically detected via type introspection
|
|
24
|
+
and resolved from generation IDs to artifact objects.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
video_url: VideoArtifact = Field(description="Video to apply lip-sync animation to")
|
|
28
|
+
audio_url: AudioArtifact = Field(description="Audio to synchronize with the video")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FalVeedLipsyncGenerator(BaseGenerator):
|
|
32
|
+
"""Generator for realistic lip-synchronization using VEED's model."""
|
|
33
|
+
|
|
34
|
+
name = "veed-lipsync"
|
|
35
|
+
description = "VEED: Lipsync - Generate realistic lipsync from any audio"
|
|
36
|
+
artifact_type = "video"
|
|
37
|
+
|
|
38
|
+
def get_input_schema(self) -> type[VeedLipsyncInput]:
|
|
39
|
+
"""Return the input schema for this generator."""
|
|
40
|
+
return VeedLipsyncInput
|
|
41
|
+
|
|
42
|
+
async def generate(
|
|
43
|
+
self, inputs: VeedLipsyncInput, context: GeneratorExecutionContext
|
|
44
|
+
) -> GeneratorResult:
|
|
45
|
+
"""Generate lip-synced video using VEED lipsync."""
|
|
46
|
+
# Check for API key
|
|
47
|
+
if not os.getenv("FAL_KEY"):
|
|
48
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
49
|
+
|
|
50
|
+
# Import fal_client
|
|
51
|
+
try:
|
|
52
|
+
import fal_client
|
|
53
|
+
except ImportError as e:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
"fal.ai SDK is required for FalVeedLipsyncGenerator. "
|
|
56
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
57
|
+
) from e
|
|
58
|
+
|
|
59
|
+
# Upload video and audio artifacts to Fal's public storage
|
|
60
|
+
# Fal API requires publicly accessible URLs
|
|
61
|
+
# Upload video and audio separately
|
|
62
|
+
video_urls = await upload_artifacts_to_fal([inputs.video_url], context)
|
|
63
|
+
audio_urls = await upload_artifacts_to_fal([inputs.audio_url], context)
|
|
64
|
+
|
|
65
|
+
# Prepare arguments for fal.ai API
|
|
66
|
+
arguments = {
|
|
67
|
+
"video_url": video_urls[0],
|
|
68
|
+
"audio_url": audio_urls[0],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Submit async job
|
|
72
|
+
handler = await fal_client.submit_async(
|
|
73
|
+
"veed/lipsync",
|
|
74
|
+
arguments=arguments,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Store external job ID
|
|
78
|
+
await context.set_external_job_id(handler.request_id)
|
|
79
|
+
|
|
80
|
+
# Stream progress updates
|
|
81
|
+
from .....progress.models import ProgressUpdate
|
|
82
|
+
|
|
83
|
+
event_count = 0
|
|
84
|
+
async for event in handler.iter_events(with_logs=True):
|
|
85
|
+
event_count += 1
|
|
86
|
+
# Sample every 3rd event to avoid spamming progress updates
|
|
87
|
+
# This provides regular feedback without overwhelming the system
|
|
88
|
+
if event_count % 3 == 0:
|
|
89
|
+
# Extract logs if available
|
|
90
|
+
logs = getattr(event, "logs", None)
|
|
91
|
+
if logs:
|
|
92
|
+
# Join log entries into a single message
|
|
93
|
+
if isinstance(logs, list):
|
|
94
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
95
|
+
else:
|
|
96
|
+
message = str(logs)
|
|
97
|
+
|
|
98
|
+
if message:
|
|
99
|
+
await context.publish_progress(
|
|
100
|
+
ProgressUpdate(
|
|
101
|
+
job_id=handler.request_id,
|
|
102
|
+
status="processing",
|
|
103
|
+
# Using fixed 50% since API doesn't provide granular progress
|
|
104
|
+
# This indicates processing is underway without false precision
|
|
105
|
+
progress=50.0,
|
|
106
|
+
phase="processing",
|
|
107
|
+
message=message,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Get final result
|
|
112
|
+
result = await handler.get()
|
|
113
|
+
|
|
114
|
+
# Extract video from result
|
|
115
|
+
# VEED API returns: {"video": {"url": "...", "content_type": "video/mp4", ...}}
|
|
116
|
+
video_data = result.get("video")
|
|
117
|
+
|
|
118
|
+
if not video_data:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"No video returned from VEED API. Response structure: {list(result.keys())}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
video_url = video_data.get("url")
|
|
124
|
+
if not video_url:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Video missing URL in VEED response. Video data keys: {list(video_data.keys())}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Determine video format with fallback strategy:
|
|
130
|
+
# 1. Try to extract from URL extension (most reliable)
|
|
131
|
+
# 2. Parse content_type only if it's a video/* MIME type
|
|
132
|
+
# 3. Default to mp4 (most common format for this API)
|
|
133
|
+
video_format = "mp4" # Default
|
|
134
|
+
|
|
135
|
+
# Try extracting extension from URL
|
|
136
|
+
if video_url:
|
|
137
|
+
url_parts = video_url.split(".")
|
|
138
|
+
if len(url_parts) > 1:
|
|
139
|
+
ext = url_parts[-1].split("?")[0].lower() # Remove query params
|
|
140
|
+
if ext in ["mp4", "webm", "mov", "avi"]: # Common video formats
|
|
141
|
+
video_format = ext
|
|
142
|
+
|
|
143
|
+
# If no valid extension found, try content_type (only if it's video/*)
|
|
144
|
+
if video_format == "mp4": # Still using default
|
|
145
|
+
content_type = video_data.get("content_type", "")
|
|
146
|
+
if content_type.startswith("video/"):
|
|
147
|
+
video_format = content_type.split("/")[-1]
|
|
148
|
+
|
|
149
|
+
# Store the video result
|
|
150
|
+
# Note: The API doesn't return width/height/duration/fps in documentation
|
|
151
|
+
# Using input video dimensions and audio duration
|
|
152
|
+
artifact = await context.store_video_result(
|
|
153
|
+
storage_url=video_url,
|
|
154
|
+
format=video_format,
|
|
155
|
+
width=inputs.video_url.width,
|
|
156
|
+
height=inputs.video_url.height,
|
|
157
|
+
duration=inputs.audio_url.duration,
|
|
158
|
+
fps=inputs.video_url.fps,
|
|
159
|
+
output_index=0,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return GeneratorResult(outputs=[artifact])
|
|
163
|
+
|
|
164
|
+
async def estimate_cost(self, inputs: VeedLipsyncInput) -> float:
|
|
165
|
+
"""Estimate cost for VEED lipsync generation in USD.
|
|
166
|
+
|
|
167
|
+
Pricing not specified in documentation, using estimate based on
|
|
168
|
+
typical video lipsync processing costs.
|
|
169
|
+
"""
|
|
170
|
+
# Fixed cost estimate of $0.05 per generation
|
|
171
|
+
# Based on typical AI video processing costs (~$0.03-0.07 per minute)
|
|
172
|
+
# This is a conservative estimate and should be updated when official
|
|
173
|
+
# pricing information becomes available from VEED/FAL
|
|
174
|
+
return 0.05
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Veo 3 text-to-video generator.
|
|
3
|
+
|
|
4
|
+
The most advanced AI video generation model in the world by Google, capable of
|
|
5
|
+
generating high-quality videos from text prompts with optional audio synthesis.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's fal-ai/veo3 model.
|
|
8
|
+
See: https://fal.ai/models/fal-ai/veo3
|
|
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 Veo3Input(BaseModel):
|
|
20
|
+
"""Input schema for Google Veo 3 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", "1:1"] = Field(
|
|
24
|
+
default="16:9",
|
|
25
|
+
description="Aspect ratio of the generated video. If 1:1, video will be outpainted",
|
|
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, 50% 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 FalVeo3Generator(BaseGenerator):
|
|
58
|
+
"""Generator for text-to-video using Google Veo 3."""
|
|
59
|
+
|
|
60
|
+
name = "fal-veo3"
|
|
61
|
+
description = "Fal: Veo 3 - Google's most advanced AI video generation model"
|
|
62
|
+
artifact_type = "video"
|
|
63
|
+
|
|
64
|
+
def get_input_schema(self) -> type[Veo3Input]:
|
|
65
|
+
"""Return the input schema for this generator."""
|
|
66
|
+
return Veo3Input
|
|
67
|
+
|
|
68
|
+
async def generate(
|
|
69
|
+
self, inputs: Veo3Input, context: GeneratorExecutionContext
|
|
70
|
+
) -> GeneratorResult:
|
|
71
|
+
"""Generate video using fal.ai veo3."""
|
|
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 FalVeo3Generator. "
|
|
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",
|
|
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
|
+
elif inputs.aspect_ratio == "9:16":
|
|
157
|
+
width, height = 720, 1280
|
|
158
|
+
else: # 1:1
|
|
159
|
+
width, height = 720, 720
|
|
160
|
+
else: # 1080p
|
|
161
|
+
if inputs.aspect_ratio == "16:9":
|
|
162
|
+
width, height = 1920, 1080
|
|
163
|
+
elif inputs.aspect_ratio == "9:16":
|
|
164
|
+
width, height = 1080, 1920
|
|
165
|
+
else: # 1:1
|
|
166
|
+
width, height = 1080, 1080
|
|
167
|
+
|
|
168
|
+
# Parse duration from "8s" format
|
|
169
|
+
duration_seconds = int(inputs.duration.rstrip("s"))
|
|
170
|
+
|
|
171
|
+
# Store video result
|
|
172
|
+
artifact = await context.store_video_result(
|
|
173
|
+
storage_url=video_url,
|
|
174
|
+
format="mp4",
|
|
175
|
+
width=width,
|
|
176
|
+
height=height,
|
|
177
|
+
duration=duration_seconds,
|
|
178
|
+
output_index=0,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return GeneratorResult(outputs=[artifact])
|
|
182
|
+
|
|
183
|
+
async def estimate_cost(self, inputs: Veo3Input) -> float:
|
|
184
|
+
"""Estimate cost for this generation in USD.
|
|
185
|
+
|
|
186
|
+
Note: Pricing information not available in Fal documentation.
|
|
187
|
+
Using placeholder value that should be updated with actual pricing.
|
|
188
|
+
"""
|
|
189
|
+
# TODO: Update with actual pricing from Fal when available
|
|
190
|
+
# Base cost, with 50% reduction if audio is disabled
|
|
191
|
+
base_cost = 0.15 # Placeholder estimate
|
|
192
|
+
if not inputs.generate_audio:
|
|
193
|
+
return base_cost * 0.5
|
|
194
|
+
return base_cost
|
|
@@ -34,7 +34,7 @@ class Veo31FirstLastFrameToVideoInput(BaseModel):
|
|
|
34
34
|
aspect_ratio: Literal["auto", "9:16", "16:9", "1:1"] = Field(
|
|
35
35
|
default="auto",
|
|
36
36
|
description=(
|
|
37
|
-
"Aspect ratio of the generated video.
|
|
37
|
+
"Aspect ratio of the generated video. 'auto' uses the aspect ratio from input images"
|
|
38
38
|
),
|
|
39
39
|
)
|
|
40
40
|
resolution: Literal["720p", "1080p"] = Field(
|
package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WAN-Pro 2.1 image-to-video generator.
|
|
3
|
+
|
|
4
|
+
A premium image-to-video model that generates high-quality 1080p videos at 30fps
|
|
5
|
+
with up to 6 seconds duration, converting static images into dynamic video content
|
|
6
|
+
with exceptional motion diversity.
|
|
7
|
+
|
|
8
|
+
Based on Fal AI's fal-ai/wan-pro/image-to-video model.
|
|
9
|
+
See: https://fal.ai/models/fal-ai/wan-pro/image-to-video
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ....artifacts import ImageArtifact
|
|
17
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WanProImageToVideoInput(BaseModel):
|
|
21
|
+
"""Input schema for WAN-Pro 2.1 image-to-video generation.
|
|
22
|
+
|
|
23
|
+
Artifact fields (image) are automatically detected via type introspection
|
|
24
|
+
and resolved from generation IDs to ImageArtifact objects.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
image: ImageArtifact = Field(description="The image to generate the video from")
|
|
28
|
+
prompt: str = Field(description="Text prompt describing the desired video content and motion")
|
|
29
|
+
seed: int | None = Field(
|
|
30
|
+
default=None,
|
|
31
|
+
description="Random seed for reproducibility. If not specified, a random seed will be used",
|
|
32
|
+
)
|
|
33
|
+
enable_safety_checker: bool = Field(
|
|
34
|
+
default=True,
|
|
35
|
+
description="Whether to enable the safety checker for content moderation",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FalWanProImageToVideoGenerator(BaseGenerator):
|
|
40
|
+
"""Generator for creating videos from static images using WAN-Pro 2.1."""
|
|
41
|
+
|
|
42
|
+
name = "fal-wan-pro-image-to-video"
|
|
43
|
+
description = "Fal: WAN-Pro 2.1 - Generate high-quality 1080p videos from static images"
|
|
44
|
+
artifact_type = "video"
|
|
45
|
+
|
|
46
|
+
def get_input_schema(self) -> type[WanProImageToVideoInput]:
|
|
47
|
+
"""Return the input schema for this generator."""
|
|
48
|
+
return WanProImageToVideoInput
|
|
49
|
+
|
|
50
|
+
async def generate(
|
|
51
|
+
self, inputs: WanProImageToVideoInput, context: GeneratorExecutionContext
|
|
52
|
+
) -> GeneratorResult:
|
|
53
|
+
"""Generate video using fal.ai wan-pro/image-to-video."""
|
|
54
|
+
# Check for API key
|
|
55
|
+
if not os.getenv("FAL_KEY"):
|
|
56
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
57
|
+
|
|
58
|
+
# Import fal_client
|
|
59
|
+
try:
|
|
60
|
+
import fal_client
|
|
61
|
+
except ImportError as e:
|
|
62
|
+
raise ImportError(
|
|
63
|
+
"fal.ai SDK is required for FalWanProImageToVideoGenerator. "
|
|
64
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
65
|
+
) from e
|
|
66
|
+
|
|
67
|
+
# Upload image artifact to Fal's public storage
|
|
68
|
+
# Fal API requires publicly accessible URLs, but our storage_url might be:
|
|
69
|
+
# - Localhost URLs (not publicly accessible)
|
|
70
|
+
# - Private S3 buckets (not publicly accessible)
|
|
71
|
+
# So we upload to Fal's temporary storage first
|
|
72
|
+
from ..utils import upload_artifacts_to_fal
|
|
73
|
+
|
|
74
|
+
image_urls = await upload_artifacts_to_fal([inputs.image], context)
|
|
75
|
+
|
|
76
|
+
# Prepare arguments for fal.ai API
|
|
77
|
+
arguments = {
|
|
78
|
+
"image_url": image_urls[0],
|
|
79
|
+
"prompt": inputs.prompt,
|
|
80
|
+
"enable_safety_checker": inputs.enable_safety_checker,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Only add seed if provided (allow API to use random if not specified)
|
|
84
|
+
if inputs.seed is not None:
|
|
85
|
+
arguments["seed"] = inputs.seed
|
|
86
|
+
|
|
87
|
+
# Submit async job
|
|
88
|
+
handler = await fal_client.submit_async(
|
|
89
|
+
"fal-ai/wan-pro/image-to-video",
|
|
90
|
+
arguments=arguments,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Store external job ID
|
|
94
|
+
await context.set_external_job_id(handler.request_id)
|
|
95
|
+
|
|
96
|
+
# Stream progress updates
|
|
97
|
+
from .....progress.models import ProgressUpdate
|
|
98
|
+
|
|
99
|
+
event_count = 0
|
|
100
|
+
async for event in handler.iter_events(with_logs=True):
|
|
101
|
+
event_count += 1
|
|
102
|
+
# Sample every 3rd event to avoid spam
|
|
103
|
+
if event_count % 3 == 0:
|
|
104
|
+
# Extract logs if available
|
|
105
|
+
logs = getattr(event, "logs", None)
|
|
106
|
+
if logs:
|
|
107
|
+
# Join log entries into a single message
|
|
108
|
+
if isinstance(logs, list):
|
|
109
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
110
|
+
else:
|
|
111
|
+
message = str(logs)
|
|
112
|
+
|
|
113
|
+
if message:
|
|
114
|
+
await context.publish_progress(
|
|
115
|
+
ProgressUpdate(
|
|
116
|
+
job_id=handler.request_id,
|
|
117
|
+
status="processing",
|
|
118
|
+
progress=50.0,
|
|
119
|
+
phase="processing",
|
|
120
|
+
message=message,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Get final result
|
|
125
|
+
result = await handler.get()
|
|
126
|
+
|
|
127
|
+
# Extract video from result
|
|
128
|
+
# Expected structure: {"video": {"url": "...", "content_type": "...", ...}}
|
|
129
|
+
video_data = result.get("video")
|
|
130
|
+
if not video_data:
|
|
131
|
+
raise ValueError("No video returned from fal.ai API")
|
|
132
|
+
|
|
133
|
+
video_url = video_data.get("url")
|
|
134
|
+
if not video_url:
|
|
135
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
136
|
+
|
|
137
|
+
# Store video result
|
|
138
|
+
# WAN-Pro generates 1080p videos at 30fps with up to 6 seconds duration
|
|
139
|
+
artifact = await context.store_video_result(
|
|
140
|
+
storage_url=video_url,
|
|
141
|
+
format="mp4",
|
|
142
|
+
width=1920,
|
|
143
|
+
height=1080,
|
|
144
|
+
duration=6, # Maximum duration
|
|
145
|
+
fps=30,
|
|
146
|
+
output_index=0,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return GeneratorResult(outputs=[artifact])
|
|
150
|
+
|
|
151
|
+
async def estimate_cost(self, inputs: WanProImageToVideoInput) -> float:
|
|
152
|
+
"""Estimate cost for this generation in USD.
|
|
153
|
+
|
|
154
|
+
Note: Pricing information not available in Fal documentation.
|
|
155
|
+
Using placeholder value that should be updated with actual pricing.
|
|
156
|
+
"""
|
|
157
|
+
# TODO: Update with actual pricing from Fal when available
|
|
158
|
+
return 0.10 # Placeholder estimate for premium image-to-video generation
|