Reubencf commited on
Commit
113aa9f
·
1 Parent(s): 3765acf

good night

Browse files
app/api/process/route.ts CHANGED
@@ -104,12 +104,30 @@ export async function POST(req: NextRequest) {
104
  if (clothesRef) referenceParts.push({ inlineData: clothesRef });
105
  }
106
 
107
- // Style blending
108
- if (params.styleImage) {
109
- const strength = params.blendStrength || 50;
110
- prompts.push(`Apply artistic style blending using the provided style reference image (attached below) at ${strength}% strength.`);
111
- const styleRef = await toInlineDataFromAny(params.styleImage);
112
- if (styleRef) referenceParts.push({ inlineData: styleRef });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
  // Edit prompt
 
104
  if (clothesRef) referenceParts.push({ inlineData: clothesRef });
105
  }
106
 
107
+ // Style application
108
+ if (params.stylePreset) {
109
+ const strength = params.styleStrength || 50;
110
+ const styleMap: { [key: string]: string } = {
111
+ "90s-anime": "Convert the image to 90's anime art style with classic anime features: large expressive eyes, detailed hair, soft shading, nostalgic colors reminiscent of Studio Ghibli and classic anime productions",
112
+ "mha": "Transform the image into My Hero Academia anime style with modern crisp lines, vibrant colors, dynamic character design, and heroic aesthetics typical of the series",
113
+ "dbz": "Apply Dragon Ball Z anime style with sharp angular features, spiky hair, intense expressions, bold outlines, high contrast shading, and dramatic action-oriented aesthetics",
114
+ "ukiyo-e": "Render in traditional Japanese Ukiyo-e woodblock print style with flat colors, bold outlines, stylized waves and clouds, traditional Japanese artistic elements",
115
+ "cyberpunk": "Transform into cyberpunk aesthetic with neon colors (cyan, magenta, yellow), dark backgrounds, futuristic elements, holographic effects, tech-noir atmosphere",
116
+ "steampunk": "Apply steampunk style with Victorian-era brass and copper tones, mechanical gears, steam effects, vintage industrial aesthetic, sepia undertones",
117
+ "cubism": "Render in Cubist art style with geometric fragmentation, multiple perspectives shown simultaneously, abstract angular forms, Picasso-inspired decomposition",
118
+ "van-gogh": "Apply Post-Impressionist Van Gogh style with thick swirling brushstrokes, vibrant yellows and blues, expressive texture, starry night-like patterns",
119
+ "simpsons": "Convert to The Simpsons cartoon style with yellow skin tones, simple rounded features, bulging eyes, overbite, Matt Groening's distinctive character design",
120
+ "family-guy": "Transform into Family Guy animation style with rounded character design, simplified features, Seth MacFarlane's distinctive art style, bold outlines",
121
+ "arcane": "Apply Arcane (League of Legends) style with painterly brush-stroke textures, neon rim lighting, hand-painted feel, stylized realism, vibrant color grading",
122
+ "wildwest": "Render in Wild West style with dusty desert tones, sunset orange lighting, vintage film grain, cowboy aesthetic, sepia and brown color palette",
123
+ "stranger-things": "Apply Stranger Things 80s aesthetic with Kodak film push-process look, neon magenta backlight, grainy vignette, retro sci-fi horror atmosphere",
124
+ "breaking-bad": "Transform with Breaking Bad cinematography style featuring dusty New Mexico orange and teal color grading, 35mm film grain, desert atmosphere, dramatic lighting"
125
+ };
126
+
127
+ const styleDescription = styleMap[params.stylePreset];
128
+ if (styleDescription) {
129
+ prompts.push(`${styleDescription}. Apply this style transformation at ${strength}% intensity while preserving the core subject matter.`);
130
+ }
131
  }
132
 
133
  // Edit prompt
app/editor/nodes.tsx CHANGED
@@ -926,45 +926,30 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
926
  );
927
  }
928
 
929
- export function BlendNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
930
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
931
 
932
- const onDrop = async (e: React.DragEvent) => {
933
- e.preventDefault();
934
- const files = e.dataTransfer.files;
935
- if (files && files.length) {
936
- const reader = new FileReader();
937
- reader.onload = () => onUpdate(node.id, { styleImage: reader.result });
938
- reader.readAsDataURL(files[0]);
939
- }
940
- };
941
-
942
- const onPaste = async (e: React.ClipboardEvent) => {
943
- const items = e.clipboardData.items;
944
- for (let i = 0; i < items.length; i++) {
945
- if (items[i].type.startsWith("image/")) {
946
- const file = items[i].getAsFile();
947
- if (file) {
948
- const reader = new FileReader();
949
- reader.onload = () => onUpdate(node.id, { styleImage: reader.result });
950
- reader.readAsDataURL(file);
951
- return;
952
- }
953
- }
954
- }
955
- const text = e.clipboardData.getData("text");
956
- if (text && (text.startsWith("http") || text.startsWith("data:image"))) {
957
- onUpdate(node.id, { styleImage: text });
958
- }
959
- };
960
 
961
  return (
962
  <div
963
  className="nb-node absolute text-white w-[320px]"
964
  style={{ left: localPos.x, top: localPos.y }}
965
- onDrop={onDrop}
966
- onDragOver={(e) => e.preventDefault()}
967
- onPaste={onPaste}
968
  >
969
  <div
970
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
@@ -973,7 +958,7 @@ export function BlendNodeView({ node, onDelete, onUpdate, onStartConnection, onE
973
  onPointerUp={onPointerUp}
974
  >
975
  <Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
976
- <div className="font-semibold text-sm flex-1 text-center">BLEND</div>
977
  <div className="flex items-center gap-2">
978
  <Button
979
  variant="ghost"
@@ -1008,55 +993,37 @@ export function BlendNodeView({ node, onDelete, onUpdate, onStartConnection, onE
1008
  </Button>
1009
  </div>
1010
  )}
1011
- <div className="text-xs text-white/70">Style Reference Image</div>
1012
- <div className="text-xs text-white/50">Upload an artistic style image to blend with your input</div>
1013
- {node.styleImage ? (
1014
- <div className="relative">
1015
- <img src={node.styleImage} className="w-full rounded" alt="Style" />
1016
- <button
1017
- className="absolute top-2 right-2 bg-red-500/80 text-white text-xs px-2 py-1 rounded"
1018
- onClick={() => onUpdate(node.id, { styleImage: null })}
1019
- >
1020
- Remove
1021
- </button>
1022
- </div>
1023
- ) : (
1024
- <label className="block">
1025
- <input
1026
- type="file"
1027
- accept="image/*"
1028
- className="hidden"
1029
- onChange={(e) => {
1030
- if (e.target.files?.length) {
1031
- const reader = new FileReader();
1032
- reader.onload = () => onUpdate(node.id, { styleImage: reader.result });
1033
- reader.readAsDataURL(e.target.files[0]);
1034
- }
1035
- }}
1036
- />
1037
- <div className="border-2 border-dashed border-white/20 rounded-lg p-4 text-center cursor-pointer hover:border-white/40">
1038
- <p className="text-xs text-white/60">Drop, upload, or paste style image</p>
1039
- <p className="text-xs text-white/40 mt-1">Art style will be applied to input</p>
1040
- </div>
1041
- </label>
1042
- )}
1043
  <div>
1044
  <Slider
1045
- label="Blend Strength"
1046
- valueLabel={`${node.blendStrength || 50}%`}
1047
  min={0}
1048
  max={100}
1049
- value={node.blendStrength || 50}
1050
- onChange={(e) => onUpdate(node.id, { blendStrength: parseInt((e.target as HTMLInputElement).value) })}
1051
  />
1052
  </div>
1053
  <Button
1054
  className="w-full"
1055
  onClick={() => onProcess(node.id)}
1056
- disabled={node.isRunning || !node.styleImage}
1057
- title={!node.input ? "Connect an input first" : !node.styleImage ? "Add a style image first" : "Blend the style with your input image"}
1058
  >
1059
- {node.isRunning ? "Blending..." : "Blend Style Transfer"}
1060
  </Button>
1061
  {node.output && (
1062
  <div className="space-y-2">
@@ -1064,7 +1031,7 @@ export function BlendNodeView({ node, onDelete, onUpdate, onStartConnection, onE
1064
  <Button
1065
  className="w-full"
1066
  variant="secondary"
1067
- onClick={() => downloadImage(node.output, `blend-${Date.now()}.png`)}
1068
  >
1069
  📥 Download Output
1070
  </Button>
 
926
  );
927
  }
928
 
929
+ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition }: any) {
930
  const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
931
 
932
+ const styleOptions = [
933
+ { value: "90s-anime", label: "90's Anime Style" },
934
+ { value: "mha", label: "My Hero Academia Style" },
935
+ { value: "dbz", label: "Dragon Ball Z Style" },
936
+ { value: "ukiyo-e", label: "Ukiyo-e Style" },
937
+ { value: "cyberpunk", label: "Cyberpunk Style" },
938
+ { value: "steampunk", label: "Steampunk Style" },
939
+ { value: "cubism", label: "Cubism Style" },
940
+ { value: "van-gogh", label: "Post-Impressionist (Van Gogh) Style" },
941
+ { value: "simpsons", label: "Simpsons Style" },
942
+ { value: "family-guy", label: "Family Guy Style" },
943
+ { value: "arcane", label: "Arcane – Painterly + Neon Rim Light" },
944
+ { value: "wildwest", label: "Wild West Style" },
945
+ { value: "stranger-things", label: "Stranger Things – 80s Kodak Style" },
946
+ { value: "breaking-bad", label: "Breaking Bad – Dusty Orange & Teal" },
947
+ ];
 
 
 
 
 
 
 
 
 
 
 
 
948
 
949
  return (
950
  <div
951
  className="nb-node absolute text-white w-[320px]"
952
  style={{ left: localPos.x, top: localPos.y }}
 
 
 
953
  >
954
  <div
955
  className="nb-header px-3 py-2 flex items-center justify-between rounded-t-[14px] cursor-grab active:cursor-grabbing"
 
958
  onPointerUp={onPointerUp}
959
  >
960
  <Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
961
+ <div className="font-semibold text-sm flex-1 text-center">STYLE</div>
962
  <div className="flex items-center gap-2">
963
  <Button
964
  variant="ghost"
 
993
  </Button>
994
  </div>
995
  )}
996
+ <div className="text-xs text-white/70">Art Style</div>
997
+ <div className="text-xs text-white/50 mb-2">Select an artistic style to apply to your image</div>
998
+ <Select
999
+ className="w-full bg-black border-white/20 text-white focus:border-white/40 [&>option]:bg-black [&>option]:text-white"
1000
+ value={node.stylePreset || ""}
1001
+ onChange={(e) => onUpdate(node.id, { stylePreset: (e.target as HTMLSelectElement).value })}
1002
+ >
1003
+ <option value="" className="bg-black">Select a style...</option>
1004
+ {styleOptions.map(opt => (
1005
+ <option key={opt.value} value={opt.value} className="bg-black">
1006
+ {opt.label}
1007
+ </option>
1008
+ ))}
1009
+ </Select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
  <div>
1011
  <Slider
1012
+ label="Style Strength"
1013
+ valueLabel={`${node.styleStrength || 50}%`}
1014
  min={0}
1015
  max={100}
1016
+ value={node.styleStrength || 50}
1017
+ onChange={(e) => onUpdate(node.id, { styleStrength: parseInt((e.target as HTMLInputElement).value) })}
1018
  />
1019
  </div>
1020
  <Button
1021
  className="w-full"
1022
  onClick={() => onProcess(node.id)}
1023
+ disabled={node.isRunning || !node.stylePreset}
1024
+ title={!node.input ? "Connect an input first" : !node.stylePreset ? "Select a style first" : "Apply the style to your input image"}
1025
  >
1026
+ {node.isRunning ? "Applying Style..." : "Apply Style Transfer"}
1027
  </Button>
1028
  {node.output && (
1029
  <div className="space-y-2">
 
1031
  <Button
1032
  className="w-full"
1033
  variant="secondary"
1034
+ onClick={() => downloadImage(node.output, `style-${Date.now()}.png`)}
1035
  >
1036
  📥 Download Output
1037
  </Button>
app/editor/page.tsx CHANGED
@@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
4
  import {
5
  BackgroundNodeView,
6
  ClothesNodeView,
7
- BlendNodeView,
8
  EditNodeView,
9
  CameraNodeView,
10
  AgeNodeView,
@@ -56,7 +56,7 @@ The result should look like all subjects were photographed together in the same
56
  }
57
 
58
  // Types
59
- type NodeType = "CHARACTER" | "MERGE" | "BACKGROUND" | "CLOTHES" | "BLEND" | "EDIT" | "CAMERA" | "AGE" | "FACE";
60
 
61
  type NodeBase = {
62
  id: string;
@@ -103,13 +103,12 @@ type ClothesNode = NodeBase & {
103
  error?: string | null;
104
  };
105
 
106
- type BlendNode = NodeBase & {
107
- type: "BLEND";
108
  input?: string;
109
  output?: string;
110
- styleImage?: string;
111
- stylePrompt?: string;
112
- blendStrength?: number;
113
  isRunning?: boolean;
114
  error?: string | null;
115
  };
@@ -167,7 +166,7 @@ type FaceNode = NodeBase & {
167
  error?: string | null;
168
  };
169
 
170
- type AnyNode = CharacterNode | MergeNode | BackgroundNode | ClothesNode | BlendNode | EditNode | CameraNode | AgeNode | FaceNode;
171
 
172
  // Default placeholder portrait
173
  const DEFAULT_PERSON =
@@ -772,10 +771,10 @@ export default function EditorPage() {
772
  config.selectedPreset = (node as ClothesNode).selectedPreset;
773
  }
774
  break;
775
- case "BLEND":
776
- if ((node as BlendNode).styleImage) {
777
- config.styleImage = (node as BlendNode).styleImage;
778
- config.blendStrength = (node as BlendNode).blendStrength;
779
  }
780
  break;
781
  case "EDIT":
@@ -1390,8 +1389,8 @@ export default function EditorPage() {
1390
  case "BLEND":
1391
  setNodes(prev => [...prev, { ...commonProps, type: "BLEND", blendStrength: 50 } as BlendNode]);
1392
  break;
1393
- case "EDIT":
1394
- setNodes(prev => [...prev, { ...commonProps, type: "EDIT" } as EditNode]);
1395
  break;
1396
  case "CAMERA":
1397
  setNodes(prev => [...prev, { ...commonProps, type: "CAMERA" } as CameraNode]);
@@ -1562,11 +1561,11 @@ export default function EditorPage() {
1562
  onUpdatePosition={updateNodePosition}
1563
  />
1564
  );
1565
- case "BLEND":
1566
  return (
1567
- <BlendNodeView
1568
  key={node.id}
1569
- node={node as BlendNode}
1570
  onDelete={deleteNode}
1571
  onUpdate={updateNode}
1572
  onStartConnection={handleStartConnection}
@@ -1646,7 +1645,7 @@ export default function EditorPage() {
1646
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("MERGE")}>MERGE</button>
1647
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("BACKGROUND")}>BACKGROUND</button>
1648
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("CLOTHES")}>CLOTHES</button>
1649
- <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("BLEND")}>BLEND</button>
1650
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("EDIT")}>EDIT</button>
1651
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("CAMERA")}>CAMERA</button>
1652
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("AGE")}>AGE</button>
 
4
  import {
5
  BackgroundNodeView,
6
  ClothesNodeView,
7
+ StyleNodeView,
8
  EditNodeView,
9
  CameraNodeView,
10
  AgeNodeView,
 
56
  }
57
 
58
  // Types
59
+ type NodeType = "CHARACTER" | "MERGE" | "BACKGROUND" | "CLOTHES" | "STYLE" | "EDIT" | "CAMERA" | "AGE" | "FACE";
60
 
61
  type NodeBase = {
62
  id: string;
 
103
  error?: string | null;
104
  };
105
 
106
+ type StyleNode = NodeBase & {
107
+ type: "STYLE";
108
  input?: string;
109
  output?: string;
110
+ stylePreset?: string;
111
+ styleStrength?: number;
 
112
  isRunning?: boolean;
113
  error?: string | null;
114
  };
 
166
  error?: string | null;
167
  };
168
 
169
+ type AnyNode = CharacterNode | MergeNode | BackgroundNode | ClothesNode | StyleNode | EditNode | CameraNode | AgeNode | FaceNode;
170
 
171
  // Default placeholder portrait
172
  const DEFAULT_PERSON =
 
771
  config.selectedPreset = (node as ClothesNode).selectedPreset;
772
  }
773
  break;
774
+ case "STYLE":
775
+ if ((node as StyleNode).stylePreset) {
776
+ config.stylePreset = (node as StyleNode).stylePreset;
777
+ config.styleStrength = (node as StyleNode).styleStrength;
778
  }
779
  break;
780
  case "EDIT":
 
1389
  case "BLEND":
1390
  setNodes(prev => [...prev, { ...commonProps, type: "BLEND", blendStrength: 50 } as BlendNode]);
1391
  break;
1392
+ case "STYLE":
1393
+ setNodes(prev => [...prev, { ...commonProps, type: "STYLE", styleStrength: 50 } as StyleNode]);
1394
  break;
1395
  case "CAMERA":
1396
  setNodes(prev => [...prev, { ...commonProps, type: "CAMERA" } as CameraNode]);
 
1561
  onUpdatePosition={updateNodePosition}
1562
  />
1563
  );
1564
+ case "STYLE":
1565
  return (
1566
+ <StyleNodeView
1567
  key={node.id}
1568
+ node={node as StyleNode}
1569
  onDelete={deleteNode}
1570
  onUpdate={updateNode}
1571
  onStartConnection={handleStartConnection}
 
1645
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("MERGE")}>MERGE</button>
1646
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("BACKGROUND")}>BACKGROUND</button>
1647
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("CLOTHES")}>CLOTHES</button>
1648
+ <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("STYLE")}>STYLE</button>
1649
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("EDIT")}>EDIT</button>
1650
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("CAMERA")}>CAMERA</button>
1651
  <button className="w-full text-left px-3 py-2 text-sm hover:bg-white/10 rounded-lg" onClick={() => addFromMenu("AGE")}>AGE</button>