Files changed (1) hide show
  1. app.py +0 -281
app.py DELETED
@@ -1,281 +0,0 @@
1
- """
2
- Expression Editor on ZeroGPU.
3
-
4
- Vendored from fofr/cog-expression-editor's underlying ComfyUI workflow
5
- (LoadImage -> ExpressionEditor) but executed without ComfyUI: we clone the
6
- PowerHouseMan/ComfyUI-AdvancedLivePortrait node at startup and stub the
7
- two ComfyUI internals it imports (`folder_paths` and `comfy.utils`).
8
-
9
- Weights auto-download to ./models on first run via the node's own loader
10
- (Kijai/LivePortrait_safetensors + Bingsu/adetailer for the YOLO bbox).
11
- """
12
- import os
13
- import sys
14
- import types
15
- import subprocess
16
-
17
- # ------------------------------------------------------------------
18
- # 1. Pull the custom node + stub ComfyUI internals BEFORE importing it
19
- # ------------------------------------------------------------------
20
-
21
- # Use a Python-identifier-legal directory name so we can import it as a
22
- # package (the repo's `nodes.py` uses relative imports like
23
- # `from .LivePortrait...` which only work inside a real package).
24
- CUSTOM_NODE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "advanced_live_portrait")
25
- if not os.path.exists(CUSTOM_NODE_DIR):
26
- subprocess.check_call([
27
- "git", "clone", "--depth=1",
28
- "https://github.com/PowerHouseMan/ComfyUI-AdvancedLivePortrait.git",
29
- CUSTOM_NODE_DIR,
30
- ])
31
-
32
- # Writable paths the node expects
33
- MODELS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models")
34
- TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp")
35
- os.makedirs(os.path.join(MODELS_DIR, "liveportrait"), exist_ok=True)
36
- os.makedirs(os.path.join(MODELS_DIR, "ultralytics"), exist_ok=True)
37
- os.makedirs(TEMP_DIR, exist_ok=True)
38
-
39
- # Minimal `folder_paths` shim
40
- _fp = types.ModuleType("folder_paths")
41
- _fp.models_dir = MODELS_DIR
42
- _fp.output_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs")
43
- os.makedirs(_fp.output_directory, exist_ok=True)
44
- _fp.get_folder_paths = lambda m: [os.path.join(MODELS_DIR, m)]
45
- _fp.get_save_image_path = lambda f, d, *a, **k: (d, f, 0, "", f)
46
- _fp.get_temp_directory = lambda: TEMP_DIR
47
- _fp.add_model_folder_path = lambda *a, **k: None
48
- sys.modules["folder_paths"] = _fp
49
-
50
- # Minimal `comfy.utils` shim
51
- import torch
52
- import safetensors.torch
53
-
54
- _comfy = types.ModuleType("comfy")
55
- _comfy_utils = types.ModuleType("comfy.utils")
56
-
57
- def _load_torch_file(ckpt, *args, **kwargs):
58
- s = str(ckpt)
59
- if s.endswith(".safetensors"):
60
- return safetensors.torch.load_file(s)
61
- return torch.load(s, map_location="cpu", weights_only=False)
62
- _comfy_utils.load_torch_file = _load_torch_file
63
-
64
- class _ProgressBar:
65
- def __init__(self, *a, **k):
66
- pass
67
- def update(self, *a, **k):
68
- pass
69
- def update_absolute(self, *a, **k):
70
- pass
71
- _comfy_utils.ProgressBar = _ProgressBar
72
-
73
- _comfy.utils = _comfy_utils # attach as attribute too — `import comfy.utils` then `comfy.utils.X` needs both sys.modules and attribute access
74
- sys.modules["comfy"] = _comfy
75
- sys.modules["comfy.utils"] = _comfy_utils
76
-
77
- # ------------------------------------------------------------------
78
- # 2. Now import the node
79
- # ------------------------------------------------------------------
80
-
81
- # Parent dir is already on the path (it's the app's CWD); import as a package
82
- import spaces
83
- import gradio as gr
84
- import numpy as np
85
- from PIL import Image
86
-
87
- from advanced_live_portrait.nodes import ExpressionEditor, g_engine # noqa: E402
88
-
89
- # Preload pipeline + face detector at module scope so they land in the
90
- # ZeroGPU snapshot. ZeroGPU forks per @spaces.GPU call; with the snapshot,
91
- # the models are already resident in GPU memory on the worker and inference
92
- # starts immediately. Loading them lazily inside the decorated function
93
- # would re-download / re-init on every cold worker.
94
- print("Preloading LivePortrait pipeline + YOLO detector for ZeroGPU snapshot...")
95
- g_engine.get_pipeline() # downloads + .to('cuda') the 5 LivePortrait modules
96
- g_engine.get_detect_model() # downloads + loads YOLO face bbox model
97
- print("Preload done.")
98
-
99
- # Single global editor (state cached across calls)
100
- _editor = ExpressionEditor()
101
-
102
-
103
- def _pil_to_node_tensor(img: Image.Image) -> torch.Tensor:
104
- """ComfyUI image tensors are [N, H, W, C] float32 in [0, 1]."""
105
- if img.mode != "RGB":
106
- img = img.convert("RGB")
107
- arr = np.array(img, dtype=np.float32) / 255.0
108
- return torch.from_numpy(arr).unsqueeze(0)
109
-
110
-
111
- def _node_tensor_to_pil(t: torch.Tensor) -> Image.Image:
112
- arr = (t.squeeze(0).detach().cpu().numpy() * 255).clip(0, 255).astype(np.uint8)
113
- return Image.fromarray(arr)
114
-
115
-
116
- @spaces.GPU(duration=60)
117
- def edit_expression(
118
- image, rotate_pitch, rotate_yaw, rotate_roll,
119
- blink, eyebrow, wink, pupil_x, pupil_y,
120
- aaa, eee, woo, smile,
121
- src_ratio, sample_ratio, sample_parts, crop_factor,
122
- ):
123
- if image is None:
124
- raise gr.Error("Please upload an image.")
125
- src_t = _pil_to_node_tensor(image)
126
- out = _editor.run(
127
- rotate_pitch=float(rotate_pitch),
128
- rotate_yaw=float(rotate_yaw),
129
- rotate_roll=float(rotate_roll),
130
- blink=float(blink),
131
- eyebrow=float(eyebrow),
132
- wink=float(wink),
133
- pupil_x=float(pupil_x),
134
- pupil_y=float(pupil_y),
135
- aaa=float(aaa),
136
- eee=float(eee),
137
- woo=float(woo),
138
- smile=float(smile),
139
- src_ratio=float(src_ratio),
140
- sample_ratio=float(sample_ratio),
141
- sample_parts=sample_parts,
142
- crop_factor=float(crop_factor),
143
- src_image=src_t,
144
- )
145
- # ExpressionEditor.run returns {"ui": {...}, "result": (out_img, motion_link, exp_data)}
146
- out_img_t = out["result"][0]
147
- return _node_tensor_to_pil(out_img_t)
148
-
149
-
150
- # ------------------------------------------------------------------
151
- # 3. Image preprocess (mirrors original: resize so max side <= 1024)
152
- # ------------------------------------------------------------------
153
-
154
- def preprocess_image(img: Image.Image):
155
- if img is None:
156
- return None
157
- if img.mode != "RGB":
158
- img = img.convert("RGB")
159
- w, h = img.size
160
- if w <= 1024 and h <= 1024:
161
- return img
162
- if w >= h:
163
- new_w = 1024
164
- new_h = int(round(new_w / w * h))
165
- else:
166
- new_h = 1024
167
- new_w = int(round(new_h / h * w))
168
- return img.resize((new_w, new_h), Image.LANCZOS)
169
-
170
-
171
- def reset_parameters():
172
- return (
173
- gr.update(value=0), gr.update(value=0), gr.update(value=0),
174
- gr.update(value=0), gr.update(value=0), gr.update(value=0),
175
- gr.update(value=0), gr.update(value=0),
176
- gr.update(value=0), gr.update(value=0), gr.update(value=0), gr.update(value=0),
177
- )
178
-
179
-
180
- # ------------------------------------------------------------------
181
- # 4. Gradio UI (mirrors fffiloni/expression-editor exactly)
182
- # ------------------------------------------------------------------
183
-
184
- css = """
185
- #col-container{max-width: 800px; margin: 0 auto;}
186
- """
187
-
188
- with gr.Blocks(css=css, title="Expression Editor") as demo:
189
- with gr.Column(elem_id="col-container"):
190
- gr.Markdown("# Expression Editor")
191
- gr.Markdown("Edit a face's expression with sliders. Uses the <a href='https://github.com/PowerHouseMan/ComfyUI-AdvancedLivePortrait' target='_blank'>Expression Editor ComfyUI node</a>, originally packaged by <a href='https://replicate.com/fofr' target='_blank'>fofr</a>.")
192
-
193
- with gr.Row():
194
- with gr.Column():
195
- image_in = gr.Image(
196
- label="Input image",
197
- sources=["upload"],
198
- type="pil",
199
- )
200
-
201
- with gr.Tab("HEAD"):
202
- with gr.Column():
203
- rotate_pitch = gr.Slider(label="Rotate Up-Down", value=0, minimum=-20, maximum=20)
204
- rotate_yaw = gr.Slider(label="Rotate Left-Right turn", value=0, minimum=-20, maximum=20)
205
- rotate_roll = gr.Slider(label="Rotate Left-Right tilt", value=0, minimum=-20, maximum=20)
206
-
207
- with gr.Tab("EYES"):
208
- with gr.Column():
209
- eyebrow = gr.Slider(label="Eyebrow", value=0, minimum=-10, maximum=15)
210
- with gr.Row():
211
- blink = gr.Slider(label="Blink", value=0, minimum=-20, maximum=5)
212
- wink = gr.Slider(label="Wink", value=0, minimum=0, maximum=25)
213
- with gr.Row():
214
- pupil_x = gr.Slider(label="Pupil X", value=0, minimum=-15, maximum=15)
215
- pupil_y = gr.Slider(label="Pupil Y", value=0, minimum=-15, maximum=15)
216
-
217
- with gr.Tab("MOUTH"):
218
- with gr.Column():
219
- with gr.Row():
220
- aaa = gr.Slider(label="Aaa", value=0, minimum=-30, maximum=120)
221
- eee = gr.Slider(label="Eee", value=0, minimum=-20, maximum=15)
222
- woo = gr.Slider(label="Woo", value=0, minimum=-20, maximum=15)
223
- smile = gr.Slider(label="Smile", value=0, minimum=-0.3, maximum=1.3)
224
-
225
- with gr.Tab("More Settings"):
226
- with gr.Column():
227
- src_ratio = gr.Number(label="Src Ratio", info="Source ratio", value=1)
228
- sample_ratio = gr.Slider(label="Sample Ratio", info="Sample ratio", value=1, minimum=-0.2, maximum=1.2)
229
- sample_parts = gr.Dropdown(
230
- choices=["OnlyExpression", "OnlyRotation", "OnlyMouth", "OnlyEyes", "All"],
231
- value="OnlyExpression",
232
- label="Sample parts",
233
- )
234
- crop_factor = gr.Slider(label="Crop Factor", info="Crop factor", value=1.7, minimum=1.5, maximum=2.5)
235
-
236
- with gr.Row():
237
- reset_btn = gr.Button("Reset")
238
- submit_btn = gr.Button("Submit", variant="primary")
239
-
240
- with gr.Column():
241
- result_image = gr.Image(label="Output", elem_id="top")
242
-
243
- inputs = [
244
- image_in, rotate_pitch, rotate_yaw, rotate_roll,
245
- blink, eyebrow, wink, pupil_x, pupil_y,
246
- aaa, eee, woo, smile,
247
- src_ratio, sample_ratio, sample_parts, crop_factor,
248
- ]
249
- outputs = [result_image]
250
-
251
- # Resize on upload (matches original 1024-max preprocess)
252
- image_in.upload(
253
- fn=preprocess_image,
254
- inputs=[image_in],
255
- outputs=[image_in],
256
- queue=False,
257
- )
258
-
259
- reset_btn.click(
260
- fn=reset_parameters,
261
- inputs=None,
262
- outputs=[
263
- rotate_pitch, rotate_yaw, rotate_roll,
264
- blink, eyebrow, wink, pupil_x, pupil_y,
265
- aaa, eee, woo, smile,
266
- ],
267
- queue=False,
268
- ).then(fn=edit_expression, inputs=inputs, outputs=outputs)
269
-
270
- submit_btn.click(fn=edit_expression, inputs=inputs, outputs=outputs)
271
-
272
- # Regenerate on slider release (matches original's live-editing feel)
273
- for slider in (
274
- rotate_pitch, rotate_yaw, rotate_roll,
275
- blink, eyebrow, wink, pupil_x, pupil_y,
276
- aaa, eee, woo, smile,
277
- ):
278
- slider.release(fn=edit_expression, inputs=inputs, outputs=outputs, show_progress="minimal")
279
-
280
-
281
- demo.queue().launch()