File size: 4,761 Bytes
00c7586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from pathlib import Path
import argparse
import json
import re
import sys
from .api import DataEnginePipeline

def parser():
    p = argparse.ArgumentParser(description='FLORA: TempSeg -> Captioner with native video preprocessing.')
    source = p.add_mutually_exclusive_group(required=True)
    source.add_argument('--video', type=Path, help='Main/egocentric local video.')
    source.add_argument('--input-jsonl', type=Path, help='One video job per line; paths relative to this JSONL.')
    p.add_argument('--profile', choices=['human', 'yam', 'dex', 'tianji'], default='human')
    p.add_argument('--left-wrist', type=Path)
    p.add_argument('--right-wrist', type=Path)
    p.add_argument('--yam32', action='store_true', help='Use the native 32-second three-view YAM temporal profile.')
    p.add_argument('--instruction', default='', help='Optional episode instruction; blank by default.')
    p.add_argument('--output', type=Path, help='Single-video result; default annotations.json.')
    p.add_argument('--output-dir', type=Path, help='Required with --input-jsonl for inference.')
    p.add_argument('--validate-only', action='store_true', help='Check media/timestamps on CPU; no weight downloads or generation.')
    p.add_argument('--include-audit', action='store_true', help='Include temporal/model-input audit records in the result.')
    p.add_argument('--overwrite', action='store_true', help='Explicitly allow replacing existing output files.')
    return p

def jobs(args, p):
    if args.video:
        if args.output_dir:
            p.error('--output-dir is for JSONL jobs; use --output for a single video')
        return [(dict(video=args.video, profile=args.profile, left_wrist=args.left_wrist, right_wrist=args.right_wrist, yam32=args.yam32, instruction=args.instruction), args.output or Path('annotations.json'))]
    if args.output or args.left_wrist or args.right_wrist or args.yam32 or args.instruction or (args.profile != 'human'):
        p.error('Put per-video profile/cameras/instruction in JSONL rows.')
    if not args.output_dir and (not args.validate_only):
        p.error('--output-dir is required with --input-jsonl')
    source = args.input_jsonl.expanduser().resolve(strict=True)
    items, ids = ([], set())
    allowed = {'id', 'video', 'profile', 'left_wrist', 'right_wrist', 'yam32', 'instruction'}
    for line_number, line in enumerate(source.read_text().splitlines(), 1):
        if not line.strip():
            continue
        row = json.loads(line)
        if not isinstance(row, dict) or set(row) - allowed or 'video' not in row:
            raise ValueError(f'Invalid JSONL job on line {line_number}.')
        name = row.pop('id', f'video_{line_number:06d}')
        if not isinstance(name, str) or not re.fullmatch('[A-Za-z0-9][A-Za-z0-9_.-]{0,95}', name) or name in ids:
            raise ValueError(f'Invalid or duplicate job id on line {line_number}.')
        ids.add(name)
        for key in ['video', 'left_wrist', 'right_wrist']:
            if row.get(key) is not None:
                if not isinstance(row[key], str) or not row[key].strip():
                    raise ValueError(f'Invalid camera path on JSONL line {line_number}.')
                value = Path(row[key]).expanduser()
                row[key] = value if value.is_absolute() else source.parent / value
        items.append((row, (args.output_dir or Path('.')) / (name + '.json')))
    if not items:
        raise ValueError('The JSONL contains no video jobs.')
    return items

def main(argv=None):
    p = parser()
    args = p.parse_args(argv)
    try:
        items = jobs(args, p)
        if not args.validate_only and (not args.overwrite):
            for _, output in items:
                if output.exists():
                    raise FileExistsError(f'Output already exists: {output}. Use --overwrite to replace it.')
        engine = DataEnginePipeline()
        for kwargs, output in items:
            if args.validate_only:
                print(json.dumps(engine.inspect_inputs(**kwargs), allow_nan=False))
                continue
            result = engine.annotate(**kwargs, include_audit=args.include_audit, progress=lambda fraction, desc='': print(f'{fraction:.0%} {desc}', file=sys.stderr, flush=True))
            payload = json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False) + '\n'
            output.parent.mkdir(parents=True, exist_ok=True)
            with output.open('w' if args.overwrite else 'x') as f:
                f.write(payload)
            print(json.dumps({'output': str(output), 'segments': len(result['segments'])}))
        return 0
    except (OSError, ValueError, RuntimeError) as error:
        print(f'FLORA inference failed: {error}', file=sys.stderr)
        return 1