@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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/templates/api/alembic/env.py +9 -1
  4. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +107 -49
  5. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +7 -3
  6. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +57 -1
  7. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +134 -0
  8. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +8 -5
  9. package/templates/api/config/generators.yaml +111 -0
  10. package/templates/api/src/boards/__init__.py +1 -1
  11. package/templates/api/src/boards/api/app.py +2 -1
  12. package/templates/api/src/boards/api/endpoints/tenant_registration.py +1 -1
  13. package/templates/api/src/boards/api/endpoints/uploads.py +150 -0
  14. package/templates/api/src/boards/auth/factory.py +1 -1
  15. package/templates/api/src/boards/dbmodels/__init__.py +8 -22
  16. package/templates/api/src/boards/generators/artifact_resolution.py +45 -12
  17. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +16 -1
  18. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +171 -0
  19. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +167 -0
  20. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +194 -0
  21. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +209 -0
  22. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +206 -0
  23. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +237 -0
  24. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +1 -1
  25. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +30 -0
  26. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +220 -0
  27. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +173 -0
  28. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +227 -0
  29. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +203 -0
  30. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +230 -0
  31. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +204 -0
  32. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +221 -0
  33. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +177 -0
  34. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +182 -0
  35. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +167 -0
  36. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +299 -0
  37. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +190 -0
  38. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +226 -0
  39. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +249 -0
  40. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +244 -0
  41. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +42 -0
  42. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +209 -0
  43. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +161 -0
  44. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +222 -0
  45. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +152 -0
  46. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +197 -0
  47. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +173 -0
  48. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +221 -0
  49. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +175 -0
  50. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +153 -0
  51. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +172 -0
  52. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +175 -0
  53. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +163 -0
  54. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +155 -0
  55. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +174 -0
  56. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +194 -0
  57. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +1 -1
  58. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +158 -0
  59. package/templates/api/src/boards/graphql/access_control.py +1 -1
  60. package/templates/api/src/boards/graphql/mutations/root.py +16 -4
  61. package/templates/api/src/boards/graphql/resolvers/board.py +0 -2
  62. package/templates/api/src/boards/graphql/resolvers/generation.py +10 -233
  63. package/templates/api/src/boards/graphql/resolvers/lineage.py +381 -0
  64. package/templates/api/src/boards/graphql/resolvers/upload.py +463 -0
  65. package/templates/api/src/boards/graphql/types/generation.py +62 -26
  66. package/templates/api/src/boards/middleware.py +1 -1
  67. package/templates/api/src/boards/storage/factory.py +2 -2
  68. package/templates/api/src/boards/tenant_isolation.py +9 -9
  69. package/templates/api/src/boards/workers/actors.py +10 -1
  70. package/templates/web/package.json +1 -1
  71. package/templates/web/src/app/boards/[boardId]/page.tsx +14 -5
  72. package/templates/web/src/app/lineage/[generationId]/page.tsx +233 -0
  73. package/templates/web/src/components/boards/ArtifactPreview.tsx +20 -1
  74. 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. " "'auto' uses the aspect ratio from input images"
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(
@@ -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