Spaces:
Running
Running
Refactor ClothesNode to use text prompts and add NodeTimer
Browse files- app/api/hf-process/route.ts +5 -0
- app/nodes.tsx +52 -103
- app/page.tsx +56 -19
app/api/hf-process/route.ts
CHANGED
|
@@ -264,6 +264,11 @@ export async function POST(req: NextRequest) {
|
|
| 264 |
prompts.push(params.editPrompt);
|
| 265 |
}
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
// Age transformation
|
| 268 |
if (params.targetAge) {
|
| 269 |
prompts.push(`Transform the person to look ${params.targetAge} years old.`);
|
|
|
|
| 264 |
prompts.push(params.editPrompt);
|
| 265 |
}
|
| 266 |
|
| 267 |
+
// Clothing modifications
|
| 268 |
+
if (params.clothesPrompt) {
|
| 269 |
+
prompts.push(`Change clothing to: ${params.clothesPrompt}`);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
// Age transformation
|
| 273 |
if (params.targetAge) {
|
| 274 |
prompts.push(`Transform the person to look ${params.targetAge} years old.`);
|
app/nodes.tsx
CHANGED
|
@@ -61,6 +61,39 @@ import { Textarea } from "../components/ui/textarea"; // Multi-line text input
|
|
| 61 |
import { Slider } from "../components/ui/slider"; // Range slider input component
|
| 62 |
import { ColorPicker } from "../components/ui/color-picker"; // Color selection component
|
| 63 |
import { Checkbox } from "../components/ui/checkbox"; // Checkbox input component
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
/**
|
| 66 |
* Helper function to download processed images
|
|
@@ -496,6 +529,7 @@ export function BackgroundNodeView({
|
|
| 496 |
onDragOver={(e) => e.preventDefault()}
|
| 497 |
onPaste={handleImagePaste}
|
| 498 |
>
|
|
|
|
| 499 |
<div
|
| 500 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 501 |
onPointerDown={onPointerDown}
|
|
@@ -835,58 +869,12 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 835 |
// Handle node dragging functionality
|
| 836 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 837 |
|
| 838 |
-
/**
|
| 839 |
-
* Preset clothing options available for quick selection
|
| 840 |
-
* Each preset includes a display name and path to the reference image
|
| 841 |
-
*/
|
| 842 |
-
const presetClothes = [
|
| 843 |
-
{ name: "Sukajan", path: "/clothes/sukajan.png" }, // Japanese-style embroidered jacket
|
| 844 |
-
{ name: "Blazer", path: "/clothes/blazzer.png" }, // Business blazer/jacket
|
| 845 |
-
{ name: "Suit", path: "/clothes/suit.png" }, // Formal business suit
|
| 846 |
-
{ name: "Women's Outfit", path: "/clothes/womenoutfit.png" }, // Women's clothing ensemble
|
| 847 |
-
];
|
| 848 |
-
|
| 849 |
-
const onDrop = async (e: React.DragEvent) => {
|
| 850 |
-
e.preventDefault();
|
| 851 |
-
const files = e.dataTransfer.files;
|
| 852 |
-
if (files && files.length) {
|
| 853 |
-
const reader = new FileReader();
|
| 854 |
-
reader.onload = () => onUpdate(node.id, { clothesImage: reader.result, selectedPreset: null });
|
| 855 |
-
reader.readAsDataURL(files[0]);
|
| 856 |
-
}
|
| 857 |
-
};
|
| 858 |
-
|
| 859 |
-
const onPaste = async (e: React.ClipboardEvent) => {
|
| 860 |
-
const items = e.clipboardData.items;
|
| 861 |
-
for (let i = 0; i < items.length; i++) {
|
| 862 |
-
if (items[i].type.startsWith("image/")) {
|
| 863 |
-
const file = items[i].getAsFile();
|
| 864 |
-
if (file) {
|
| 865 |
-
const reader = new FileReader();
|
| 866 |
-
reader.onload = () => onUpdate(node.id, { clothesImage: reader.result, selectedPreset: null });
|
| 867 |
-
reader.readAsDataURL(file);
|
| 868 |
-
return;
|
| 869 |
-
}
|
| 870 |
-
}
|
| 871 |
-
}
|
| 872 |
-
const text = e.clipboardData.getData("text");
|
| 873 |
-
if (text && (text.startsWith("http") || text.startsWith("data:image"))) {
|
| 874 |
-
onUpdate(node.id, { clothesImage: text, selectedPreset: null });
|
| 875 |
-
}
|
| 876 |
-
};
|
| 877 |
-
|
| 878 |
-
const selectPreset = (presetPath: string, presetName: string) => {
|
| 879 |
-
onUpdate(node.id, { clothesImage: presetPath, selectedPreset: presetName });
|
| 880 |
-
};
|
| 881 |
-
|
| 882 |
return (
|
| 883 |
<div
|
| 884 |
className="nb-node absolute w-[320px]"
|
| 885 |
style={{ left: localPos.x, top: localPos.y }}
|
| 886 |
-
onDrop={onDrop}
|
| 887 |
-
onDragOver={(e) => e.preventDefault()}
|
| 888 |
-
onPaste={onPaste}
|
| 889 |
>
|
|
|
|
| 890 |
<div
|
| 891 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 892 |
onPointerDown={onPointerDown}
|
|
@@ -930,67 +918,21 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 930 |
</Button>
|
| 931 |
</div>
|
| 932 |
)}
|
| 933 |
-
<div className="text-xs text-muted-foreground">
|
| 934 |
-
|
| 935 |
-
{/* Preset clothes options */}
|
| 936 |
-
<div className="flex gap-2">
|
| 937 |
-
{presetClothes.map((preset) => (
|
| 938 |
-
<button
|
| 939 |
-
key={preset.name}
|
| 940 |
-
className={`flex-1 p-2 rounded border ${node.selectedPreset === preset.name
|
| 941 |
-
? "border-primary bg-primary/20"
|
| 942 |
-
: "border-border hover:border-primary/50"
|
| 943 |
-
}`}
|
| 944 |
-
onClick={() => selectPreset(preset.path, preset.name)}
|
| 945 |
-
>
|
| 946 |
-
<img src={preset.path} alt={preset.name} className="w-full h-28 object-contain rounded mb-1" />
|
| 947 |
-
<div className="text-xs">{preset.name}</div>
|
| 948 |
-
</button>
|
| 949 |
-
))}
|
| 950 |
-
</div>
|
| 951 |
-
|
| 952 |
-
<div className="text-xs text-muted-foreground/50 text-center">— or —</div>
|
| 953 |
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
className="absolute top-2 right-2"
|
| 962 |
-
onClick={() => onUpdate(node.id, { clothesImage: null, selectedPreset: null })}
|
| 963 |
-
>
|
| 964 |
-
Remove
|
| 965 |
-
</Button>
|
| 966 |
-
</div>
|
| 967 |
-
) : !node.selectedPreset ? (
|
| 968 |
-
<label className="block">
|
| 969 |
-
<input
|
| 970 |
-
type="file"
|
| 971 |
-
accept="image/*"
|
| 972 |
-
className="hidden"
|
| 973 |
-
onChange={(e) => {
|
| 974 |
-
if (e.target.files?.length) {
|
| 975 |
-
const reader = new FileReader();
|
| 976 |
-
reader.onload = () => onUpdate(node.id, { clothesImage: reader.result, selectedPreset: null });
|
| 977 |
-
reader.readAsDataURL(e.target.files[0]);
|
| 978 |
-
}
|
| 979 |
-
}}
|
| 980 |
-
/>
|
| 981 |
-
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center cursor-pointer hover:border-primary/50 transition-colors">
|
| 982 |
-
<div className="text-muted-foreground/40 text-lg mb-2">📁</div>
|
| 983 |
-
<p className="text-sm text-muted-foreground font-medium">Drop, upload, or paste clothes image</p>
|
| 984 |
-
<p className="text-xs text-muted-foreground/50 mt-1">JPG, PNG, WebP supported</p>
|
| 985 |
-
</div>
|
| 986 |
-
</label>
|
| 987 |
-
) : null}
|
| 988 |
|
| 989 |
<Button
|
| 990 |
className="w-full"
|
| 991 |
onClick={() => onProcess(node.id)}
|
| 992 |
-
disabled={node.isRunning || !node.
|
| 993 |
-
title={!node.input ? "Connect an input first" : "
|
| 994 |
>
|
| 995 |
{node.isRunning ? "Processing..." : "Apply Clothes"}
|
| 996 |
</Button>
|
|
@@ -1043,6 +985,7 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 1043 |
|
| 1044 |
return (
|
| 1045 |
<div className="nb-node absolute w-[280px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 1046 |
<div
|
| 1047 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1048 |
onPointerDown={onPointerDown}
|
|
@@ -1188,6 +1131,7 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 1188 |
|
| 1189 |
return (
|
| 1190 |
<div className="nb-node absolute w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 1191 |
<div
|
| 1192 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1193 |
onPointerDown={onPointerDown}
|
|
@@ -1428,6 +1372,7 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1428 |
|
| 1429 |
return (
|
| 1430 |
<div className="nb-node absolute w-[340px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 1431 |
<div
|
| 1432 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1433 |
onPointerDown={onPointerDown}
|
|
@@ -1694,6 +1639,7 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1694 |
className="nb-node absolute w-[320px]"
|
| 1695 |
style={{ left: localPos.x, top: localPos.y }}
|
| 1696 |
>
|
|
|
|
| 1697 |
<div
|
| 1698 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1699 |
onPointerDown={onPointerDown}
|
|
@@ -1866,6 +1812,7 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1866 |
|
| 1867 |
return (
|
| 1868 |
<div className="nb-node absolute text-white w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 1869 |
<div
|
| 1870 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1871 |
onPointerDown={onPointerDown}
|
|
@@ -2034,6 +1981,7 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 2034 |
|
| 2035 |
return (
|
| 2036 |
<div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 2037 |
<div
|
| 2038 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 2039 |
onPointerDown={onPointerDown}
|
|
@@ -2224,6 +2172,7 @@ export function EditNodeView({
|
|
| 2224 |
|
| 2225 |
return (
|
| 2226 |
<div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 2227 |
{/* Node Header - Contains title, delete button, and connection ports */}
|
| 2228 |
<div
|
| 2229 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
|
|
|
| 61 |
import { Slider } from "../components/ui/slider"; // Range slider input component
|
| 62 |
import { ColorPicker } from "../components/ui/color-picker"; // Color selection component
|
| 63 |
import { Checkbox } from "../components/ui/checkbox"; // Checkbox input component
|
| 64 |
+
import { Loader2 } from "lucide-react"; // Loading spinner icon
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Timer component that shows execution time
|
| 68 |
+
* Uses a green checkmark when finished or a spinner when running
|
| 69 |
+
*/
|
| 70 |
+
function NodeTimer({ startTime, executionTime, isRunning }: { startTime?: number, executionTime?: number, isRunning?: boolean }) {
|
| 71 |
+
const [elapsed, setElapsed] = useState(0);
|
| 72 |
+
|
| 73 |
+
useEffect(() => {
|
| 74 |
+
if (!isRunning || !startTime) return;
|
| 75 |
+
const interval = setInterval(() => {
|
| 76 |
+
setElapsed(Date.now() - startTime);
|
| 77 |
+
}, 100);
|
| 78 |
+
return () => clearInterval(interval);
|
| 79 |
+
}, [isRunning, startTime]);
|
| 80 |
+
|
| 81 |
+
if (!startTime && !executionTime) return null;
|
| 82 |
+
|
| 83 |
+
const timeToShow = isRunning ? elapsed : (executionTime || 0);
|
| 84 |
+
const seconds = (timeToShow / 1000).toFixed(1);
|
| 85 |
+
|
| 86 |
+
return (
|
| 87 |
+
<div className="absolute top-2 right-2 flex items-center gap-1.5 bg-background/80 text-foreground text-[10px] px-2 py-1 rounded-md border shadow-sm z-50 backdrop-blur-sm">
|
| 88 |
+
{isRunning ? (
|
| 89 |
+
<Loader2 className="w-3 h-3 animate-spin text-banana-500" />
|
| 90 |
+
) : (
|
| 91 |
+
<span className="text-green-500 font-bold">✓</span>
|
| 92 |
+
)}
|
| 93 |
+
<span className="font-mono">{seconds}s</span>
|
| 94 |
+
</div>
|
| 95 |
+
);
|
| 96 |
+
}
|
| 97 |
|
| 98 |
/**
|
| 99 |
* Helper function to download processed images
|
|
|
|
| 529 |
onDragOver={(e) => e.preventDefault()}
|
| 530 |
onPaste={handleImagePaste}
|
| 531 |
>
|
| 532 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 533 |
<div
|
| 534 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 535 |
onPointerDown={onPointerDown}
|
|
|
|
| 869 |
// Handle node dragging functionality
|
| 870 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 871 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 872 |
return (
|
| 873 |
<div
|
| 874 |
className="nb-node absolute w-[320px]"
|
| 875 |
style={{ left: localPos.x, top: localPos.y }}
|
|
|
|
|
|
|
|
|
|
| 876 |
>
|
| 877 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 878 |
<div
|
| 879 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 880 |
onPointerDown={onPointerDown}
|
|
|
|
| 918 |
</Button>
|
| 919 |
</div>
|
| 920 |
)}
|
| 921 |
+
<div className="text-xs text-muted-foreground">Clothing Description</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 922 |
|
| 923 |
+
<Textarea
|
| 924 |
+
className="w-full"
|
| 925 |
+
placeholder="Describe the clothing (e.g. 'black leather jacket', 'floral summer dress', 'navy blue business suit')"
|
| 926 |
+
value={node.clothesPrompt || ""}
|
| 927 |
+
onChange={(e) => onUpdate(node.id, { clothesPrompt: e.target.value })}
|
| 928 |
+
rows={3}
|
| 929 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
|
| 931 |
<Button
|
| 932 |
className="w-full"
|
| 933 |
onClick={() => onProcess(node.id)}
|
| 934 |
+
disabled={node.isRunning || !node.clothesPrompt}
|
| 935 |
+
title={!node.input ? "Connect an input first" : !node.clothesPrompt ? "Enter a clothing description" : "Apply Clothing"}
|
| 936 |
>
|
| 937 |
{node.isRunning ? "Processing..." : "Apply Clothes"}
|
| 938 |
</Button>
|
|
|
|
| 985 |
|
| 986 |
return (
|
| 987 |
<div className="nb-node absolute w-[280px]" style={{ left: localPos.x, top: localPos.y }}>
|
| 988 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 989 |
<div
|
| 990 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 991 |
onPointerDown={onPointerDown}
|
|
|
|
| 1131 |
|
| 1132 |
return (
|
| 1133 |
<div className="nb-node absolute w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
|
| 1134 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 1135 |
<div
|
| 1136 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1137 |
onPointerDown={onPointerDown}
|
|
|
|
| 1372 |
|
| 1373 |
return (
|
| 1374 |
<div className="nb-node absolute w-[340px]" style={{ left: localPos.x, top: localPos.y }}>
|
| 1375 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 1376 |
<div
|
| 1377 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1378 |
onPointerDown={onPointerDown}
|
|
|
|
| 1639 |
className="nb-node absolute w-[320px]"
|
| 1640 |
style={{ left: localPos.x, top: localPos.y }}
|
| 1641 |
>
|
| 1642 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 1643 |
<div
|
| 1644 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1645 |
onPointerDown={onPointerDown}
|
|
|
|
| 1812 |
|
| 1813 |
return (
|
| 1814 |
<div className="nb-node absolute text-white w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
| 1815 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 1816 |
<div
|
| 1817 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1818 |
onPointerDown={onPointerDown}
|
|
|
|
| 1981 |
|
| 1982 |
return (
|
| 1983 |
<div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
| 1984 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 1985 |
<div
|
| 1986 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
| 1987 |
onPointerDown={onPointerDown}
|
|
|
|
| 2172 |
|
| 2173 |
return (
|
| 2174 |
<div className="nb-node absolute w-[320px]" style={{ left: localPos.x, top: localPos.y }}>
|
| 2175 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 2176 |
{/* Node Header - Contains title, delete button, and connection ports */}
|
| 2177 |
<div
|
| 2178 |
className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
|
app/page.tsx
CHANGED
|
@@ -153,6 +153,14 @@ async function copyImageToClipboard(dataUrl: string) {
|
|
| 153 |
*/
|
| 154 |
type NodeType = "CHARACTER" | "MERGE" | "BACKGROUND" | "CLOTHES" | "STYLE" | "EDIT" | "CAMERA" | "AGE" | "FACE" | "BLEND" | "LIGHTNING" | "POSES";
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
/**
|
| 157 |
* Base properties that all nodes share
|
| 158 |
* Every node has an ID, type, and position in the editor world space
|
|
@@ -162,6 +170,8 @@ type NodeBase = {
|
|
| 162 |
type: NodeType; // What kind of operation this node performs
|
| 163 |
x: number; // X position in world coordinates (not screen pixels)
|
| 164 |
y: number; // Y position in world coordinates (not screen pixels)
|
|
|
|
|
|
|
| 165 |
};
|
| 166 |
|
| 167 |
/**
|
|
@@ -514,6 +524,38 @@ function Port({
|
|
| 514 |
);
|
| 515 |
}
|
| 516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
function CharacterNodeView({
|
| 518 |
node,
|
| 519 |
scaleRef,
|
|
@@ -731,6 +773,7 @@ function MergeNodeView({
|
|
| 731 |
onPointerMove={onPointerMove}
|
| 732 |
onPointerUp={onPointerUp}
|
| 733 |
>
|
|
|
|
| 734 |
<Port
|
| 735 |
className="in"
|
| 736 |
nodeId={node.id}
|
|
@@ -1070,15 +1113,9 @@ export default function EditorPage() {
|
|
| 1070 |
type: "image-to-image",
|
| 1071 |
description: "Powerful image editing and manipulation",
|
| 1072 |
},
|
| 1073 |
-
"FLUX.1-dev": {
|
| 1074 |
-
id: "black-forest-labs/FLUX.1-dev",
|
| 1075 |
-
name: "FLUX.1 Dev",
|
| 1076 |
-
type: "text-to-image",
|
| 1077 |
-
description: "High-quality text-to-image generation",
|
| 1078 |
-
},
|
| 1079 |
};
|
| 1080 |
|
| 1081 |
-
const [selectedHfModel, setSelectedHfModel] = useState<keyof typeof HF_MODELS>("
|
| 1082 |
|
| 1083 |
|
| 1084 |
// HF PRO AUTHENTICATION
|
|
@@ -1259,9 +1296,8 @@ export default function EditorPage() {
|
|
| 1259 |
}
|
| 1260 |
break;
|
| 1261 |
case "CLOTHES":
|
| 1262 |
-
if ((node as ClothesNode).
|
| 1263 |
-
config.
|
| 1264 |
-
config.selectedPreset = (node as ClothesNode).selectedPreset;
|
| 1265 |
}
|
| 1266 |
break;
|
| 1267 |
case "STYLE":
|
|
@@ -1511,9 +1547,10 @@ export default function EditorPage() {
|
|
| 1511 |
}
|
| 1512 |
|
| 1513 |
// Set loading state for all nodes being processed
|
|
|
|
| 1514 |
setNodes(prev => prev.map(n => {
|
| 1515 |
if (n.id === nodeId || processedNodes.includes(n.id)) {
|
| 1516 |
-
return { ...n, isRunning: true, error: null };
|
| 1517 |
}
|
| 1518 |
return n;
|
| 1519 |
}));
|
|
@@ -1525,12 +1562,9 @@ export default function EditorPage() {
|
|
| 1525 |
}
|
| 1526 |
|
| 1527 |
// Check if params contains custom images and validate them
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
throw new Error("Invalid clothes image format. Please upload a valid image.");
|
| 1532 |
-
}
|
| 1533 |
-
}
|
| 1534 |
|
| 1535 |
if (params.customBackgroundImage) {
|
| 1536 |
// Validate it's a proper data URL
|
|
@@ -1617,14 +1651,17 @@ export default function EditorPage() {
|
|
| 1617 |
|
| 1618 |
// Only update the current node with the output
|
| 1619 |
// Don't show output in intermediate nodes - they were just used for configuration
|
|
|
|
|
|
|
|
|
|
| 1620 |
setNodes(prev => prev.map(n => {
|
| 1621 |
if (n.id === nodeId) {
|
| 1622 |
// Only the current node gets the final output displayed
|
| 1623 |
-
return { ...n, output: data.image, isRunning: false, error: null };
|
| 1624 |
} else if (processedNodes.includes(n.id)) {
|
| 1625 |
// Mark intermediate nodes as no longer running but don't give them output
|
| 1626 |
// This way they remain unprocessed visually but their configs were used
|
| 1627 |
-
return { ...n, isRunning: false, error: null };
|
| 1628 |
}
|
| 1629 |
return n;
|
| 1630 |
}));
|
|
|
|
| 153 |
*/
|
| 154 |
type NodeType = "CHARACTER" | "MERGE" | "BACKGROUND" | "CLOTHES" | "STYLE" | "EDIT" | "CAMERA" | "AGE" | "FACE" | "BLEND" | "LIGHTNING" | "POSES";
|
| 155 |
|
| 156 |
+
/**
|
| 157 |
+
* Base properties that all nodes share
|
| 158 |
+
* Every node has an ID, type, and position in the editor world space
|
| 159 |
+
*/
|
| 160 |
+
import { Loader2, Clock } from "lucide-react";
|
| 161 |
+
|
| 162 |
+
// ... existing imports ...
|
| 163 |
+
|
| 164 |
/**
|
| 165 |
* Base properties that all nodes share
|
| 166 |
* Every node has an ID, type, and position in the editor world space
|
|
|
|
| 170 |
type: NodeType; // What kind of operation this node performs
|
| 171 |
x: number; // X position in world coordinates (not screen pixels)
|
| 172 |
y: number; // Y position in world coordinates (not screen pixels)
|
| 173 |
+
startTime?: number; // Timestamp when processing started
|
| 174 |
+
executionTime?: number; // Total processing time in milliseconds
|
| 175 |
};
|
| 176 |
|
| 177 |
/**
|
|
|
|
| 524 |
);
|
| 525 |
}
|
| 526 |
|
| 527 |
+
/**
|
| 528 |
+
* Timer component that shows execution time
|
| 529 |
+
* Uses a green checkmark when finished or a spinner when running
|
| 530 |
+
*/
|
| 531 |
+
export function NodeTimer({ startTime, executionTime, isRunning }: { startTime?: number, executionTime?: number, isRunning?: boolean }) {
|
| 532 |
+
const [elapsed, setElapsed] = React.useState(0);
|
| 533 |
+
|
| 534 |
+
React.useEffect(() => {
|
| 535 |
+
if (!isRunning || !startTime) return;
|
| 536 |
+
const interval = setInterval(() => {
|
| 537 |
+
setElapsed(Date.now() - startTime);
|
| 538 |
+
}, 100);
|
| 539 |
+
return () => clearInterval(interval);
|
| 540 |
+
}, [isRunning, startTime]);
|
| 541 |
+
|
| 542 |
+
if (!startTime && !executionTime) return null;
|
| 543 |
+
|
| 544 |
+
const timeToShow = isRunning ? elapsed : (executionTime || 0);
|
| 545 |
+
const seconds = (timeToShow / 1000).toFixed(1);
|
| 546 |
+
|
| 547 |
+
return (
|
| 548 |
+
<div className="absolute top-2 right-2 flex items-center gap-1.5 bg-background/80 text-foreground text-[10px] px-2 py-1 rounded-md border shadow-sm z-50 backdrop-blur-sm">
|
| 549 |
+
{isRunning ? (
|
| 550 |
+
<Loader2 className="w-3 h-3 animate-spin text-banana-500" />
|
| 551 |
+
) : (
|
| 552 |
+
<span className="text-green-500 font-bold">✓</span>
|
| 553 |
+
)}
|
| 554 |
+
<span className="font-mono">{seconds}s</span>
|
| 555 |
+
</div>
|
| 556 |
+
);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
function CharacterNodeView({
|
| 560 |
node,
|
| 561 |
scaleRef,
|
|
|
|
| 773 |
onPointerMove={onPointerMove}
|
| 774 |
onPointerUp={onPointerUp}
|
| 775 |
>
|
| 776 |
+
<NodeTimer startTime={node.startTime} executionTime={node.executionTime} isRunning={node.isRunning} />
|
| 777 |
<Port
|
| 778 |
className="in"
|
| 779 |
nodeId={node.id}
|
|
|
|
| 1113 |
type: "image-to-image",
|
| 1114 |
description: "Powerful image editing and manipulation",
|
| 1115 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1116 |
};
|
| 1117 |
|
| 1118 |
+
const [selectedHfModel, setSelectedHfModel] = useState<keyof typeof HF_MODELS>("Qwen-Image-Edit");
|
| 1119 |
|
| 1120 |
|
| 1121 |
// HF PRO AUTHENTICATION
|
|
|
|
| 1296 |
}
|
| 1297 |
break;
|
| 1298 |
case "CLOTHES":
|
| 1299 |
+
if ((node as ClothesNode).clothesPrompt) {
|
| 1300 |
+
config.clothesPrompt = (node as ClothesNode).clothesPrompt;
|
|
|
|
| 1301 |
}
|
| 1302 |
break;
|
| 1303 |
case "STYLE":
|
|
|
|
| 1547 |
}
|
| 1548 |
|
| 1549 |
// Set loading state for all nodes being processed
|
| 1550 |
+
const startTime = Date.now();
|
| 1551 |
setNodes(prev => prev.map(n => {
|
| 1552 |
if (n.id === nodeId || processedNodes.includes(n.id)) {
|
| 1553 |
+
return { ...n, isRunning: true, error: null, startTime, executionTime: undefined };
|
| 1554 |
}
|
| 1555 |
return n;
|
| 1556 |
}));
|
|
|
|
| 1562 |
}
|
| 1563 |
|
| 1564 |
// Check if params contains custom images and validate them
|
| 1565 |
+
|
| 1566 |
+
// Removed clothesImage validation as we now use text prompts
|
| 1567 |
+
|
|
|
|
|
|
|
|
|
|
| 1568 |
|
| 1569 |
if (params.customBackgroundImage) {
|
| 1570 |
// Validate it's a proper data URL
|
|
|
|
| 1651 |
|
| 1652 |
// Only update the current node with the output
|
| 1653 |
// Don't show output in intermediate nodes - they were just used for configuration
|
| 1654 |
+
const endTime = Date.now();
|
| 1655 |
+
const executionTime = endTime - startTime;
|
| 1656 |
+
|
| 1657 |
setNodes(prev => prev.map(n => {
|
| 1658 |
if (n.id === nodeId) {
|
| 1659 |
// Only the current node gets the final output displayed
|
| 1660 |
+
return { ...n, output: data.image, isRunning: false, error: null, executionTime };
|
| 1661 |
} else if (processedNodes.includes(n.id)) {
|
| 1662 |
// Mark intermediate nodes as no longer running but don't give them output
|
| 1663 |
// This way they remain unprocessed visually but their configs were used
|
| 1664 |
+
return { ...n, isRunning: false, error: null, executionTime };
|
| 1665 |
}
|
| 1666 |
return n;
|
| 1667 |
}));
|