champ_demo / app.py
yoyozs11's picture
Update app.py
92a0c3d verified
Raw
History Blame Contribute Delete
12.2 kB
import huggingface_hub
# 核心修補:Monkey Patch,解決新版 huggingface_hub 缺失 cached_download 的問題
if not hasattr(huggingface_hub, 'cached_download'):
from huggingface_hub import hf_hub_download
huggingface_hub.cached_download = hf_hub_download
import os
import sys
import subprocess
import threading
import time
import shutil
import gradio as gr
from huggingface_hub import snapshot_download, HfApi
from omegaconf import OmegaConf
import torch
cuda_available = torch.cuda.is_available()
print(f"Is CUDA available: {cuda_available}")
CHAMP_MASTER_DIR = "champ-master"
# 🌟 隔離區宣告:前處理專用房間的路徑
VENV_DIR = os.path.abspath("preproc_env")
VENV_PYTHON = os.path.join(VENV_DIR, "bin", "python")
# =========================================================================
# 🏗️ [Runtime 環境防禦與隔離區架設]
# =========================================================================
print("\n🏗️ [環境建置] 開始架設前處理隔離特區...")
# 1. 從模型庫同步組員的前處理專案
checkpoint_script = os.path.join(CHAMP_MASTER_DIR, "process_single_video.py")
if not os.path.exists(checkpoint_script):
print(f"⏳ 偵測到 Space 本地缺乏前處理,正在從模型庫同步中...")
try:
os.makedirs(CHAMP_MASTER_DIR, exist_ok=True)
snapshot_download(
repo_id="yoyozs11/champ_preprocess",
local_dir=CHAMP_MASTER_DIR,
repo_type="model"
)
print("✅ 前處理模組同步成功!")
except Exception as e:
print(f"❌ 同步前處理模組失敗: {e}")
# 2. 🌟【核心關鍵:蓋隔離房間】自動建立前處理專用的獨立虛擬環境
if not os.path.exists(VENV_DIR):
print(f"📦 正在為組員程式碼建立專屬隔離環境 (venv) 於: {VENV_DIR}...")
try:
# 使用系統 Python 創建一個完全乾淨隔離的子資料夾房間
subprocess.run([sys.executable, "-m", "venv", VENV_DIR], check=True)
print("⏳ 正在隔離環境內安裝舊版 NumPy 與 4D-Humans 連鎖依賴套件...")
# 🎯 在隔離房間(preproc_env)內安裝前處理需要的所有套件
# 這裡精確對齊了你提供的前處理 requirements.txt 清單,徹底解決 cv2 等缺失問題
preprocess_deps = [
"pip", "install", "--upgrade", "pip", "setuptools", "wheel",
"numpy==1.23.5", # 強制鎖定 numpy 1.x 版,修復 numpy._core 錯誤
"opencv-python-headless", # 解決 cv2 找不到的問題
"torch==2.2.2", "torchvision==0.17.2", # 補上探測器(Detector)與核心張量庫
"pytorch-lightning==2.1.0", "gdown", "webdataset",
"pandas", "scikit-image", "trimesh", "smplx==0.1.28", "chumpy",
"timm", "einops", "yacs", "onnxruntime-gpu", "pillow", "tqdm", "pyrender"
]
subprocess.run([VENV_PYTHON, "-m"] + preprocess_deps, check=True)
print("✅ 前處理專屬隔離區套件配置大成功!")
except Exception as e:
print(f"❌ 隔離環境建立或套件安裝失敗: {e}")
# 3. 對齊 Blender 軟連結 (對應 Dockerfile)
blender_target_path = os.path.join(CHAMP_MASTER_DIR, "blender")
if os.path.exists("blender") and not os.path.exists(blender_target_path):
try:
os.symlink(os.path.abspath("blender"), blender_target_path)
print("✅ Blender 軟連結架設成功,完美對齊隔離前處理路徑!")
except Exception as e:
print(f"⚠️ Blender 軟連結對齊失敗: {e}")
# 4. 自動下載 Champ 推論核心權重 (外面環境維持原本的最新狀態)
PRETRAINED_MODELS_DIR = "pretrained_models"
if not os.path.exists(PRETRAINED_MODELS_DIR) or len(os.listdir(PRETRAINED_MODELS_DIR)) < 4:
print("⏳ 正在從 Hugging Face 官方庫下載 Champ 推論核心權重...")
try:
snapshot_download(
repo_id="fudan-generative-ai/champ",
local_dir=PRETRAINED_MODELS_DIR,
repo_type="model",
ignore_patterns=[".git*", "README.md"]
)
print("✅ Champ 核心推論權重下載成功。")
except Exception as e:
print(f"❌ 核心權重下載失敗: {e}")
print("🚀 [初始化完畢] 隔離區與外圍系統全線解耦串聯就緒,Gradio 即將點亮!\n")
# =========================================================================
# 🔒 自毀防禦計時器
# =========================================================================
def delayed_pause_space(repo_id, delay_seconds=180):
print(f"🤖 [背景防禦線] 自毀計畫已啟動,倒數 {delay_seconds} 秒後將自動關機...")
time.sleep(delay_seconds)
hf_token = os.getenv("HF_TOKEN")
if hf_token:
try:
api = HfApi(token=hf_token)
api.pause_space(repo_id=repo_id)
print("💤 [背景防禦線] Space 已進入安全休眠狀態。")
except Exception: pass
# =========================================================================
# 5. 全新絕對沙盒防禦對齊版 Gradio Trigger Function (環境路徑劫持版)
# =========================================================================
def gradio_predict(input_image, input_video, start_frame, end_frame):
import shutil # 引入實體檔案搬運工
if not input_image or not input_video or not isinstance(input_video, str):
return "❌ 錯誤:請確保正確上傳了參考人像與動作影片!", None, None
# -------------------------------------------------------------------------
# 🛡️ 實體防線:既然組員腳本硬要去讀 /app/champ-master/images,我們就直接蓋在它要的地方!
# -------------------------------------------------------------------------
custom_ref_root = os.path.abspath(CHAMP_MASTER_DIR)
custom_ref_images = os.path.join(custom_ref_root, "images")
if os.path.exists(custom_ref_images):
shutil.rmtree(custom_ref_images)
os.makedirs(custom_ref_images, exist_ok=True)
target_image_path = os.path.join(custom_ref_images, "0001.png")
try:
shutil.copy(input_image, target_image_path)
print(f"🎯 [物理降維打擊成功] 參考圖已直接入駐組員大本營: {target_image_path}")
except Exception as e:
return f"❌ 參考圖絕對沙盒配置失敗: {e}", None, None
# -------------------------------------------------------------------------
print("\n" + "="*50)
print("🎬 [階段 1] 啟動隔離房間內的前處理執行組...")
print("="*50)
try:
script_name = "process_single_video.py"
# 🌟 核心破關線:複製當前的環境變數,並進行「路徑劫持」
env = os.environ.copy()
# 1. 把隔離房間的 bin 夾(裡面裝有我們剛裝好的 python)強行插到 Linux PATH 的最前面!
# 這樣組員腳本在裡面不論呼叫幾百次 python,全部都會被強行導流進隔離房間!
venv_bin_dir = os.path.join(VENV_DIR, "bin")
env["PATH"] = f"{venv_bin_dir}:{env.get('PATH', '')}"
# 2. 同步強注原始碼白名單
master_path = os.path.abspath(CHAMP_MASTER_DIR)
fourd_path = os.path.abspath(os.path.join(CHAMP_MASTER_DIR, "4D-Humans"))
env["PYTHONPATH"] = f"{master_path}:{fourd_path}:{env.get('PYTHONPATH', '')}"
# 3. 呼叫隔離房間的 Python 啟動前處理主程式
preprocess_cmd = [
VENV_PYTHON, script_name,
"--video", os.path.abspath(input_video),
"--ref", "./"
]
subprocess.run(preprocess_cmd, check=True, cwd=CHAMP_MASTER_DIR, env=env)
print("✅ 隔離區前處理連鎖通車成功!")
except Exception as e:
return f"❌ 前處理隔離階段發生錯誤: {e}", None, None
# 動態計算前處理生成的資料夾路徑
video_filename = os.path.basename(input_video)
video_name, _ = os.path.splitext(video_filename)
computed_guidance_folder = os.path.join(CHAMP_MASTER_DIR, "transferd_result", video_name)
if not os.path.exists(computed_guidance_folder):
backup_folder = os.path.join(CHAMP_MASTER_DIR, "transferd_result")
if os.path.exists(backup_folder) and len(os.listdir(backup_folder)) > 0:
computed_guidance_folder = backup_folder
print(f"🔍 最終定位條件影像資料夾: {computed_guidance_folder}")
if not os.path.exists(computed_guidance_folder):
return f"❌ 錯誤:找不到前處理生成目錄 {computed_guidance_folder}", None, None
# 外圍主要大腦(Champ 推理層)路徑對齊
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path: sys.path.insert(0, current_dir)
if master_abs not in sys.path: sys.path.insert(0, master_abs)
try:
from inference import main as run_champ_inference
except Exception as e:
return f"❌ 載入 inference.py 失敗: {e}", None, None
config = OmegaConf.load(os.path.join("configs", "inference", "inference.yaml"))
config.data.ref_image_path = os.path.abspath(target_image_path)
config.data.guidance_data_folder = os.path.abspath(computed_guidance_folder)
config.data.frame_range = [int(start_frame), int(end_frame)]
try:
print("🚀 [階段 2] 外圍核心啟動擴散模型推理...")
run_champ_inference(config)
video_animation, video_grid, video_grid_wguidance = None, None, None
results_dir = "results"
if os.path.exists(results_dir):
subdirs = [os.path.join(results_dir, d) for d in os.listdir(results_dir) if os.path.isdir(os.path.join(results_dir, d))]
if subdirs:
latest_dir = max(subdirs, key=os.path.getmtime)
if os.path.exists(os.path.join(latest_dir, "animation.mp4")): video_animation = os.path.join(latest_dir, "animation.mp4")
if os.path.exists(os.path.join(latest_dir, "grid.mp4")): video_grid = os.path.join(latest_dir, "grid.mp4")
if os.path.exists(os.path.join(latest_dir, "grid_wguidance.mp4")): video_grid_wguidance = os.path.join(latest_dir, "grid_wguidance.mp4")
threading.Thread(target=delayed_pause_space, args=("yoyozs11/champ_demo", 180), daemon=True).start()
return video_animation, video_grid, video_grid_wguidance
except Exception as e:
return f"❌ 推論階段發生崩潰,錯誤訊息: {e}", None, None
# ==========================================
# 6. Gradio Web UI 佈局
# ==========================================
with gr.Blocks() as demo:
gr.Markdown("# Champ 3D 動作生成系統 (高階獨立虛擬環境隔離版)")
with gr.Row():
with gr.Column(scale=1):
input_img = gr.Image(type="filepath", label="1. 上傳人像圖片")
input_vid = gr.Video(label="2. 上傳自訂動作影片 (MP4)", format="mp4")
with gr.Row():
start_f = gr.Number(value=0, label="動作起始幀", precision=0)
end_f = gr.Number(value=20, label="動作結束幀", precision=0)
btn = gr.Button("開始前處理與影片生成", variant="primary")
with gr.Column(scale=1):
output_video_anim = gr.Video(label="3-1. 純動作生成結果")
output_video_grid = gr.Video(label="3-2. 人像與結果對照組")
output_video_grid_wguid = gr.Video(label="3-3. 完整骨架引導對照組")
btn.click(
fn=gradio_predict,
inputs=[input_img, input_vid, start_f, end_f],
outputs=[output_video_anim, output_video_grid, output_video_grid_wguid]
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Default())