@weirdfingers/baseboards 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,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
@@ -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