From 73020236504b5df07d0f84f2164da55387107b97 Mon Sep 17 00:00:00 2001 From: blendi-remade Date: Tue, 21 Oct 2025 17:00:30 -0700 Subject: [PATCH 1/2] feat: implement background removal feature with API integration and UI support in timeline --- .../src/app/api/ai/remove-background/route.ts | 44 +++++++++++++++ .../editor/timeline/timeline-element.tsx | 54 +++++++++++++++++++ apps/web/src/lib/fal-client.ts | 41 +++++++++++++- apps/web/src/types/ai.ts | 10 ++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/api/ai/remove-background/route.ts diff --git a/apps/web/src/app/api/ai/remove-background/route.ts b/apps/web/src/app/api/ai/remove-background/route.ts new file mode 100644 index 0000000..38a703e --- /dev/null +++ b/apps/web/src/app/api/ai/remove-background/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server"; +import { fal } from "@fal-ai/client"; +import { z } from "zod"; + +const requestSchema = z.object({ + image_url: z.string().url(), +}); + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { image_url } = requestSchema.parse(body); + + const result = await fal.subscribe("fal-ai/bria/background/remove", { + input: { + image_url, + sync_mode: true, + }, + logs: true, + onQueueUpdate: (update) => { + if (update.status === "IN_PROGRESS") { + update.logs.map((log) => log.message).forEach(console.log); + } + }, + }); + + return NextResponse.json(result.data); + } catch (error) { + console.error("Background removal error:", error); + + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: "Invalid request parameters", details: error.errors }, + { status: 400 } + ); + } + + return NextResponse.json( + { error: "Failed to remove background" }, + { status: 500 } + ); + } +} + diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx index f9ae2f4..18c65ec 100644 --- a/apps/web/src/components/editor/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/timeline/timeline-element.tsx @@ -12,6 +12,7 @@ import { VolumeX, Sparkles, Video, + Eraser, } from "lucide-react"; import { useMediaStore } from "@/stores/media-store"; import { useTimelineStore } from "@/stores/timeline-store"; @@ -214,6 +215,55 @@ export function TimelineElement({ }); }; + const handleRemoveBackground = async (e: React.MouseEvent) => { + e.stopPropagation(); + + // Check if element is an image + if (element.type !== "media") return; + + const mediaItem = mediaFiles.find((file) => file.id === element.mediaId); + if (!mediaItem || mediaItem.type !== "image" || !mediaItem.url) return; + + // Show processing toast + const toastId = toast.loading("Removing background..."); + + try { + // Dynamically import the removeBackground function + const { removeBackground } = await import("@/lib/fal-client"); + + // Call the background removal API + const result = await removeBackground({ image_url: mediaItem.url }); + + // Fetch the result image and create a blob URL + const response = await fetch(result.image.url); + const blob = await response.blob(); + const newBlobUrl = URL.createObjectURL(blob); + + // Update the media store with the new image + const { mediaFiles } = useMediaStore.getState(); + useMediaStore.setState({ + mediaFiles: mediaFiles.map(file => + file.id === mediaItem.id + ? { ...file, url: newBlobUrl, name: `${file.name}-no-bg` } + : file + ) + }); + + // Clean up the old blob URL if it was a blob + if (mediaItem.url.startsWith('blob:')) { + URL.revokeObjectURL(mediaItem.url); + } + + toast.success("Background removed successfully", { id: toastId }); + } catch (error) { + console.error("Background removal error:", error); + toast.error("Failed to remove background", { + id: toastId, + description: error instanceof Error ? error.message : "Please try again", + }); + } + }; + const renderElementContent = () => { if (element.type === "text") { return ( @@ -437,6 +487,10 @@ export function TimelineElement({