| import os, json, pandas as pd, csv |
| from typing import Dict, Any, List, Set |
| from config import VLLMConfig |
| from utils import norm_key, ensure_dir |
| from sampling import read_video_meta, sample_indices_uniform, sample_indices_fps, grab_frames |
| from client import to_jpeg_base64, call_vllm |
| from prompt import build_messages |
| from parser import parse_step5_line, validate_bbox_sequences |
| from video_utils import compose_videos, build_out_paths |
| from tqdm import tqdm |
|
|
| def load_dataframe(csv_path: str) -> pd.DataFrame: |
| |
| with open(csv_path, 'r', encoding='utf-8') as f: |
| reader = csv.reader(f) |
| try: |
| header = next(reader) |
| except StopIteration: |
| return pd.DataFrame(columns=['videoid','source_video_path','instruction','chinese_instruction']) |
| norm_header = [h.strip() for h in header] |
| if len(norm_header) >= 4 and \ |
| norm_header[0].lower() == 'videoid' and \ |
| norm_header[1].lower() == 'source_video_path' and \ |
| norm_header[2].lower() == 'instruction' and \ |
| norm_header[3].lower() == 'chinese_instruction': |
| rows = [] |
| for row in reader: |
| if not row or all((x is None or str(x).strip() == '') for x in row): continue |
| vid = (row[0].strip() if len(row) > 0 else '') |
| vpath= (row[1].strip() if len(row) > 1 else '') |
| rest = row[2:] if len(row) > 2 else [] |
| joined = ','.join([x if x is not None else '' for x in rest]) |
| instr, zh = ('','') |
| if ',' in joined: instr, zh = joined.rsplit(',', 1) |
| else: instr, zh = joined, '' |
| rows.append({ |
| 'videoid': vid, |
| 'source_video_path': vpath, |
| 'instruction': instr.strip(), |
| 'chinese_instruction': zh.strip(), |
| }) |
| return pd.DataFrame(rows, columns=['videoid','source_video_path','instruction','chinese_instruction']) |
| else: |
| return pd.read_csv(csv_path, engine='python') |
|
|
| def _stable_key(row: Dict[str,Any]) -> str: |
| cols = {norm_key(k): k for k in row.keys()} |
| def pick(*cands, default=None): |
| for c in cands: |
| if c in cols: return row[cols[c]] |
| return default |
| uid = pick('videoid','uid','id','task_id','clip_id','name','key', default=None) |
| video_path = pick('source_video_path','video_path','video','filepath','file_path') |
| sampling_mode = pick('sampling_mode','sampling','mode', default='uniform_5') |
| stem = str(uid) if uid not in (None, "") else os.path.splitext(os.path.basename(video_path or ""))[0] |
| return f"{stem}::{sampling_mode}" |
|
|
| def _append_results_csv(results_csv: str, row: Dict[str,Any], header_written: bool): |
| df = pd.DataFrame([row]) |
| write_header = not os.path.exists(results_csv) or (os.path.getsize(results_csv)==0 and not header_written) |
| df.to_csv(results_csv, mode='a', header=write_header, index=False, encoding='utf-8-sig') |
|
|
| def process_one(row: Dict[str,Any], vllm: VLLMConfig, outroot: str, dry_run: bool=False) -> Dict[str,Any]: |
| cols = {norm_key(k): k for k in row.keys()} |
| def pick(*cands, default=None): |
| for c in cands: |
| if c in cols: return row[cols[c]] |
| return default |
|
|
| video_path = pick('source_video_path','video_path','video','filepath','file_path') |
| cn = pick('chinese_instruction'); en = pick('instruction','edit_instruction','edit','prompt','text') |
| edit_instruction = (cn if cn and str(cn).strip() else en) |
| sampling_mode = pick('sampling_mode','sampling','mode', default='uniform_5') |
| fps_val = pick('fps', default=None); n_frames_val = pick('n_frames','n','nframes','num_frames', default=None) |
| uid = pick('videoid','uid','id','task_id','clip_id','name','key', default=None) |
|
|
| fps = 1.0 if fps_val in (None,"") else float(fps_val) |
| n_frames = 5 if n_frames_val in (None,"") else int(n_frames_val) |
|
|
| if not video_path or not os.path.exists(video_path): |
| raise AssertionError(f'视频不存在或未提供:{video_path}') |
| if not (isinstance(edit_instruction,str) and len(str(edit_instruction).strip())>0): |
| raise AssertionError('缺少编辑指令(chinese_instruction / instruction)') |
|
|
| meta = read_video_meta(video_path) |
| if sampling_mode == 'fps_1': |
| indices = sample_indices_fps(meta['total'], meta['fps'], fps or 1.0) |
| mode_tag = f'fps_{int(fps)}' |
| else: |
| indices = sample_indices_uniform(meta['total'], n_frames or 5) |
| mode_tag = f'uniform_{len(indices)}' |
| frames_rgb, mapping = grab_frames(video_path, indices) |
| frames_b64 = [to_jpeg_base64(f) for f in frames_rgb] |
| messages = build_messages(frames_b64, edit_instruction) |
|
|
| step5_line = "" |
| raw_resp = None |
| latency = None |
| if not dry_run: |
| step5_line, latency, raw_resp = call_vllm(vllm.api_base, vllm.model, messages) |
| |
| raw_dir = os.path.join(outroot, 'raw_text') |
| os.makedirs(raw_dir, exist_ok=True) |
| stem = str(uid) if uid not in (None, "") else os.path.splitext(os.path.basename(video_path))[0] |
| raw_txt_path = os.path.join(raw_dir, f'{stem}__{mode_tag}.txt') |
| with open(raw_txt_path, 'w', encoding='utf-8') as fw: |
| fw.write(step5_line if isinstance(step5_line, str) else str(step5_line)) |
| |
| lines = [l.strip() for l in step5_line.strip().splitlines() if l.strip()] |
| step5_line = lines[-1] if lines else step5_line |
| else: |
| raw_txt_path = "" |
|
|
| parsed = {} |
| enhanced_instruction = "" |
| tasks = {} |
| if step5_line: |
| parsed = parse_step5_line(step5_line) |
| validate_bbox_sequences(parsed) |
| enhanced_instruction = parsed.get('enhanced_instruction','') |
| tasks = parsed.get('tasks',{}) |
|
|
| |
| stem = str(uid) if uid not in (None, "") else os.path.splitext(os.path.basename(video_path))[0] |
| out_inspect, out_mask = build_out_paths(outroot, video_path, stem, mode_tag) |
| out_json = os.path.join(outroot, 'parsed_json', f'{stem}__{mode_tag}.json') |
| for d in [os.path.dirname(out_inspect), os.path.dirname(out_mask), os.path.dirname(out_json)]: |
| os.makedirs(d, exist_ok=True) |
|
|
| |
| with open(out_json, 'w', encoding='utf-8') as f: |
| json.dump({ |
| 'video_path': video_path, |
| 'sampling_mode': sampling_mode, |
| 'indices': indices, |
| 'mapping_local_t_to_global_index': mapping, |
| 'model_api_base': vllm.api_base, |
| 'model_name': vllm.model, |
| 'enhanced_instruction': enhanced_instruction, |
| 'tasks': tasks, |
| 'raw_response': raw_resp, |
| 'raw_text_path': raw_txt_path, |
| 'parsed_ok': bool(parsed), |
| }, f, ensure_ascii=False, indent=2) |
|
|
| |
| compose_videos(video_path, mapping, parsed or {"tasks": {}}, out_inspect, out_mask, allow_empty=True) |
|
|
| return { |
| 'key': f"{stem}::{sampling_mode}", |
| 'uid': uid, |
| 'video_path': video_path, |
| 'edit_instruction_src': edit_instruction, |
| 'enhanced_instruction': enhanced_instruction, |
| 'sampling_mode': sampling_mode, |
| 'n_sampled': len(indices), |
| 'indices_json': json.dumps(indices, ensure_ascii=False), |
| 'mapping_json': json.dumps(mapping, ensure_ascii=False), |
| 'tasks_json': json.dumps(tasks, ensure_ascii=False), |
| 'latency_s': latency, |
| 'out_inspect': out_inspect, |
| 'out_mask': out_mask, |
| 'out_json': out_json, |
| 'raw_text_path': raw_txt_path, |
| 'parsed_ok': bool(parsed), |
| 'error': '' if parsed else 'parsed_empty_or_invalid' |
| } |
|
|
| def run_csv(csv_path: str, outroot: str, vllm: VLLMConfig, dry_run: bool=False, |
| results_csv: str=None, resume: bool=False, show_progress: bool=True) -> List[Dict[str,Any]]: |
| df = load_dataframe(csv_path) |
| done_keys: Set[str] = set() |
| header_written = False |
| if results_csv and os.path.exists(results_csv) and resume: |
| try: |
| existing = pd.read_csv(results_csv) |
| if 'key' in existing.columns: |
| done_keys = set(existing['key'].astype(str).tolist()) |
| except Exception: |
| pass |
|
|
| iterator = df.iterrows() |
| if show_progress: |
| iterator = tqdm(df.iterrows(), total=len(df), desc="Processing", ncols=100) |
|
|
| results = [] |
| for _, row in iterator: |
| rdict = {} |
| try: |
| key = _stable_key(row.to_dict()) |
| if resume and key in done_keys: |
| continue |
| rdict = process_one(row.to_dict(), vllm, outroot, dry_run=dry_run) |
| except Exception as e: |
| rdict = { |
| 'key': _stable_key(row.to_dict()), |
| 'uid': row.to_dict().get('videoid', None), |
| 'video_path': row.to_dict().get('source_video_path', None), |
| 'edit_instruction_src': row.to_dict().get('chinese_instruction', '') or row.to_dict().get('instruction', ''), |
| 'enhanced_instruction': '', |
| 'sampling_mode': row.to_dict().get('sampling_mode',''), |
| 'n_sampled': None, |
| 'indices_json': '', |
| 'mapping_json': '', |
| 'tasks_json': '', |
| 'latency_s': None, |
| 'out_inspect': '', |
| 'out_mask': '', |
| 'out_json': '', |
| 'raw_text_path': '', |
| 'parsed_ok': False, |
| 'error': str(e) |
| } |
| results.append(rdict) |
| if results_csv: |
| _append_results_csv(results_csv, rdict, header_written=header_written) |
| header_written = True |
| |
| ckpt_path = os.path.join(outroot, 'logs', 'pipeline_checkpoint.jsonl') |
| with open(ckpt_path, 'a', encoding='utf-8') as fp: |
| fp.write(json.dumps(rdict, ensure_ascii=False) + '\n') |
| return results |
|
|