Nanny7 commited on
Commit
152b352
·
1 Parent(s): 0730e45

Deploy ARTalk ZeroGPU Space

Browse files
Files changed (5) hide show
  1. .gitattributes +3 -0
  2. .gitignore +5 -0
  3. README.md +12 -3
  4. app.py +36 -9
  5. inference_engine.py +48 -0
.gitattributes CHANGED
@@ -43,3 +43,6 @@ assets/GAGAvatar/examples/6.jpg filter=lfs diff=lfs merge=lfs -text
43
  assets/GAGAvatar/examples/7.jpg filter=lfs diff=lfs merge=lfs -text
44
  assets/GAGAvatar/examples/9.jpg filter=lfs diff=lfs merge=lfs -text
45
  assets/GAGAvatar/gagavatar_logo.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
43
  assets/GAGAvatar/examples/7.jpg filter=lfs diff=lfs merge=lfs -text
44
  assets/GAGAvatar/examples/9.jpg filter=lfs diff=lfs merge=lfs -text
45
  assets/GAGAvatar/gagavatar_logo.png filter=lfs diff=lfs merge=lfs -text
46
+ wheels/*.whl filter=lfs diff=lfs merge=lfs -text
47
+ assets/*.pt filter=lfs diff=lfs merge=lfs -text
48
+ app/**/*.pt filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .space_checkout/
4
+ render_results/
5
+ .env
README.md CHANGED
@@ -4,11 +4,20 @@ emoji: 🎭
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.9.1
8
  app_file: app.py
9
  pinned: false
 
 
 
 
 
 
 
10
  ---
11
 
12
- # ARTalk
13
 
14
- Speech-driven 3D head animation. Powered by HuggingFace ZeroGPU.
 
 
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Speech-driven 3D head animation for YouTube automation
12
+ models:
13
+ - xg-chu/ARTalk
14
+ - xg-chu/GAGAvatar
15
+ hardware: zero-a10g
16
+ suggested_hardware: zero-a10g
17
  ---
18
 
19
+ # ARTalk on ZeroGPU
20
 
21
+ Speech Talking head mp4. Powered by [ARTalk](https://github.com/xg-chu/ARTalk) + GAGAvatar + ZeroGPU.
22
+
23
+ See [C:/DK/AT/deploy/hf-space/README.md] for deployment details.
app.py CHANGED
@@ -88,14 +88,18 @@ def _duration_for(audio_path, shape_id, style_id, clip_length):
88
 
89
 
90
  @spaces.GPU(duration=_duration_for)
91
- def generate(audio_path: str, shape_id: str, style_id: str, clip_length: int):
92
  """Generate talking-head mp4 from audio + shape + style.
93
 
94
  Inputs:
95
- audio_path: uploaded audio file path (Gradio writes to temp)
96
- shape_id: "mesh" or pre-registered character name
97
- style_id: one of STYLE_MOTIONS (or "default")
98
- clip_length: max frames at 25fps (750 = 30s)
 
 
 
 
99
 
100
  Returns:
101
  mp4 file path (Gradio streams to client)
@@ -121,11 +125,23 @@ def generate(audio_path: str, shape_id: str, style_id: str, clip_length: int):
121
  else:
122
  ENGINE.style_motion = None
123
 
124
- # 3. Shape fallback
125
- if shape_id not in AVAILABLE_SHAPES:
 
 
 
 
 
 
 
 
 
 
 
 
126
  shape_id = "mesh"
127
 
128
- # 4. Inference
129
  pred_motions = ENGINE.inference(audio, clip_length=int(clip_length))
130
 
131
  # 5. Render → mp4
@@ -145,9 +161,10 @@ demo = gr.Interface(
145
  fn=generate,
146
  inputs=[
147
  gr.Audio(type="filepath", label="Audio (wav/mp3/m4a)"),
148
- gr.Textbox(value="mesh", label="shape_id (use 'mesh' or pre-registered avatar name)"),
149
  gr.Textbox(value="natural_0", label="style_id (e.g. natural_0, happy_0, curious_0)"),
150
  gr.Number(value=750, label="clip_length (max frames @ 25fps, 750=30s)", precision=0),
 
151
  ],
152
  outputs=gr.Video(label="Generated talking head"),
153
  title="ARTalk — Speech-Driven 3D Head Animation",
@@ -208,6 +225,16 @@ _API_INFO = {
208
  "component": "Number",
209
  "example_input": 750,
210
  },
 
 
 
 
 
 
 
 
 
 
211
  ],
212
  "returns": [
213
  {
 
88
 
89
 
90
  @spaces.GPU(duration=_duration_for)
91
+ def generate(audio_path: str, shape_id: str, style_id: str, clip_length: int, tracked_pt_file=None):
92
  """Generate talking-head mp4 from audio + shape + style.
93
 
94
  Inputs:
95
+ audio_path: uploaded audio file path (Gradio writes to temp)
96
+ shape_id: "mesh", pre-registered character, or custom avatar id
97
+ (matching the key in an uploaded tracked_pt_file)
98
+ style_id: one of STYLE_MOTIONS (or "default")
99
+ clip_length: max frames at 25fps (750 = 30s)
100
+ tracked_pt_file: (optional) user-supplied tracked.pt produced by the
101
+ character-tracking Colab notebook. Registers new
102
+ avatar ids at runtime.
103
 
104
  Returns:
105
  mp4 file path (Gradio streams to client)
 
125
  else:
126
  ENGINE.style_motion = None
127
 
128
+ # 3. Optional custom avatar registration
129
+ custom_ids = []
130
+ if tracked_pt_file:
131
+ custom_path = tracked_pt_file if isinstance(tracked_pt_file, str) else getattr(tracked_pt_file, "name", None)
132
+ if custom_path and os.path.exists(custom_path):
133
+ try:
134
+ custom_ids = ENGINE.add_custom_avatars(custom_path)
135
+ print(f"[generate] registered custom avatars: {custom_ids}")
136
+ except Exception as e:
137
+ raise gr.Error(f"Custom tracked.pt load failed: {e}")
138
+
139
+ # 4. Shape fallback — include freshly-registered custom ids
140
+ valid_shapes = set(AVAILABLE_SHAPES) | set(custom_ids) | set(ENGINE.GAGAvatar.all_gagavatar_id.keys())
141
+ if shape_id not in valid_shapes:
142
  shape_id = "mesh"
143
 
144
+ # 5. Inference
145
  pred_motions = ENGINE.inference(audio, clip_length=int(clip_length))
146
 
147
  # 5. Render → mp4
 
161
  fn=generate,
162
  inputs=[
163
  gr.Audio(type="filepath", label="Audio (wav/mp3/m4a)"),
164
+ gr.Textbox(value="mesh", label="shape_id (use 'mesh', a built-in, or a custom avatar id from tracked_pt_file)"),
165
  gr.Textbox(value="natural_0", label="style_id (e.g. natural_0, happy_0, curious_0)"),
166
  gr.Number(value=750, label="clip_length (max frames @ 25fps, 750=30s)", precision=0),
167
+ gr.File(label="tracked_pt_file (optional — custom character from Colab notebook)", file_types=[".pt"]),
168
  ],
169
  outputs=gr.Video(label="Generated talking head"),
170
  title="ARTalk — Speech-Driven 3D Head Animation",
 
225
  "component": "Number",
226
  "example_input": 750,
227
  },
228
+ {
229
+ "label": "tracked_pt_file",
230
+ "parameter_name": "tracked_pt_file",
231
+ "parameter_has_default": True,
232
+ "parameter_default": None,
233
+ "type": {"type": "string"},
234
+ "python_type": {"type": "Dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None, is_stream: bool, meta: Dict(_type: Literal['gradio.FileData'])) | None", "description": ""},
235
+ "component": "File",
236
+ "example_input": None,
237
+ },
238
  ],
239
  "returns": [
240
  {
inference_engine.py CHANGED
@@ -156,6 +156,54 @@ class ARTAvatarInferEngine:
156
  n_shape=300, n_exp=100, scale=5.0, no_lmks=True
157
  ).to(device)
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  def set_style_motion(self, style_motion):
160
  if isinstance(style_motion, str):
161
  style_motion = torch.load(
 
156
  n_shape=300, n_exp=100, scale=5.0, no_lmks=True
157
  ).to(device)
158
 
159
+ def add_custom_avatars(self, tracked_pt_path):
160
+ """Merge a user-supplied tracked.pt into GAGAvatar.all_gagavatar_id.
161
+
162
+ Accepted formats:
163
+ 1. dict-of-dicts: {"alice": {image, shapecode, transform_matrix, ...}}
164
+ → all entries merged as-is.
165
+ 2. single entry dict: {image, shapecode, transform_matrix, ...}
166
+ → registered under key derived from filename (without 'tracked_' prefix and '.pt').
167
+
168
+ Returns list of newly registered avatar_ids.
169
+ """
170
+ if not hasattr(self, "GAGAvatar"):
171
+ raise RuntimeError("GAGAvatar not loaded (load_gaga=False)")
172
+ loaded = torch.load(tracked_pt_path, map_location="cpu", weights_only=False)
173
+ if not isinstance(loaded, dict):
174
+ raise ValueError(f"tracked.pt must be a dict, got {type(loaded).__name__}")
175
+
176
+ required = {"image", "shapecode", "transform_matrix"}
177
+ added = []
178
+
179
+ # Detect format: dict-of-dicts vs single entry
180
+ values_are_dicts = all(isinstance(v, dict) for v in loaded.values()) if loaded else False
181
+ looks_like_single = required.issubset(loaded.keys())
182
+
183
+ if looks_like_single and not values_are_dicts:
184
+ stem = Path(tracked_pt_path).stem
185
+ if stem.startswith("tracked_"):
186
+ stem = stem[len("tracked_"):]
187
+ avatar_id = stem or "custom"
188
+ entries = {avatar_id: loaded}
189
+ elif values_are_dicts:
190
+ entries = loaded
191
+ else:
192
+ raise ValueError(
193
+ "tracked.pt must be either a single-avatar dict with keys "
194
+ "{image, shapecode, transform_matrix} or a dict-of-dicts mapping "
195
+ "avatar_id -> {image, shapecode, transform_matrix, ...}"
196
+ )
197
+
198
+ for avatar_id, entry in entries.items():
199
+ missing = required - set(entry.keys())
200
+ if missing:
201
+ raise ValueError(f"avatar '{avatar_id}' missing keys: {missing}")
202
+ self.GAGAvatar.all_gagavatar_id[avatar_id] = entry
203
+ added.append(avatar_id)
204
+
205
+ return added
206
+
207
  def set_style_motion(self, style_motion):
208
  if isinstance(style_motion, str):
209
  style_motion = torch.load(