Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m43s
Details
CI/CD / deploy (push) Successful in 2m43s
Details
This commit is contained in:
commit
2d6226630a
|
|
@ -324,6 +324,14 @@ export class FolkImageGen extends FolkShape {
|
|||
this.dispatchEvent(new CustomEvent("close"));
|
||||
});
|
||||
|
||||
// Listen for incoming port data (arrow connections)
|
||||
this.addEventListener("port-value-changed", ((e: CustomEvent) => {
|
||||
const { name, value } = e.detail;
|
||||
if (name === "prompt" && typeof value === "string" && this.#promptInput) {
|
||||
this.#promptInput.value = value;
|
||||
}
|
||||
}) as EventListener);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
|
|
@ -361,6 +369,7 @@ export class FolkImageGen extends FolkShape {
|
|||
|
||||
this.#images.unshift(image);
|
||||
this.#renderImages();
|
||||
this.setPortValue("image", image.url);
|
||||
this.dispatchEvent(new CustomEvent("image-generated", { detail: { image } }));
|
||||
|
||||
// Clear input
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ const styles = css`
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.duration-select {
|
||||
.duration-select, .model-select, .resolution-select, .aspect-select {
|
||||
padding: 6px 10px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
|
|
@ -156,6 +156,27 @@ const styles = css`
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-select {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.seedance-options {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.seedance-options label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
|
|
@ -314,8 +335,14 @@ export class FolkVideoGen extends FolkShape {
|
|||
#progress = 0;
|
||||
#error: string | null = null;
|
||||
|
||||
#model = "seedance";
|
||||
#promptInput: HTMLTextAreaElement | null = null;
|
||||
#durationSelect: HTMLSelectElement | null = null;
|
||||
#modelSelect: HTMLSelectElement | null = null;
|
||||
#resolutionSelect: HTMLSelectElement | null = null;
|
||||
#aspectSelect: HTMLSelectElement | null = null;
|
||||
#audioToggle: HTMLInputElement | null = null;
|
||||
#seedanceOptions: HTMLElement | null = null;
|
||||
#videoArea: HTMLElement | null = null;
|
||||
#generateBtn: HTMLButtonElement | null = null;
|
||||
#imageUpload: HTMLElement | null = null;
|
||||
|
|
@ -352,12 +379,34 @@ export class FolkVideoGen extends FolkShape {
|
|||
<input type="file" class="hidden-input" accept="image/*" />
|
||||
<textarea class="prompt-input" placeholder="Describe the motion/action for the video..." rows="2"></textarea>
|
||||
<div class="controls">
|
||||
<select class="duration-select">
|
||||
<option value="4">4 seconds</option>
|
||||
<option value="5">5 seconds</option>
|
||||
<select class="model-select">
|
||||
<option value="seedance">Seedance 2.0</option>
|
||||
<option value="seedance-fast">Seedance 2.0 Fast</option>
|
||||
<option value="kling">Kling (i2v)</option>
|
||||
<option value="wan">WAN (t2v)</option>
|
||||
</select>
|
||||
<button class="generate-btn">Generate Video</button>
|
||||
</div>
|
||||
<div class="seedance-options">
|
||||
<select class="duration-select">
|
||||
<option value="5">5s</option>
|
||||
<option value="4">4s</option>
|
||||
<option value="6">6s</option>
|
||||
<option value="8">8s</option>
|
||||
<option value="10">10s</option>
|
||||
</select>
|
||||
<select class="resolution-select">
|
||||
<option value="480p">480p</option>
|
||||
<option value="720p">720p</option>
|
||||
</select>
|
||||
<select class="aspect-select">
|
||||
<option value="16:9">16:9</option>
|
||||
<option value="9:16">9:16</option>
|
||||
<option value="1:1">1:1</option>
|
||||
<option value="4:3">4:3</option>
|
||||
</select>
|
||||
<label><input type="checkbox" class="audio-toggle" /> Audio</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-area">
|
||||
<div class="placeholder">
|
||||
|
|
@ -377,6 +426,11 @@ export class FolkVideoGen extends FolkShape {
|
|||
|
||||
this.#promptInput = wrapper.querySelector(".prompt-input");
|
||||
this.#durationSelect = wrapper.querySelector(".duration-select");
|
||||
this.#modelSelect = wrapper.querySelector(".model-select");
|
||||
this.#resolutionSelect = wrapper.querySelector(".resolution-select");
|
||||
this.#aspectSelect = wrapper.querySelector(".aspect-select");
|
||||
this.#audioToggle = wrapper.querySelector(".audio-toggle");
|
||||
this.#seedanceOptions = wrapper.querySelector(".seedance-options");
|
||||
this.#videoArea = wrapper.querySelector(".video-area");
|
||||
this.#generateBtn = wrapper.querySelector(".generate-btn");
|
||||
this.#imageUpload = wrapper.querySelector(".image-upload");
|
||||
|
|
@ -415,12 +469,42 @@ export class FolkVideoGen extends FolkShape {
|
|||
// Prevent drag on inputs
|
||||
this.#promptInput?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
|
||||
// Model selector
|
||||
this.#modelSelect?.addEventListener("change", (e) => {
|
||||
e.stopPropagation();
|
||||
this.#model = this.#modelSelect?.value || "seedance";
|
||||
this.#updateModelUI(modeTabs);
|
||||
});
|
||||
this.#modelSelect?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
this.#resolutionSelect?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
this.#aspectSelect?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
this.#durationSelect?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
this.#updateModelUI(modeTabs);
|
||||
|
||||
// Close button
|
||||
closeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("close"));
|
||||
});
|
||||
|
||||
// Listen for incoming port data (arrow connections)
|
||||
this.addEventListener("port-value-changed", ((e: CustomEvent) => {
|
||||
const { name, value } = e.detail;
|
||||
if (name === "prompt" && typeof value === "string" && this.#promptInput) {
|
||||
this.#promptInput.value = value;
|
||||
} else if (name === "image" && typeof value === "string") {
|
||||
this.#sourceImage = value;
|
||||
this.#setMode("i2v");
|
||||
modeTabs.forEach((t) => {
|
||||
t.classList.toggle("active", (t as HTMLElement).dataset.mode === "i2v");
|
||||
});
|
||||
if (this.#imageUpload) {
|
||||
this.#imageUpload.classList.add("has-image");
|
||||
this.#imageUpload.innerHTML = `<img class="uploaded-image" src="${this.#escapeHtml(value)}" alt="Source" />`;
|
||||
}
|
||||
}
|
||||
}) as EventListener);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
|
|
@ -431,6 +515,21 @@ export class FolkVideoGen extends FolkShape {
|
|||
}
|
||||
}
|
||||
|
||||
#updateModelUI(modeTabs: NodeListOf<Element>) {
|
||||
const isSeedance = this.#model === "seedance" || this.#model === "seedance-fast";
|
||||
if (this.#seedanceOptions) {
|
||||
this.#seedanceOptions.style.display = isSeedance ? "flex" : "none";
|
||||
}
|
||||
// WAN is t2v-only, Kling is i2v-only — auto-switch mode
|
||||
if (this.#model === "wan") {
|
||||
this.#setMode("t2v");
|
||||
modeTabs.forEach((t) => t.classList.toggle("active", (t as HTMLElement).dataset.mode === "t2v"));
|
||||
} else if (this.#model === "kling") {
|
||||
this.#setMode("i2v");
|
||||
modeTabs.forEach((t) => t.classList.toggle("active", (t as HTMLElement).dataset.mode === "i2v"));
|
||||
}
|
||||
}
|
||||
|
||||
#handleImageUpload(file: File) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
|
|
@ -463,10 +562,15 @@ export class FolkVideoGen extends FolkShape {
|
|||
|
||||
try {
|
||||
const endpoint = this.#mode === "i2v" ? "/api/video-gen/i2v" : "/api/video-gen/t2v";
|
||||
const body =
|
||||
const body: Record<string, any> =
|
||||
this.#mode === "i2v"
|
||||
? { image: this.#sourceImage, prompt, duration }
|
||||
: { prompt, duration };
|
||||
body.model = this.#model;
|
||||
body.duration = String(duration);
|
||||
body.resolution = this.#resolutionSelect?.value || "480p";
|
||||
body.aspect_ratio = this.#aspectSelect?.value || "16:9";
|
||||
body.generate_audio = this.#audioToggle?.checked ?? false;
|
||||
|
||||
const submitRes = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
|
|
@ -522,6 +626,7 @@ export class FolkVideoGen extends FolkShape {
|
|||
|
||||
this.#videos.unshift(video);
|
||||
this.#renderVideos();
|
||||
this.setPortValue("video", video.url);
|
||||
this.dispatchEvent(new CustomEvent("video-generated", { detail: { video } }));
|
||||
if (this.#promptInput) this.#promptInput.value = "";
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1168,6 +1168,11 @@ interface VideoGenJob {
|
|||
completedAt?: number;
|
||||
queuePosition?: number;
|
||||
falStatus?: string;
|
||||
model?: string;
|
||||
duration?: string;
|
||||
resolution?: string;
|
||||
aspectRatio?: string;
|
||||
generateAudio?: boolean;
|
||||
}
|
||||
|
||||
const videoGenJobs = new Map<string, VideoGenJob>();
|
||||
|
|
@ -1185,13 +1190,38 @@ async function processVideoGenJob(job: VideoGenJob) {
|
|||
const falHeaders = { Authorization: `Key ${FAL_KEY}`, "Content-Type": "application/json" };
|
||||
|
||||
try {
|
||||
const MODEL = job.type === "i2v"
|
||||
? "fal-ai/kling-video/v1/standard/image-to-video"
|
||||
: "fal-ai/wan-t2v";
|
||||
let MODEL: string;
|
||||
let body: Record<string, any>;
|
||||
const m = job.model || (job.type === "i2v" ? "kling" : "wan");
|
||||
|
||||
const body = job.type === "i2v"
|
||||
? { image_url: job.sourceImage, prompt: job.prompt || "", duration: 5, aspect_ratio: "16:9" }
|
||||
: { prompt: job.prompt, num_frames: 81, resolution: "480p" };
|
||||
switch (m) {
|
||||
case "seedance":
|
||||
case "seedance-fast": {
|
||||
const variant = m === "seedance-fast" ? "seedance-2.0-fast" : "seedance-2.0";
|
||||
const mode = job.type === "i2v" ? "image-to-video" : "text-to-video";
|
||||
MODEL = `fal-ai/bytedance/${variant}/${mode}`;
|
||||
body = {
|
||||
prompt: job.prompt,
|
||||
duration: job.duration || "5",
|
||||
resolution: job.resolution || "480p",
|
||||
aspect_ratio: job.aspectRatio || "16:9",
|
||||
generate_audio: job.generateAudio ?? false,
|
||||
};
|
||||
if (job.type === "i2v" && job.sourceImage) {
|
||||
body.image_url = job.sourceImage;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "kling":
|
||||
MODEL = "fal-ai/kling-video/v1/standard/image-to-video";
|
||||
body = { image_url: job.sourceImage, prompt: job.prompt || "", duration: 5, aspect_ratio: "16:9" };
|
||||
break;
|
||||
case "wan":
|
||||
default:
|
||||
MODEL = "fal-ai/wan-t2v";
|
||||
body = { prompt: job.prompt, num_frames: 81, resolution: "480p" };
|
||||
break;
|
||||
}
|
||||
|
||||
// Submit to fal.ai queue
|
||||
const submitRes = await fetch(`https://queue.fal.run/${MODEL}`, {
|
||||
|
|
@ -1790,13 +1820,15 @@ app.post("/api/image-gen/img2img", async (c) => {
|
|||
app.post("/api/video-gen/t2v", async (c) => {
|
||||
if (!FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);
|
||||
|
||||
const { prompt } = await c.req.json();
|
||||
const { prompt, model, duration, resolution, aspect_ratio, generate_audio } = await c.req.json();
|
||||
if (!prompt) return c.json({ error: "prompt required" }, 400);
|
||||
|
||||
const jobId = crypto.randomUUID();
|
||||
const job: VideoGenJob = {
|
||||
id: jobId, status: "pending", type: "t2v",
|
||||
prompt, createdAt: Date.now(),
|
||||
model: model || "wan",
|
||||
duration, resolution, aspectRatio: aspect_ratio, generateAudio: generate_audio,
|
||||
};
|
||||
videoGenJobs.set(jobId, job);
|
||||
processVideoGenJob(job);
|
||||
|
|
@ -1807,7 +1839,7 @@ app.post("/api/video-gen/t2v", async (c) => {
|
|||
app.post("/api/video-gen/i2v", async (c) => {
|
||||
if (!FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);
|
||||
|
||||
const { image, prompt } = await c.req.json();
|
||||
const { image, prompt, model, duration, resolution, aspect_ratio, generate_audio } = await c.req.json();
|
||||
if (!image) return c.json({ error: "image required" }, 400);
|
||||
|
||||
// Stage the source image if it's a data URL
|
||||
|
|
@ -1823,6 +1855,8 @@ app.post("/api/video-gen/i2v", async (c) => {
|
|||
const job: VideoGenJob = {
|
||||
id: jobId, status: "pending", type: "i2v",
|
||||
prompt: prompt || "", sourceImage: imageUrl, createdAt: Date.now(),
|
||||
model: model || "kling",
|
||||
duration, resolution, aspectRatio: aspect_ratio, generateAudio: generate_audio,
|
||||
};
|
||||
videoGenJobs.set(jobId, job);
|
||||
processVideoGenJob(job);
|
||||
|
|
|
|||
|
|
@ -654,6 +654,47 @@ export default defineConfig({
|
|||
},
|
||||
});
|
||||
|
||||
// Build docs module component (uses Automerge shim from shell-offline)
|
||||
await wasmBuild({
|
||||
configFile: false,
|
||||
root: resolve(__dirname, "modules/rdocs/components"),
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
outDir: resolve(__dirname, "dist/modules/rdocs"),
|
||||
lib: {
|
||||
entry: resolve(__dirname, "modules/rdocs/components/folk-docs-app.ts"),
|
||||
formats: ["es"],
|
||||
fileName: () => "folk-docs-app.js",
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: "folk-docs-app.js",
|
||||
inlineDynamicImports: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Build voice recorder component
|
||||
await wasmBuild({
|
||||
configFile: false,
|
||||
root: resolve(__dirname, "modules/rdocs/components"),
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
outDir: resolve(__dirname, "dist/modules/rdocs"),
|
||||
lib: {
|
||||
entry: resolve(__dirname, "modules/rdocs/components/folk-voice-recorder.ts"),
|
||||
formats: ["es"],
|
||||
fileName: () => "folk-voice-recorder.js",
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: "folk-voice-recorder.js",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Copy docs CSS
|
||||
mkdirSync(resolve(__dirname, "dist/modules/rdocs"), { recursive: true });
|
||||
copyFileSync(
|
||||
|
|
|
|||
Loading…
Reference in New Issue