brainable commited on
Commit
50443f8
·
verified ·
1 Parent(s): 1f4445b

Upload demo.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. demo.py +338 -1
demo.py CHANGED
@@ -517,6 +517,343 @@ def run_cli(args):
517
  TOT = 0
518
 
519
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  def _gpu_duration(
521
  files,
522
  top_k,
@@ -533,7 +870,7 @@ def _gpu_duration(
533
  # Cold workers spend ~30–60 s importing bpy + loading the model before
534
  # any GPU work. Give every request a generous 240 s floor.
535
  file_count = len(files) if files is not None else 1
536
- return min(900, max(240, 240 + 60 * file_count))
537
 
538
 
539
  @spaces.GPU(duration=_gpu_duration)
 
517
  TOT = 0
518
 
519
 
520
+ @spaces.GPU` worker process with a hard time budget (≈60 s on free tier).
521
+ # Importing the Blender shared object inside that budget burns 30–60 s, so
522
+ # the worker is killed *during* bpy import — manifesting as
523
+ # "GPU task aborted" before any model code runs.
524
+ #
525
+ # We start `bpy_server.py` here, in the always-running main process, so the
526
+ # slow bpy import happens exactly once at Space boot.
527
+ #
528
+ # Communication between the main process / ZeroGPU workers and the bpy
529
+ # worker happens via **file-based IPC** under ``/tmp/bpy_ipc/``, avoiding
530
+ # TCP listening ports (which trigger HF Space proxy issues) and HTTP body-
531
+ # size limits.
532
+ # ---------------------------------------------------------------------------
533
+
534
+ MODEL_CKPTS = [
535
+ "experiments/articulation_xl_quantization_256_token_4/grpo_1400.ckpt",
536
+ ]
537
+
538
+ HF_PATHS = [
539
+ "None",
540
+ ]
541
+
542
+
543
+ def get_dataloader_workers() -> int:
544
+ # Always use 0 workers — the bpy IPC server is single-threaded and
545
+ # must process requests sequentially, so parallel loading provides
546
+ # no benefit. More importantly, DataLoader worker processes wrap
547
+ # exceptions from asset loading in a way that can leave the
548
+ # DataLoader iterator in an inconsistent state, blocking Gradio's
549
+ # event loop on subsequent requests.
550
+ return 0
551
+
552
+
553
+ # ---------------------------------------------------------------------------
554
+ # bpy_server lifecycle — lazy start so the heavy import doesn't fight ZeroGPU
555
+ # during module load. Uses file-based IPC (no TCP listening).
556
+ # ---------------------------------------------------------------------------
557
+ _BPY_CLIENT: "Optional[BpyClient]" = None
558
+
559
+
560
+ def ensure_bpy_server_started(force: bool = False):
561
+ """Start the bpy worker (if not already running) and wait until ready.
562
+
563
+ Uses a fast PID-file check to detect an already-running bpy_server
564
+ (works across processes, including ZeroGPU workers). Falls back to
565
+ launching a fresh subprocess when no server is found.
566
+
567
+ When *force* is True the existing worker (if any) is killed and a
568
+ fresh one is started unconditionally — use after a timeout to
569
+ recover from a hung server.
570
+ """
571
+ global _BPY_CLIENT
572
+
573
+ if not force:
574
+ # Fast path: we hold a live client handle (same-process, e.g. main).
575
+ if _BPY_CLIENT is not None and _BPY_CLIENT.is_alive():
576
+ return
577
+
578
+ # Cross-process fast path: another process already started the worker.
579
+ if is_bpy_server_alive():
580
+ return
581
+
582
+ # If we had a dead client handle, clean it up.
583
+ if _BPY_CLIENT is not None:
584
+ try:
585
+ _BPY_CLIENT.shutdown()
586
+ except Exception:
587
+ pass
588
+
589
+ # Launch a fresh bpy worker (this blocks until bpy is imported, ~30–60 s
590
+ # on a cold container, but the @spaces.GPU duration budget accommodates).
591
+ _BPY_CLIENT = BpyClient.launch()
592
+
593
+
594
+ # ---------------------------------------------------------------------------
595
+ # Lazy model loading.
596
+ # ---------------------------------------------------------------------------
597
+ model = None
598
+ tokenizer = None
599
+ transform = None
600
+ CURRENT_MODEL_CKPT: Optional[str] = None
601
+ CURRENT_HF_PATH: Optional[str] = None
602
+
603
+
604
+ def load_model(model_ckpt: str, hf_path: Optional[str]) -> Tuple[str, str]:
605
+ global model, tokenizer, transform, CURRENT_MODEL_CKPT, CURRENT_HF_PATH
606
+ if hf_path == "None":
607
+ hf_path = None
608
+ if model is not None and model_ckpt == CURRENT_MODEL_CKPT and hf_path == CURRENT_HF_PATH:
609
+ return ("Model already loaded.", model_ckpt)
610
+
611
+ if not model_ckpt:
612
+ raise RuntimeError("model_ckpt is empty. Please select a checkpoint.")
613
+
614
+ print(f"Loading model: {model_ckpt}, hf_path={hf_path}")
615
+ model = get_model(model_ckpt, hf_path=hf_path)
616
+ assert model.tokenizer_config is not None
617
+ tokenizer = get_tokenizer(**model.tokenizer_config)
618
+ transform = Transform.parse(**model.transform_config["predict_transform"])
619
+ CURRENT_MODEL_CKPT = model_ckpt
620
+ CURRENT_HF_PATH = hf_path
621
+ return ("Model loaded.", model_ckpt)
622
+
623
+
624
+ # ---------------------------------------------------------------------------
625
+ # File utilities (CLI-side).
626
+ # ---------------------------------------------------------------------------
627
+ SUPPORTED_EXT = {".obj", ".fbx", ".glb"}
628
+
629
+
630
+ def collect_files(input_path: Path) -> List[Path]:
631
+ if input_path.is_file():
632
+ return [input_path]
633
+
634
+ files = []
635
+ for p in input_path.rglob("*"):
636
+ if p.suffix.lower() in SUPPORTED_EXT:
637
+ files.append(p)
638
+ return files
639
+
640
+
641
+ def map_output_path(in_path: Path, input_root: Path, output_root: Path) -> Path:
642
+ rel = in_path.relative_to(input_root)
643
+ return (output_root / rel).with_suffix(".glb")
644
+
645
+
646
+ # ---------------------------------------------------------------------------
647
+ # Core inference (shared by CLI and Gradio).
648
+ # ---------------------------------------------------------------------------
649
+ def run_rig(
650
+ filepaths: List[Path],
651
+ top_k: int,
652
+ top_p: float,
653
+ temperature: float,
654
+ repetition_penalty: float,
655
+ num_beams: int,
656
+ use_skeleton: bool,
657
+ use_transfer: bool,
658
+ use_postprocess: bool,
659
+ output_paths: List[Path],
660
+ model_ckpt: str,
661
+ hf_path: Optional[str],
662
+ ):
663
+ assert len(filepaths) == len(output_paths)
664
+ ensure_bpy_server_started()
665
+ load_model(model_ckpt, hf_path)
666
+
667
+ datapath = {
668
+ "data_name": None,
669
+ "loader": "bpy_server",
670
+ "filepaths": {"articulation": [str(p) for p in filepaths]},
671
+ }
672
+
673
+ dataset_config = DatasetConfig.parse(
674
+ shuffle=False,
675
+ batch_size=1,
676
+ num_workers=get_dataloader_workers(),
677
+ pin_memory=get_dataloader_workers() > 0,
678
+ persistent_workers=False,
679
+ datapath=datapath,
680
+ ).split_by_cls()
681
+
682
+ module = RigDatasetModule(
683
+ predict_dataset_config=dataset_config,
684
+ predict_transform=transform,
685
+ tokenizer=tokenizer,
686
+ process_fn=model._process_fn,
687
+ )
688
+
689
+ dataloader = module.predict_dataloader()["articulation"]
690
+
691
+ results_out = []
692
+ infer_device = model.device if model is not None else "cuda"
693
+
694
+ # Use explicit next() so we can catch errors from asset loading
695
+ # (which happens inside __getitem__ → next(dataloader)) per-file.
696
+ dataloader_iter = iter(dataloader)
697
+ for i in tqdm(range(len(filepaths)), desc="Processing"):
698
+ filepath = filepaths[i]
699
+ out_path = output_paths[i]
700
+ try:
701
+ batch = next(dataloader_iter)
702
+ except StopIteration:
703
+ break
704
+ except Exception as exc:
705
+ err_msg = str(exc)
706
+ print(f"[Error] Failed to load '{filepath}': {err_msg}", flush=True)
707
+ import traceback
708
+ traceback.print_exc()
709
+ # Force-restart if the server timed out (hung) so the next file
710
+ # gets a clean interpreter instead of hitting the same hang.
711
+ _restart_bpy_if_dead(force=("timeout" in err_msg.lower()))
712
+ continue
713
+
714
+ try:
715
+ batch = {
716
+ k: v.to(infer_device) if isinstance(v, Tensor) else v
717
+ for k, v in batch.items()
718
+ }
719
+
720
+ if not use_skeleton:
721
+ batch.pop("skeleton_tokens", None)
722
+ batch.pop("skeleton_mask", None)
723
+
724
+ batch["generate_kwargs"] = dict(
725
+ max_length=2048,
726
+ top_k=int(top_k),
727
+ top_p=float(top_p),
728
+ temperature=float(temperature),
729
+ repetition_penalty=float(repetition_penalty),
730
+ num_return_sequences=1,
731
+ num_beams=int(num_beams),
732
+ do_sample=True,
733
+ )
734
+
735
+ if "skeleton_tokens" in batch and "skeleton_mask" in batch:
736
+ mask = batch["skeleton_mask"][0] == 1
737
+ skeleton_tokens = batch["skeleton_tokens"][0][mask].cpu().numpy()
738
+ else:
739
+ skeleton_tokens = None
740
+
741
+ preds: List[TokenRigResult] = model.predict_step(
742
+ batch,
743
+ skeleton_tokens=[skeleton_tokens] if skeleton_tokens is not None else None,
744
+ make_asset=True,
745
+ )["results"]
746
+
747
+ asset = preds[0].asset
748
+ assert asset is not None
749
+
750
+ if use_postprocess:
751
+ voxel = asset.voxel(resolution=196)
752
+ asset.skin *= voxel_skin(
753
+ grid=0,
754
+ grid_coords=voxel.coords,
755
+ joints=asset.joints,
756
+ vertices=asset.vertices,
757
+ faces=asset.faces,
758
+ mode="square",
759
+ voxel_size=voxel.voxel_size,
760
+ )
761
+ asset.normalize_skin()
762
+
763
+ out_path.parent.mkdir(parents=True, exist_ok=True)
764
+
765
+ if use_transfer:
766
+ payload = dict(
767
+ source_asset=asset,
768
+ target_path=asset.path,
769
+ export_path=str(out_path),
770
+ group_per_vertex=4,
771
+ )
772
+ res = bpy_server_request("transfer", payload)
773
+ else:
774
+ payload = dict(
775
+ asset=asset,
776
+ filepath=str(out_path),
777
+ group_per_vertex=4,
778
+ )
779
+ res = bpy_server_request("export", payload)
780
+
781
+ if res != "ok":
782
+ print(f"[Error] {filepath}: {res}")
783
+ else:
784
+ print(f"[OK] Exported: {out_path}")
785
+ results_out.append(out_path)
786
+ except Exception as exc:
787
+ err_msg = str(exc)
788
+ print(f"[Error] Failed to process '{filepath}': {err_msg}", flush=True)
789
+ import traceback
790
+ traceback.print_exc()
791
+ # Force-restart if the server timed out (hung).
792
+ _restart_bpy_if_dead(force=("timeout" in err_msg.lower()))
793
+ continue
794
+
795
+ return results_out
796
+
797
+
798
+ def _restart_bpy_if_dead(force: bool = False):
799
+ """If the bpy worker process died or hung, start a new one.
800
+
801
+ When *force* is True, the existing worker is killed and replaced
802
+ unconditionally — use after a request timeout to guarantee a clean
803
+ bpy interpreter for the next file.
804
+ """
805
+ if force:
806
+ print("[run_rig] bpy worker timed out — force-restarting...", flush=True)
807
+ try:
808
+ ensure_bpy_server_started(force=True)
809
+ except Exception as exc:
810
+ print(f"[run_rig] Failed to force-restart bpy worker: {exc}", flush=True)
811
+ elif not is_bpy_server_alive():
812
+ print("[run_rig] bpy worker is dead — restarting...", flush=True)
813
+ try:
814
+ ensure_bpy_server_started()
815
+ except Exception as exc:
816
+ print(f"[run_rig] Failed to restart bpy worker: {exc}", flush=True)
817
+
818
+
819
+ # ---------------------------------------------------------------------------
820
+ # CLI entry point.
821
+ # ---------------------------------------------------------------------------
822
+ def run_cli(args):
823
+ input_path = Path(args.input).resolve()
824
+ output_path = Path(args.output).resolve()
825
+
826
+ files = collect_files(input_path)
827
+ if not files:
828
+ raise RuntimeError("No valid 3D files found.")
829
+
830
+ if len(files) == 1 and output_path.suffix:
831
+ outputs = [output_path]
832
+ else:
833
+ outputs = [map_output_path(f, input_path, output_path) for f in files]
834
+
835
+ run_rig(
836
+ files,
837
+ args.top_k,
838
+ args.top_p,
839
+ args.temperature,
840
+ args.repetition_penalty,
841
+ args.num_beams,
842
+ args.use_skeleton,
843
+ args.use_transfer,
844
+ args.use_postprocess,
845
+ outputs,
846
+ args.model_ckpt,
847
+ args.hf_path,
848
+ )
849
+
850
+
851
+ # ---------------------------------------------------------------------------
852
+ # Gradio wrapper (with ZeroGPU duration estimator).
853
+ # ---------------------------------------------------------------------------
854
+ TOT = 0
855
+
856
+
857
  def _gpu_duration(
858
  files,
859
  top_k,
 
870
  # Cold workers spend ~30–60 s importing bpy + loading the model before
871
  # any GPU work. Give every request a generous 240 s floor.
872
  file_count = len(files) if files is not None else 1
873
+ return 190 # patched: stay under free-tier per-call cap
874
 
875
 
876
  @spaces.GPU(duration=_gpu_duration)