@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
+ 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,8 @@
1
+ """Kie.ai video generators."""
2
+
3
+ from .veo3 import KieVeo3Generator, KieVeo3Input
4
+
5
+ __all__ = [
6
+ "KieVeo3Generator",
7
+ "KieVeo3Input",
8
+ ]
@@ -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. " "Only http and https are supported.",
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.6.2",
19
+ "@weirdfingers/boards": "^0.8.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"