Spaces:
Running
Running
Adding More features
Browse files- .claude/settings.local.json +2 -1
- app/api/hf-process/route.ts +0 -178
- app/api/process/route.ts +1 -1
- app/nodes.tsx +138 -90
- app/page.tsx +226 -55
- next.config.ts +0 -5
- package-lock.json +5 -5
- package.json +1 -1
.claude/settings.local.json
CHANGED
|
@@ -10,7 +10,8 @@
|
|
| 10 |
"Bash(npm run build:*)",
|
| 11 |
"Bash(git add:*)",
|
| 12 |
"Bash(git commit:*)",
|
| 13 |
-
"Bash(git push:*)"
|
|
|
|
| 14 |
],
|
| 15 |
"deny": [],
|
| 16 |
"ask": []
|
|
|
|
| 10 |
"Bash(npm run build:*)",
|
| 11 |
"Bash(git add:*)",
|
| 12 |
"Bash(git commit:*)",
|
| 13 |
+
"Bash(git push:*)",
|
| 14 |
+
"WebSearch"
|
| 15 |
],
|
| 16 |
"deny": [],
|
| 17 |
"ask": []
|
app/api/hf-process/route.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* API ROUTE: /api/hf-process
|
| 3 |
-
*
|
| 4 |
-
* Hugging Face Inference API integration with fal.ai Gemini 2.5 Flash Image.
|
| 5 |
-
* Uses HF Inference API to access fal.ai's Gemini 2.5 Flash Image models.
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
-
import { HfInference } from "@huggingface/inference";
|
| 10 |
-
import { cookies } from "next/headers";
|
| 11 |
-
|
| 12 |
-
export const runtime = "nodejs";
|
| 13 |
-
export const maxDuration = 60;
|
| 14 |
-
|
| 15 |
-
export async function POST(req: NextRequest) {
|
| 16 |
-
try {
|
| 17 |
-
// Check if user is authenticated with HF Pro
|
| 18 |
-
const cookieStore = await cookies();
|
| 19 |
-
const hfToken = cookieStore.get('hf_token');
|
| 20 |
-
|
| 21 |
-
if (!hfToken?.value) {
|
| 22 |
-
return NextResponse.json(
|
| 23 |
-
{ error: "Please login with HF Pro to use fal.ai Gemini 2.5 Flash Image." },
|
| 24 |
-
{ status: 401 }
|
| 25 |
-
);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
// Initialize HF Inference client
|
| 29 |
-
const hf = new HfInference(hfToken.value);
|
| 30 |
-
|
| 31 |
-
const body = await req.json() as {
|
| 32 |
-
type: string;
|
| 33 |
-
image?: string;
|
| 34 |
-
images?: string[];
|
| 35 |
-
prompt?: string;
|
| 36 |
-
params?: any;
|
| 37 |
-
};
|
| 38 |
-
|
| 39 |
-
// Convert data URL to blob for HF API
|
| 40 |
-
const dataUrlToBlob = (dataUrl: string): Blob => {
|
| 41 |
-
const arr = dataUrl.split(',');
|
| 42 |
-
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png';
|
| 43 |
-
const bstr = atob(arr[1]);
|
| 44 |
-
let n = bstr.length;
|
| 45 |
-
const u8arr = new Uint8Array(n);
|
| 46 |
-
while (n--) {
|
| 47 |
-
u8arr[n] = bstr.charCodeAt(n);
|
| 48 |
-
}
|
| 49 |
-
return new Blob([u8arr], { type: mime });
|
| 50 |
-
};
|
| 51 |
-
|
| 52 |
-
// Handle MERGE operation using Stable Diffusion
|
| 53 |
-
if (body.type === "MERGE") {
|
| 54 |
-
if (!body.images || body.images.length < 2) {
|
| 55 |
-
return NextResponse.json(
|
| 56 |
-
{ error: "MERGE requires at least two images" },
|
| 57 |
-
{ status: 400 }
|
| 58 |
-
);
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
const prompt = body.prompt || `Create a cohesive group photo combining all subjects from the provided images. Ensure consistent lighting, natural positioning, and unified background.`;
|
| 62 |
-
|
| 63 |
-
try {
|
| 64 |
-
// Use fal.ai's Gemini 2.5 Flash Image through HF
|
| 65 |
-
const result = await hf.textToImage({
|
| 66 |
-
model: "fal-ai/gemini-25-flash-image/edit",
|
| 67 |
-
inputs: prompt,
|
| 68 |
-
parameters: {
|
| 69 |
-
width: 1024,
|
| 70 |
-
height: 1024,
|
| 71 |
-
num_inference_steps: 20,
|
| 72 |
-
}
|
| 73 |
-
});
|
| 74 |
-
|
| 75 |
-
// HF returns a Blob, convert to base64
|
| 76 |
-
const arrayBuffer = await (result as unknown as Blob).arrayBuffer();
|
| 77 |
-
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
| 78 |
-
|
| 79 |
-
return NextResponse.json({
|
| 80 |
-
image: `data:image/png;base64,${base64}`,
|
| 81 |
-
model: "fal-ai/gemini-25-flash-image/edit"
|
| 82 |
-
});
|
| 83 |
-
} catch (error: unknown) {
|
| 84 |
-
console.error('HF Merge error:', error);
|
| 85 |
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
| 86 |
-
return NextResponse.json(
|
| 87 |
-
{ error: `HF processing failed: ${errorMessage}` },
|
| 88 |
-
{ status: 500 }
|
| 89 |
-
);
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
// Handle COMBINED and single image processing
|
| 94 |
-
if (body.type === "COMBINED" || !body.image) {
|
| 95 |
-
if (!body.image) {
|
| 96 |
-
return NextResponse.json(
|
| 97 |
-
{ error: "No image provided" },
|
| 98 |
-
{ status: 400 }
|
| 99 |
-
);
|
| 100 |
-
}
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
const inputBlob = dataUrlToBlob(body.image);
|
| 104 |
-
|
| 105 |
-
// Build prompt from parameters
|
| 106 |
-
const prompts: string[] = [];
|
| 107 |
-
const params = body.params || {};
|
| 108 |
-
|
| 109 |
-
// Background changes
|
| 110 |
-
if (params.backgroundType) {
|
| 111 |
-
if (params.backgroundType === "color") {
|
| 112 |
-
prompts.push(`Change background to ${params.backgroundColor || "white"}`);
|
| 113 |
-
} else if (params.backgroundType === "image") {
|
| 114 |
-
prompts.push(`Change background to ${params.backgroundImage || "beautiful landscape"}`);
|
| 115 |
-
} else if (params.customPrompt) {
|
| 116 |
-
prompts.push(params.customPrompt);
|
| 117 |
-
}
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
// Style applications
|
| 121 |
-
if (params.stylePreset) {
|
| 122 |
-
const styleMap: { [key: string]: string } = {
|
| 123 |
-
"90s-anime": "90s anime style, classic animation",
|
| 124 |
-
"cyberpunk": "cyberpunk aesthetic, neon lights, futuristic",
|
| 125 |
-
"van-gogh": "Van Gogh painting style, impressionist",
|
| 126 |
-
"simpsons": "The Simpsons cartoon style",
|
| 127 |
-
"arcane": "Arcane League of Legends art style"
|
| 128 |
-
};
|
| 129 |
-
const styleDesc = styleMap[params.stylePreset] || params.stylePreset;
|
| 130 |
-
prompts.push(`Apply ${styleDesc} art style`);
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
// Other modifications
|
| 134 |
-
if (params.editPrompt) {
|
| 135 |
-
prompts.push(params.editPrompt);
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
const prompt = prompts.length > 0
|
| 139 |
-
? prompts.join(", ")
|
| 140 |
-
: "High quality image processing";
|
| 141 |
-
|
| 142 |
-
try {
|
| 143 |
-
// Use fal.ai's Gemini 2.5 Flash Image for image editing
|
| 144 |
-
const result = await hf.imageToImage({
|
| 145 |
-
model: "fal-ai/gemini-25-flash-image/edit",
|
| 146 |
-
inputs: inputBlob,
|
| 147 |
-
parameters: {
|
| 148 |
-
prompt: prompt,
|
| 149 |
-
strength: 0.8,
|
| 150 |
-
num_inference_steps: 25,
|
| 151 |
-
}
|
| 152 |
-
});
|
| 153 |
-
|
| 154 |
-
const arrayBuffer = await (result as unknown as Blob).arrayBuffer();
|
| 155 |
-
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
| 156 |
-
|
| 157 |
-
return NextResponse.json({
|
| 158 |
-
image: `data:image/png;base64,${base64}`,
|
| 159 |
-
model: "fal-ai/gemini-25-flash-image/edit"
|
| 160 |
-
});
|
| 161 |
-
} catch (error: unknown) {
|
| 162 |
-
console.error('HF processing error:', error);
|
| 163 |
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
| 164 |
-
return NextResponse.json(
|
| 165 |
-
{ error: `HF processing failed: ${errorMessage}` },
|
| 166 |
-
{ status: 500 }
|
| 167 |
-
);
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
} catch (error: unknown) {
|
| 171 |
-
console.error('HF API error:', error);
|
| 172 |
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
| 173 |
-
return NextResponse.json(
|
| 174 |
-
{ error: `API error: ${errorMessage}` },
|
| 175 |
-
{ status: 500 }
|
| 176 |
-
);
|
| 177 |
-
}
|
| 178 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/process/route.ts
CHANGED
|
@@ -358,7 +358,7 @@ The result should look like all subjects were photographed together in the same
|
|
| 358 |
if (params.whiteBalance) cameraSettings.push(`White Balance: ${params.whiteBalance}`);
|
| 359 |
if (params.angle) cameraSettings.push(`Camera Angle: ${params.angle}`);
|
| 360 |
if (params.iso) cameraSettings.push(`${params.iso}`);
|
| 361 |
-
if (params.filmStyle) cameraSettings.push(
|
| 362 |
if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
|
| 363 |
if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
|
| 364 |
if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
|
|
|
|
| 358 |
if (params.whiteBalance) cameraSettings.push(`White Balance: ${params.whiteBalance}`);
|
| 359 |
if (params.angle) cameraSettings.push(`Camera Angle: ${params.angle}`);
|
| 360 |
if (params.iso) cameraSettings.push(`${params.iso}`);
|
| 361 |
+
if (params.filmStyle) cameraSettings.push(`${params.filmStyle}`);
|
| 362 |
if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
|
| 363 |
if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
|
| 364 |
if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
|
app/nodes.tsx
CHANGED
|
@@ -47,6 +47,75 @@ function downloadImage(dataUrl: string, filename: string) {
|
|
| 47 |
document.body.removeChild(link); // Clean up temporary link
|
| 48 |
}
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
/* ========================================
|
| 51 |
TYPE DEFINITIONS (TEMPORARY)
|
| 52 |
======================================== */
|
|
@@ -204,6 +273,9 @@ export function BackgroundNodeView({
|
|
| 204 |
onEndConnection,
|
| 205 |
onProcess,
|
| 206 |
onUpdatePosition,
|
|
|
|
|
|
|
|
|
|
| 207 |
}: any) {
|
| 208 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 209 |
|
|
@@ -372,18 +444,14 @@ export function BackgroundNodeView({
|
|
| 372 |
{node.isRunning ? "Processing..." : "Apply Background"}
|
| 373 |
</Button>
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
📥 Download Output
|
| 384 |
-
</Button>
|
| 385 |
-
</div>
|
| 386 |
-
)}
|
| 387 |
{node.error && (
|
| 388 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 389 |
)}
|
|
@@ -392,7 +460,7 @@ export function BackgroundNodeView({
|
|
| 392 |
);
|
| 393 |
}
|
| 394 |
|
| 395 |
-
export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
|
| 396 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 397 |
|
| 398 |
const presetClothes = [
|
|
@@ -546,18 +614,14 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 546 |
>
|
| 547 |
{node.isRunning ? "Processing..." : "Apply Clothes"}
|
| 548 |
</Button>
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
📥 Download Output
|
| 558 |
-
</Button>
|
| 559 |
-
</div>
|
| 560 |
-
)}
|
| 561 |
{node.error && (
|
| 562 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 563 |
)}
|
|
@@ -566,7 +630,7 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 566 |
);
|
| 567 |
}
|
| 568 |
|
| 569 |
-
export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
|
| 570 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 571 |
|
| 572 |
return (
|
|
@@ -631,18 +695,14 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 631 |
>
|
| 632 |
{node.isRunning ? "Processing..." : "Apply Age"}
|
| 633 |
</Button>
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
📥 Download Output
|
| 643 |
-
</Button>
|
| 644 |
-
</div>
|
| 645 |
-
)}
|
| 646 |
{node.error && (
|
| 647 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 648 |
)}
|
|
@@ -651,7 +711,7 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 651 |
);
|
| 652 |
}
|
| 653 |
|
| 654 |
-
export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
|
| 655 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 656 |
const focalLengths = ["None", "8mm fisheye", "12mm", "24mm", "35mm", "50mm", "85mm", "135mm", "200mm", "300mm", "400mm"];
|
| 657 |
const apertures = ["None", "f/0.95", "f/1.2", "f/1.4", "f/1.8", "f/2", "f/2.8", "f/4", "f/5.6", "f/8", "f/11", "f/16", "f/22"];
|
|
@@ -841,18 +901,16 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 841 |
>
|
| 842 |
{node.isRunning ? "Processing..." : "Apply Camera Settings"}
|
| 843 |
</Button>
|
| 844 |
-
|
| 845 |
-
<
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
</div>
|
| 855 |
-
)}
|
| 856 |
{node.error && (
|
| 857 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 858 |
)}
|
|
@@ -861,7 +919,7 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 861 |
);
|
| 862 |
}
|
| 863 |
|
| 864 |
-
export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
|
| 865 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 866 |
const hairstyles = ["None", "short", "long", "curly", "straight", "bald", "mohawk", "ponytail"];
|
| 867 |
const expressions = ["None", "happy", "serious", "smiling", "laughing", "sad", "surprised", "angry"];
|
|
@@ -988,18 +1046,16 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 988 |
>
|
| 989 |
{node.isRunning ? "Processing..." : "Apply Face Changes"}
|
| 990 |
</Button>
|
| 991 |
-
|
| 992 |
-
<
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
</div>
|
| 1002 |
-
)}
|
| 1003 |
{node.error && (
|
| 1004 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 1005 |
)}
|
|
@@ -1008,7 +1064,7 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1008 |
);
|
| 1009 |
}
|
| 1010 |
|
| 1011 |
-
export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
|
| 1012 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1013 |
|
| 1014 |
const styleOptions = [
|
|
@@ -1107,18 +1163,14 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1107 |
>
|
| 1108 |
{node.isRunning ? "Applying Style..." : "Apply Style Transfer"}
|
| 1109 |
</Button>
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
📥 Download Output
|
| 1119 |
-
</Button>
|
| 1120 |
-
</div>
|
| 1121 |
-
)}
|
| 1122 |
{node.error && (
|
| 1123 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 1124 |
)}
|
|
@@ -1127,7 +1179,7 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1127 |
);
|
| 1128 |
}
|
| 1129 |
|
| 1130 |
-
export function EditNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
|
| 1131 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1132 |
|
| 1133 |
return (
|
|
@@ -1182,18 +1234,14 @@ export function EditNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1182 |
>
|
| 1183 |
{node.isRunning ? "Processing..." : "Apply Edit"}
|
| 1184 |
</Button>
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
📥 Download Output
|
| 1194 |
-
</Button>
|
| 1195 |
-
</div>
|
| 1196 |
-
)}
|
| 1197 |
</div>
|
| 1198 |
</div>
|
| 1199 |
);
|
|
|
|
| 47 |
document.body.removeChild(link); // Clean up temporary link
|
| 48 |
}
|
| 49 |
|
| 50 |
+
/**
|
| 51 |
+
* Reusable output section with history navigation for node components
|
| 52 |
+
*/
|
| 53 |
+
function NodeOutputSection({
|
| 54 |
+
nodeId,
|
| 55 |
+
output,
|
| 56 |
+
downloadFileName,
|
| 57 |
+
getNodeHistoryInfo,
|
| 58 |
+
navigateNodeHistory,
|
| 59 |
+
getCurrentNodeImage,
|
| 60 |
+
}: {
|
| 61 |
+
nodeId: string;
|
| 62 |
+
output?: string;
|
| 63 |
+
downloadFileName: string;
|
| 64 |
+
getNodeHistoryInfo?: (id: string) => any;
|
| 65 |
+
navigateNodeHistory?: (id: string, direction: 'prev' | 'next') => void;
|
| 66 |
+
getCurrentNodeImage?: (id: string, fallback?: string) => string;
|
| 67 |
+
}) {
|
| 68 |
+
const currentImage = getCurrentNodeImage ? getCurrentNodeImage(nodeId, output) : output;
|
| 69 |
+
|
| 70 |
+
if (!currentImage) return null;
|
| 71 |
+
|
| 72 |
+
const historyInfo = getNodeHistoryInfo ? getNodeHistoryInfo(nodeId) : { hasHistory: false, currentDescription: '' };
|
| 73 |
+
|
| 74 |
+
return (
|
| 75 |
+
<div className="space-y-2">
|
| 76 |
+
<div className="space-y-1">
|
| 77 |
+
<div className="flex items-center justify-between">
|
| 78 |
+
<div className="text-xs text-white/70">Output</div>
|
| 79 |
+
{historyInfo.hasHistory ? (
|
| 80 |
+
<div className="flex items-center gap-1">
|
| 81 |
+
<button
|
| 82 |
+
className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
|
| 83 |
+
onClick={() => navigateNodeHistory && navigateNodeHistory(nodeId, 'prev')}
|
| 84 |
+
disabled={!historyInfo.canGoBack}
|
| 85 |
+
>
|
| 86 |
+
←
|
| 87 |
+
</button>
|
| 88 |
+
<span className="text-xs text-white/60 px-1">
|
| 89 |
+
{historyInfo.current}/{historyInfo.total}
|
| 90 |
+
</span>
|
| 91 |
+
<button
|
| 92 |
+
className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
|
| 93 |
+
onClick={() => navigateNodeHistory && navigateNodeHistory(nodeId, 'next')}
|
| 94 |
+
disabled={!historyInfo.canGoForward}
|
| 95 |
+
>
|
| 96 |
+
→
|
| 97 |
+
</button>
|
| 98 |
+
</div>
|
| 99 |
+
) : null}
|
| 100 |
+
</div>
|
| 101 |
+
<img src={currentImage} className="w-full rounded" alt="Output" />
|
| 102 |
+
{historyInfo.currentDescription ? (
|
| 103 |
+
<div className="text-xs text-white/60 bg-black/20 rounded px-2 py-1">
|
| 104 |
+
{historyInfo.currentDescription}
|
| 105 |
+
</div>
|
| 106 |
+
) : null}
|
| 107 |
+
</div>
|
| 108 |
+
<Button
|
| 109 |
+
className="w-full"
|
| 110 |
+
variant="secondary"
|
| 111 |
+
onClick={() => downloadImage(currentImage, downloadFileName)}
|
| 112 |
+
>
|
| 113 |
+
📥 Download Output
|
| 114 |
+
</Button>
|
| 115 |
+
</div>
|
| 116 |
+
);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
/* ========================================
|
| 120 |
TYPE DEFINITIONS (TEMPORARY)
|
| 121 |
======================================== */
|
|
|
|
| 273 |
onEndConnection,
|
| 274 |
onProcess,
|
| 275 |
onUpdatePosition,
|
| 276 |
+
getNodeHistoryInfo,
|
| 277 |
+
navigateNodeHistory,
|
| 278 |
+
getCurrentNodeImage,
|
| 279 |
}: any) {
|
| 280 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 281 |
|
|
|
|
| 444 |
{node.isRunning ? "Processing..." : "Apply Background"}
|
| 445 |
</Button>
|
| 446 |
|
| 447 |
+
<NodeOutputSection
|
| 448 |
+
nodeId={node.id}
|
| 449 |
+
output={node.output}
|
| 450 |
+
downloadFileName={`background-${Date.now()}.png`}
|
| 451 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 452 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 453 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 454 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
{node.error && (
|
| 456 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 457 |
)}
|
|
|
|
| 460 |
);
|
| 461 |
}
|
| 462 |
|
| 463 |
+
export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 464 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 465 |
|
| 466 |
const presetClothes = [
|
|
|
|
| 614 |
>
|
| 615 |
{node.isRunning ? "Processing..." : "Apply Clothes"}
|
| 616 |
</Button>
|
| 617 |
+
<NodeOutputSection
|
| 618 |
+
nodeId={node.id}
|
| 619 |
+
output={node.output}
|
| 620 |
+
downloadFileName={`clothes-${Date.now()}.png`}
|
| 621 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 622 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 623 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 624 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
{node.error && (
|
| 626 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 627 |
)}
|
|
|
|
| 630 |
);
|
| 631 |
}
|
| 632 |
|
| 633 |
+
export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 634 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 635 |
|
| 636 |
return (
|
|
|
|
| 695 |
>
|
| 696 |
{node.isRunning ? "Processing..." : "Apply Age"}
|
| 697 |
</Button>
|
| 698 |
+
<NodeOutputSection
|
| 699 |
+
nodeId={node.id}
|
| 700 |
+
output={node.output}
|
| 701 |
+
downloadFileName={`age-${Date.now()}.png`}
|
| 702 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 703 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 704 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 705 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
{node.error && (
|
| 707 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 708 |
)}
|
|
|
|
| 711 |
);
|
| 712 |
}
|
| 713 |
|
| 714 |
+
export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 715 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 716 |
const focalLengths = ["None", "8mm fisheye", "12mm", "24mm", "35mm", "50mm", "85mm", "135mm", "200mm", "300mm", "400mm"];
|
| 717 |
const apertures = ["None", "f/0.95", "f/1.2", "f/1.4", "f/1.8", "f/2", "f/2.8", "f/4", "f/5.6", "f/8", "f/11", "f/16", "f/22"];
|
|
|
|
| 901 |
>
|
| 902 |
{node.isRunning ? "Processing..." : "Apply Camera Settings"}
|
| 903 |
</Button>
|
| 904 |
+
<div className="mt-2">
|
| 905 |
+
<NodeOutputSection
|
| 906 |
+
nodeId={node.id}
|
| 907 |
+
output={node.output}
|
| 908 |
+
downloadFileName={`camera-${Date.now()}.png`}
|
| 909 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 910 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 911 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 912 |
+
/>
|
| 913 |
+
</div>
|
|
|
|
|
|
|
| 914 |
{node.error && (
|
| 915 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 916 |
)}
|
|
|
|
| 919 |
);
|
| 920 |
}
|
| 921 |
|
| 922 |
+
export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 923 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 924 |
const hairstyles = ["None", "short", "long", "curly", "straight", "bald", "mohawk", "ponytail"];
|
| 925 |
const expressions = ["None", "happy", "serious", "smiling", "laughing", "sad", "surprised", "angry"];
|
|
|
|
| 1046 |
>
|
| 1047 |
{node.isRunning ? "Processing..." : "Apply Face Changes"}
|
| 1048 |
</Button>
|
| 1049 |
+
<div className="mt-2">
|
| 1050 |
+
<NodeOutputSection
|
| 1051 |
+
nodeId={node.id}
|
| 1052 |
+
output={node.output}
|
| 1053 |
+
downloadFileName={`face-${Date.now()}.png`}
|
| 1054 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1055 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 1056 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 1057 |
+
/>
|
| 1058 |
+
</div>
|
|
|
|
|
|
|
| 1059 |
{node.error && (
|
| 1060 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 1061 |
)}
|
|
|
|
| 1064 |
);
|
| 1065 |
}
|
| 1066 |
|
| 1067 |
+
export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 1068 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1069 |
|
| 1070 |
const styleOptions = [
|
|
|
|
| 1163 |
>
|
| 1164 |
{node.isRunning ? "Applying Style..." : "Apply Style Transfer"}
|
| 1165 |
</Button>
|
| 1166 |
+
<NodeOutputSection
|
| 1167 |
+
nodeId={node.id}
|
| 1168 |
+
output={node.output}
|
| 1169 |
+
downloadFileName={`style-${Date.now()}.png`}
|
| 1170 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1171 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 1172 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 1173 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
{node.error && (
|
| 1175 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
| 1176 |
)}
|
|
|
|
| 1179 |
);
|
| 1180 |
}
|
| 1181 |
|
| 1182 |
+
export function EditNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 1183 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1184 |
|
| 1185 |
return (
|
|
|
|
| 1234 |
>
|
| 1235 |
{node.isRunning ? "Processing..." : "Apply Edit"}
|
| 1236 |
</Button>
|
| 1237 |
+
<NodeOutputSection
|
| 1238 |
+
nodeId={node.id}
|
| 1239 |
+
output={node.output}
|
| 1240 |
+
downloadFileName={`edit-${Date.now()}.png`}
|
| 1241 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1242 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 1243 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 1244 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1245 |
</div>
|
| 1246 |
</div>
|
| 1247 |
);
|
app/page.tsx
CHANGED
|
@@ -31,8 +31,8 @@ import {
|
|
| 31 |
// UI components from shadcn/ui library
|
| 32 |
import { Button } from "../components/ui/button";
|
| 33 |
import { Input } from "../components/ui/input";
|
| 34 |
-
// Hugging Face OAuth functionality
|
| 35 |
-
|
| 36 |
|
| 37 |
/**
|
| 38 |
* Utility function to combine CSS class names conditionally
|
|
@@ -698,29 +698,66 @@ function MergeNodeView({
|
|
| 698 |
</div>
|
| 699 |
|
| 700 |
<div className="mt-2">
|
| 701 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
<div className="w-full min-h-[200px] max-h-[400px] rounded-xl bg-black/40 grid place-items-center">
|
| 703 |
-
{node.output ? (
|
| 704 |
-
<img src={node.output} className="w-full h-auto max-h-[400px] object-contain rounded-xl" alt="output" />
|
| 705 |
) : (
|
| 706 |
<span className="text-white/40 text-xs py-16">Run merge to see result</span>
|
| 707 |
)}
|
| 708 |
</div>
|
| 709 |
-
{node.output && (
|
| 710 |
-
<
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
)}
|
| 725 |
{node.error && (
|
| 726 |
<div className="mt-2">
|
|
@@ -767,10 +804,10 @@ export default function EditorPage() {
|
|
| 767 |
scaleRef.current = scale;
|
| 768 |
}, [scale]);
|
| 769 |
|
| 770 |
-
// HF OAUTH CHECK
|
| 771 |
-
/*
|
| 772 |
useEffect(() => {
|
| 773 |
(async () => {
|
|
|
|
| 774 |
try {
|
| 775 |
// Handle OAuth redirect if present
|
| 776 |
const oauth = await oauthHandleRedirectIfPresent();
|
|
@@ -797,10 +834,8 @@ export default function EditorPage() {
|
|
| 797 |
}
|
| 798 |
})();
|
| 799 |
}, []);
|
| 800 |
-
*/
|
| 801 |
|
| 802 |
-
// HF PRO LOGIN HANDLER
|
| 803 |
-
/*
|
| 804 |
const handleHfProLogin = async () => {
|
| 805 |
if (isHfProLoggedIn) {
|
| 806 |
// Logout: clear the token
|
|
@@ -825,12 +860,6 @@ export default function EditorPage() {
|
|
| 825 |
});
|
| 826 |
}
|
| 827 |
};
|
| 828 |
-
*/
|
| 829 |
-
|
| 830 |
-
// Placeholder function for manual review
|
| 831 |
-
const handleHfProLogin = () => {
|
| 832 |
-
console.log('HF Pro login disabled - see HF_INTEGRATION_CHANGES.md for details');
|
| 833 |
-
};
|
| 834 |
|
| 835 |
// Connection dragging state
|
| 836 |
const [draggingFrom, setDraggingFrom] = useState<string | null>(null);
|
|
@@ -840,11 +869,52 @@ export default function EditorPage() {
|
|
| 840 |
const [apiToken, setApiToken] = useState("");
|
| 841 |
const [showHelpSidebar, setShowHelpSidebar] = useState(false);
|
| 842 |
|
| 843 |
-
// HF PRO AUTHENTICATION
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 848 |
|
| 849 |
const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
|
| 850 |
const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
|
|
@@ -923,6 +993,84 @@ export default function EditorPage() {
|
|
| 923 |
setNodes((prev) => prev.map((n) => (n.id === id ? { ...n, ...updates } : n)));
|
| 924 |
};
|
| 925 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
// Handle single input connections for new nodes
|
| 927 |
const handleEndSingleConnection = (nodeId: string) => {
|
| 928 |
if (draggingFrom) {
|
|
@@ -1334,6 +1482,13 @@ export default function EditorPage() {
|
|
| 1334 |
}
|
| 1335 |
return n;
|
| 1336 |
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1337 |
|
| 1338 |
if (unprocessedNodeCount > 1) {
|
| 1339 |
console.log(`✅ Successfully applied ${unprocessedNodeCount} transformations in ONE API call!`);
|
|
@@ -1600,6 +1755,19 @@ export default function EditorPage() {
|
|
| 1600 |
}
|
| 1601 |
const out = js.image || (js.images?.[0] as string) || null;
|
| 1602 |
setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, output: out, isRunning: false } : n)));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1603 |
} catch (e: any) {
|
| 1604 |
console.error("Merge error:", e);
|
| 1605 |
setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, isRunning: false, error: e?.message || "Error" } : n)));
|
|
@@ -1824,25 +1992,7 @@ export default function EditorPage() {
|
|
| 1824 |
Help
|
| 1825 |
</Button>
|
| 1826 |
|
| 1827 |
-
|
| 1828 |
-
{/*
|
| 1829 |
-
<Button
|
| 1830 |
-
variant={isHfProLoggedIn ? "default" : "secondary"}
|
| 1831 |
-
size="sm"
|
| 1832 |
-
className="h-8 px-3"
|
| 1833 |
-
type="button"
|
| 1834 |
-
onClick={handleHfProLogin}
|
| 1835 |
-
disabled={isCheckingAuth}
|
| 1836 |
-
title={isHfProLoggedIn ? "Using fal.ai Gemini 2.5 Flash Image via HF" : "Click to login and use fal.ai Gemini 2.5 Flash"}
|
| 1837 |
-
>
|
| 1838 |
-
{isCheckingAuth ? "Checking..." : (isHfProLoggedIn ? "🤗 HF PRO ✓" : "Login HF PRO")}
|
| 1839 |
-
</Button>
|
| 1840 |
-
{isHfProLoggedIn && (
|
| 1841 |
-
<div className="text-xs text-muted-foreground">
|
| 1842 |
-
Using fal.ai Gemini 2.5 Flash
|
| 1843 |
-
</div>
|
| 1844 |
-
)}
|
| 1845 |
-
*/}
|
| 1846 |
</div>
|
| 1847 |
</header>
|
| 1848 |
|
|
@@ -2052,6 +2202,9 @@ export default function EditorPage() {
|
|
| 2052 |
onEndConnection={handleEndSingleConnection}
|
| 2053 |
onProcess={processNode}
|
| 2054 |
onUpdatePosition={updateNodePosition}
|
|
|
|
|
|
|
|
|
|
| 2055 |
/>
|
| 2056 |
);
|
| 2057 |
case "CLOTHES":
|
|
@@ -2065,6 +2218,9 @@ export default function EditorPage() {
|
|
| 2065 |
onEndConnection={handleEndSingleConnection}
|
| 2066 |
onProcess={processNode}
|
| 2067 |
onUpdatePosition={updateNodePosition}
|
|
|
|
|
|
|
|
|
|
| 2068 |
/>
|
| 2069 |
);
|
| 2070 |
case "STYLE":
|
|
@@ -2078,6 +2234,9 @@ export default function EditorPage() {
|
|
| 2078 |
onEndConnection={handleEndSingleConnection}
|
| 2079 |
onProcess={processNode}
|
| 2080 |
onUpdatePosition={updateNodePosition}
|
|
|
|
|
|
|
|
|
|
| 2081 |
/>
|
| 2082 |
);
|
| 2083 |
case "EDIT":
|
|
@@ -2091,6 +2250,9 @@ export default function EditorPage() {
|
|
| 2091 |
onEndConnection={handleEndSingleConnection}
|
| 2092 |
onProcess={processNode}
|
| 2093 |
onUpdatePosition={updateNodePosition}
|
|
|
|
|
|
|
|
|
|
| 2094 |
/>
|
| 2095 |
);
|
| 2096 |
case "CAMERA":
|
|
@@ -2104,6 +2266,9 @@ export default function EditorPage() {
|
|
| 2104 |
onEndConnection={handleEndSingleConnection}
|
| 2105 |
onProcess={processNode}
|
| 2106 |
onUpdatePosition={updateNodePosition}
|
|
|
|
|
|
|
|
|
|
| 2107 |
/>
|
| 2108 |
);
|
| 2109 |
case "AGE":
|
|
@@ -2117,6 +2282,9 @@ export default function EditorPage() {
|
|
| 2117 |
onEndConnection={handleEndSingleConnection}
|
| 2118 |
onProcess={processNode}
|
| 2119 |
onUpdatePosition={updateNodePosition}
|
|
|
|
|
|
|
|
|
|
| 2120 |
/>
|
| 2121 |
);
|
| 2122 |
case "FACE":
|
|
@@ -2130,6 +2298,9 @@ export default function EditorPage() {
|
|
| 2130 |
onEndConnection={handleEndSingleConnection}
|
| 2131 |
onProcess={processNode}
|
| 2132 |
onUpdatePosition={updateNodePosition}
|
|
|
|
|
|
|
|
|
|
| 2133 |
/>
|
| 2134 |
);
|
| 2135 |
default:
|
|
|
|
| 31 |
// UI components from shadcn/ui library
|
| 32 |
import { Button } from "../components/ui/button";
|
| 33 |
import { Input } from "../components/ui/input";
|
| 34 |
+
// Hugging Face OAuth functionality
|
| 35 |
+
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
|
| 36 |
|
| 37 |
/**
|
| 38 |
* Utility function to combine CSS class names conditionally
|
|
|
|
| 698 |
</div>
|
| 699 |
|
| 700 |
<div className="mt-2">
|
| 701 |
+
<div className="flex items-center justify-between mb-1">
|
| 702 |
+
<div className="text-xs text-white/70">Output</div>
|
| 703 |
+
{(() => {
|
| 704 |
+
const historyInfo = getNodeHistoryInfo(node.id);
|
| 705 |
+
return historyInfo.hasHistory ? (
|
| 706 |
+
<div className="flex items-center gap-1">
|
| 707 |
+
<button
|
| 708 |
+
className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
|
| 709 |
+
onClick={() => navigateNodeHistory(node.id, 'prev')}
|
| 710 |
+
disabled={!historyInfo.canGoBack}
|
| 711 |
+
>
|
| 712 |
+
←
|
| 713 |
+
</button>
|
| 714 |
+
<span className="text-xs text-white/60 px-1">
|
| 715 |
+
{historyInfo.current}/{historyInfo.total}
|
| 716 |
+
</span>
|
| 717 |
+
<button
|
| 718 |
+
className="p-1 text-xs bg-white/10 hover:bg-white/20 rounded disabled:opacity-40"
|
| 719 |
+
onClick={() => navigateNodeHistory(node.id, 'next')}
|
| 720 |
+
disabled={!historyInfo.canGoForward}
|
| 721 |
+
>
|
| 722 |
+
→
|
| 723 |
+
</button>
|
| 724 |
+
</div>
|
| 725 |
+
) : null;
|
| 726 |
+
})()}
|
| 727 |
+
</div>
|
| 728 |
<div className="w-full min-h-[200px] max-h-[400px] rounded-xl bg-black/40 grid place-items-center">
|
| 729 |
+
{getCurrentNodeImage(node.id, node.output) ? (
|
| 730 |
+
<img src={getCurrentNodeImage(node.id, node.output)} className="w-full h-auto max-h-[400px] object-contain rounded-xl" alt="output" />
|
| 731 |
) : (
|
| 732 |
<span className="text-white/40 text-xs py-16">Run merge to see result</span>
|
| 733 |
)}
|
| 734 |
</div>
|
| 735 |
+
{getCurrentNodeImage(node.id, node.output) && (
|
| 736 |
+
<div className="mt-2 space-y-2">
|
| 737 |
+
{(() => {
|
| 738 |
+
const historyInfo = getNodeHistoryInfo(node.id);
|
| 739 |
+
return historyInfo.currentDescription ? (
|
| 740 |
+
<div className="text-xs text-white/60 bg-black/20 rounded px-2 py-1">
|
| 741 |
+
{historyInfo.currentDescription}
|
| 742 |
+
</div>
|
| 743 |
+
) : null;
|
| 744 |
+
})()}
|
| 745 |
+
<Button
|
| 746 |
+
className="w-full"
|
| 747 |
+
variant="secondary"
|
| 748 |
+
onClick={() => {
|
| 749 |
+
const link = document.createElement('a');
|
| 750 |
+
const currentImage = getCurrentNodeImage(node.id, node.output);
|
| 751 |
+
link.href = currentImage as string;
|
| 752 |
+
link.download = `merge-${Date.now()}.png`;
|
| 753 |
+
document.body.appendChild(link);
|
| 754 |
+
link.click();
|
| 755 |
+
document.body.removeChild(link);
|
| 756 |
+
}}
|
| 757 |
+
>
|
| 758 |
+
📥 Download Merged Image
|
| 759 |
+
</Button>
|
| 760 |
+
</div>
|
| 761 |
)}
|
| 762 |
{node.error && (
|
| 763 |
<div className="mt-2">
|
|
|
|
| 804 |
scaleRef.current = scale;
|
| 805 |
}, [scale]);
|
| 806 |
|
| 807 |
+
// HF OAUTH CHECK
|
|
|
|
| 808 |
useEffect(() => {
|
| 809 |
(async () => {
|
| 810 |
+
setIsCheckingAuth(true);
|
| 811 |
try {
|
| 812 |
// Handle OAuth redirect if present
|
| 813 |
const oauth = await oauthHandleRedirectIfPresent();
|
|
|
|
| 834 |
}
|
| 835 |
})();
|
| 836 |
}, []);
|
|
|
|
| 837 |
|
| 838 |
+
// HF PRO LOGIN HANDLER
|
|
|
|
| 839 |
const handleHfProLogin = async () => {
|
| 840 |
if (isHfProLoggedIn) {
|
| 841 |
// Logout: clear the token
|
|
|
|
| 860 |
});
|
| 861 |
}
|
| 862 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
|
| 864 |
// Connection dragging state
|
| 865 |
const [draggingFrom, setDraggingFrom] = useState<string | null>(null);
|
|
|
|
| 869 |
const [apiToken, setApiToken] = useState("");
|
| 870 |
const [showHelpSidebar, setShowHelpSidebar] = useState(false);
|
| 871 |
|
| 872 |
+
// HF PRO AUTHENTICATION
|
| 873 |
+
const [isHfProLoggedIn, setIsHfProLoggedIn] = useState(false);
|
| 874 |
+
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
| 875 |
+
|
| 876 |
+
// NODE HISTORY (per-node image history)
|
| 877 |
+
const [nodeHistories, setNodeHistories] = useState<Record<string, Array<{
|
| 878 |
+
id: string;
|
| 879 |
+
image: string;
|
| 880 |
+
timestamp: number;
|
| 881 |
+
description: string;
|
| 882 |
+
}>>>({});
|
| 883 |
+
|
| 884 |
+
const [nodeHistoryIndex, setNodeHistoryIndex] = useState<Record<string, number>>({});
|
| 885 |
+
|
| 886 |
+
// Load node histories from localStorage on startup
|
| 887 |
+
useEffect(() => {
|
| 888 |
+
try {
|
| 889 |
+
const savedHistories = localStorage.getItem('nano-banana-node-histories');
|
| 890 |
+
const savedIndices = localStorage.getItem('nano-banana-node-history-indices');
|
| 891 |
+
if (savedHistories) {
|
| 892 |
+
setNodeHistories(JSON.parse(savedHistories));
|
| 893 |
+
}
|
| 894 |
+
if (savedIndices) {
|
| 895 |
+
setNodeHistoryIndex(JSON.parse(savedIndices));
|
| 896 |
+
}
|
| 897 |
+
} catch (error) {
|
| 898 |
+
console.error('Failed to load node histories from localStorage:', error);
|
| 899 |
+
}
|
| 900 |
+
}, []);
|
| 901 |
+
|
| 902 |
+
// Save node histories to localStorage whenever they change
|
| 903 |
+
useEffect(() => {
|
| 904 |
+
try {
|
| 905 |
+
localStorage.setItem('nano-banana-node-histories', JSON.stringify(nodeHistories));
|
| 906 |
+
} catch (error) {
|
| 907 |
+
console.error('Failed to save node histories to localStorage:', error);
|
| 908 |
+
}
|
| 909 |
+
}, [nodeHistories]);
|
| 910 |
+
|
| 911 |
+
useEffect(() => {
|
| 912 |
+
try {
|
| 913 |
+
localStorage.setItem('nano-banana-node-history-indices', JSON.stringify(nodeHistoryIndex));
|
| 914 |
+
} catch (error) {
|
| 915 |
+
console.error('Failed to save node history indices to localStorage:', error);
|
| 916 |
+
}
|
| 917 |
+
}, [nodeHistoryIndex]);
|
| 918 |
|
| 919 |
const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
|
| 920 |
const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
|
|
|
|
| 993 |
setNodes((prev) => prev.map((n) => (n.id === id ? { ...n, ...updates } : n)));
|
| 994 |
};
|
| 995 |
|
| 996 |
+
// Add image to node's history
|
| 997 |
+
const addToNodeHistory = (nodeId: string, image: string, description: string) => {
|
| 998 |
+
const historyEntry = {
|
| 999 |
+
id: uid(),
|
| 1000 |
+
image,
|
| 1001 |
+
timestamp: Date.now(),
|
| 1002 |
+
description
|
| 1003 |
+
};
|
| 1004 |
+
|
| 1005 |
+
setNodeHistories(prev => {
|
| 1006 |
+
const nodeHistory = prev[nodeId] || [];
|
| 1007 |
+
const newHistory = [historyEntry, ...nodeHistory].slice(0, 10); // Keep last 10 per node
|
| 1008 |
+
return {
|
| 1009 |
+
...prev,
|
| 1010 |
+
[nodeId]: newHistory
|
| 1011 |
+
};
|
| 1012 |
+
});
|
| 1013 |
+
|
| 1014 |
+
// Set this as the current (latest) image for the node
|
| 1015 |
+
setNodeHistoryIndex(prev => ({
|
| 1016 |
+
...prev,
|
| 1017 |
+
[nodeId]: 0
|
| 1018 |
+
}));
|
| 1019 |
+
};
|
| 1020 |
+
|
| 1021 |
+
// Navigate node history
|
| 1022 |
+
const navigateNodeHistory = (nodeId: string, direction: 'prev' | 'next') => {
|
| 1023 |
+
const history = nodeHistories[nodeId];
|
| 1024 |
+
if (!history || history.length <= 1) return;
|
| 1025 |
+
|
| 1026 |
+
const currentIndex = nodeHistoryIndex[nodeId] || 0;
|
| 1027 |
+
let newIndex = currentIndex;
|
| 1028 |
+
|
| 1029 |
+
if (direction === 'prev' && currentIndex < history.length - 1) {
|
| 1030 |
+
newIndex = currentIndex + 1;
|
| 1031 |
+
} else if (direction === 'next' && currentIndex > 0) {
|
| 1032 |
+
newIndex = currentIndex - 1;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
if (newIndex !== currentIndex) {
|
| 1036 |
+
setNodeHistoryIndex(prev => ({
|
| 1037 |
+
...prev,
|
| 1038 |
+
[nodeId]: newIndex
|
| 1039 |
+
}));
|
| 1040 |
+
|
| 1041 |
+
// Update the node's output to show the historical image
|
| 1042 |
+
const historicalImage = history[newIndex].image;
|
| 1043 |
+
updateNode(nodeId, { output: historicalImage });
|
| 1044 |
+
}
|
| 1045 |
+
};
|
| 1046 |
+
|
| 1047 |
+
// Get current image for a node (either latest or from history navigation)
|
| 1048 |
+
const getCurrentNodeImage = (nodeId: string, defaultOutput?: string) => {
|
| 1049 |
+
const history = nodeHistories[nodeId];
|
| 1050 |
+
const index = nodeHistoryIndex[nodeId] || 0;
|
| 1051 |
+
|
| 1052 |
+
if (history && history[index]) {
|
| 1053 |
+
return history[index].image;
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
return defaultOutput;
|
| 1057 |
+
};
|
| 1058 |
+
|
| 1059 |
+
// Get history info for a node
|
| 1060 |
+
const getNodeHistoryInfo = (nodeId: string) => {
|
| 1061 |
+
const history = nodeHistories[nodeId] || [];
|
| 1062 |
+
const index = nodeHistoryIndex[nodeId] || 0;
|
| 1063 |
+
|
| 1064 |
+
return {
|
| 1065 |
+
hasHistory: history.length > 1,
|
| 1066 |
+
current: index + 1,
|
| 1067 |
+
total: history.length,
|
| 1068 |
+
canGoBack: index < history.length - 1,
|
| 1069 |
+
canGoForward: index > 0,
|
| 1070 |
+
currentDescription: history[index]?.description || ''
|
| 1071 |
+
};
|
| 1072 |
+
};
|
| 1073 |
+
|
| 1074 |
// Handle single input connections for new nodes
|
| 1075 |
const handleEndSingleConnection = (nodeId: string) => {
|
| 1076 |
if (draggingFrom) {
|
|
|
|
| 1482 |
}
|
| 1483 |
return n;
|
| 1484 |
}));
|
| 1485 |
+
|
| 1486 |
+
// Add to node's history
|
| 1487 |
+
const description = unprocessedNodeCount > 1
|
| 1488 |
+
? `Combined ${unprocessedNodeCount} transformations`
|
| 1489 |
+
: `${node.type} transformation`;
|
| 1490 |
+
|
| 1491 |
+
addToNodeHistory(nodeId, data.image, description);
|
| 1492 |
|
| 1493 |
if (unprocessedNodeCount > 1) {
|
| 1494 |
console.log(`✅ Successfully applied ${unprocessedNodeCount} transformations in ONE API call!`);
|
|
|
|
| 1755 |
}
|
| 1756 |
const out = js.image || (js.images?.[0] as string) || null;
|
| 1757 |
setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, output: out, isRunning: false } : n)));
|
| 1758 |
+
|
| 1759 |
+
// Add merge result to node's history
|
| 1760 |
+
if (out) {
|
| 1761 |
+
const inputLabels = merge.inputs.map((id, index) => {
|
| 1762 |
+
const inputNode = nodes.find(n => n.id === id);
|
| 1763 |
+
if (inputNode?.type === "CHARACTER") {
|
| 1764 |
+
return (inputNode as CharacterNode).label || `Character ${index + 1}`;
|
| 1765 |
+
}
|
| 1766 |
+
return `${inputNode?.type || 'Node'} ${index + 1}`;
|
| 1767 |
+
});
|
| 1768 |
+
|
| 1769 |
+
addToNodeHistory(mergeId, out, `Merged: ${inputLabels.join(" + ")}`);
|
| 1770 |
+
}
|
| 1771 |
} catch (e: any) {
|
| 1772 |
console.error("Merge error:", e);
|
| 1773 |
setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, isRunning: false, error: e?.message || "Error" } : n)));
|
|
|
|
| 1992 |
Help
|
| 1993 |
</Button>
|
| 1994 |
|
| 1995 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1996 |
</div>
|
| 1997 |
</header>
|
| 1998 |
|
|
|
|
| 2202 |
onEndConnection={handleEndSingleConnection}
|
| 2203 |
onProcess={processNode}
|
| 2204 |
onUpdatePosition={updateNodePosition}
|
| 2205 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 2206 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 2207 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 2208 |
/>
|
| 2209 |
);
|
| 2210 |
case "CLOTHES":
|
|
|
|
| 2218 |
onEndConnection={handleEndSingleConnection}
|
| 2219 |
onProcess={processNode}
|
| 2220 |
onUpdatePosition={updateNodePosition}
|
| 2221 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 2222 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 2223 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 2224 |
/>
|
| 2225 |
);
|
| 2226 |
case "STYLE":
|
|
|
|
| 2234 |
onEndConnection={handleEndSingleConnection}
|
| 2235 |
onProcess={processNode}
|
| 2236 |
onUpdatePosition={updateNodePosition}
|
| 2237 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 2238 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 2239 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 2240 |
/>
|
| 2241 |
);
|
| 2242 |
case "EDIT":
|
|
|
|
| 2250 |
onEndConnection={handleEndSingleConnection}
|
| 2251 |
onProcess={processNode}
|
| 2252 |
onUpdatePosition={updateNodePosition}
|
| 2253 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 2254 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 2255 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 2256 |
/>
|
| 2257 |
);
|
| 2258 |
case "CAMERA":
|
|
|
|
| 2266 |
onEndConnection={handleEndSingleConnection}
|
| 2267 |
onProcess={processNode}
|
| 2268 |
onUpdatePosition={updateNodePosition}
|
| 2269 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 2270 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 2271 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 2272 |
/>
|
| 2273 |
);
|
| 2274 |
case "AGE":
|
|
|
|
| 2282 |
onEndConnection={handleEndSingleConnection}
|
| 2283 |
onProcess={processNode}
|
| 2284 |
onUpdatePosition={updateNodePosition}
|
| 2285 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 2286 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 2287 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 2288 |
/>
|
| 2289 |
);
|
| 2290 |
case "FACE":
|
|
|
|
| 2298 |
onEndConnection={handleEndSingleConnection}
|
| 2299 |
onProcess={processNode}
|
| 2300 |
onUpdatePosition={updateNodePosition}
|
| 2301 |
+
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 2302 |
+
navigateNodeHistory={navigateNodeHistory}
|
| 2303 |
+
getCurrentNodeImage={getCurrentNodeImage}
|
| 2304 |
/>
|
| 2305 |
);
|
| 2306 |
default:
|
next.config.ts
CHANGED
|
@@ -8,11 +8,6 @@ const nextConfig: NextConfig = {
|
|
| 8 |
serverRuntimeConfig: {
|
| 9 |
bodySizeLimit: '50mb',
|
| 10 |
},
|
| 11 |
-
api: {
|
| 12 |
-
bodyParser: {
|
| 13 |
-
sizeLimit: '50mb',
|
| 14 |
-
},
|
| 15 |
-
},
|
| 16 |
};
|
| 17 |
|
| 18 |
export default nextConfig;
|
|
|
|
| 8 |
serverRuntimeConfig: {
|
| 9 |
bodySizeLimit: '50mb',
|
| 10 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
};
|
| 12 |
|
| 13 |
export default nextConfig;
|
package-lock.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
"@fal-ai/serverless-client": "^0.15.0",
|
| 12 |
"@google/genai": "^1.17.0",
|
| 13 |
"@huggingface/hub": "^2.6.3",
|
| 14 |
-
"@huggingface/inference": "^4.
|
| 15 |
"class-variance-authority": "^0.7.0",
|
| 16 |
"clsx": "^2.1.1",
|
| 17 |
"lucide-react": "^0.542.0",
|
|
@@ -273,13 +273,13 @@
|
|
| 273 |
}
|
| 274 |
},
|
| 275 |
"node_modules/@huggingface/inference": {
|
| 276 |
-
"version": "4.
|
| 277 |
-
"resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.
|
| 278 |
-
"integrity": "sha512-
|
| 279 |
"license": "MIT",
|
| 280 |
"dependencies": {
|
| 281 |
"@huggingface/jinja": "^0.5.1",
|
| 282 |
-
"@huggingface/tasks": "^0.19.
|
| 283 |
},
|
| 284 |
"engines": {
|
| 285 |
"node": ">=18"
|
|
|
|
| 11 |
"@fal-ai/serverless-client": "^0.15.0",
|
| 12 |
"@google/genai": "^1.17.0",
|
| 13 |
"@huggingface/hub": "^2.6.3",
|
| 14 |
+
"@huggingface/inference": "^4.8.0",
|
| 15 |
"class-variance-authority": "^0.7.0",
|
| 16 |
"clsx": "^2.1.1",
|
| 17 |
"lucide-react": "^0.542.0",
|
|
|
|
| 273 |
}
|
| 274 |
},
|
| 275 |
"node_modules/@huggingface/inference": {
|
| 276 |
+
"version": "4.8.0",
|
| 277 |
+
"resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.8.0.tgz",
|
| 278 |
+
"integrity": "sha512-Eq98EAXqYn4rKMfrbEXuhc3IjKfaeIO6eXNOZk9xk6v5akrIWRtd6d1h0fjAWyX4zRbdUpXRh6MvsqXnzGvXCA==",
|
| 279 |
"license": "MIT",
|
| 280 |
"dependencies": {
|
| 281 |
"@huggingface/jinja": "^0.5.1",
|
| 282 |
+
"@huggingface/tasks": "^0.19.45"
|
| 283 |
},
|
| 284 |
"engines": {
|
| 285 |
"node": ">=18"
|
package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
"@fal-ai/serverless-client": "^0.15.0",
|
| 13 |
"@google/genai": "^1.17.0",
|
| 14 |
"@huggingface/hub": "^2.6.3",
|
| 15 |
-
"@huggingface/inference": "^4.
|
| 16 |
"class-variance-authority": "^0.7.0",
|
| 17 |
"clsx": "^2.1.1",
|
| 18 |
"lucide-react": "^0.542.0",
|
|
|
|
| 12 |
"@fal-ai/serverless-client": "^0.15.0",
|
| 13 |
"@google/genai": "^1.17.0",
|
| 14 |
"@huggingface/hub": "^2.6.3",
|
| 15 |
+
"@huggingface/inference": "^4.8.0",
|
| 16 |
"class-variance-authority": "^0.7.0",
|
| 17 |
"clsx": "^2.1.1",
|
| 18 |
"lucide-react": "^0.542.0",
|