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

Upload demo.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. demo.py +1 -338
demo.py CHANGED
@@ -517,343 +517,6 @@ def run_cli(args):
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,7 +533,7 @@ def _gpu_duration(
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)
 
517
  TOT = 0
518
 
519
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  def _gpu_duration(
521
  files,
522
  top_k,
 
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 190 # patched: stay under free-tier per-call duration cap
537
 
538
 
539
  @spaces.GPU(duration=_gpu_duration)