@weirdfingers/baseboards 0.4.0 → 0.5.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/package.json +1 -1
- package/templates/api/config/generators.yaml +9 -0
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/config.py +22 -7
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +2 -0
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +179 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +4 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +183 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +172 -0
- package/templates/api/src/boards/generators/resolution.py +58 -1
- package/templates/api/src/boards/jobs/repository.py +3 -3
- package/templates/api/src/boards/workers/context.py +7 -3
- package/templates/compose.dev.yaml +1 -2
- package/templates/compose.yaml +6 -0
- package/templates/web/package.json +1 -1
- package/templates/web/src/app/boards/[boardId]/page.tsx +44 -64
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +67 -3
- package/templates/web/src/components/boards/ArtifactPreview.tsx +292 -20
- package/templates/web/src/components/boards/GenerationGrid.tsx +51 -11
- package/templates/web/src/components/boards/GenerationInput.tsx +26 -23
- package/templates/web/src/components/boards/GeneratorSelector.tsx +10 -1
|
@@ -24,6 +24,55 @@ from .artifacts import (
|
|
|
24
24
|
logger = get_logger(__name__)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _rewrite_storage_url(storage_url: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Rewrite storage URL for Docker internal networking.
|
|
30
|
+
|
|
31
|
+
Similar to the Next.js imageLoader, this rewrites public API URLs
|
|
32
|
+
to internal Docker network URLs when running in containers.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
storage_url: The original storage URL
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: Rewritten URL if internal_api_url is configured, otherwise original URL
|
|
39
|
+
"""
|
|
40
|
+
from ..config import settings
|
|
41
|
+
|
|
42
|
+
logger.debug(
|
|
43
|
+
"Checking URL rewriting configuration",
|
|
44
|
+
internal_api_url=settings.internal_api_url,
|
|
45
|
+
storage_url=storage_url[:100] if storage_url else None,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if not settings.internal_api_url:
|
|
49
|
+
logger.debug("No internal_api_url configured, skipping URL rewrite")
|
|
50
|
+
return storage_url
|
|
51
|
+
|
|
52
|
+
# Common patterns to replace (localhost and 127.0.0.1 with various ports)
|
|
53
|
+
# In Docker, the public URL is typically http://localhost:8800 or http://localhost:8088
|
|
54
|
+
# We need to replace it with the internal URL (http://api:8800)
|
|
55
|
+
replacements = [
|
|
56
|
+
("http://localhost:8800", settings.internal_api_url),
|
|
57
|
+
("http://127.0.0.1:8800", settings.internal_api_url),
|
|
58
|
+
("http://localhost:8088", settings.internal_api_url),
|
|
59
|
+
("http://127.0.0.1:8088", settings.internal_api_url),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
rewritten_url = storage_url
|
|
63
|
+
for public_pattern, internal_url in replacements:
|
|
64
|
+
if public_pattern in storage_url:
|
|
65
|
+
rewritten_url = storage_url.replace(public_pattern, internal_url)
|
|
66
|
+
logger.info(
|
|
67
|
+
"Rewrote storage URL for internal Docker networking",
|
|
68
|
+
original_url=storage_url,
|
|
69
|
+
rewritten_url=rewritten_url,
|
|
70
|
+
)
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
return rewritten_url
|
|
74
|
+
|
|
75
|
+
|
|
27
76
|
async def resolve_artifact(
|
|
28
77
|
artifact: AudioArtifact | VideoArtifact | ImageArtifact | LoRArtifact,
|
|
29
78
|
) -> str:
|
|
@@ -97,9 +146,17 @@ async def download_artifact_to_temp(
|
|
|
97
146
|
os.chmod(temp_path, 0o600)
|
|
98
147
|
|
|
99
148
|
try:
|
|
149
|
+
# Rewrite URL for Docker internal networking
|
|
150
|
+
download_url = _rewrite_storage_url(artifact.storage_url)
|
|
151
|
+
|
|
100
152
|
# Stream the download to avoid loading large files into memory
|
|
101
153
|
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
102
|
-
|
|
154
|
+
logger.info(
|
|
155
|
+
"Attempting to download artifact",
|
|
156
|
+
original_url=artifact.storage_url,
|
|
157
|
+
download_url=download_url,
|
|
158
|
+
)
|
|
159
|
+
async with client.stream("GET", download_url) as response:
|
|
103
160
|
response.raise_for_status()
|
|
104
161
|
|
|
105
162
|
# Close the file descriptor returned by mkstemp and use aiofiles
|
|
@@ -115,7 +115,7 @@ async def create_batch_generation(
|
|
|
115
115
|
input_params: dict,
|
|
116
116
|
batch_id: str,
|
|
117
117
|
batch_index: int,
|
|
118
|
-
) ->
|
|
118
|
+
) -> str:
|
|
119
119
|
"""Create a batch generation record for multi-output generators.
|
|
120
120
|
|
|
121
121
|
This creates a new generation record that is part of a batch, with
|
|
@@ -133,7 +133,7 @@ async def create_batch_generation(
|
|
|
133
133
|
batch_index: Index of this output in the batch
|
|
134
134
|
|
|
135
135
|
Returns:
|
|
136
|
-
|
|
136
|
+
ID of the created generation record
|
|
137
137
|
"""
|
|
138
138
|
gen = Generations()
|
|
139
139
|
gen.tenant_id = tenant_id
|
|
@@ -150,4 +150,4 @@ async def create_batch_generation(
|
|
|
150
150
|
}
|
|
151
151
|
session.add(gen)
|
|
152
152
|
await session.flush()
|
|
153
|
-
return gen
|
|
153
|
+
return str(gen.id)
|
|
@@ -6,7 +6,12 @@ from uuid import UUID, uuid4
|
|
|
6
6
|
|
|
7
7
|
from ..database.connection import get_async_session
|
|
8
8
|
from ..generators import resolution
|
|
9
|
-
from ..generators.artifacts import
|
|
9
|
+
from ..generators.artifacts import (
|
|
10
|
+
AudioArtifact,
|
|
11
|
+
ImageArtifact,
|
|
12
|
+
TextArtifact,
|
|
13
|
+
VideoArtifact,
|
|
14
|
+
)
|
|
10
15
|
from ..jobs import repository as jobs_repo
|
|
11
16
|
from ..logging import get_logger
|
|
12
17
|
from ..progress.models import ProgressUpdate
|
|
@@ -319,7 +324,7 @@ class GeneratorExecutionContext:
|
|
|
319
324
|
|
|
320
325
|
# Create new batch generation record
|
|
321
326
|
async with get_async_session() as session:
|
|
322
|
-
|
|
327
|
+
batch_gen_id = await jobs_repo.create_batch_generation(
|
|
323
328
|
session,
|
|
324
329
|
tenant_id=UUID(self.tenant_id),
|
|
325
330
|
board_id=UUID(self.board_id),
|
|
@@ -331,7 +336,6 @@ class GeneratorExecutionContext:
|
|
|
331
336
|
batch_index=output_index,
|
|
332
337
|
)
|
|
333
338
|
await session.commit()
|
|
334
|
-
batch_gen_id = str(batch_gen.id)
|
|
335
339
|
|
|
336
340
|
self._batch_generations.append(batch_gen_id)
|
|
337
341
|
logger.info(
|
|
@@ -5,7 +5,6 @@ services:
|
|
|
5
5
|
api:
|
|
6
6
|
volumes:
|
|
7
7
|
- ./api:/app
|
|
8
|
-
- ./data/storage:/app/data/storage
|
|
9
8
|
environment:
|
|
10
9
|
- PYTHONUNBUFFERED=1
|
|
11
10
|
command:
|
|
@@ -24,9 +23,9 @@ services:
|
|
|
24
23
|
worker:
|
|
25
24
|
volumes:
|
|
26
25
|
- ./api:/app
|
|
27
|
-
- ./data/storage:/app/data/storage
|
|
28
26
|
environment:
|
|
29
27
|
- PYTHONUNBUFFERED=1
|
|
28
|
+
- BOARDS_INTERNAL_API_URL=http://api:8800
|
|
30
29
|
|
|
31
30
|
web:
|
|
32
31
|
command: sh -c "pnpm install && pnpm dev"
|
package/templates/compose.yaml
CHANGED
|
@@ -34,6 +34,8 @@ services:
|
|
|
34
34
|
env_file:
|
|
35
35
|
- docker/.env
|
|
36
36
|
- api/.env
|
|
37
|
+
volumes:
|
|
38
|
+
- ./data/storage:/app/data/storage
|
|
37
39
|
depends_on:
|
|
38
40
|
db:
|
|
39
41
|
condition: service_healthy
|
|
@@ -67,6 +69,10 @@ services:
|
|
|
67
69
|
env_file:
|
|
68
70
|
- docker/.env
|
|
69
71
|
- api/.env
|
|
72
|
+
volumes:
|
|
73
|
+
- ./data/storage:/app/data/storage
|
|
74
|
+
environment:
|
|
75
|
+
- BOARDS_INTERNAL_API_URL=http://api:8800
|
|
70
76
|
depends_on:
|
|
71
77
|
db:
|
|
72
78
|
condition: service_healthy
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
15
15
|
"@radix-ui/react-slot": "^1.2.3",
|
|
16
16
|
"@tailwindcss/postcss": "^4.1.13",
|
|
17
|
-
"@weirdfingers/boards": "^0.
|
|
17
|
+
"@weirdfingers/boards": "^0.5.0",
|
|
18
18
|
"class-variance-authority": "^0.7.1",
|
|
19
19
|
"clsx": "^2.0.0",
|
|
20
20
|
"graphql": "^16.11.0",
|
|
@@ -2,32 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useParams } from "next/navigation";
|
|
5
|
-
import { useBoard, useGenerators, useGeneration } from "@weirdfingers/boards";
|
|
5
|
+
import { useBoard, useGenerators, useGeneration, GeneratorSelectionProvider } from "@weirdfingers/boards";
|
|
6
6
|
import { GenerationGrid } from "@/components/boards/GenerationGrid";
|
|
7
7
|
import { GenerationInput } from "@/components/boards/GenerationInput";
|
|
8
8
|
|
|
9
9
|
export default function BoardPage() {
|
|
10
10
|
const params = useParams();
|
|
11
11
|
const boardId = params.boardId as string;
|
|
12
|
-
console.log("[BoardPage] Rendering with boardId:", boardId);
|
|
13
12
|
|
|
14
|
-
const boardHookResult = useBoard(boardId);
|
|
15
|
-
console.log("[BoardPage] useBoard result:", boardHookResult);
|
|
16
13
|
const {
|
|
17
14
|
board,
|
|
18
15
|
loading: boardLoading,
|
|
19
16
|
error: boardError,
|
|
20
17
|
refresh: refreshBoard,
|
|
21
|
-
} =
|
|
18
|
+
} = useBoard(boardId);
|
|
22
19
|
|
|
23
20
|
// Fetch available generators
|
|
24
|
-
const generatorsHookResult = useGenerators();
|
|
25
|
-
console.log("[BoardPage] useGenerators result:", generatorsHookResult);
|
|
26
21
|
const {
|
|
27
22
|
generators,
|
|
28
23
|
loading: generatorsLoading,
|
|
29
24
|
error: generatorsError,
|
|
30
|
-
} =
|
|
25
|
+
} = useGenerators();
|
|
31
26
|
|
|
32
27
|
// Use generation hook for submitting generations and real-time progress
|
|
33
28
|
const {
|
|
@@ -38,10 +33,6 @@ export default function BoardPage() {
|
|
|
38
33
|
result,
|
|
39
34
|
} = useGeneration();
|
|
40
35
|
|
|
41
|
-
console.log("[BoardPage] board:", board);
|
|
42
|
-
console.log("[BoardPage] boardError:", boardError);
|
|
43
|
-
console.log("[BoardPage] board is null/undefined?", !board);
|
|
44
|
-
|
|
45
36
|
// Refresh board when a generation completes or fails
|
|
46
37
|
// MUST be before conditional returns to satisfy Rules of Hooks
|
|
47
38
|
React.useEffect(() => {
|
|
@@ -49,10 +40,6 @@ export default function BoardPage() {
|
|
|
49
40
|
progress &&
|
|
50
41
|
(progress.status === "completed" || progress.status === "failed")
|
|
51
42
|
) {
|
|
52
|
-
console.log(
|
|
53
|
-
"[BoardPage] Generation finished, refreshing board:",
|
|
54
|
-
progress.status
|
|
55
|
-
);
|
|
56
43
|
refreshBoard();
|
|
57
44
|
}
|
|
58
45
|
}, [progress, refreshBoard]);
|
|
@@ -121,12 +108,6 @@ export default function BoardPage() {
|
|
|
121
108
|
|
|
122
109
|
// Handle loading state
|
|
123
110
|
if (boardLoading || !board) {
|
|
124
|
-
console.log(
|
|
125
|
-
"[BoardPage] Showing loading spinner - boardLoading:",
|
|
126
|
-
boardLoading,
|
|
127
|
-
"board:",
|
|
128
|
-
board
|
|
129
|
-
);
|
|
130
111
|
return (
|
|
131
112
|
<div className="flex items-center justify-center min-h-screen">
|
|
132
113
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
|
|
@@ -134,8 +115,6 @@ export default function BoardPage() {
|
|
|
134
115
|
);
|
|
135
116
|
}
|
|
136
117
|
|
|
137
|
-
console.log("[BoardPage] Board loaded successfully:", board);
|
|
138
|
-
|
|
139
118
|
// Filter completed generations that can be used as inputs
|
|
140
119
|
const availableArtifacts = generations.filter(
|
|
141
120
|
(gen) => gen.status === "COMPLETED" && gen.storageUrl
|
|
@@ -186,47 +165,48 @@ export default function BoardPage() {
|
|
|
186
165
|
};
|
|
187
166
|
|
|
188
167
|
return (
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/>
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
{/* Generation Input */}
|
|
211
|
-
<div className="sticky bottom-6 z-10">
|
|
212
|
-
{generatorsLoading ? (
|
|
213
|
-
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
214
|
-
<p className="text-gray-500">Loading generators...</p>
|
|
215
|
-
</div>
|
|
216
|
-
) : generators.length === 0 ? (
|
|
217
|
-
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
218
|
-
<p className="text-gray-500">No generators available</p>
|
|
219
|
-
</div>
|
|
220
|
-
) : (
|
|
221
|
-
<GenerationInput
|
|
222
|
-
generators={generators}
|
|
223
|
-
availableArtifacts={availableArtifacts}
|
|
224
|
-
onSubmit={handleGenerationSubmit}
|
|
225
|
-
isGenerating={isGenerating}
|
|
168
|
+
<GeneratorSelectionProvider>
|
|
169
|
+
<main className="min-h-screen bg-gray-50">
|
|
170
|
+
<div className="container mx-auto px-4 py-6 max-w-7xl">
|
|
171
|
+
{/* Header */}
|
|
172
|
+
<div className="mb-6">
|
|
173
|
+
<h1 className="text-3xl font-bold text-gray-900">{board.title}</h1>
|
|
174
|
+
{board.description && (
|
|
175
|
+
<p className="text-gray-600 mt-2">{board.description}</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Generation Grid */}
|
|
180
|
+
<div className="mb-8">
|
|
181
|
+
<GenerationGrid
|
|
182
|
+
generations={generations}
|
|
183
|
+
onGenerationClick={() => {
|
|
184
|
+
// TODO: Open generation detail modal
|
|
185
|
+
}}
|
|
226
186
|
/>
|
|
227
|
-
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Generation Input */}
|
|
190
|
+
<div id="generation-input" className="sticky bottom-6 z-10">
|
|
191
|
+
{generatorsLoading ? (
|
|
192
|
+
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
193
|
+
<p className="text-gray-500">Loading generators...</p>
|
|
194
|
+
</div>
|
|
195
|
+
) : generators.length === 0 ? (
|
|
196
|
+
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
197
|
+
<p className="text-gray-500">No generators available</p>
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<GenerationInput
|
|
201
|
+
generators={generators}
|
|
202
|
+
availableArtifacts={availableArtifacts}
|
|
203
|
+
onSubmit={handleGenerationSubmit}
|
|
204
|
+
isGenerating={isGenerating}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
228
208
|
</div>
|
|
229
|
-
</
|
|
230
|
-
</
|
|
209
|
+
</main>
|
|
210
|
+
</GeneratorSelectionProvider>
|
|
231
211
|
);
|
|
232
212
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import React, { useState } from "react";
|
|
3
4
|
import { FileVideo, Volume2, X } from "lucide-react";
|
|
4
5
|
import Image from "next/image";
|
|
5
6
|
|
|
@@ -29,6 +30,8 @@ export function ArtifactInputSlots({
|
|
|
29
30
|
availableArtifacts,
|
|
30
31
|
onSelectArtifact,
|
|
31
32
|
}: ArtifactInputSlotsProps) {
|
|
33
|
+
const [dragOverSlot, setDragOverSlot] = useState<string | null>(null);
|
|
34
|
+
|
|
32
35
|
const getIcon = (type: string) => {
|
|
33
36
|
switch (type.toLowerCase()) {
|
|
34
37
|
case "video":
|
|
@@ -47,6 +50,56 @@ export function ArtifactInputSlots({
|
|
|
47
50
|
);
|
|
48
51
|
};
|
|
49
52
|
|
|
53
|
+
const handleDragOver = (
|
|
54
|
+
e: React.DragEvent,
|
|
55
|
+
slotType: string,
|
|
56
|
+
slotName: string
|
|
57
|
+
) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
e.stopPropagation();
|
|
60
|
+
|
|
61
|
+
// Check if the dragged artifact type matches the slot type
|
|
62
|
+
try {
|
|
63
|
+
const data = e.dataTransfer.types.includes("application/json");
|
|
64
|
+
if (data) {
|
|
65
|
+
e.dataTransfer.dropEffect = "copy";
|
|
66
|
+
setDragOverSlot(slotName);
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// Ignore errors during drag over
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
setDragOverSlot(null);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleDrop = (
|
|
80
|
+
e: React.DragEvent,
|
|
81
|
+
slotType: string,
|
|
82
|
+
slotName: string
|
|
83
|
+
) => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
setDragOverSlot(null);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const jsonData = e.dataTransfer.getData("application/json");
|
|
90
|
+
if (jsonData) {
|
|
91
|
+
const artifact = JSON.parse(jsonData) as Generation;
|
|
92
|
+
|
|
93
|
+
// Check if artifact type matches slot type
|
|
94
|
+
if (artifact.artifactType.toLowerCase() === slotType.toLowerCase()) {
|
|
95
|
+
onSelectArtifact(slotName, artifact);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("Error handling drop:", err);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
50
103
|
return (
|
|
51
104
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
52
105
|
{slots.map((slot) => {
|
|
@@ -100,11 +153,22 @@ export function ArtifactInputSlots({
|
|
|
100
153
|
</div>
|
|
101
154
|
) : (
|
|
102
155
|
// Show slot placeholder
|
|
103
|
-
<div
|
|
156
|
+
<div
|
|
157
|
+
className={`border-2 border-dashed rounded-lg p-6 transition-all ${
|
|
158
|
+
dragOverSlot === slot.name
|
|
159
|
+
? "border-orange-500 bg-orange-50"
|
|
160
|
+
: "border-gray-300 hover:border-gray-400"
|
|
161
|
+
}`}
|
|
162
|
+
onDragOver={(e) => handleDragOver(e, slot.type, slot.name)}
|
|
163
|
+
onDragLeave={handleDragLeave}
|
|
164
|
+
onDrop={(e) => handleDrop(e, slot.type, slot.name)}
|
|
165
|
+
>
|
|
104
166
|
<div className="flex flex-col items-center justify-center text-center">
|
|
105
167
|
<div className="mb-2">{getIcon(slot.type)}</div>
|
|
106
168
|
<p className="text-sm font-medium text-gray-700 mb-1">
|
|
107
|
-
|
|
169
|
+
{dragOverSlot === slot.name
|
|
170
|
+
? `Drop ${slot.type} here`
|
|
171
|
+
: `Add a ${slot.type}`}
|
|
108
172
|
</p>
|
|
109
173
|
{matchingArtifacts.length > 0 ? (
|
|
110
174
|
<select
|
|
@@ -128,7 +192,7 @@ export function ArtifactInputSlots({
|
|
|
128
192
|
</select>
|
|
129
193
|
) : (
|
|
130
194
|
<p className="text-xs text-gray-500 mt-1">
|
|
131
|
-
No {slot.type} artifacts in this board yet
|
|
195
|
+
No {slot.type} artifacts in this board yet.
|
|
132
196
|
</p>
|
|
133
197
|
)}
|
|
134
198
|
</div>
|