Align output layout to partner (task/episode/1 + prompt/ + video frames); --view all|head
Browse files- ge_modal_app.py +80 -18
ge_modal_app.py
CHANGED
|
@@ -121,6 +121,34 @@ def stable_uniform_0_1(*parts) -> float:
|
|
| 121 |
return int(h[:16], 16) / float(0xFFFFFFFFFFFFFFFF)
|
| 122 |
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
@app.function(image=image, volumes={DATA_DIR: data_vol}, timeout=60 * 30)
|
| 125 |
def prepare_samples(
|
| 126 |
source: str = "agibot_world",
|
|
@@ -159,9 +187,10 @@ def prepare_samples(
|
|
| 159 |
made = []
|
| 160 |
n_skip_hash = 0
|
| 161 |
for i, e in enumerate(entries[:limit]):
|
| 162 |
-
|
|
|
|
| 163 |
# 哈希采样的 sample_id:优先用 gt_path 原始字符串(与搭档 run_local.py 完全一致)
|
| 164 |
-
hash_id = e.get("gt_path") or f"{source}/{
|
| 165 |
|
| 166 |
# 先按 (gt_path, prompt_key) 逐 variant 做哈希采样,决定哪些要跑
|
| 167 |
variants_to_run = []
|
|
@@ -198,7 +227,14 @@ def prepare_samples(
|
|
| 198 |
|
| 199 |
with open(os.path.join(sdir, "prompt.txt"), "w") as f:
|
| 200 |
f.write(prompt + "\n")
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
data_vol.commit()
|
| 204 |
print(f"已准备 {len(made)} 个样本目录(哈希按 variant 跳过 {n_skip_hash} 个)到 {samples_root}")
|
|
@@ -215,13 +251,14 @@ def prepare_samples(
|
|
| 215 |
timeout=60 * 60 * 12, # 一个 shard 可能跑几小时
|
| 216 |
)
|
| 217 |
def run_inference(
|
| 218 |
-
|
| 219 |
config_name: str = "video_model_infer_slow.yaml",
|
| 220 |
n_chunk: int = 4,
|
| 221 |
seed: int = 42,
|
|
|
|
| 222 |
):
|
| 223 |
-
"""处理一个 shard
|
| 224 |
-
断点续跑
|
| 225 |
import os, sys, yaml, shutil, torch
|
| 226 |
from einops import rearrange
|
| 227 |
|
|
@@ -250,21 +287,28 @@ def run_inference(
|
|
| 250 |
video_fps = 30
|
| 251 |
|
| 252 |
device = "cuda"
|
| 253 |
-
print(f"加载模型中 (本 shard 共 {len(
|
| 254 |
tokenizer, text_encoder, vae, diffusion_model, scheduler, pipe = prepare_model(args, device=device)
|
| 255 |
valid_cams = [c + "_color" for c in args.data["train"]["valid_cam"]]
|
|
|
|
| 256 |
TEMPORAL_DOWN_RATIO = vae.temporal_compression_ratio
|
| 257 |
sample_size = (args.data["train"]["sample_size"][1], args.data["train"]["sample_size"][0])
|
| 258 |
|
| 259 |
samples_root = os.path.join(DATA_DIR, "samples")
|
| 260 |
done, skipped, failed = [], [], []
|
| 261 |
|
| 262 |
-
for i,
|
|
|
|
| 263 |
image_root = os.path.join(samples_root, sid)
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
skipped.append(sid)
|
| 269 |
continue
|
| 270 |
try:
|
|
@@ -283,15 +327,32 @@ def run_inference(
|
|
| 283 |
history_action_state=None, pixel_wise_timestep=args.pixel_wise_timestep,
|
| 284 |
n_chunk=n_chunk,
|
| 285 |
)[0]
|
| 286 |
-
os.makedirs(
|
|
|
|
|
|
|
| 287 |
video = preds["video"].data.cpu()
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
done.append(sid)
|
| 291 |
-
print(f" ✅ [{i+1}/{len(
|
| 292 |
except Exception as e:
|
| 293 |
failed.append((sid, str(e)[:200]))
|
| 294 |
-
print(f" ❌ [{i+1}/{len(
|
| 295 |
|
| 296 |
if (i + 1) % 20 == 0: # 每 20 条提交一次,崩了也保得住进度
|
| 297 |
output_vol.commit()
|
|
@@ -316,6 +377,7 @@ def main(
|
|
| 316 |
keep_prob: float = 1.0, # 哈希采样:0.3 = 约跑 30% 子集(确定性、可复现、可断点续跑)
|
| 317 |
sample_seed: int = 20260609, # 与搭档对齐用
|
| 318 |
sample_model_key: str = "", # 与搭档对齐的关键:两边相同才抽到同一子集;空则用 "ge"
|
|
|
|
| 319 |
):
|
| 320 |
if not skip_weights:
|
| 321 |
print("== 步骤 1: 下载权重 ==")
|
|
@@ -332,7 +394,7 @@ def main(
|
|
| 332 |
print("== 步骤 3: GPU 并行推理 (断点续跑,已生成的会跳过) ==")
|
| 333 |
chunks = [sample_ids[i::shards] for i in range(shards)] # 轮转切片,负载均衡
|
| 334 |
chunks = [c for c in chunks if c]
|
| 335 |
-
handles = [run_inference.spawn(c, config_name=config_name, n_chunk=n_chunk) for c in chunks]
|
| 336 |
results = [h.get() for h in handles]
|
| 337 |
|
| 338 |
tot_done = sum(len(r["done"]) for r in results)
|
|
|
|
| 121 |
return int(h[:16], 16) / float(0xFFFFFFFFFFFFFFFF)
|
| 122 |
|
| 123 |
|
| 124 |
+
def _resolve_task_episode(e):
|
| 125 |
+
"""与搭档 run_local.py 的 resolve_task_episode 一致:优先字段,否则从 gt_path 推。"""
|
| 126 |
+
if "task_name" in e and "episode_name" in e:
|
| 127 |
+
return str(e["task_name"]), str(e["episode_name"])
|
| 128 |
+
gt = e.get("gt_path")
|
| 129 |
+
if gt:
|
| 130 |
+
parts = str(gt).split("/")
|
| 131 |
+
if len(parts) >= 3:
|
| 132 |
+
return parts[-3], parts[-2]
|
| 133 |
+
return "unknown_task", str(e.get("episode_name", "episode_00000"))
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _extract_frames(video_path, frames_dir):
|
| 137 |
+
"""与搭档一致:逐帧抽成 video/frame_00000.jpg ..."""
|
| 138 |
+
import os, cv2
|
| 139 |
+
os.makedirs(frames_dir, exist_ok=True)
|
| 140 |
+
cap = cv2.VideoCapture(video_path)
|
| 141 |
+
idx = 0
|
| 142 |
+
while True:
|
| 143 |
+
ret, frame = cap.read()
|
| 144 |
+
if not ret:
|
| 145 |
+
break
|
| 146 |
+
cv2.imwrite(os.path.join(frames_dir, f"frame_{idx:05d}.jpg"), frame)
|
| 147 |
+
idx += 1
|
| 148 |
+
cap.release()
|
| 149 |
+
return idx
|
| 150 |
+
|
| 151 |
+
|
| 152 |
@app.function(image=image, volumes={DATA_DIR: data_vol}, timeout=60 * 30)
|
| 153 |
def prepare_samples(
|
| 154 |
source: str = "agibot_world",
|
|
|
|
| 187 |
made = []
|
| 188 |
n_skip_hash = 0
|
| 189 |
for i, e in enumerate(entries[:limit]):
|
| 190 |
+
task_name, episode_name = _resolve_task_episode(e)
|
| 191 |
+
base_id = f"{source}_{task_name}_{episode_name}"
|
| 192 |
# 哈希采样的 sample_id:优先用 gt_path 原始字符串(与搭档 run_local.py 完全一致)
|
| 193 |
+
hash_id = e.get("gt_path") or f"{source}/{task_name}/{episode_name}"
|
| 194 |
|
| 195 |
# 先按 (gt_path, prompt_key) 逐 variant 做哈希采样,决定哪些要跑
|
| 196 |
variants_to_run = []
|
|
|
|
| 227 |
|
| 228 |
with open(os.path.join(sdir, "prompt.txt"), "w") as f:
|
| 229 |
f.write(prompt + "\n")
|
| 230 |
+
shutil.copy(init_png, os.path.join(sdir, "init_frame.png")) # 供输出 prompt bundle
|
| 231 |
+
made.append({
|
| 232 |
+
"sample_id": sample_id, # data volume 上的输入目录名
|
| 233 |
+
"source": source,
|
| 234 |
+
"task_name": task_name,
|
| 235 |
+
"episode_name": episode_name,
|
| 236 |
+
"prompt_key": field, # prompt_prefix / prompt_rewrite
|
| 237 |
+
})
|
| 238 |
|
| 239 |
data_vol.commit()
|
| 240 |
print(f"已准备 {len(made)} 个样本目录(哈希按 variant 跳过 {n_skip_hash} 个)到 {samples_root}")
|
|
|
|
| 251 |
timeout=60 * 60 * 12, # 一个 shard 可能跑几小时
|
| 252 |
)
|
| 253 |
def run_inference(
|
| 254 |
+
samples: list, # 每项是 prepare_samples 返回的 dict
|
| 255 |
config_name: str = "video_model_infer_slow.yaml",
|
| 256 |
n_chunk: int = 4,
|
| 257 |
seed: int = 42,
|
| 258 |
+
view: str = "all", # "all"=三视角横拼(默认,信息全); "head"=只存head单视角
|
| 259 |
):
|
| 260 |
+
"""处理一个 shard:模型只加载一次,循环推理;
|
| 261 |
+
断点续跑 + 逐样本容错。输出对齐搭档 run_local.py 的目录结构,视频只保留 head 单视角。"""
|
| 262 |
import os, sys, yaml, shutil, torch
|
| 263 |
from einops import rearrange
|
| 264 |
|
|
|
|
| 287 |
video_fps = 30
|
| 288 |
|
| 289 |
device = "cuda"
|
| 290 |
+
print(f"加载模型中 (本 shard 共 {len(samples)} 个样本) ...")
|
| 291 |
tokenizer, text_encoder, vae, diffusion_model, scheduler, pipe = prepare_model(args, device=device)
|
| 292 |
valid_cams = [c + "_color" for c in args.data["train"]["valid_cam"]]
|
| 293 |
+
head_view_idx = args.data["train"]["valid_cam"].index("head") # 只保留 head 视角
|
| 294 |
TEMPORAL_DOWN_RATIO = vae.temporal_compression_ratio
|
| 295 |
sample_size = (args.data["train"]["sample_size"][1], args.data["train"]["sample_size"][0])
|
| 296 |
|
| 297 |
samples_root = os.path.join(DATA_DIR, "samples")
|
| 298 |
done, skipped, failed = [], [], []
|
| 299 |
|
| 300 |
+
for i, s in enumerate(samples):
|
| 301 |
+
sid = s["sample_id"]
|
| 302 |
image_root = os.path.join(samples_root, sid)
|
| 303 |
+
# 搭档风格输出路径: <source>/<prompt_key>/<task>/<episode>/1/
|
| 304 |
+
out_dir = os.path.join(OUT_DIR, s["source"], s["prompt_key"],
|
| 305 |
+
s["task_name"], s["episode_name"], "1")
|
| 306 |
+
final_mp4 = os.path.join(out_dir, f"{s['task_name']}_{s['episode_name']}.mp4")
|
| 307 |
+
frames_dir = os.path.join(out_dir, "video")
|
| 308 |
+
prompt_dir = os.path.join(out_dir, "prompt")
|
| 309 |
+
|
| 310 |
+
# 断点续跑:视频和抽帧都在则跳过
|
| 311 |
+
if os.path.exists(final_mp4) and os.path.isdir(frames_dir) and os.listdir(frames_dir):
|
| 312 |
skipped.append(sid)
|
| 313 |
continue
|
| 314 |
try:
|
|
|
|
| 327 |
history_action_state=None, pixel_wise_timestep=args.pixel_wise_timestep,
|
| 328 |
n_chunk=n_chunk,
|
| 329 |
)[0]
|
| 330 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 331 |
+
|
| 332 |
+
# 保存视频:默认 all=三视角横拼(信息全),可选 head=只存 head 单视角
|
| 333 |
video = preds["video"].data.cpu()
|
| 334 |
+
if view == "head":
|
| 335 |
+
video = rearrange(video, "(b v) c t h w -> b v c t h w", v=v)
|
| 336 |
+
out_video = video[0, head_view_idx] # c t h w (单视角)
|
| 337 |
+
else:
|
| 338 |
+
out_video = rearrange(video, "(b v) c t h w -> b c t h (v w)", v=v)[0] # 三视角横拼
|
| 339 |
+
save_video(out_video, final_mp4, fps=video_fps)
|
| 340 |
+
|
| 341 |
+
# prompt bundle: prompt/prompt.txt + prompt/init_frame.png
|
| 342 |
+
os.makedirs(prompt_dir, exist_ok=True)
|
| 343 |
+
shutil.copy(os.path.join(image_root, "prompt.txt"), os.path.join(prompt_dir, "prompt.txt"))
|
| 344 |
+
init_src = os.path.join(image_root, "init_frame.png")
|
| 345 |
+
if os.path.exists(init_src):
|
| 346 |
+
shutil.copy(init_src, os.path.join(prompt_dir, "init_frame.png"))
|
| 347 |
+
|
| 348 |
+
# 抽帧: video/frame_XXXXX.jpg
|
| 349 |
+
_extract_frames(final_mp4, frames_dir)
|
| 350 |
+
|
| 351 |
done.append(sid)
|
| 352 |
+
print(f" ✅ [{i+1}/{len(samples)}] {s['task_name']}/{s['episode_name']} [{s['prompt_key']}]")
|
| 353 |
except Exception as e:
|
| 354 |
failed.append((sid, str(e)[:200]))
|
| 355 |
+
print(f" ❌ [{i+1}/{len(samples)}] {sid}: {e}")
|
| 356 |
|
| 357 |
if (i + 1) % 20 == 0: # 每 20 条提交一次,崩了也保得住进度
|
| 358 |
output_vol.commit()
|
|
|
|
| 377 |
keep_prob: float = 1.0, # 哈希采样:0.3 = 约跑 30% 子集(确定性、可复现、可断点续跑)
|
| 378 |
sample_seed: int = 20260609, # 与搭档对齐用
|
| 379 |
sample_model_key: str = "", # 与搭档对齐的关键:两边相同才抽到同一子集;空则用 "ge"
|
| 380 |
+
view: str = "all", # "all"=三视角(默认); "head"=只存head单视角
|
| 381 |
):
|
| 382 |
if not skip_weights:
|
| 383 |
print("== 步骤 1: 下载权重 ==")
|
|
|
|
| 394 |
print("== 步骤 3: GPU 并行推理 (断点续跑,已生成的会跳过) ==")
|
| 395 |
chunks = [sample_ids[i::shards] for i in range(shards)] # 轮转切片,负载均衡
|
| 396 |
chunks = [c for c in chunks if c]
|
| 397 |
+
handles = [run_inference.spawn(c, config_name=config_name, n_chunk=n_chunk, view=view) for c in chunks]
|
| 398 |
results = [h.get() for h in handles]
|
| 399 |
|
| 400 |
tot_done = sum(len(r["done"]) for r in results)
|