@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.
- package/dist/index.js +54 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +2 -0
- package/templates/api/.env.example +3 -0
- package/templates/api/config/generators.yaml +58 -0
- package/templates/api/pyproject.toml +1 -1
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/api/endpoints/storage.py +85 -4
- package/templates/api/src/boards/api/endpoints/uploads.py +1 -2
- package/templates/api/src/boards/database/connection.py +98 -58
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +176 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +195 -0
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +14 -0
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +219 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +208 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +216 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +177 -0
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +178 -0
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +155 -0
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +180 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +18 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +168 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +159 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +180 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +190 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +190 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +191 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +13 -6
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +212 -0
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +208 -0
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +11 -0
- package/templates/api/src/boards/generators/implementations/kie/base.py +316 -0
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +190 -0
- package/templates/api/src/boards/generators/implementations/kie/utils.py +98 -0
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +8 -0
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +161 -0
- package/templates/api/src/boards/graphql/resolvers/upload.py +1 -1
- package/templates/web/package.json +4 -1
- package/templates/web/src/app/boards/[boardId]/page.tsx +156 -24
- package/templates/web/src/app/globals.css +3 -0
- package/templates/web/src/app/layout.tsx +15 -5
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +9 -9
- package/templates/web/src/components/boards/ArtifactPreview.tsx +34 -18
- package/templates/web/src/components/boards/GenerationGrid.tsx +101 -7
- package/templates/web/src/components/boards/GenerationInput.tsx +21 -21
- package/templates/web/src/components/boards/GeneratorSelector.tsx +232 -30
- package/templates/web/src/components/boards/UploadArtifact.tsx +385 -75
- package/templates/web/src/components/header.tsx +3 -1
- package/templates/web/src/components/theme-provider.tsx +10 -0
- package/templates/web/src/components/theme-toggle.tsx +75 -0
- package/templates/web/src/components/ui/alert-dialog.tsx +157 -0
- package/templates/web/src/components/ui/toast.tsx +128 -0
- package/templates/web/src/components/ui/toaster.tsx +35 -0
- package/templates/web/src/components/ui/use-toast.ts +186 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fal.ai Reve text-to-image generator.
|
|
3
|
+
|
|
4
|
+
Reve's text-to-image model generates detailed visual output that closely follows
|
|
5
|
+
your instructions, with strong aesthetic quality and accurate text rendering.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's fal-ai/reve/text-to-image model.
|
|
8
|
+
See: https://fal.ai/models/fal-ai/reve/text-to-image
|
|
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 ReveTextToImageInput(BaseModel):
|
|
20
|
+
"""Input schema for Reve text-to-image generation."""
|
|
21
|
+
|
|
22
|
+
prompt: str = Field(
|
|
23
|
+
description="Text description of desired image",
|
|
24
|
+
min_length=1,
|
|
25
|
+
max_length=2560,
|
|
26
|
+
)
|
|
27
|
+
num_images: int = Field(
|
|
28
|
+
default=1,
|
|
29
|
+
ge=1,
|
|
30
|
+
le=4,
|
|
31
|
+
description="Number of images to generate",
|
|
32
|
+
)
|
|
33
|
+
aspect_ratio: Literal["16:9", "9:16", "3:2", "2:3", "4:3", "3:4", "1:1"] = Field(
|
|
34
|
+
default="3:2",
|
|
35
|
+
description="Desired image aspect ratio",
|
|
36
|
+
)
|
|
37
|
+
output_format: Literal["png", "jpeg", "webp"] = Field(
|
|
38
|
+
default="png",
|
|
39
|
+
description="Output image format",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FalReveTextToImageGenerator(BaseGenerator):
|
|
44
|
+
"""Reve text-to-image generator using fal.ai."""
|
|
45
|
+
|
|
46
|
+
name = "fal-reve-text-to-image"
|
|
47
|
+
artifact_type = "image"
|
|
48
|
+
description = (
|
|
49
|
+
"Fal: Reve - detailed text-to-image with strong aesthetic quality "
|
|
50
|
+
"and accurate text rendering"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def get_input_schema(self) -> type[ReveTextToImageInput]:
|
|
54
|
+
return ReveTextToImageInput
|
|
55
|
+
|
|
56
|
+
async def generate(
|
|
57
|
+
self, inputs: ReveTextToImageInput, context: GeneratorExecutionContext
|
|
58
|
+
) -> GeneratorResult:
|
|
59
|
+
"""Generate images using fal.ai Reve text-to-image model."""
|
|
60
|
+
# Check for API key (fal-client uses FAL_KEY environment variable)
|
|
61
|
+
if not os.getenv("FAL_KEY"):
|
|
62
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
63
|
+
|
|
64
|
+
# Import fal_client
|
|
65
|
+
try:
|
|
66
|
+
import fal_client
|
|
67
|
+
except ImportError as e:
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"fal.ai SDK is required for FalReveTextToImageGenerator. "
|
|
70
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
# Prepare arguments for fal.ai API
|
|
74
|
+
arguments = {
|
|
75
|
+
"prompt": inputs.prompt,
|
|
76
|
+
"num_images": inputs.num_images,
|
|
77
|
+
"aspect_ratio": inputs.aspect_ratio,
|
|
78
|
+
"output_format": inputs.output_format,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Submit async job and get handler
|
|
82
|
+
handler = await fal_client.submit_async(
|
|
83
|
+
"fal-ai/reve/text-to-image",
|
|
84
|
+
arguments=arguments,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Store the external job ID for tracking
|
|
88
|
+
await context.set_external_job_id(handler.request_id)
|
|
89
|
+
|
|
90
|
+
# Stream progress updates (sample every 3rd event to avoid spam)
|
|
91
|
+
from .....progress.models import ProgressUpdate
|
|
92
|
+
|
|
93
|
+
event_count = 0
|
|
94
|
+
async for event in handler.iter_events(with_logs=True):
|
|
95
|
+
event_count += 1
|
|
96
|
+
|
|
97
|
+
# Process every 3rd event to provide feedback without overwhelming
|
|
98
|
+
if event_count % 3 == 0:
|
|
99
|
+
# Extract logs if available
|
|
100
|
+
logs = getattr(event, "logs", None)
|
|
101
|
+
if logs:
|
|
102
|
+
# Join log entries into a single message
|
|
103
|
+
if isinstance(logs, list):
|
|
104
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
105
|
+
else:
|
|
106
|
+
message = str(logs)
|
|
107
|
+
|
|
108
|
+
if message:
|
|
109
|
+
await context.publish_progress(
|
|
110
|
+
ProgressUpdate(
|
|
111
|
+
job_id=handler.request_id,
|
|
112
|
+
status="processing",
|
|
113
|
+
progress=50.0, # Approximate mid-point progress
|
|
114
|
+
phase="processing",
|
|
115
|
+
message=message,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Get final result
|
|
120
|
+
result = await handler.get()
|
|
121
|
+
|
|
122
|
+
# Extract image URLs from result
|
|
123
|
+
# fal.ai returns: {"images": [{"url": "...", "width": ..., "height": ...}, ...]}
|
|
124
|
+
images = result.get("images", [])
|
|
125
|
+
if not images:
|
|
126
|
+
raise ValueError("No images returned from fal.ai API")
|
|
127
|
+
|
|
128
|
+
# Store each image using output_index
|
|
129
|
+
artifacts = []
|
|
130
|
+
for idx, image_data in enumerate(images):
|
|
131
|
+
image_url = image_data.get("url")
|
|
132
|
+
width = image_data.get("width")
|
|
133
|
+
height = image_data.get("height")
|
|
134
|
+
|
|
135
|
+
if not image_url:
|
|
136
|
+
raise ValueError(f"Image {idx} missing URL in fal.ai response")
|
|
137
|
+
|
|
138
|
+
# Store with appropriate output_index
|
|
139
|
+
artifact = await context.store_image_result(
|
|
140
|
+
storage_url=image_url,
|
|
141
|
+
format=inputs.output_format,
|
|
142
|
+
width=width,
|
|
143
|
+
height=height,
|
|
144
|
+
output_index=idx,
|
|
145
|
+
)
|
|
146
|
+
artifacts.append(artifact)
|
|
147
|
+
|
|
148
|
+
return GeneratorResult(outputs=artifacts)
|
|
149
|
+
|
|
150
|
+
async def estimate_cost(self, inputs: ReveTextToImageInput) -> float:
|
|
151
|
+
"""Estimate cost for Reve text-to-image generation.
|
|
152
|
+
|
|
153
|
+
Reve typically costs around $0.03 per image.
|
|
154
|
+
"""
|
|
155
|
+
return 0.03 * inputs.num_images
|
package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate high-quality images using ByteDance's Seedream 4.5 text-to-image model.
|
|
3
|
+
|
|
4
|
+
Based on Fal AI's fal-ai/bytedance/seedream/v4.5/text-to-image model.
|
|
5
|
+
See: https://fal.ai/models/fal-ai/bytedance/seedream/v4.5/text-to-image
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SeedreamV45TextToImageInput(BaseModel):
|
|
17
|
+
"""Input schema for Seedream V4.5 text-to-image generation.
|
|
18
|
+
|
|
19
|
+
Seedream 4.5 is ByteDance's new-generation image creation model that integrates
|
|
20
|
+
image generation and editing capabilities into a unified architecture.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
prompt: str = Field(description="The text prompt used to generate the image")
|
|
24
|
+
num_images: int = Field(
|
|
25
|
+
default=1,
|
|
26
|
+
ge=1,
|
|
27
|
+
le=6,
|
|
28
|
+
description="Number of images to generate",
|
|
29
|
+
)
|
|
30
|
+
image_size: (
|
|
31
|
+
Literal[
|
|
32
|
+
"square_hd",
|
|
33
|
+
"portrait_4_3",
|
|
34
|
+
"landscape_16_9",
|
|
35
|
+
"auto_2K",
|
|
36
|
+
"auto_4K",
|
|
37
|
+
]
|
|
38
|
+
| None
|
|
39
|
+
) = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description=(
|
|
42
|
+
"The size preset for the generated image. Options include "
|
|
43
|
+
"square_hd, portrait_4_3, landscape_16_9, auto_2K, auto_4K"
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
seed: int | None = Field(
|
|
47
|
+
default=None,
|
|
48
|
+
description="Random seed for reproducibility",
|
|
49
|
+
)
|
|
50
|
+
enable_safety_checker: bool = Field(
|
|
51
|
+
default=True,
|
|
52
|
+
description="Enable or disable the safety checker",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FalSeedreamV45TextToImageGenerator(BaseGenerator):
|
|
57
|
+
"""Generator for high-quality images using ByteDance's Seedream 4.5 model."""
|
|
58
|
+
|
|
59
|
+
name = "fal-seedream-v45-text-to-image"
|
|
60
|
+
artifact_type = "image"
|
|
61
|
+
description = "Fal: ByteDance Seedream 4.5 - high-quality text-to-image generation"
|
|
62
|
+
|
|
63
|
+
def get_input_schema(self) -> type[SeedreamV45TextToImageInput]:
|
|
64
|
+
"""Return the input schema for this generator."""
|
|
65
|
+
return SeedreamV45TextToImageInput
|
|
66
|
+
|
|
67
|
+
async def generate(
|
|
68
|
+
self, inputs: SeedreamV45TextToImageInput, context: GeneratorExecutionContext
|
|
69
|
+
) -> GeneratorResult:
|
|
70
|
+
"""Generate images using fal.ai ByteDance Seedream 4.5 model."""
|
|
71
|
+
# Check for API key (fal-client uses FAL_KEY environment variable)
|
|
72
|
+
if not os.getenv("FAL_KEY"):
|
|
73
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
74
|
+
|
|
75
|
+
# Import fal_client
|
|
76
|
+
try:
|
|
77
|
+
import fal_client
|
|
78
|
+
except ImportError as e:
|
|
79
|
+
raise ImportError(
|
|
80
|
+
"fal.ai SDK is required for FalSeedreamV45TextToImageGenerator. "
|
|
81
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
82
|
+
) from e
|
|
83
|
+
|
|
84
|
+
# Prepare arguments for fal.ai API
|
|
85
|
+
arguments: dict[str, object] = {
|
|
86
|
+
"prompt": inputs.prompt,
|
|
87
|
+
"num_images": inputs.num_images,
|
|
88
|
+
"enable_safety_checker": inputs.enable_safety_checker,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Add optional parameters
|
|
92
|
+
if inputs.image_size is not None:
|
|
93
|
+
arguments["image_size"] = inputs.image_size
|
|
94
|
+
|
|
95
|
+
if inputs.seed is not None:
|
|
96
|
+
arguments["seed"] = inputs.seed
|
|
97
|
+
|
|
98
|
+
# Submit async job and get handler
|
|
99
|
+
handler = await fal_client.submit_async(
|
|
100
|
+
"fal-ai/bytedance/seedream/v4.5/text-to-image",
|
|
101
|
+
arguments=arguments,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Store the external job ID for tracking
|
|
105
|
+
await context.set_external_job_id(handler.request_id)
|
|
106
|
+
|
|
107
|
+
# Stream progress updates (sample every 3rd event to avoid spam)
|
|
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
|
+
|
|
114
|
+
# Process every 3rd event to provide feedback without overwhelming
|
|
115
|
+
if event_count % 3 == 0:
|
|
116
|
+
# Extract logs if available
|
|
117
|
+
logs = getattr(event, "logs", None)
|
|
118
|
+
if logs:
|
|
119
|
+
# Join log entries into a single message
|
|
120
|
+
if isinstance(logs, list):
|
|
121
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
122
|
+
else:
|
|
123
|
+
message = str(logs)
|
|
124
|
+
|
|
125
|
+
if message:
|
|
126
|
+
await context.publish_progress(
|
|
127
|
+
ProgressUpdate(
|
|
128
|
+
job_id=handler.request_id,
|
|
129
|
+
status="processing",
|
|
130
|
+
progress=50.0, # Approximate mid-point progress
|
|
131
|
+
phase="processing",
|
|
132
|
+
message=message,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Get final result
|
|
137
|
+
result = await handler.get()
|
|
138
|
+
|
|
139
|
+
# Extract image data from result
|
|
140
|
+
# fal.ai seedream returns:
|
|
141
|
+
# {"images": [{"url": "...", "width": ..., "height": ..., ...}], "seed": ...}
|
|
142
|
+
images = result.get("images", [])
|
|
143
|
+
if not images:
|
|
144
|
+
raise ValueError("No images returned from fal.ai API")
|
|
145
|
+
|
|
146
|
+
# Store each image using output_index
|
|
147
|
+
artifacts = []
|
|
148
|
+
for idx, image_data in enumerate(images):
|
|
149
|
+
image_url = image_data.get("url")
|
|
150
|
+
|
|
151
|
+
if not image_url:
|
|
152
|
+
raise ValueError(f"Image {idx} missing URL in fal.ai response")
|
|
153
|
+
|
|
154
|
+
# Extract dimensions if available, use defaults otherwise
|
|
155
|
+
width = image_data.get("width", 2048)
|
|
156
|
+
height = image_data.get("height", 2048)
|
|
157
|
+
|
|
158
|
+
# Determine format from content_type (e.g., "image/png" -> "png")
|
|
159
|
+
content_type = image_data.get("content_type", "image/png")
|
|
160
|
+
format = content_type.split("/")[-1] if "/" in content_type else "png"
|
|
161
|
+
|
|
162
|
+
# Store with appropriate output_index
|
|
163
|
+
artifact = await context.store_image_result(
|
|
164
|
+
storage_url=image_url,
|
|
165
|
+
format=format,
|
|
166
|
+
width=width,
|
|
167
|
+
height=height,
|
|
168
|
+
output_index=idx,
|
|
169
|
+
)
|
|
170
|
+
artifacts.append(artifact)
|
|
171
|
+
|
|
172
|
+
return GeneratorResult(outputs=artifacts)
|
|
173
|
+
|
|
174
|
+
async def estimate_cost(self, inputs: SeedreamV45TextToImageInput) -> float:
|
|
175
|
+
"""Estimate cost for Seedream V4.5 generation.
|
|
176
|
+
|
|
177
|
+
Seedream V4.5 pricing is approximately $0.03 per image generation.
|
|
178
|
+
Note: Actual pricing may vary. Check Fal AI documentation for current rates.
|
|
179
|
+
"""
|
|
180
|
+
return 0.03 * inputs.num_images
|
|
@@ -13,6 +13,10 @@ from .fal_minimax_hailuo_02_standard_text_to_video import (
|
|
|
13
13
|
from .fal_pixverse_lipsync import FalPixverseLipsyncGenerator
|
|
14
14
|
from .fal_sora_2_text_to_video import FalSora2TextToVideoGenerator
|
|
15
15
|
from .infinitalk import FalInfinitalkGenerator
|
|
16
|
+
from .kling_video_ai_avatar_v2_pro import FalKlingVideoAiAvatarV2ProGenerator
|
|
17
|
+
from .kling_video_ai_avatar_v2_standard import (
|
|
18
|
+
FalKlingVideoAiAvatarV2StandardGenerator,
|
|
19
|
+
)
|
|
16
20
|
from .kling_video_v2_5_turbo_pro_image_to_video import (
|
|
17
21
|
FalKlingVideoV25TurboProImageToVideoGenerator,
|
|
18
22
|
)
|
|
@@ -27,11 +31,17 @@ from .sora_2_image_to_video_pro import FalSora2ImageToVideoProGenerator
|
|
|
27
31
|
from .sora_2_text_to_video_pro import FalSora2TextToVideoProGenerator
|
|
28
32
|
from .sync_lipsync_v2 import FalSyncLipsyncV2Generator
|
|
29
33
|
from .sync_lipsync_v2_pro import FalSyncLipsyncV2ProGenerator
|
|
34
|
+
from .veed_fabric_1_0 import FalVeedFabric10Generator
|
|
30
35
|
from .veed_lipsync import FalVeedLipsyncGenerator
|
|
31
36
|
from .veo3 import FalVeo3Generator
|
|
37
|
+
from .veo31 import FalVeo31Generator
|
|
38
|
+
from .veo31_fast import FalVeo31FastGenerator
|
|
39
|
+
from .veo31_fast_image_to_video import FalVeo31FastImageToVideoGenerator
|
|
32
40
|
from .veo31_first_last_frame_to_video import FalVeo31FirstLastFrameToVideoGenerator
|
|
33
41
|
from .veo31_image_to_video import FalVeo31ImageToVideoGenerator
|
|
34
42
|
from .veo31_reference_to_video import FalVeo31ReferenceToVideoGenerator
|
|
43
|
+
from .wan_25_preview_image_to_video import FalWan25PreviewImageToVideoGenerator
|
|
44
|
+
from .wan_25_preview_text_to_video import FalWan25PreviewTextToVideoGenerator
|
|
35
45
|
from .wan_pro_image_to_video import FalWanProImageToVideoGenerator
|
|
36
46
|
|
|
37
47
|
__all__ = [
|
|
@@ -39,6 +49,8 @@ __all__ = [
|
|
|
39
49
|
"FalCreatifyLipsyncGenerator",
|
|
40
50
|
"FalBytedanceSeedanceV1ProImageToVideoGenerator",
|
|
41
51
|
"FalBytedanceSeedanceV1ProTextToVideoGenerator",
|
|
52
|
+
"FalKlingVideoAiAvatarV2ProGenerator",
|
|
53
|
+
"FalKlingVideoAiAvatarV2StandardGenerator",
|
|
42
54
|
"FalKlingVideoV25TurboProImageToVideoGenerator",
|
|
43
55
|
"FalKlingVideoV25TurboProTextToVideoGenerator",
|
|
44
56
|
"FalPixverseLipsyncGenerator",
|
|
@@ -49,11 +61,17 @@ __all__ = [
|
|
|
49
61
|
"FalSora2ImageToVideoGenerator",
|
|
50
62
|
"FalSora2ImageToVideoProGenerator",
|
|
51
63
|
"FalSyncLipsyncV2Generator",
|
|
64
|
+
"FalVeedFabric10Generator",
|
|
52
65
|
"FalVeedLipsyncGenerator",
|
|
53
66
|
"FalSyncLipsyncV2ProGenerator",
|
|
54
67
|
"FalVeo3Generator",
|
|
68
|
+
"FalVeo31Generator",
|
|
69
|
+
"FalVeo31FastGenerator",
|
|
70
|
+
"FalVeo31FastImageToVideoGenerator",
|
|
55
71
|
"FalVeo31FirstLastFrameToVideoGenerator",
|
|
56
72
|
"FalVeo31ImageToVideoGenerator",
|
|
57
73
|
"FalVeo31ReferenceToVideoGenerator",
|
|
74
|
+
"FalWan25PreviewImageToVideoGenerator",
|
|
75
|
+
"FalWan25PreviewTextToVideoGenerator",
|
|
58
76
|
"FalWanProImageToVideoGenerator",
|
|
59
77
|
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fal.ai Kling Video AI Avatar v2 Pro generator.
|
|
3
|
+
|
|
4
|
+
Transforms static portrait images into synchronized talking avatar videos
|
|
5
|
+
with audio-driven facial animation. Supports realistic humans, animals,
|
|
6
|
+
cartoons, and stylized figures.
|
|
7
|
+
|
|
8
|
+
Based on Fal AI's fal-ai/kling-video/ai-avatar/v2/pro model.
|
|
9
|
+
See: https://fal.ai/models/fal-ai/kling-video/ai-avatar/v2/pro
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ....artifacts import AudioArtifact, ImageArtifact
|
|
17
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KlingVideoAiAvatarV2ProInput(BaseModel):
|
|
21
|
+
"""Input schema for kling-video/ai-avatar/v2/pro.
|
|
22
|
+
|
|
23
|
+
Artifact fields are automatically detected via type introspection
|
|
24
|
+
and resolved from generation IDs to artifact objects.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
image: ImageArtifact = Field(description="The image to use as your avatar")
|
|
28
|
+
audio: AudioArtifact = Field(description="The audio file for lip-sync animation")
|
|
29
|
+
prompt: str = Field(
|
|
30
|
+
default=".",
|
|
31
|
+
description="Optional prompt to refine animation details",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FalKlingVideoAiAvatarV2ProGenerator(BaseGenerator):
|
|
36
|
+
"""Generator for AI avatar talking videos using Kling Video AI Avatar v2 Pro."""
|
|
37
|
+
|
|
38
|
+
name = "fal-kling-video-ai-avatar-v2-pro"
|
|
39
|
+
description = (
|
|
40
|
+
"Fal: Kling Video AI Avatar v2 Pro - "
|
|
41
|
+
"Transform portraits into talking avatar videos with audio-driven facial animation"
|
|
42
|
+
)
|
|
43
|
+
artifact_type = "video"
|
|
44
|
+
|
|
45
|
+
def get_input_schema(self) -> type[KlingVideoAiAvatarV2ProInput]:
|
|
46
|
+
"""Return the input schema for this generator."""
|
|
47
|
+
return KlingVideoAiAvatarV2ProInput
|
|
48
|
+
|
|
49
|
+
async def generate(
|
|
50
|
+
self, inputs: KlingVideoAiAvatarV2ProInput, context: GeneratorExecutionContext
|
|
51
|
+
) -> GeneratorResult:
|
|
52
|
+
"""Generate talking avatar video using fal.ai kling-video/ai-avatar/v2/pro."""
|
|
53
|
+
# Check for API key
|
|
54
|
+
if not os.getenv("FAL_KEY"):
|
|
55
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
56
|
+
|
|
57
|
+
# Import fal_client
|
|
58
|
+
try:
|
|
59
|
+
import fal_client
|
|
60
|
+
except ImportError as e:
|
|
61
|
+
raise ImportError(
|
|
62
|
+
"fal.ai SDK is required for FalKlingVideoAiAvatarV2ProGenerator. "
|
|
63
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
64
|
+
) from e
|
|
65
|
+
|
|
66
|
+
# Upload image and audio artifacts to Fal's public storage
|
|
67
|
+
# Fal API requires publicly accessible URLs
|
|
68
|
+
from ..utils import upload_artifacts_to_fal
|
|
69
|
+
|
|
70
|
+
# Upload image and audio separately
|
|
71
|
+
image_urls = await upload_artifacts_to_fal([inputs.image], context)
|
|
72
|
+
audio_urls = await upload_artifacts_to_fal([inputs.audio], context)
|
|
73
|
+
|
|
74
|
+
# Prepare arguments for fal.ai API
|
|
75
|
+
arguments: dict[str, str] = {
|
|
76
|
+
"image_url": image_urls[0],
|
|
77
|
+
"audio_url": audio_urls[0],
|
|
78
|
+
"prompt": inputs.prompt,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Submit async job
|
|
82
|
+
handler = await fal_client.submit_async(
|
|
83
|
+
"fal-ai/kling-video/ai-avatar/v2/pro",
|
|
84
|
+
arguments=arguments,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Store external job ID
|
|
88
|
+
await context.set_external_job_id(handler.request_id)
|
|
89
|
+
|
|
90
|
+
# Stream progress updates
|
|
91
|
+
from .....progress.models import ProgressUpdate
|
|
92
|
+
|
|
93
|
+
event_count = 0
|
|
94
|
+
async for event in handler.iter_events(with_logs=True):
|
|
95
|
+
event_count += 1
|
|
96
|
+
# Sample every 3rd event to avoid spam
|
|
97
|
+
if event_count % 3 == 0:
|
|
98
|
+
# Extract logs if available
|
|
99
|
+
logs = getattr(event, "logs", None)
|
|
100
|
+
if logs:
|
|
101
|
+
# Join log entries into a single message
|
|
102
|
+
if isinstance(logs, list):
|
|
103
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
104
|
+
else:
|
|
105
|
+
message = str(logs)
|
|
106
|
+
|
|
107
|
+
if message:
|
|
108
|
+
await context.publish_progress(
|
|
109
|
+
ProgressUpdate(
|
|
110
|
+
job_id=handler.request_id,
|
|
111
|
+
status="processing",
|
|
112
|
+
progress=50.0, # Approximate mid-point progress
|
|
113
|
+
phase="processing",
|
|
114
|
+
message=message,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Get final result
|
|
119
|
+
result = await handler.get()
|
|
120
|
+
|
|
121
|
+
# Extract video from result
|
|
122
|
+
# fal.ai returns: {"video": {"url": "...", "content_type": "..."}, "duration": ...}
|
|
123
|
+
video_data = result.get("video")
|
|
124
|
+
|
|
125
|
+
if not video_data:
|
|
126
|
+
raise ValueError("No video returned from fal.ai API")
|
|
127
|
+
|
|
128
|
+
video_url = video_data.get("url")
|
|
129
|
+
if not video_url:
|
|
130
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
131
|
+
|
|
132
|
+
# Extract format from content_type (e.g., "video/mp4" -> "mp4")
|
|
133
|
+
content_type = video_data.get("content_type", "video/mp4")
|
|
134
|
+
if content_type.startswith("video/"):
|
|
135
|
+
video_format = content_type.split("/")[-1]
|
|
136
|
+
else:
|
|
137
|
+
# Default to mp4 if content_type is not a video mime type
|
|
138
|
+
video_format = "mp4"
|
|
139
|
+
|
|
140
|
+
# Get duration from result if available
|
|
141
|
+
duration = result.get("duration")
|
|
142
|
+
|
|
143
|
+
# Store the video result
|
|
144
|
+
# Note: The API doesn't return width/height/fps, so we use reasonable defaults
|
|
145
|
+
artifact = await context.store_video_result(
|
|
146
|
+
storage_url=video_url,
|
|
147
|
+
format=video_format,
|
|
148
|
+
width=None,
|
|
149
|
+
height=None,
|
|
150
|
+
duration=duration,
|
|
151
|
+
fps=None,
|
|
152
|
+
output_index=0,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return GeneratorResult(outputs=[artifact])
|
|
156
|
+
|
|
157
|
+
async def estimate_cost(self, inputs: KlingVideoAiAvatarV2ProInput) -> float:
|
|
158
|
+
"""Estimate cost for this generation in USD.
|
|
159
|
+
|
|
160
|
+
Pricing: $0.115 per second of generated video.
|
|
161
|
+
Cost depends on audio duration since the output video matches audio length.
|
|
162
|
+
"""
|
|
163
|
+
# If audio duration is available, calculate based on that
|
|
164
|
+
if inputs.audio.duration is not None:
|
|
165
|
+
return 0.115 * inputs.audio.duration
|
|
166
|
+
|
|
167
|
+
# Default estimate for unknown duration (assume ~10 second video)
|
|
168
|
+
return 1.15
|