@weirdfingers/baseboards 0.6.2 → 0.8.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 (57) hide show
  1. package/dist/index.js +54 -28
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/README.md +2 -0
  5. package/templates/api/.env.example +3 -0
  6. package/templates/api/config/generators.yaml +58 -0
  7. package/templates/api/pyproject.toml +1 -1
  8. package/templates/api/src/boards/__init__.py +1 -1
  9. package/templates/api/src/boards/api/endpoints/storage.py +85 -4
  10. package/templates/api/src/boards/api/endpoints/uploads.py +1 -2
  11. package/templates/api/src/boards/database/connection.py +98 -58
  12. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
  13. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +176 -0
  14. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +195 -0
  15. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +14 -0
  16. package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +219 -0
  17. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +208 -0
  18. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +216 -0
  19. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +177 -0
  20. package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +178 -0
  21. package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +155 -0
  22. package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +180 -0
  23. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +18 -0
  24. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +168 -0
  25. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +159 -0
  26. package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +180 -0
  27. package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +190 -0
  28. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +190 -0
  29. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +191 -0
  30. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +13 -6
  31. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +212 -0
  32. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +208 -0
  33. package/templates/api/src/boards/generators/implementations/kie/__init__.py +11 -0
  34. package/templates/api/src/boards/generators/implementations/kie/base.py +316 -0
  35. package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +3 -0
  36. package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +190 -0
  37. package/templates/api/src/boards/generators/implementations/kie/utils.py +98 -0
  38. package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +8 -0
  39. package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +161 -0
  40. package/templates/api/src/boards/graphql/resolvers/upload.py +1 -1
  41. package/templates/web/package.json +4 -1
  42. package/templates/web/src/app/boards/[boardId]/page.tsx +156 -24
  43. package/templates/web/src/app/globals.css +3 -0
  44. package/templates/web/src/app/layout.tsx +15 -5
  45. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +9 -9
  46. package/templates/web/src/components/boards/ArtifactPreview.tsx +34 -18
  47. package/templates/web/src/components/boards/GenerationGrid.tsx +101 -7
  48. package/templates/web/src/components/boards/GenerationInput.tsx +21 -21
  49. package/templates/web/src/components/boards/GeneratorSelector.tsx +232 -30
  50. package/templates/web/src/components/boards/UploadArtifact.tsx +385 -75
  51. package/templates/web/src/components/header.tsx +3 -1
  52. package/templates/web/src/components/theme-provider.tsx +10 -0
  53. package/templates/web/src/components/theme-toggle.tsx +75 -0
  54. package/templates/web/src/components/ui/alert-dialog.tsx +157 -0
  55. package/templates/web/src/components/ui/toast.tsx +128 -0
  56. package/templates/web/src/components/ui/toaster.tsx +35 -0
  57. 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
@@ -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 (currently only 8s is supported)",
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
- # Base cost, with 50% reduction if audio is disabled
177
- base_cost = 0.15 # Placeholder estimate
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 base_cost * 0.5
180
- return base_cost
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