jbilcke-hf HF staff commited on
Commit
6989c25
โ€ข
1 Parent(s): c2ac436

fix image switch + add download button

Browse files
.gitignore CHANGED
@@ -5,6 +5,8 @@ __pycache__/
5
  **/*.py[cod]
6
  *$py.class
7
 
 
 
8
  # Model weights
9
  **/*.pth
10
  **/*.onnx
 
5
  **/*.py[cod]
6
  *$py.class
7
 
8
+ miniserver.py
9
+
10
  # Model weights
11
  **/*.pth
12
  **/*.onnx
client/src/app.tsx CHANGED
@@ -1,12 +1,11 @@
1
  import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
- import { RotateCcw } from 'lucide-react';
3
 
4
  import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
5
  import { truncateFileName } from './lib/utils';
6
  import { useFaceLandmarkDetection } from './hooks/useFaceLandmarkDetection';
7
  import { PoweredBy } from './components/PoweredBy';
8
  import { Spinner } from './components/Spinner';
9
- import { DoubleCard } from './components/DoubleCard';
10
  import { useFacePokeAPI } from './hooks/useFacePokeAPI';
11
  import { Layout } from './layout';
12
  import { useMainStore } from './hooks/useMainStore';
@@ -22,6 +21,7 @@ export function App() {
22
  const previewImage = useMainStore(s => s.previewImage);
23
  const setPreviewImage = useMainStore(s => s.setPreviewImage);
24
  const resetImage = useMainStore(s => s.resetImage);
 
25
 
26
  const {
27
  status,
@@ -65,12 +65,14 @@ export function App() {
65
  const image = await convertImageToBase64(files[0]);
66
  setPreviewImage(image);
67
  setOriginalImage(image);
 
68
  } catch (err) {
69
  console.log(`failed to convert the image: `, err);
70
  setImageFile(null);
71
  setStatus('');
72
  setPreviewImage('');
73
  setOriginalImage('');
 
74
  setFaceLandmarks([]);
75
  setBlendShapes([]);
76
  }
@@ -79,10 +81,22 @@ export function App() {
79
  setStatus('');
80
  setPreviewImage('');
81
  setOriginalImage('');
 
82
  setFaceLandmarks([]);
83
  setBlendShapes([]);
84
  }
85
- }, [isMediaPipeReady, setImageFile, setPreviewImage, setOriginalImage, setFaceLandmarks, setBlendShapes, setStatus]);
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  const canDisplayBlendShapes = false
88
 
@@ -124,24 +138,35 @@ export function App() {
124
  )}
125
  <div className="mb-5 relative">
126
  <div className="flex flex-row items-center justify-between w-full">
127
- <div className="relative">
128
- <input
129
- id="imageInput"
130
- type="file"
131
- accept="image/*"
132
- onChange={handleFileChange}
133
- className="hidden"
134
- disabled={!isMediaPipeReady}
135
- />
136
- <label
137
- htmlFor="imageInput"
138
- className={`cursor-pointer inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white ${
139
- isMediaPipeReady ? 'bg-gray-600 hover:bg-gray-500' : 'bg-gray-500 cursor-not-allowed'
140
- } focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 shadow-xl`}
141
- >
142
- <Spinner />
143
- {imageFile ? truncateFileName(imageFile.name, 32) : (isMediaPipeReady ? 'Choose an image' : 'Initializing...')}
144
- </label>
 
 
 
 
 
 
 
 
 
 
 
145
  </div>
146
  {previewImage && <label className="mt-4 flex items-center">
147
  <input
@@ -177,14 +202,12 @@ export function App() {
177
  opacity: isDebugMode ? currentOpacity : 0.0,
178
  transition: 'opacity 0.2s ease-in-out'
179
  }}
180
-
181
  />
182
  </div>
183
  )}
184
  {canDisplayBlendShapes && displayBlendShapes}
185
  </div>
186
  <PoweredBy />
187
-
188
  </Layout>
189
  );
190
  }
 
1
  import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
+ import { Download } from 'lucide-react';
3
 
4
  import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
5
  import { truncateFileName } from './lib/utils';
6
  import { useFaceLandmarkDetection } from './hooks/useFaceLandmarkDetection';
7
  import { PoweredBy } from './components/PoweredBy';
8
  import { Spinner } from './components/Spinner';
 
9
  import { useFacePokeAPI } from './hooks/useFacePokeAPI';
10
  import { Layout } from './layout';
11
  import { useMainStore } from './hooks/useMainStore';
 
21
  const previewImage = useMainStore(s => s.previewImage);
22
  const setPreviewImage = useMainStore(s => s.setPreviewImage);
23
  const resetImage = useMainStore(s => s.resetImage);
24
+ const setOriginalImageHash = useMainStore(s => s.setOriginalImageHash);
25
 
26
  const {
27
  status,
 
65
  const image = await convertImageToBase64(files[0]);
66
  setPreviewImage(image);
67
  setOriginalImage(image);
68
+ setOriginalImageHash('');
69
  } catch (err) {
70
  console.log(`failed to convert the image: `, err);
71
  setImageFile(null);
72
  setStatus('');
73
  setPreviewImage('');
74
  setOriginalImage('');
75
+ setOriginalImageHash('');
76
  setFaceLandmarks([]);
77
  setBlendShapes([]);
78
  }
 
81
  setStatus('');
82
  setPreviewImage('');
83
  setOriginalImage('');
84
+ setOriginalImageHash('');
85
  setFaceLandmarks([]);
86
  setBlendShapes([]);
87
  }
88
+ }, [isMediaPipeReady, setImageFile, setPreviewImage, setOriginalImage, setOriginalImageHash, setFaceLandmarks, setBlendShapes, setStatus]);
89
+
90
+ const handleDownload = useCallback(() => {
91
+ if (previewImage) {
92
+ const link = document.createElement('a');
93
+ link.href = previewImage;
94
+ link.download = 'modified_image.png';
95
+ document.body.appendChild(link);
96
+ link.click();
97
+ document.body.removeChild(link);
98
+ }
99
+ }, [previewImage]);
100
 
101
  const canDisplayBlendShapes = false
102
 
 
138
  )}
139
  <div className="mb-5 relative">
140
  <div className="flex flex-row items-center justify-between w-full">
141
+ <div className="flex items-center space-x-2">
142
+ <div className="relative">
143
+ <input
144
+ id="imageInput"
145
+ type="file"
146
+ accept="image/*"
147
+ onChange={handleFileChange}
148
+ className="hidden"
149
+ disabled={!isMediaPipeReady}
150
+ />
151
+ <label
152
+ htmlFor="imageInput"
153
+ className={`cursor-pointer inline-flex items-center px-3 h-10 border border-transparent text-sm font-medium rounded-md text-white ${
154
+ isMediaPipeReady ? 'bg-slate-600 hover:bg-slate-500' : 'bg-slate-500 cursor-not-allowed'
155
+ } focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 shadow-xl`}
156
+ >
157
+ <Spinner />
158
+ {imageFile ? truncateFileName(imageFile.name, 32) : (isMediaPipeReady ? 'Choose a portrait photo (.jpg, .png, .webp)' : 'Initializing...')}
159
+ </label>
160
+ </div>
161
+ {previewImage && (
162
+ <button
163
+ onClick={handleDownload}
164
+ className="inline-flex items-center px-3 h-10 border border-transparent text-sm font-medium rounded-md text-white bg-zinc-600 hover:bg-zinc-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-zinc-500 shadow-xl"
165
+ >
166
+ <Download className="w-4 h-4 mr-2" />
167
+ Download
168
+ </button>
169
+ )}
170
  </div>
171
  {previewImage && <label className="mt-4 flex items-center">
172
  <input
 
202
  opacity: isDebugMode ? currentOpacity : 0.0,
203
  transition: 'opacity 0.2s ease-in-out'
204
  }}
 
205
  />
206
  </div>
207
  )}
208
  {canDisplayBlendShapes && displayBlendShapes}
209
  </div>
210
  <PoweredBy />
 
211
  </Layout>
212
  );
213
  }
client/src/components/PoweredBy.tsx CHANGED
@@ -5,7 +5,7 @@ export function PoweredBy() {
5
  style={{ textShadow: "rgb(255 255 255 / 80%) 0px 0px 2px" }}>
6
  Best hosted on
7
  </span>*/}
8
- <span className="ml-2 mr-1">
9
  <img src="/hf-logo.svg" alt="Hugging Face" className="w-5 h-5" />
10
  </span>
11
  <span className="text-neutral-900 text-sm font-semibold"
 
5
  style={{ textShadow: "rgb(255 255 255 / 80%) 0px 0px 2px" }}>
6
  Best hosted on
7
  </span>*/}
8
+ <span className="mr-1">
9
  <img src="/hf-logo.svg" alt="Hugging Face" className="w-5 h-5" />
10
  </span>
11
  <span className="text-neutral-900 text-sm font-semibold"
public/index.js CHANGED
@@ -23683,8 +23683,77 @@ var require_lodash = __commonJS((exports, module) => {
23683
  var client = __toESM(require_client(), 1);
23684
 
23685
  // src/app.tsx
23686
- var import_react7 = __toESM(require_react(), 1);
 
 
 
23687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23688
  // src/components/ui/alert.tsx
23689
  var React = __toESM(require_react(), 1);
23690
 
@@ -25133,7 +25202,7 @@ var AlertDescription = React.forwardRef(({ className, ...props }, ref) => jsx_de
25133
  AlertDescription.displayName = "AlertDescription";
25134
 
25135
  // src/hooks/useFaceLandmarkDetection.tsx
25136
- var import_react5 = __toESM(require_react(), 1);
25137
 
25138
  // node_modules/@mediapipe/tasks-vision/vision_bundle.mjs
25139
  var exports_vision_bundle = {};
@@ -29683,10 +29752,10 @@ var createStoreImpl = (createState) => {
29683
  var createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
29684
 
29685
  // node_modules/zustand/esm/react.mjs
29686
- var import_react = __toESM(require_react(), 1);
29687
  var useStore = function(api, selector = identity) {
29688
- const slice = import_react.default.useSyncExternalStore(api.subscribe, () => selector(api.getState()), () => selector(api.getInitialState()));
29689
- import_react.default.useDebugValue(slice);
29690
  return slice;
29691
  };
29692
  var identity = (arg) => arg;
@@ -29932,21 +30001,21 @@ class FacePoke {
29932
  var facePoke = new FacePoke;
29933
 
29934
  // node_modules/beautiful-react-hooks/esm/useThrottledCallback.js
29935
- var import_react4 = __toESM(require_react(), 1);
29936
  var import_lodash = __toESM(require_lodash(), 1);
29937
 
29938
  // node_modules/beautiful-react-hooks/esm/useWillUnmount.js
29939
- var import_react3 = __toESM(require_react(), 1);
29940
 
29941
  // node_modules/beautiful-react-hooks/esm/shared/isFunction.js
29942
  var isFunction = (functionToCheck) => typeof functionToCheck === "function" && !!functionToCheck.constructor && !!functionToCheck.call && !!functionToCheck.apply;
29943
  var isFunction_default = isFunction;
29944
 
29945
  // node_modules/beautiful-react-hooks/esm/factory/createHandlerSetter.js
29946
- var import_react2 = __toESM(require_react(), 1);
29947
  var createHandlerSetter = (callback) => {
29948
- const handlerRef = import_react2.useRef(callback);
29949
- const setHandler = import_react2.useRef((nextCallback) => {
29950
  if (typeof nextCallback !== "function") {
29951
  throw new Error("the argument supplied to the \'setHandler\' function should be of type function");
29952
  }
@@ -29958,9 +30027,9 @@ var createHandlerSetter_default = createHandlerSetter;
29958
 
29959
  // node_modules/beautiful-react-hooks/esm/useWillUnmount.js
29960
  var useWillUnmount = (callback) => {
29961
- const mountRef = import_react3.useRef(false);
29962
  const [handler, setHandler] = createHandlerSetter_default(callback);
29963
- import_react3.useLayoutEffect(() => {
29964
  mountRef.current = true;
29965
  return () => {
29966
  if (isFunction_default(handler === null || handler === undefined ? undefined : handler.current) && mountRef.current) {
@@ -29978,15 +30047,15 @@ var defaultOptions = {
29978
  trailing: true
29979
  };
29980
  var useThrottledCallback = (fn2, dependencies, wait = 600, options = defaultOptions) => {
29981
- const throttled = import_react4.useRef(import_lodash.default(fn2, wait, options));
29982
- import_react4.useEffect(() => {
29983
  throttled.current = import_lodash.default(fn2, wait, options);
29984
  }, [fn2, wait, options]);
29985
  useWillUnmount_default(() => {
29986
  var _a2;
29987
  (_a2 = throttled.current) === null || _a2 === undefined || _a2.cancel();
29988
  });
29989
- return import_react4.useCallback(throttled.current, dependencies !== null && dependencies !== undefined ? dependencies : []);
29990
  };
29991
  var useThrottledCallback_default = useThrottledCallback;
29992
 
@@ -32761,34 +32830,34 @@ function useFaceLandmarkDetection() {
32761
  const resetImage = useMainStore((s2) => s2.resetImage);
32762
  window.debugJuju = useMainStore;
32763
  const averageLatency = 220;
32764
- const [faceLandmarks, setFaceLandmarks] = import_react5.useState([]);
32765
- const [isMediaPipeReady, setIsMediaPipeReady] = import_react5.useState(false);
32766
- const [isDrawingUtilsReady, setIsDrawingUtilsReady] = import_react5.useState(false);
32767
- const [blendShapes, setBlendShapes] = import_react5.useState([]);
32768
- const [dragStart, setDragStart] = import_react5.useState(null);
32769
- const [dragEnd, setDragEnd] = import_react5.useState(null);
32770
- const [isDragging, setIsDragging] = import_react5.useState(false);
32771
- const [isWaitingForResponse, setIsWaitingForResponse] = import_react5.useState(false);
32772
- const dragStartRef = import_react5.useRef(null);
32773
- const currentMousePosRef = import_react5.useRef(null);
32774
- const lastModifiedImageHashRef = import_react5.useRef(null);
32775
- const [currentLandmark, setCurrentLandmark] = import_react5.useState(null);
32776
- const [previousLandmark, setPreviousLandmark] = import_react5.useState(null);
32777
- const [currentOpacity, setCurrentOpacity] = import_react5.useState(0);
32778
- const [previousOpacity, setPreviousOpacity] = import_react5.useState(0);
32779
- const [isHovering, setIsHovering] = import_react5.useState(false);
32780
- const canvasRef = import_react5.useRef(null);
32781
- const mediaPipeRef = import_react5.useRef({
32782
  faceLandmarker: null,
32783
  drawingUtils: null
32784
  });
32785
- const setActiveLandmark = import_react5.useCallback((newLandmark) => {
32786
  setPreviousLandmark(currentLandmark || null);
32787
  setCurrentLandmark(newLandmark || null);
32788
  setCurrentOpacity(0);
32789
  setPreviousOpacity(1);
32790
  }, [currentLandmark, setPreviousLandmark, setCurrentLandmark, setCurrentOpacity, setPreviousOpacity]);
32791
- import_react5.useEffect(() => {
32792
  console.log("Initializing MediaPipe...");
32793
  let isMounted = true;
32794
  const initializeMediaPipe = async () => {
@@ -32826,8 +32895,8 @@ function useFaceLandmarkDetection() {
32826
  }
32827
  };
32828
  }, []);
32829
- const [landmarkCenters, setLandmarkCenters] = import_react5.useState({});
32830
- const computeLandmarkCenters = import_react5.useCallback((landmarks2) => {
32831
  const centers = {};
32832
  const computeGroupCenter = (group) => {
32833
  let sumX = 0, sumY = 0, sumZ = 0, count = 0;
@@ -32850,7 +32919,7 @@ function useFaceLandmarkDetection() {
32850
  centers.background = { x: 0.5, y: 0.5, z: 0 };
32851
  setLandmarkCenters(centers);
32852
  }, []);
32853
- const findClosestLandmark = import_react5.useCallback((mouseX, mouseY, isGroup) => {
32854
  const defaultLandmark = {
32855
  group: "background",
32856
  distance: 0,
@@ -32899,7 +32968,7 @@ function useFaceLandmarkDetection() {
32899
  return defaultLandmark;
32900
  }
32901
  }, [landmarkCenters]);
32902
- const detectFaceLandmarks = import_react5.useCallback(async (imageDataUrl) => {
32903
  if (!isMediaPipeReady) {
32904
  console.log("MediaPipe not ready. Skipping detection.");
32905
  return;
@@ -32925,7 +32994,7 @@ function useFaceLandmarkDetection() {
32925
  drawLandmarks(faceLandmarkerResult.faceLandmarks[0], canvasRef.current, drawingUtils);
32926
  }
32927
  }, [isMediaPipeReady, isDrawingUtilsReady, computeLandmarkCenters]);
32928
- const drawLandmarks = import_react5.useCallback((landmarks2, canvas, drawingUtils) => {
32929
  const ctx = canvas.getContext("2d");
32930
  if (!ctx)
32931
  return;
@@ -32951,12 +33020,12 @@ function useFaceLandmarkDetection() {
32951
  img.src = previewImage;
32952
  }
32953
  }, [previewImage, currentLandmark, previousLandmark, currentOpacity, previousOpacity]);
32954
- import_react5.useEffect(() => {
32955
  if (isMediaPipeReady && isDrawingUtilsReady && faceLandmarks.length > 0 && canvasRef.current && mediaPipeRef.current.drawingUtils) {
32956
  drawLandmarks(faceLandmarks[0], canvasRef.current, mediaPipeRef.current.drawingUtils);
32957
  }
32958
  }, [isMediaPipeReady, isDrawingUtilsReady, faceLandmarks, currentLandmark, previousLandmark, currentOpacity, previousOpacity, drawLandmarks]);
32959
- import_react5.useEffect(() => {
32960
  let animationFrame;
32961
  const animate = () => {
32962
  setCurrentOpacity((prev) => Math.min(prev + 0.2, 1));
@@ -32968,7 +33037,7 @@ function useFaceLandmarkDetection() {
32968
  animationFrame = requestAnimationFrame(animate);
32969
  return () => cancelAnimationFrame(animationFrame);
32970
  }, [currentLandmark]);
32971
- const canvasRefCallback = import_react5.useCallback((node) => {
32972
  if (node !== null) {
32973
  const ctx = node.getContext("2d");
32974
  if (ctx) {
@@ -32984,7 +33053,7 @@ function useFaceLandmarkDetection() {
32984
  canvasRef.current = node;
32985
  }
32986
  }, []);
32987
- import_react5.useEffect(() => {
32988
  if (!isMediaPipeReady) {
32989
  console.log("MediaPipe not ready. Skipping landmark detection.");
32990
  return;
@@ -32999,7 +33068,7 @@ function useFaceLandmarkDetection() {
32999
  }
33000
  detectFaceLandmarks(previewImage);
33001
  }, [isMediaPipeReady, isDrawingUtilsReady, previewImage]);
33002
- const modifyImage = import_react5.useCallback(({ landmark, vector }) => {
33003
  const {
33004
  originalImage: originalImage2,
33005
  originalImageHash: originalImageHash2,
@@ -33090,13 +33159,13 @@ function useFaceLandmarkDetection() {
33090
  const modifyImageWithRateLimit = useThrottledCallback_default((params) => {
33091
  modifyImage(params);
33092
  }, [modifyImage], averageLatency);
33093
- const handleMouseEnter = import_react5.useCallback(() => {
33094
  setIsHovering(true);
33095
  }, []);
33096
- const handleMouseLeave = import_react5.useCallback(() => {
33097
  setIsHovering(false);
33098
  }, []);
33099
- const handleMouseDown = import_react5.useCallback((event) => {
33100
  if (!canvasRef.current)
33101
  return;
33102
  const rect = canvasRef.current.getBoundingClientRect();
@@ -33108,7 +33177,7 @@ function useFaceLandmarkDetection() {
33108
  setDragStart({ x: x2, y: y2 });
33109
  dragStartRef.current = { x: x2, y: y2 };
33110
  }, [findClosestLandmark, setActiveLandmark, setDragStart]);
33111
- const handleMouseMove = import_react5.useCallback((event) => {
33112
  if (!canvasRef.current)
33113
  return;
33114
  const rect = canvasRef.current.getBoundingClientRect();
@@ -33134,7 +33203,7 @@ function useFaceLandmarkDetection() {
33134
  setIsHovering(true);
33135
  }
33136
  }, [currentLandmark, dragStart, setIsHovering, setActiveLandmark, setIsDragging, modifyImageWithRateLimit, landmarkCenters]);
33137
- const handleMouseUp = import_react5.useCallback((event) => {
33138
  if (!canvasRef.current)
33139
  return;
33140
  const rect = canvasRef.current.getBoundingClientRect();
@@ -33156,7 +33225,7 @@ function useFaceLandmarkDetection() {
33156
  dragStartRef.current = null;
33157
  setActiveLandmark(undefined);
33158
  }, [currentLandmark, isDragging, modifyImageWithRateLimit, findClosestLandmark, setActiveLandmark, landmarkCenters, modifyImageWithRateLimit, setIsDragging]);
33159
- import_react5.useEffect(() => {
33160
  facePoke.setOnModifiedImage((image, image_hash) => {
33161
  if (image) {
33162
  setPreviewImage(image);
@@ -33192,7 +33261,7 @@ function PoweredBy() {
33192
  className: "flex flex-row items-center justify-center font-sans mt-4 w-full",
33193
  children: [
33194
  jsx_dev_runtime2.jsxDEV("span", {
33195
- className: "ml-2 mr-1",
33196
  children: jsx_dev_runtime2.jsxDEV("img", {
33197
  src: "/hf-logo.svg",
33198
  alt: "Hugging Face",
@@ -33226,17 +33295,17 @@ function Spinner() {
33226
  }
33227
 
33228
  // src/hooks/useFacePokeAPI.ts
33229
- var import_react6 = __toESM(require_react(), 1);
33230
  function useFacePokeAPI() {
33231
- const [status, setStatus] = import_react6.useState("");
33232
- const [isDebugMode, setIsDebugMode] = import_react6.useState(false);
33233
- const [interruptMessage, setInterruptMessage] = import_react6.useState(null);
33234
- const [isLoading, setIsLoading] = import_react6.useState(false);
33235
- import_react6.useEffect(() => {
33236
  const urlParams = new URLSearchParams(window.location.search);
33237
  setIsDebugMode(urlParams.get("debug") === "true");
33238
  }, []);
33239
- import_react6.useEffect(() => {
33240
  const handleInterruption = (event) => {
33241
  setInterruptMessage(event.detail.message);
33242
  };
@@ -33303,6 +33372,7 @@ function App() {
33303
  const previewImage = useMainStore((s2) => s2.previewImage);
33304
  const setPreviewImage = useMainStore((s2) => s2.setPreviewImage);
33305
  const resetImage = useMainStore((s2) => s2.resetImage);
 
33306
  const {
33307
  status,
33308
  setStatus,
@@ -33326,8 +33396,8 @@ function App() {
33326
  handleMouseLeave,
33327
  currentOpacity
33328
  } = useFaceLandmarkDetection();
33329
- const videoRef = import_react7.useRef(null);
33330
- const handleFileChange = import_react7.useCallback(async (event) => {
33331
  const files = event.target.files;
33332
  if (files && files[0]) {
33333
  setImageFile(files[0]);
@@ -33336,12 +33406,14 @@ function App() {
33336
  const image = await convertImageToBase64(files[0]);
33337
  setPreviewImage(image);
33338
  setOriginalImage(image);
 
33339
  } catch (err) {
33340
  console.log(`failed to convert the image: `, err);
33341
  setImageFile(null);
33342
  setStatus("");
33343
  setPreviewImage("");
33344
  setOriginalImage("");
 
33345
  setFaceLandmarks([]);
33346
  setBlendShapes([]);
33347
  }
@@ -33350,12 +33422,23 @@ function App() {
33350
  setStatus("");
33351
  setPreviewImage("");
33352
  setOriginalImage("");
 
33353
  setFaceLandmarks([]);
33354
  setBlendShapes([]);
33355
  }
33356
- }, [isMediaPipeReady, setImageFile, setPreviewImage, setOriginalImage, setFaceLandmarks, setBlendShapes, setStatus]);
 
 
 
 
 
 
 
 
 
 
33357
  const canDisplayBlendShapes = false;
33358
- const displayBlendShapes = import_react7.useMemo(() => jsx_dev_runtime5.jsxDEV("div", {
33359
  className: "mt-4",
33360
  children: [
33361
  jsx_dev_runtime5.jsxDEV("h3", {
@@ -33417,22 +33500,37 @@ function App() {
33417
  className: "flex flex-row items-center justify-between w-full",
33418
  children: [
33419
  jsx_dev_runtime5.jsxDEV("div", {
33420
- className: "relative",
33421
  children: [
33422
- jsx_dev_runtime5.jsxDEV("input", {
33423
- id: "imageInput",
33424
- type: "file",
33425
- accept: "image/*",
33426
- onChange: handleFileChange,
33427
- className: "hidden",
33428
- disabled: !isMediaPipeReady
33429
- }, undefined, false, undefined, this),
33430
- jsx_dev_runtime5.jsxDEV("label", {
33431
- htmlFor: "imageInput",
33432
- className: `cursor-pointer inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white ${isMediaPipeReady ? "bg-gray-600 hover:bg-gray-500" : "bg-gray-500 cursor-not-allowed"} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 shadow-xl`,
 
 
 
 
 
 
 
 
 
 
 
 
 
33433
  children: [
33434
- jsx_dev_runtime5.jsxDEV(Spinner, {}, undefined, false, undefined, this),
33435
- imageFile ? truncateFileName(imageFile.name, 32) : isMediaPipeReady ? "Choose an image" : "Initializing..."
 
 
33436
  ]
33437
  }, undefined, true, undefined, this)
33438
  ]
 
23683
  var client = __toESM(require_client(), 1);
23684
 
23685
  // src/app.tsx
23686
+ var import_react9 = __toESM(require_react(), 1);
23687
+
23688
+ // node_modules/lucide-react/dist/esm/createLucideIcon.js
23689
+ var import_react2 = __toESM(require_react(), 1);
23690
 
23691
+ // node_modules/lucide-react/dist/esm/shared/src/utils.js
23692
+ var toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
23693
+ var mergeClasses = (...classes) => classes.filter((className, index, array) => {
23694
+ return Boolean(className) && array.indexOf(className) === index;
23695
+ }).join(" ");
23696
+
23697
+ // node_modules/lucide-react/dist/esm/Icon.js
23698
+ var import_react = __toESM(require_react(), 1);
23699
+
23700
+ // node_modules/lucide-react/dist/esm/defaultAttributes.js
23701
+ var defaultAttributes = {
23702
+ xmlns: "http://www.w3.org/2000/svg",
23703
+ width: 24,
23704
+ height: 24,
23705
+ viewBox: "0 0 24 24",
23706
+ fill: "none",
23707
+ stroke: "currentColor",
23708
+ strokeWidth: 2,
23709
+ strokeLinecap: "round",
23710
+ strokeLinejoin: "round"
23711
+ };
23712
+
23713
+ // node_modules/lucide-react/dist/esm/Icon.js
23714
+ var Icon = import_react.forwardRef(({
23715
+ color = "currentColor",
23716
+ size = 24,
23717
+ strokeWidth = 2,
23718
+ absoluteStrokeWidth,
23719
+ className = "",
23720
+ children,
23721
+ iconNode,
23722
+ ...rest
23723
+ }, ref) => {
23724
+ return import_react.createElement("svg", {
23725
+ ref,
23726
+ ...defaultAttributes,
23727
+ width: size,
23728
+ height: size,
23729
+ stroke: color,
23730
+ strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
23731
+ className: mergeClasses("lucide", className),
23732
+ ...rest
23733
+ }, [
23734
+ ...iconNode.map(([tag, attrs]) => import_react.createElement(tag, attrs)),
23735
+ ...Array.isArray(children) ? children : [children]
23736
+ ]);
23737
+ });
23738
+
23739
+ // node_modules/lucide-react/dist/esm/createLucideIcon.js
23740
+ var createLucideIcon = (iconName, iconNode) => {
23741
+ const Component = import_react2.forwardRef(({ className, ...props }, ref) => import_react2.createElement(Icon, {
23742
+ ref,
23743
+ iconNode,
23744
+ className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
23745
+ ...props
23746
+ }));
23747
+ Component.displayName = `${iconName}`;
23748
+ return Component;
23749
+ };
23750
+
23751
+ // node_modules/lucide-react/dist/esm/icons/download.js
23752
+ var Download = createLucideIcon("Download", [
23753
+ ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }],
23754
+ ["polyline", { points: "7 10 12 15 17 10", key: "2ggqvy" }],
23755
+ ["line", { x1: "12", x2: "12", y1: "15", y2: "3", key: "1vk2je" }]
23756
+ ]);
23757
  // src/components/ui/alert.tsx
23758
  var React = __toESM(require_react(), 1);
23759
 
 
25202
  AlertDescription.displayName = "AlertDescription";
25203
 
25204
  // src/hooks/useFaceLandmarkDetection.tsx
25205
+ var import_react7 = __toESM(require_react(), 1);
25206
 
25207
  // node_modules/@mediapipe/tasks-vision/vision_bundle.mjs
25208
  var exports_vision_bundle = {};
 
29752
  var createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
29753
 
29754
  // node_modules/zustand/esm/react.mjs
29755
+ var import_react3 = __toESM(require_react(), 1);
29756
  var useStore = function(api, selector = identity) {
29757
+ const slice = import_react3.default.useSyncExternalStore(api.subscribe, () => selector(api.getState()), () => selector(api.getInitialState()));
29758
+ import_react3.default.useDebugValue(slice);
29759
  return slice;
29760
  };
29761
  var identity = (arg) => arg;
 
30001
  var facePoke = new FacePoke;
30002
 
30003
  // node_modules/beautiful-react-hooks/esm/useThrottledCallback.js
30004
+ var import_react6 = __toESM(require_react(), 1);
30005
  var import_lodash = __toESM(require_lodash(), 1);
30006
 
30007
  // node_modules/beautiful-react-hooks/esm/useWillUnmount.js
30008
+ var import_react5 = __toESM(require_react(), 1);
30009
 
30010
  // node_modules/beautiful-react-hooks/esm/shared/isFunction.js
30011
  var isFunction = (functionToCheck) => typeof functionToCheck === "function" && !!functionToCheck.constructor && !!functionToCheck.call && !!functionToCheck.apply;
30012
  var isFunction_default = isFunction;
30013
 
30014
  // node_modules/beautiful-react-hooks/esm/factory/createHandlerSetter.js
30015
+ var import_react4 = __toESM(require_react(), 1);
30016
  var createHandlerSetter = (callback) => {
30017
+ const handlerRef = import_react4.useRef(callback);
30018
+ const setHandler = import_react4.useRef((nextCallback) => {
30019
  if (typeof nextCallback !== "function") {
30020
  throw new Error("the argument supplied to the \'setHandler\' function should be of type function");
30021
  }
 
30027
 
30028
  // node_modules/beautiful-react-hooks/esm/useWillUnmount.js
30029
  var useWillUnmount = (callback) => {
30030
+ const mountRef = import_react5.useRef(false);
30031
  const [handler, setHandler] = createHandlerSetter_default(callback);
30032
+ import_react5.useLayoutEffect(() => {
30033
  mountRef.current = true;
30034
  return () => {
30035
  if (isFunction_default(handler === null || handler === undefined ? undefined : handler.current) && mountRef.current) {
 
30047
  trailing: true
30048
  };
30049
  var useThrottledCallback = (fn2, dependencies, wait = 600, options = defaultOptions) => {
30050
+ const throttled = import_react6.useRef(import_lodash.default(fn2, wait, options));
30051
+ import_react6.useEffect(() => {
30052
  throttled.current = import_lodash.default(fn2, wait, options);
30053
  }, [fn2, wait, options]);
30054
  useWillUnmount_default(() => {
30055
  var _a2;
30056
  (_a2 = throttled.current) === null || _a2 === undefined || _a2.cancel();
30057
  });
30058
+ return import_react6.useCallback(throttled.current, dependencies !== null && dependencies !== undefined ? dependencies : []);
30059
  };
30060
  var useThrottledCallback_default = useThrottledCallback;
30061
 
 
32830
  const resetImage = useMainStore((s2) => s2.resetImage);
32831
  window.debugJuju = useMainStore;
32832
  const averageLatency = 220;
32833
+ const [faceLandmarks, setFaceLandmarks] = import_react7.useState([]);
32834
+ const [isMediaPipeReady, setIsMediaPipeReady] = import_react7.useState(false);
32835
+ const [isDrawingUtilsReady, setIsDrawingUtilsReady] = import_react7.useState(false);
32836
+ const [blendShapes, setBlendShapes] = import_react7.useState([]);
32837
+ const [dragStart, setDragStart] = import_react7.useState(null);
32838
+ const [dragEnd, setDragEnd] = import_react7.useState(null);
32839
+ const [isDragging, setIsDragging] = import_react7.useState(false);
32840
+ const [isWaitingForResponse, setIsWaitingForResponse] = import_react7.useState(false);
32841
+ const dragStartRef = import_react7.useRef(null);
32842
+ const currentMousePosRef = import_react7.useRef(null);
32843
+ const lastModifiedImageHashRef = import_react7.useRef(null);
32844
+ const [currentLandmark, setCurrentLandmark] = import_react7.useState(null);
32845
+ const [previousLandmark, setPreviousLandmark] = import_react7.useState(null);
32846
+ const [currentOpacity, setCurrentOpacity] = import_react7.useState(0);
32847
+ const [previousOpacity, setPreviousOpacity] = import_react7.useState(0);
32848
+ const [isHovering, setIsHovering] = import_react7.useState(false);
32849
+ const canvasRef = import_react7.useRef(null);
32850
+ const mediaPipeRef = import_react7.useRef({
32851
  faceLandmarker: null,
32852
  drawingUtils: null
32853
  });
32854
+ const setActiveLandmark = import_react7.useCallback((newLandmark) => {
32855
  setPreviousLandmark(currentLandmark || null);
32856
  setCurrentLandmark(newLandmark || null);
32857
  setCurrentOpacity(0);
32858
  setPreviousOpacity(1);
32859
  }, [currentLandmark, setPreviousLandmark, setCurrentLandmark, setCurrentOpacity, setPreviousOpacity]);
32860
+ import_react7.useEffect(() => {
32861
  console.log("Initializing MediaPipe...");
32862
  let isMounted = true;
32863
  const initializeMediaPipe = async () => {
 
32895
  }
32896
  };
32897
  }, []);
32898
+ const [landmarkCenters, setLandmarkCenters] = import_react7.useState({});
32899
+ const computeLandmarkCenters = import_react7.useCallback((landmarks2) => {
32900
  const centers = {};
32901
  const computeGroupCenter = (group) => {
32902
  let sumX = 0, sumY = 0, sumZ = 0, count = 0;
 
32919
  centers.background = { x: 0.5, y: 0.5, z: 0 };
32920
  setLandmarkCenters(centers);
32921
  }, []);
32922
+ const findClosestLandmark = import_react7.useCallback((mouseX, mouseY, isGroup) => {
32923
  const defaultLandmark = {
32924
  group: "background",
32925
  distance: 0,
 
32968
  return defaultLandmark;
32969
  }
32970
  }, [landmarkCenters]);
32971
+ const detectFaceLandmarks = import_react7.useCallback(async (imageDataUrl) => {
32972
  if (!isMediaPipeReady) {
32973
  console.log("MediaPipe not ready. Skipping detection.");
32974
  return;
 
32994
  drawLandmarks(faceLandmarkerResult.faceLandmarks[0], canvasRef.current, drawingUtils);
32995
  }
32996
  }, [isMediaPipeReady, isDrawingUtilsReady, computeLandmarkCenters]);
32997
+ const drawLandmarks = import_react7.useCallback((landmarks2, canvas, drawingUtils) => {
32998
  const ctx = canvas.getContext("2d");
32999
  if (!ctx)
33000
  return;
 
33020
  img.src = previewImage;
33021
  }
33022
  }, [previewImage, currentLandmark, previousLandmark, currentOpacity, previousOpacity]);
33023
+ import_react7.useEffect(() => {
33024
  if (isMediaPipeReady && isDrawingUtilsReady && faceLandmarks.length > 0 && canvasRef.current && mediaPipeRef.current.drawingUtils) {
33025
  drawLandmarks(faceLandmarks[0], canvasRef.current, mediaPipeRef.current.drawingUtils);
33026
  }
33027
  }, [isMediaPipeReady, isDrawingUtilsReady, faceLandmarks, currentLandmark, previousLandmark, currentOpacity, previousOpacity, drawLandmarks]);
33028
+ import_react7.useEffect(() => {
33029
  let animationFrame;
33030
  const animate = () => {
33031
  setCurrentOpacity((prev) => Math.min(prev + 0.2, 1));
 
33037
  animationFrame = requestAnimationFrame(animate);
33038
  return () => cancelAnimationFrame(animationFrame);
33039
  }, [currentLandmark]);
33040
+ const canvasRefCallback = import_react7.useCallback((node) => {
33041
  if (node !== null) {
33042
  const ctx = node.getContext("2d");
33043
  if (ctx) {
 
33053
  canvasRef.current = node;
33054
  }
33055
  }, []);
33056
+ import_react7.useEffect(() => {
33057
  if (!isMediaPipeReady) {
33058
  console.log("MediaPipe not ready. Skipping landmark detection.");
33059
  return;
 
33068
  }
33069
  detectFaceLandmarks(previewImage);
33070
  }, [isMediaPipeReady, isDrawingUtilsReady, previewImage]);
33071
+ const modifyImage = import_react7.useCallback(({ landmark, vector }) => {
33072
  const {
33073
  originalImage: originalImage2,
33074
  originalImageHash: originalImageHash2,
 
33159
  const modifyImageWithRateLimit = useThrottledCallback_default((params) => {
33160
  modifyImage(params);
33161
  }, [modifyImage], averageLatency);
33162
+ const handleMouseEnter = import_react7.useCallback(() => {
33163
  setIsHovering(true);
33164
  }, []);
33165
+ const handleMouseLeave = import_react7.useCallback(() => {
33166
  setIsHovering(false);
33167
  }, []);
33168
+ const handleMouseDown = import_react7.useCallback((event) => {
33169
  if (!canvasRef.current)
33170
  return;
33171
  const rect = canvasRef.current.getBoundingClientRect();
 
33177
  setDragStart({ x: x2, y: y2 });
33178
  dragStartRef.current = { x: x2, y: y2 };
33179
  }, [findClosestLandmark, setActiveLandmark, setDragStart]);
33180
+ const handleMouseMove = import_react7.useCallback((event) => {
33181
  if (!canvasRef.current)
33182
  return;
33183
  const rect = canvasRef.current.getBoundingClientRect();
 
33203
  setIsHovering(true);
33204
  }
33205
  }, [currentLandmark, dragStart, setIsHovering, setActiveLandmark, setIsDragging, modifyImageWithRateLimit, landmarkCenters]);
33206
+ const handleMouseUp = import_react7.useCallback((event) => {
33207
  if (!canvasRef.current)
33208
  return;
33209
  const rect = canvasRef.current.getBoundingClientRect();
 
33225
  dragStartRef.current = null;
33226
  setActiveLandmark(undefined);
33227
  }, [currentLandmark, isDragging, modifyImageWithRateLimit, findClosestLandmark, setActiveLandmark, landmarkCenters, modifyImageWithRateLimit, setIsDragging]);
33228
+ import_react7.useEffect(() => {
33229
  facePoke.setOnModifiedImage((image, image_hash) => {
33230
  if (image) {
33231
  setPreviewImage(image);
 
33261
  className: "flex flex-row items-center justify-center font-sans mt-4 w-full",
33262
  children: [
33263
  jsx_dev_runtime2.jsxDEV("span", {
33264
+ className: "mr-1",
33265
  children: jsx_dev_runtime2.jsxDEV("img", {
33266
  src: "/hf-logo.svg",
33267
  alt: "Hugging Face",
 
33295
  }
33296
 
33297
  // src/hooks/useFacePokeAPI.ts
33298
+ var import_react8 = __toESM(require_react(), 1);
33299
  function useFacePokeAPI() {
33300
+ const [status, setStatus] = import_react8.useState("");
33301
+ const [isDebugMode, setIsDebugMode] = import_react8.useState(false);
33302
+ const [interruptMessage, setInterruptMessage] = import_react8.useState(null);
33303
+ const [isLoading, setIsLoading] = import_react8.useState(false);
33304
+ import_react8.useEffect(() => {
33305
  const urlParams = new URLSearchParams(window.location.search);
33306
  setIsDebugMode(urlParams.get("debug") === "true");
33307
  }, []);
33308
+ import_react8.useEffect(() => {
33309
  const handleInterruption = (event) => {
33310
  setInterruptMessage(event.detail.message);
33311
  };
 
33372
  const previewImage = useMainStore((s2) => s2.previewImage);
33373
  const setPreviewImage = useMainStore((s2) => s2.setPreviewImage);
33374
  const resetImage = useMainStore((s2) => s2.resetImage);
33375
+ const setOriginalImageHash = useMainStore((s2) => s2.setOriginalImageHash);
33376
  const {
33377
  status,
33378
  setStatus,
 
33396
  handleMouseLeave,
33397
  currentOpacity
33398
  } = useFaceLandmarkDetection();
33399
+ const videoRef = import_react9.useRef(null);
33400
+ const handleFileChange = import_react9.useCallback(async (event) => {
33401
  const files = event.target.files;
33402
  if (files && files[0]) {
33403
  setImageFile(files[0]);
 
33406
  const image = await convertImageToBase64(files[0]);
33407
  setPreviewImage(image);
33408
  setOriginalImage(image);
33409
+ setOriginalImageHash("");
33410
  } catch (err) {
33411
  console.log(`failed to convert the image: `, err);
33412
  setImageFile(null);
33413
  setStatus("");
33414
  setPreviewImage("");
33415
  setOriginalImage("");
33416
+ setOriginalImageHash("");
33417
  setFaceLandmarks([]);
33418
  setBlendShapes([]);
33419
  }
 
33422
  setStatus("");
33423
  setPreviewImage("");
33424
  setOriginalImage("");
33425
+ setOriginalImageHash("");
33426
  setFaceLandmarks([]);
33427
  setBlendShapes([]);
33428
  }
33429
+ }, [isMediaPipeReady, setImageFile, setPreviewImage, setOriginalImage, setOriginalImageHash, setFaceLandmarks, setBlendShapes, setStatus]);
33430
+ const handleDownload = import_react9.useCallback(() => {
33431
+ if (previewImage) {
33432
+ const link = document.createElement("a");
33433
+ link.href = previewImage;
33434
+ link.download = "modified_image.png";
33435
+ document.body.appendChild(link);
33436
+ link.click();
33437
+ document.body.removeChild(link);
33438
+ }
33439
+ }, [previewImage]);
33440
  const canDisplayBlendShapes = false;
33441
+ const displayBlendShapes = import_react9.useMemo(() => jsx_dev_runtime5.jsxDEV("div", {
33442
  className: "mt-4",
33443
  children: [
33444
  jsx_dev_runtime5.jsxDEV("h3", {
 
33500
  className: "flex flex-row items-center justify-between w-full",
33501
  children: [
33502
  jsx_dev_runtime5.jsxDEV("div", {
33503
+ className: "flex items-center space-x-2",
33504
  children: [
33505
+ jsx_dev_runtime5.jsxDEV("div", {
33506
+ className: "relative",
33507
+ children: [
33508
+ jsx_dev_runtime5.jsxDEV("input", {
33509
+ id: "imageInput",
33510
+ type: "file",
33511
+ accept: "image/*",
33512
+ onChange: handleFileChange,
33513
+ className: "hidden",
33514
+ disabled: !isMediaPipeReady
33515
+ }, undefined, false, undefined, this),
33516
+ jsx_dev_runtime5.jsxDEV("label", {
33517
+ htmlFor: "imageInput",
33518
+ className: `cursor-pointer inline-flex items-center px-3 h-10 border border-transparent text-sm font-medium rounded-md text-white ${isMediaPipeReady ? "bg-slate-600 hover:bg-slate-500" : "bg-slate-500 cursor-not-allowed"} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 shadow-xl`,
33519
+ children: [
33520
+ jsx_dev_runtime5.jsxDEV(Spinner, {}, undefined, false, undefined, this),
33521
+ imageFile ? truncateFileName(imageFile.name, 32) : isMediaPipeReady ? "Choose a portrait photo (.jpg, .png, .webp)" : "Initializing..."
33522
+ ]
33523
+ }, undefined, true, undefined, this)
33524
+ ]
33525
+ }, undefined, true, undefined, this),
33526
+ previewImage && jsx_dev_runtime5.jsxDEV("button", {
33527
+ onClick: handleDownload,
33528
+ className: "inline-flex items-center px-3 h-10 border border-transparent text-sm font-medium rounded-md text-white bg-zinc-600 hover:bg-zinc-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-zinc-500 shadow-xl",
33529
  children: [
33530
+ jsx_dev_runtime5.jsxDEV(Download, {
33531
+ className: "w-4 h-4 mr-2"
33532
+ }, undefined, false, undefined, this),
33533
+ "Download"
33534
  ]
33535
  }, undefined, true, undefined, this)
33536
  ]