@weirdfingers/baseboards 0.6.2 → 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.
- 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,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kie.ai nano-banana image-to-image editing generator.
|
|
3
|
+
|
|
4
|
+
Edit images using Kie.ai's google/nano-banana-edit model (powered by Google Gemini).
|
|
5
|
+
Supports editing multiple input images with a text prompt.
|
|
6
|
+
|
|
7
|
+
Based on Kie.ai's google/nano-banana-edit model.
|
|
8
|
+
See: https://docs.kie.ai/market/google/nano-banana-edit
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from ....artifacts import ImageArtifact
|
|
16
|
+
from ....base import GeneratorExecutionContext, GeneratorResult
|
|
17
|
+
from ..base import KieMarketAPIGenerator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NanoBananaEditInput(BaseModel):
|
|
21
|
+
"""Input schema for nano-banana image editing.
|
|
22
|
+
|
|
23
|
+
Artifact fields (like image_sources) are automatically detected via type
|
|
24
|
+
introspection and resolved from generation IDs to ImageArtifact objects.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
prompt: str = Field(
|
|
28
|
+
description="The prompt for image editing",
|
|
29
|
+
max_length=5000,
|
|
30
|
+
)
|
|
31
|
+
image_sources: list[ImageArtifact] = Field(
|
|
32
|
+
description="List of input images for editing (from previous generations)",
|
|
33
|
+
min_length=1,
|
|
34
|
+
max_length=10,
|
|
35
|
+
)
|
|
36
|
+
output_format: Literal["png", "jpeg"] = Field(
|
|
37
|
+
default="png",
|
|
38
|
+
description="Output image format",
|
|
39
|
+
)
|
|
40
|
+
image_size: Literal[
|
|
41
|
+
"1:1",
|
|
42
|
+
"9:16",
|
|
43
|
+
"16:9",
|
|
44
|
+
"3:4",
|
|
45
|
+
"4:3",
|
|
46
|
+
"3:2",
|
|
47
|
+
"2:3",
|
|
48
|
+
"5:4",
|
|
49
|
+
"4:5",
|
|
50
|
+
"21:9",
|
|
51
|
+
"auto",
|
|
52
|
+
] = Field(
|
|
53
|
+
default="1:1",
|
|
54
|
+
description="Output image aspect ratio",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KieNanoBananaEditGenerator(KieMarketAPIGenerator):
|
|
59
|
+
"""nano-banana image editing generator using Kie.ai Market API."""
|
|
60
|
+
|
|
61
|
+
name = "kie-nano-banana-edit"
|
|
62
|
+
artifact_type = "image"
|
|
63
|
+
description = "Kie.ai: Google nano-banana edit - AI-powered image editing with Gemini"
|
|
64
|
+
|
|
65
|
+
# Market API configuration
|
|
66
|
+
model_id = "google/nano-banana-edit"
|
|
67
|
+
|
|
68
|
+
def get_input_schema(self) -> type[NanoBananaEditInput]:
|
|
69
|
+
return NanoBananaEditInput
|
|
70
|
+
|
|
71
|
+
async def generate(
|
|
72
|
+
self, inputs: NanoBananaEditInput, context: GeneratorExecutionContext
|
|
73
|
+
) -> GeneratorResult:
|
|
74
|
+
"""Edit images using Kie.ai google/nano-banana-edit model."""
|
|
75
|
+
# Get API key using base class method
|
|
76
|
+
api_key = self._get_api_key()
|
|
77
|
+
|
|
78
|
+
# Upload image artifacts to Kie.ai's public storage
|
|
79
|
+
# Kie.ai API requires publicly accessible URLs, but our storage_url might be:
|
|
80
|
+
# - Localhost URLs (not publicly accessible)
|
|
81
|
+
# - Private S3 buckets (not publicly accessible)
|
|
82
|
+
# So we upload to Kie.ai's temporary storage first
|
|
83
|
+
from ..utils import upload_artifacts_to_kie
|
|
84
|
+
|
|
85
|
+
image_urls = await upload_artifacts_to_kie(inputs.image_sources, context)
|
|
86
|
+
|
|
87
|
+
# Prepare request body for Market API
|
|
88
|
+
body = {
|
|
89
|
+
"model": self.model_id,
|
|
90
|
+
"input": {
|
|
91
|
+
"prompt": inputs.prompt,
|
|
92
|
+
"image_urls": image_urls,
|
|
93
|
+
"output_format": inputs.output_format,
|
|
94
|
+
"image_size": inputs.image_size,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Submit task using base class method
|
|
99
|
+
submit_url = "https://api.kie.ai/api/v1/jobs/createTask"
|
|
100
|
+
result = await self._make_request(submit_url, "POST", api_key, json=body)
|
|
101
|
+
|
|
102
|
+
# Extract task ID with safe dictionary access
|
|
103
|
+
data = result.get("data", {})
|
|
104
|
+
task_id = data.get("taskId")
|
|
105
|
+
|
|
106
|
+
if not task_id:
|
|
107
|
+
raise ValueError(f"No taskId returned from Kie.ai API. Response: {result}")
|
|
108
|
+
|
|
109
|
+
# Store external job ID
|
|
110
|
+
await context.set_external_job_id(task_id)
|
|
111
|
+
|
|
112
|
+
# Poll for completion using base class method
|
|
113
|
+
task_data = await self._poll_for_completion(task_id, api_key, context)
|
|
114
|
+
|
|
115
|
+
# Extract outputs from resultJson
|
|
116
|
+
result_json = task_data.get("resultJson")
|
|
117
|
+
if result_json:
|
|
118
|
+
import json
|
|
119
|
+
|
|
120
|
+
result_data = json.loads(result_json)
|
|
121
|
+
else:
|
|
122
|
+
result_data = task_data.get("result")
|
|
123
|
+
|
|
124
|
+
if not result_data:
|
|
125
|
+
raise ValueError("No result data returned from Kie.ai API")
|
|
126
|
+
|
|
127
|
+
# Extract image URLs from result
|
|
128
|
+
# The response structure may vary, but typically contains image URLs
|
|
129
|
+
# Based on the API pattern, result should contain the generated images
|
|
130
|
+
images = result_data.get("images", [])
|
|
131
|
+
|
|
132
|
+
if not images:
|
|
133
|
+
# Sometimes the result might directly contain URLs in different structure
|
|
134
|
+
# Try to extract from common patterns
|
|
135
|
+
if isinstance(result_data, dict):
|
|
136
|
+
# Check for common response patterns
|
|
137
|
+
if "resultUrls" in result_data:
|
|
138
|
+
# Market API returns resultUrls as array of strings
|
|
139
|
+
images = [{"url": url} for url in result_data["resultUrls"]]
|
|
140
|
+
elif "image_urls" in result_data:
|
|
141
|
+
images = [{"url": url} for url in result_data["image_urls"]]
|
|
142
|
+
elif "url" in result_data:
|
|
143
|
+
images = [{"url": result_data["url"]}]
|
|
144
|
+
else:
|
|
145
|
+
raise ValueError(f"No images found in result: {result_data}")
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError("No images returned from Kie.ai API")
|
|
148
|
+
|
|
149
|
+
# Store each image using output_index
|
|
150
|
+
artifacts = []
|
|
151
|
+
for idx, image_data in enumerate(images):
|
|
152
|
+
if isinstance(image_data, str):
|
|
153
|
+
image_url = image_data
|
|
154
|
+
width = 1024
|
|
155
|
+
height = 1024
|
|
156
|
+
elif isinstance(image_data, dict):
|
|
157
|
+
image_url = image_data.get("url")
|
|
158
|
+
# Extract dimensions if available, otherwise use sensible defaults
|
|
159
|
+
width = image_data.get("width", 1024)
|
|
160
|
+
height = image_data.get("height", 1024)
|
|
161
|
+
else:
|
|
162
|
+
raise ValueError(f"Unexpected image data format: {type(image_data)}")
|
|
163
|
+
|
|
164
|
+
if not image_url:
|
|
165
|
+
raise ValueError(f"Image {idx} missing URL in Kie.ai response")
|
|
166
|
+
|
|
167
|
+
# Store with appropriate output_index
|
|
168
|
+
artifact = await context.store_image_result(
|
|
169
|
+
storage_url=image_url,
|
|
170
|
+
format=inputs.output_format,
|
|
171
|
+
width=width,
|
|
172
|
+
height=height,
|
|
173
|
+
output_index=idx,
|
|
174
|
+
)
|
|
175
|
+
artifacts.append(artifact)
|
|
176
|
+
|
|
177
|
+
return GeneratorResult(outputs=artifacts)
|
|
178
|
+
|
|
179
|
+
async def estimate_cost(self, inputs: NanoBananaEditInput) -> float:
|
|
180
|
+
"""Estimate cost for nano-banana edit generation.
|
|
181
|
+
|
|
182
|
+
nano-banana/edit uses Google Gemini for image editing.
|
|
183
|
+
Estimated at $0.025 per image (approximately 35% cheaper than Fal's $0.039).
|
|
184
|
+
|
|
185
|
+
Note: Actual pricing should be verified at https://kie.ai/pricing
|
|
186
|
+
"""
|
|
187
|
+
# Cost per image - this is an estimate and should be updated with actual pricing
|
|
188
|
+
per_image_cost = 0.025
|
|
189
|
+
num_images = len(inputs.image_sources)
|
|
190
|
+
return per_image_cost * num_images
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for Kie.ai generators.
|
|
3
|
+
|
|
4
|
+
Provides helper functions for common operations across Kie generators.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ...artifacts import AudioArtifact, DigitalArtifact, ImageArtifact, VideoArtifact
|
|
13
|
+
from ...base import GeneratorExecutionContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def upload_artifacts_to_kie[T: DigitalArtifact](
|
|
17
|
+
artifacts: list[ImageArtifact] | list[VideoArtifact] | list[AudioArtifact] | list[T],
|
|
18
|
+
context: GeneratorExecutionContext,
|
|
19
|
+
) -> list[str]:
|
|
20
|
+
"""
|
|
21
|
+
Upload artifacts to Kie.ai's temporary storage for use in API requests.
|
|
22
|
+
|
|
23
|
+
Kie.ai API endpoints require publicly accessible URLs for file inputs. Since our
|
|
24
|
+
storage URLs might be local or private (localhost, private S3 buckets, etc.),
|
|
25
|
+
we need to:
|
|
26
|
+
1. Resolve each artifact to a local file path
|
|
27
|
+
2. Upload to Kie.ai's public temporary storage
|
|
28
|
+
3. Get back publicly accessible URLs
|
|
29
|
+
|
|
30
|
+
Note: Files uploaded to Kie.ai storage expire after 3 days.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
artifacts: List of artifacts (image, video, or audio) to upload
|
|
34
|
+
context: Generator execution context for artifact resolution
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of publicly accessible URLs from Kie.ai storage
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If KIE_API_KEY is not set
|
|
41
|
+
Any exceptions from file resolution or upload are propagated
|
|
42
|
+
"""
|
|
43
|
+
api_key = os.getenv("KIE_API_KEY")
|
|
44
|
+
if not api_key:
|
|
45
|
+
raise ValueError("KIE_API_KEY environment variable is required for file uploads")
|
|
46
|
+
|
|
47
|
+
async def upload_single_artifact(artifact: DigitalArtifact) -> str:
|
|
48
|
+
"""Upload a single artifact and return its public URL."""
|
|
49
|
+
# Resolve artifact to local file path (downloads if needed)
|
|
50
|
+
file_path_str = await context.resolve_artifact(artifact)
|
|
51
|
+
|
|
52
|
+
# Upload to Kie.ai's temporary storage
|
|
53
|
+
# Using file stream upload API
|
|
54
|
+
async with httpx.AsyncClient() as client:
|
|
55
|
+
with open(file_path_str, "rb") as f:
|
|
56
|
+
files = {"file": f}
|
|
57
|
+
# uploadPath is required by Kie.ai API - specifies the storage path
|
|
58
|
+
data = {"uploadPath": "boards/temp"}
|
|
59
|
+
response = await client.post(
|
|
60
|
+
"https://kieai.redpandaai.co/api/file-stream-upload",
|
|
61
|
+
files=files,
|
|
62
|
+
data=data,
|
|
63
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
64
|
+
timeout=120.0, # 2 minute timeout for uploads
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if response.status_code != 200:
|
|
68
|
+
raise ValueError(f"File upload failed: {response.status_code} {response.text}")
|
|
69
|
+
|
|
70
|
+
result = response.json()
|
|
71
|
+
|
|
72
|
+
if not result.get("success"):
|
|
73
|
+
raise ValueError(f"File upload failed: {result.get('msg')}")
|
|
74
|
+
|
|
75
|
+
# Extract the public URL from response data
|
|
76
|
+
data = result.get("data", {})
|
|
77
|
+
|
|
78
|
+
# The actual field name is 'downloadUrl' based on API response
|
|
79
|
+
file_url = data.get("downloadUrl")
|
|
80
|
+
|
|
81
|
+
if not file_url:
|
|
82
|
+
# Fallback to other possible field names
|
|
83
|
+
file_url = data.get("fileUrl") or data.get("file_url") or data.get("url")
|
|
84
|
+
|
|
85
|
+
if not file_url:
|
|
86
|
+
# If we still can't find the URL, provide detailed error message
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"File upload succeeded but couldn't find URL in response. "
|
|
89
|
+
f"Response data keys: {list(data.keys())}, "
|
|
90
|
+
f"Full response: {result}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return file_url
|
|
94
|
+
|
|
95
|
+
# Upload all artifacts in parallel for performance
|
|
96
|
+
urls = await asyncio.gather(*[upload_single_artifact(artifact) for artifact in artifacts])
|
|
97
|
+
|
|
98
|
+
return list(urls)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kie.ai Veo 3.1 text-to-video and image-to-video generator.
|
|
3
|
+
|
|
4
|
+
Generate high-quality videos from text prompts with optional image inputs
|
|
5
|
+
using Kie.ai's Google Veo 3.1 model (Dedicated API).
|
|
6
|
+
|
|
7
|
+
Based on Kie.ai's Veo 3.1 API.
|
|
8
|
+
See: https://docs.kie.ai/veo3-api/generate-veo-3-video
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from ....artifacts import ImageArtifact
|
|
16
|
+
from ....base import GeneratorExecutionContext, GeneratorResult
|
|
17
|
+
from ..base import KieDedicatedAPIGenerator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KieVeo3Input(BaseModel):
|
|
21
|
+
"""Input schema for Kie.ai Veo 3.1 video generation.
|
|
22
|
+
|
|
23
|
+
Supports both text-to-video and image-to-video modes.
|
|
24
|
+
Artifact fields (like image_sources) are automatically detected via type
|
|
25
|
+
introspection and resolved from generation IDs to ImageArtifact objects.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
prompt: str = Field(
|
|
29
|
+
description="The text prompt describing the video you want to generate",
|
|
30
|
+
max_length=5000,
|
|
31
|
+
)
|
|
32
|
+
image_sources: list[ImageArtifact] | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description="Optional list of 1-2 input images for image-to-video generation",
|
|
35
|
+
min_length=1,
|
|
36
|
+
max_length=2,
|
|
37
|
+
)
|
|
38
|
+
aspect_ratio: Literal["16:9", "9:16", "Auto"] = Field(
|
|
39
|
+
default="16:9",
|
|
40
|
+
description="Aspect ratio of the generated video",
|
|
41
|
+
)
|
|
42
|
+
model: Literal["veo3", "veo3_fast"] = Field(
|
|
43
|
+
default="veo3",
|
|
44
|
+
description="Model variant to use (veo3 for quality, veo3_fast for speed)",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class KieVeo3Generator(KieDedicatedAPIGenerator):
|
|
49
|
+
"""Veo 3.1 video generator using Kie.ai Dedicated API."""
|
|
50
|
+
|
|
51
|
+
name = "kie-veo3"
|
|
52
|
+
artifact_type = "video"
|
|
53
|
+
description = "Kie.ai: Google Veo 3.1 - High-quality AI video generation"
|
|
54
|
+
|
|
55
|
+
# Dedicated API configuration
|
|
56
|
+
model_id = "veo3"
|
|
57
|
+
|
|
58
|
+
def get_input_schema(self) -> type[KieVeo3Input]:
|
|
59
|
+
return KieVeo3Input
|
|
60
|
+
|
|
61
|
+
def _get_status_url(self, task_id: str) -> str:
|
|
62
|
+
"""Get the Veo3-specific status check URL."""
|
|
63
|
+
return f"https://api.kie.ai/api/v1/veo/record-info?taskId={task_id}"
|
|
64
|
+
|
|
65
|
+
async def generate(
|
|
66
|
+
self, inputs: KieVeo3Input, context: GeneratorExecutionContext
|
|
67
|
+
) -> GeneratorResult:
|
|
68
|
+
"""Generate video using Kie.ai Veo 3.1 model."""
|
|
69
|
+
# Get API key using base class method
|
|
70
|
+
api_key = self._get_api_key()
|
|
71
|
+
|
|
72
|
+
# Prepare request body for Dedicated API
|
|
73
|
+
body: dict[str, Any] = {
|
|
74
|
+
"prompt": inputs.prompt,
|
|
75
|
+
"aspectRatio": inputs.aspect_ratio,
|
|
76
|
+
"model": inputs.model,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Upload image artifacts if provided (for image-to-video mode)
|
|
80
|
+
if inputs.image_sources:
|
|
81
|
+
from ..utils import upload_artifacts_to_kie
|
|
82
|
+
|
|
83
|
+
image_urls = await upload_artifacts_to_kie(inputs.image_sources, context)
|
|
84
|
+
body["imageUrls"] = image_urls
|
|
85
|
+
|
|
86
|
+
# Submit task to Dedicated API endpoint using base class method
|
|
87
|
+
submit_url = "https://api.kie.ai/api/v1/veo/generate"
|
|
88
|
+
result = await self._make_request(submit_url, "POST", api_key, json=body)
|
|
89
|
+
|
|
90
|
+
# Extract task ID from Dedicated API response
|
|
91
|
+
# Try direct taskId first, then nested under 'data'
|
|
92
|
+
task_id = result.get("taskId")
|
|
93
|
+
if not task_id:
|
|
94
|
+
data = result.get("data", {})
|
|
95
|
+
task_id = data.get("taskId")
|
|
96
|
+
|
|
97
|
+
if not task_id:
|
|
98
|
+
raise ValueError(f"No taskId returned from Kie.ai API. Response: {result}")
|
|
99
|
+
|
|
100
|
+
# Store external job ID
|
|
101
|
+
await context.set_external_job_id(task_id)
|
|
102
|
+
|
|
103
|
+
# Poll for completion using base class method
|
|
104
|
+
result_data = await self._poll_for_completion(task_id, api_key, context)
|
|
105
|
+
|
|
106
|
+
# Extract video URLs from response.resultUrls field
|
|
107
|
+
# Dedicated API nests the results inside a 'response' object
|
|
108
|
+
response_data = result_data.get("response")
|
|
109
|
+
if not response_data:
|
|
110
|
+
raise ValueError(f"No response field in result. Response: {result_data}")
|
|
111
|
+
|
|
112
|
+
result_urls = response_data.get("resultUrls")
|
|
113
|
+
if not result_urls or not isinstance(result_urls, list):
|
|
114
|
+
raise ValueError(f"No resultUrls in response. Response: {result_data}")
|
|
115
|
+
|
|
116
|
+
# Determine video dimensions based on aspect ratio
|
|
117
|
+
# Default to 1080p quality
|
|
118
|
+
if inputs.aspect_ratio == "16:9":
|
|
119
|
+
width, height = 1920, 1080
|
|
120
|
+
elif inputs.aspect_ratio == "9:16":
|
|
121
|
+
width, height = 1080, 1920
|
|
122
|
+
else: # Auto
|
|
123
|
+
# Default to 16:9 for Auto
|
|
124
|
+
width, height = 1920, 1080
|
|
125
|
+
|
|
126
|
+
# Veo 3 generates ~8 second videos by default
|
|
127
|
+
duration = 8.0
|
|
128
|
+
|
|
129
|
+
# Store each video using output_index
|
|
130
|
+
artifacts = []
|
|
131
|
+
for idx, video_url in enumerate(result_urls):
|
|
132
|
+
if not video_url:
|
|
133
|
+
raise ValueError(f"Video {idx} missing URL in Kie.ai response")
|
|
134
|
+
|
|
135
|
+
artifact = await context.store_video_result(
|
|
136
|
+
storage_url=video_url,
|
|
137
|
+
format="mp4",
|
|
138
|
+
width=width,
|
|
139
|
+
height=height,
|
|
140
|
+
duration=duration,
|
|
141
|
+
output_index=idx,
|
|
142
|
+
)
|
|
143
|
+
artifacts.append(artifact)
|
|
144
|
+
|
|
145
|
+
return GeneratorResult(outputs=artifacts)
|
|
146
|
+
|
|
147
|
+
async def estimate_cost(self, inputs: KieVeo3Input) -> float:
|
|
148
|
+
"""Estimate cost for Veo 3.1 video generation.
|
|
149
|
+
|
|
150
|
+
Veo 3.1 pricing is estimated based on typical video generation costs.
|
|
151
|
+
Actual pricing should be verified at https://kie.ai/pricing
|
|
152
|
+
|
|
153
|
+
Base cost estimates:
|
|
154
|
+
- veo3: $0.08 per video (standard quality)
|
|
155
|
+
- veo3_fast: $0.04 per video (faster generation)
|
|
156
|
+
"""
|
|
157
|
+
# Cost varies by model
|
|
158
|
+
if inputs.model == "veo3_fast":
|
|
159
|
+
return 0.04
|
|
160
|
+
else: # veo3
|
|
161
|
+
return 0.08
|
|
@@ -114,7 +114,7 @@ def _is_safe_url(url: str) -> tuple[bool, str | None]:
|
|
|
114
114
|
if parsed.scheme not in ("http", "https"):
|
|
115
115
|
return (
|
|
116
116
|
False,
|
|
117
|
-
f"URL scheme '{parsed.scheme}' not allowed.
|
|
117
|
+
f"URL scheme '{parsed.scheme}' not allowed. Only http and https are supported.",
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
hostname = parsed.hostname
|
|
@@ -10,16 +10,19 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
13
14
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
14
15
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
15
16
|
"@radix-ui/react-slot": "^1.2.3",
|
|
17
|
+
"@radix-ui/react-toast": "^1.2.15",
|
|
16
18
|
"@tailwindcss/postcss": "^4.1.13",
|
|
17
|
-
"@weirdfingers/boards": "^0.
|
|
19
|
+
"@weirdfingers/boards": "^0.7.0",
|
|
18
20
|
"class-variance-authority": "^0.7.1",
|
|
19
21
|
"clsx": "^2.0.0",
|
|
20
22
|
"graphql": "^16.11.0",
|
|
21
23
|
"lucide-react": "^0.544.0",
|
|
22
24
|
"next": "14.2.5",
|
|
25
|
+
"next-themes": "^0.4.6",
|
|
23
26
|
"react": "^18.3.1",
|
|
24
27
|
"react-dom": "^18.3.1",
|
|
25
28
|
"tailwind-merge": "^3.3.1"
|