Xinyi0214 commited on
Commit
32f4ce6
·
verified ·
1 Parent(s): 3809072

Align output layout to partner (task/episode/1 + prompt/ + video frames); --view all|head

Browse files
Files changed (1) hide show
  1. 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
- base_id = f"{source}_{e.get('task_name','t')}_{e.get('episode_name','e')}"
 
163
  # 哈希采样的 sample_id:优先用 gt_path 原始字符串(与搭档 run_local.py 完全一致)
164
- hash_id = e.get("gt_path") or f"{source}/{e.get('task_name','t')}/{e.get('episode_name','e')}"
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
- made.append(sample_id)
 
 
 
 
 
 
 
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
- sample_ids: list,
219
  config_name: str = "video_model_infer_slow.yaml",
220
  n_chunk: int = 4,
221
  seed: int = 42,
 
222
  ):
223
- """处理一个 shard 的样本:模型只加载一次,循环推理;
224
- 断点续跑(已有 video.mp4 则跳过) + 逐样本容错(单条失败不影响整批)。"""
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(sample_ids)} 个样本) ...")
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, sid in enumerate(sample_ids):
 
263
  image_root = os.path.join(samples_root, sid)
264
- out_path = os.path.join(OUT_DIR, sid)
265
- video_file = os.path.join(out_path, "video.mp4")
266
-
267
- if os.path.exists(video_file): # 断点续跑:已生成则跳过
 
 
 
 
 
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(out_path, exist_ok=True)
 
 
287
  video = preds["video"].data.cpu()
288
- save_video(rearrange(video, "(b v) c t h w -> b c t h (v w)", v=v)[0], video_file, fps=video_fps)
289
- shutil.copy(os.path.join(image_root, "prompt.txt"), os.path.join(out_path, "prompt.txt"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  done.append(sid)
291
- print(f" ✅ [{i+1}/{len(sample_ids)}] {sid}")
292
  except Exception as e:
293
  failed.append((sid, str(e)[:200]))
294
- print(f" ❌ [{i+1}/{len(sample_ids)}] {sid}: {e}")
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)