HiMind commited on
Commit
64cfabb
·
verified ·
1 Parent(s): 4e9190a

Upload 2 files

Browse files
Files changed (2) hide show
  1. PackedAvatar.py +250 -2
  2. README.md +217 -0
PackedAvatar.py CHANGED
@@ -214,14 +214,238 @@ def mount_zip_payload(zip_bytes: bytes, zip_sha256: str, target_dir: Path, marke
214
  # ============================================================
215
  # AVATAR BANK RUNTIME
216
  # ============================================================
217
-
218
  class AvatarBankRuntime:
219
- def __init__(self, payload: Dict[str, Any], defaults: Optional[Dict[str, Any]] = None):
 
 
 
 
220
  self.index: Dict[str, Dict[str, Any]] = payload.get("index", {}) or {}
221
  self.embeddings: Dict[str, Dict[str, Any]] = payload.get("embeddings", {}) or {}
222
  self.previews: Dict[str, Any] = payload.get("previews", {}) or {}
223
  self.defaults = defaults or {}
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  @classmethod
226
  def load(cls, path: Path, defaults: Optional[Dict[str, Any]] = None) -> "AvatarBankRuntime":
227
  payload = torch.load(str(path), map_location="cpu", weights_only=False)
@@ -1254,6 +1478,30 @@ class PackedAvatar:
1254
  subprocess.run(cmd, cwd=str(repo), check=True)
1255
  return out_video
1256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1257
  def generate(
1258
  self,
1259
  source_image: Optional[str] = None,
 
214
  # ============================================================
215
  # AVATAR BANK RUNTIME
216
  # ============================================================
 
217
  class AvatarBankRuntime:
218
+ def __init__(
219
+ self,
220
+ payload: Dict[str, Any],
221
+ defaults: Optional[Dict[str, Any]] = None,
222
+ ):
223
  self.index: Dict[str, Dict[str, Any]] = payload.get("index", {}) or {}
224
  self.embeddings: Dict[str, Dict[str, Any]] = payload.get("embeddings", {}) or {}
225
  self.previews: Dict[str, Any] = payload.get("previews", {}) or {}
226
  self.defaults = defaults or {}
227
 
228
+ @classmethod
229
+ def load(
230
+ cls,
231
+ path: Path,
232
+ defaults: Optional[Dict[str, Any]] = None,
233
+ ) -> "AvatarBankRuntime":
234
+ payload = torch.load(str(path), map_location="cpu", weights_only=False)
235
+
236
+ if not isinstance(payload, dict):
237
+ raise ValueError(f"Avatar bank file did not contain a dictionary: {path}")
238
+
239
+ return cls(payload, defaults=defaults)
240
+
241
+ def save(self, path: Union[str, Path]) -> None:
242
+ torch.save(
243
+ {
244
+ "index": self.index,
245
+ "embeddings": self.embeddings,
246
+ "previews": self.previews,
247
+ },
248
+ str(path),
249
+ )
250
+
251
+ # --------------------------------------------------------
252
+ # BASIC ACCESS
253
+ # --------------------------------------------------------
254
+
255
+ def __contains__(self, avatar_id: str) -> bool:
256
+ return avatar_id in self.index
257
+
258
+ def exists(self, avatar_id: str) -> bool:
259
+ return avatar_id in self.index
260
+
261
+ def available_ids(self) -> List[str]:
262
+ return list(self.index.keys())
263
+
264
+ def list_avatars(self) -> List[str]:
265
+ return self.available_ids()
266
+
267
+ def get_metadata(self, avatar_id: str) -> Dict[str, Any]:
268
+ if avatar_id not in self.index:
269
+ raise KeyError(f"Avatar not found: {avatar_id}")
270
+
271
+ return dict(self.index[avatar_id])
272
+
273
+ def get_avatar(self, avatar_id: str) -> Dict[str, Any]:
274
+ return self.build_avatar_condition(avatar_id)
275
+
276
+ def get_embedding_bundle(self, avatar_id: str) -> Dict[str, Any]:
277
+ if avatar_id not in self.embeddings:
278
+ raise KeyError(f"Avatar not found: {avatar_id}")
279
+
280
+ return self.embeddings[avatar_id]
281
+
282
+ # --------------------------------------------------------
283
+ # FUZZY SEARCH
284
+ # --------------------------------------------------------
285
+
286
+ def _fuzzy_match_single(
287
+ self,
288
+ query: str,
289
+ choices: set,
290
+ cutoff: float = 0.6,
291
+ ):
292
+ if not query:
293
+ return None
294
+
295
+ query_lower = query.lower()
296
+
297
+ choice_map = {
298
+ c.lower(): c
299
+ for c in choices
300
+ }
301
+
302
+ matches = get_close_matches(
303
+ query_lower,
304
+ list(choice_map.keys()),
305
+ n=1,
306
+ cutoff=cutoff,
307
+ )
308
+
309
+ return choice_map[matches[0]] if matches else None
310
+
311
+ def fuzzy_search_id(
312
+ self,
313
+ query_id: str,
314
+ n: int = 5,
315
+ cutoff: float = 0.5,
316
+ ) -> List[str]:
317
+ query_lower = query_id.lower()
318
+
319
+ id_map = {
320
+ aid.lower(): aid
321
+ for aid in self.index.keys()
322
+ }
323
+
324
+ matches = get_close_matches(
325
+ query_lower,
326
+ list(id_map.keys()),
327
+ n=n,
328
+ cutoff=cutoff,
329
+ )
330
+
331
+ return [id_map[m] for m in matches]
332
+
333
+ # --------------------------------------------------------
334
+ # QUERY
335
+ # --------------------------------------------------------
336
+
337
+ def query(
338
+ self,
339
+ gender=None,
340
+ style=None,
341
+ fuzzy=True,
342
+ cutoff=0.6,
343
+ ) -> List[str]:
344
+
345
+ known_genders = {
346
+ meta["gender"]
347
+ for meta in self.index.values()
348
+ if meta.get("gender")
349
+ }
350
+
351
+ known_styles = {
352
+ meta["style"]
353
+ for meta in self.index.values()
354
+ if meta.get("style")
355
+ }
356
+
357
+ target_gender = gender
358
+ target_style = style
359
+
360
+ if fuzzy:
361
+ if gender:
362
+ target_gender = (
363
+ self._fuzzy_match_single(
364
+ gender,
365
+ known_genders,
366
+ cutoff,
367
+ )
368
+ or gender
369
+ )
370
+
371
+ if style:
372
+ target_style = (
373
+ self._fuzzy_match_single(
374
+ style,
375
+ known_styles,
376
+ cutoff,
377
+ )
378
+ or style
379
+ )
380
+
381
+ results = []
382
+
383
+ for aid, meta in self.index.items():
384
+
385
+ if (
386
+ target_gender
387
+ and meta.get("gender") != target_gender
388
+ ):
389
+ continue
390
+
391
+ if (
392
+ target_style
393
+ and meta.get("style") != target_style
394
+ ):
395
+ continue
396
+
397
+ results.append(aid)
398
+
399
+ return results
400
+
401
+ # --------------------------------------------------------
402
+ # PREVIEWS
403
+ # --------------------------------------------------------
404
+
405
+ def get_preview(self, avatar_id: str):
406
+ if avatar_id not in self.previews:
407
+ raise KeyError(f"Avatar not found: {avatar_id}")
408
+
409
+ return decode_png_or_zstd_image(
410
+ self.previews[avatar_id]
411
+ )
412
+
413
+ def get_preview_numpy(
414
+ self,
415
+ avatar_id: str,
416
+ ) -> Optional[np.ndarray]:
417
+ return self._preview_to_numpy(avatar_id)
418
+
419
+ def _preview_to_numpy(
420
+ self,
421
+ avatar_id: str,
422
+ ) -> Optional[np.ndarray]:
423
+
424
+ blob = self.previews.get(avatar_id)
425
+
426
+ if blob is None:
427
+ return None
428
+
429
+ try:
430
+ img = decode_png_or_zstd_image(blob)
431
+ return pil_to_numpy_rgb(img)
432
+
433
+ except Exception:
434
+ return None
435
+
436
+ # --------------------------------------------------------
437
+ # MUTATION
438
+ # --------------------------------------------------------
439
+
440
+ def delete_avatar(
441
+ self,
442
+ avatar_id: str,
443
+ ) -> None:
444
+
445
+ self.index.pop(avatar_id, None)
446
+ self.embeddings.pop(avatar_id, None)
447
+ self.previews.pop(avatar_id, None)
448
+
449
  @classmethod
450
  def load(cls, path: Path, defaults: Optional[Dict[str, Any]] = None) -> "AvatarBankRuntime":
451
  payload = torch.load(str(path), map_location="cpu", weights_only=False)
 
1478
  subprocess.run(cmd, cwd=str(repo), check=True)
1479
  return out_video
1480
 
1481
+ def list_avatars(self):
1482
+ return self.avatar_bank.list_avatars()
1483
+
1484
+ def query_avatars(self, *args, **kwargs):
1485
+ return self.avatar_bank.query(*args, **kwargs)
1486
+
1487
+ def fuzzy_search_avatar(self, query, n=5, cutoff=0.5):
1488
+ return self.avatar_bank.fuzzy_search_id(query, n, cutoff)
1489
+
1490
+ def get_avatar(self, avatar_id):
1491
+ return self.avatar_bank.get_avatar(avatar_id)
1492
+
1493
+ def get_avatar_preview(self, avatar_id):
1494
+ return self.avatar_bank.get_preview(avatar_id)
1495
+
1496
+ def get_avatar_metadata(self, avatar_id):
1497
+ return self.avatar_bank.get_metadata(avatar_id)
1498
+
1499
+ def delete_avatar(self, avatar_id):
1500
+ self.avatar_bank.delete_avatar(avatar_id)
1501
+
1502
+ def save_avatar_bank(self, path):
1503
+ self.avatar_bank.save(path)
1504
+
1505
  def generate(
1506
  self,
1507
  source_image: Optional[str] = None,
README.md CHANGED
@@ -530,6 +530,223 @@ If `avatar_condition` is provided, it supersedes `source_image` conditioning.
530
  ## 6. Optional post-processing
531
 
532
  * Wav2Lip GAN lip-sync enhancement
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
534
  ---
535
 
 
530
  ## 6. Optional post-processing
531
 
532
  * Wav2Lip GAN lip-sync enhancement
533
+ # AvatarBank API
534
+
535
+ PackedAvatar exposes the packed AvatarBank directly, allowing you to browse, search, inspect, and manage avatars at runtime.
536
+
537
+ The AvatarBank is loaded automatically when the model is initialized.
538
+
539
+ ```python
540
+ from PackedAvatar import PackedAvatar
541
+
542
+ model = PackedAvatar("PackedAvatar.pt")
543
+ ```
544
+
545
+ ## List available avatars
546
+
547
+ ```python
548
+ avatars = model.list_avatars()
549
+
550
+ print(avatars)
551
+ ```
552
+
553
+ Returns:
554
+
555
+ ```python
556
+ [
557
+ "Aaron",
558
+ "Rebecca",
559
+ "Amy",
560
+ ...
561
+ ]
562
+ ```
563
+
564
+ ---
565
+
566
+ ## Search by name
567
+
568
+ Perform a fuzzy search against avatar IDs.
569
+
570
+ ```python
571
+ matches = model.fuzzy_search_avatar("rebeca")
572
+
573
+ print(matches)
574
+ ```
575
+
576
+ Example:
577
+
578
+ ```python
579
+ ["Rebecca"]
580
+ ```
581
+
582
+ ---
583
+
584
+ ## Query by gender and style
585
+
586
+ Search the bank using metadata filters.
587
+
588
+ ```python
589
+ female_anime = model.query_avatars(
590
+ gender="female",
591
+ style="anime"
592
+ )
593
+
594
+ print(female_anime)
595
+ ```
596
+
597
+ Example:
598
+
599
+ ```python
600
+ [
601
+ "Alison",
602
+ "Amber",
603
+ "Andrea",
604
+ ...
605
+ ]
606
+ ```
607
+
608
+ Fuzzy matching is supported:
609
+
610
+ ```python
611
+ model.query_avatars(
612
+ gender="femal",
613
+ style="anim"
614
+ )
615
+ ```
616
+
617
+ ---
618
+
619
+ ## Retrieve avatar metadata
620
+
621
+ ```python
622
+ metadata = model.get_avatar_metadata("Rebecca")
623
+
624
+ print(metadata)
625
+ ```
626
+
627
+ Example:
628
+
629
+ ```python
630
+ {
631
+ "gender": "female",
632
+ "style": "anime"
633
+ }
634
+ ```
635
+
636
+ ---
637
+
638
+ ## Retrieve avatar conditioning
639
+
640
+ Access the full conditioning bundle used internally by PackedAvatar.
641
+
642
+ ```python
643
+ avatar = model.get_avatar("Rebecca")
644
+ ```
645
+
646
+ Returned fields may include:
647
+
648
+ ```python
649
+ {
650
+ "avatar_id": "Rebecca",
651
+ "gender": "female",
652
+ "style": "anime",
653
+ "coeff_3dmm": ...,
654
+ "motion_3dmm": ...,
655
+ "full_3dmm": ...,
656
+ "crop_info": ...,
657
+ "crop_preview": ...
658
+ }
659
+ ```
660
+
661
+ This bundle can be passed directly as an `avatar_condition`.
662
+
663
+ ```python
664
+ video = model.generate(
665
+ avatar_condition=avatar,
666
+ driven_audio="speech.wav"
667
+ )
668
+ ```
669
+
670
+ ---
671
+
672
+ ## Retrieve avatar previews
673
+
674
+ Preview images stored in the AvatarBank can be accessed directly.
675
+
676
+ ```python
677
+ preview = model.get_avatar_preview("Rebecca")
678
+
679
+ preview.show()
680
+ ```
681
+
682
+ Returns a PIL image.
683
+
684
+ ---
685
+
686
+ ## Delete avatars
687
+
688
+ Remove an avatar from the in-memory runtime.
689
+
690
+ ```python
691
+ model.delete_avatar("Rebecca")
692
+ ```
693
+
694
+ This only affects the current runtime session.
695
+
696
+ To persist changes, save the bank.
697
+
698
+ ---
699
+
700
+ ## Save AvatarBank changes
701
+
702
+ ```python
703
+ model.save_avatar_bank("ModifiedAvatarBank.pt")
704
+ ```
705
+
706
+ This writes the current AvatarBank state to disk.
707
+
708
+ ---
709
+
710
+ ## Check whether an avatar exists
711
+
712
+ ```python
713
+ exists = model.avatar_bank.exists("Rebecca")
714
+
715
+ print(exists)
716
+ ```
717
+
718
+ Returns:
719
+
720
+ ```python
721
+ True
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Direct AvatarBank access
727
+
728
+ The underlying AvatarBank runtime is also exposed.
729
+
730
+ ```python
731
+ bank = model.avatar_bank
732
+ ```
733
+
734
+ Available methods include:
735
+
736
+ ```python
737
+ bank.available_ids()
738
+ bank.list_avatars()
739
+ bank.exists(...)
740
+ bank.query(...)
741
+ bank.fuzzy_search_id(...)
742
+ bank.get_avatar(...)
743
+ bank.get_metadata(...)
744
+ bank.get_preview(...)
745
+ bank.delete_avatar(...)
746
+ bank.save(...)
747
+ ```
748
+
749
+ This provides full programmatic access to the packed avatar database.
750
 
751
  ---
752