Edl Drop FrameMedia & Graphics
Working environment structure
environment/How the environment is built and run2 filesVisibletests/Tests and grading material the agent never sees3 files
FROM python:3.11-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg fonts-dejavu-core fontconfig && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir Pillow==11.0.0
WORKDIR /app
COPY edit.edl /app/edit.edl
COPY generate_master.py /app/generate_master.py
RUN python3 /app/generate_master.py
#!/usr/bin/env python3
import os
import subprocess
import sys
from PIL import Image, ImageDraw, ImageFont
WIDTH = 480
HEIGHT = 270
DURATION = 300
BITS = 14
BLOCK_SIZE = 32
FONT_PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
def main():
fps = 30000 / 1001
num_frames = int(round(DURATION * fps))
if os.path.exists(FONT_PATH):
font = ImageFont.truetype(FONT_PATH, 24)
else:
font = ImageFont.load_default()
cmd = [
'ffmpeg',
'-y',
'-f', 'rawvideo',
'-pix_fmt', 'gray',
'-s', f'{WIDTH}x{HEIGHT}',
'-r', '30000/1001',
'-i', '-',
'-f', 'lavfi',
'-i', 'sine=frequency=1000:sample_rate=48000',
'-t', str(DURATION),
'-shortest',
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-crf', '0',
'-preset', 'ultrafast',
'-r', '30000/1001',
'-c:a', 'aac',
'-ar', '48000',
'-b:a', '128k',
'/app/master.mov',
]
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for n in range(num_frames):
img = Image.new('L', (WIDTH, HEIGHT), 128)
draw = ImageDraw.Draw(img)
for i in range(BITS):
x0 = i * BLOCK_SIZE
bit = (n >> i) & 1
fill = 255 if bit else 0
draw.rectangle(
[x0, 0, x0 + BLOCK_SIZE - 1, BLOCK_SIZE - 1],
fill=fill,
)
draw.text((40, 50), str(n), font=font, fill=255)
proc.stdin.write(img.tobytes())
proc.stdin.close()
proc.wait()
if proc.returncode != 0:
sys.exit(proc.returncode)
if __name__ == '__main__':
main()
# EDL Drop-Frame Recut
You are given a 29.97 fps drop-frame master video (`/app/master.mov`) and an edit decision list (`/app/edit.edl`). The EDL lists source in/out points using non-drop-frame timecode (counting frames as if the video were exactly 30 fps). The master uses drop-frame timecode, which skips frame numbers `00` and `01` at the top of every minute except multiples of ten.
Produce `/logs/artifacts/re_cut.mp4` containing the concatenation of the source clips described by the EDL, with every edit landing on the exact physical frame it refers to in the master. The output must preserve A/V sync and match the master's frame rate and resolution.
The EDL file is plain text. Lines beginning with `#` are comments. Each edit event line has the form:
```
<event> <source_in> <source_out>
```
Timecodes are `HH:MM:SS:FF` with two-digit frames.
Requirements:
- Output file: `/logs/artifacts/re_cut.mp4`
- Frame rate: `30000/1001` fps (≈29.97)
- Resolution: same as the master
- A/V sync within ±40 ms across the whole output
- The first frame of each edit event must match the physical frame indicated by resolving the drop-frame/non-drop-frame mismatch.
schema_version = "1.2"
artifacts = ["/logs/artifacts/re_cut.mp4"]
[metadata]
flags = []
sub_category = "media_processing"
author_name = "Terminal Bench"
author_email = "bench@example.com"
author_organization = "Terminal Bench"
difficulty_explanation = "The EDL lists source in/out points using non-drop-frame timecode (30 fps counting), while the 29.97 fps master uses drop-frame timecode. The agent must convert each edit point to the correct physical frame index by accounting for the two dropped frame numbers at every non-ten-minute boundary; a naive linear conversion drifts cumulatively. The master video is synthetically generated with a frame-index watermark, but the drop-frame semantics and EDL conversion are identical to real-world broadcast/post-production workflows. This is a realistic challenge for a post-production engineer or broadcast tooling developer who works with SMPTE timecode and edit decision lists."
solution_explanation = "[withheld to keep the benchmark uncontaminated]"
verification_explanation = "[withheld to keep the benchmark uncontaminated]"
category = "media"
tags = ["video", "edl", "drop-frame", "timecode", "ffmpeg"]
expert_time_estimate_hours = 2.5
[verifier]
environment_mode = "separate"
timeout_sec = 300
[agent]
timeout_sec = 3600
[environment]
build_timeout_sec = 600
FROM python:3.11-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /tests
COPY edit.edl /tests/edit.edl
COPY test_outputs.py /tests/test_outputs.py
COPY test.sh /tests/test.sh
RUN chmod +x /tests/test.sh
#!/bin/bash
mkdir -p /logs/verifier
if python3 /tests/test_outputs.py; then
echo 1 > /logs/verifier/reward.txt
else
echo 0 > /logs/verifier/reward.txt
fi
#!/usr/bin/env python3
import json, math, os, subprocess, sys
ARTIFACT = '/logs/artifacts/re_cut.mp4'
EDL = '/tests/edit.edl'
# Watermark parameters (must match the master generator).
BITS = 14
BLOCK_SIZE = 32
def fail(msg):
print(f'FAIL: {msg}')
sys.exit(1)
def tc_to_ndf(tc):
h, m, s, f = map(int, tc.split(':'))
return (((h * 60) + m) * 60 + s) * 30 + f
def ndf_to_df(n):
minutes = n // 1800
dropped = 2 * (minutes - minutes // 10)
return n - dropped
def parse_edl(path):
expected = []
with open(path) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split()
if len(parts) < 3:
continue
s = ndf_to_df(tc_to_ndf(parts[1]))
e = ndf_to_df(tc_to_ndf(parts[2]))
expected.extend(range(s, e))
return expected
def ffprobe_json(args):
cmd = ['ffprobe', '-v', 'error', '-of', 'json'] + args
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return json.loads(proc.stdout)
def get_video_info(path):
j = ffprobe_json(['-select_streams', 'v:0', '-show_entries', 'stream=width,height,avg_frame_rate,nb_frames', path])
streams = j.get('streams', [])
if not streams:
fail('no video stream found')
return streams[0]
def get_audio_samples(path):
cmd = ['ffmpeg', '-v', 'error', '-i', path, '-vn', '-ar', '48000', '-ac', '1', '-f', 's16le', '-']
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return len(proc.stdout) // 2
def decode_frame_indices(path, width, height):
cmd = ['ffmpeg', '-v', 'error', '-i', path, '-an', '-vf', 'format=gray', '-f', 'rawvideo', '-']
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
data = proc.stdout
frame_size = width * height
if len(data) % frame_size != 0:
fail(f'decoded raw video size {len(data)} not a multiple of {frame_size}')
count = len(data) // frame_size
indices = []
for f in range(count):
base = f * frame_size
avgs = []
for i in range(BITS):
x0 = i * BLOCK_SIZE
block_sum = 0
for y in range(BLOCK_SIZE):
row = base + y * width
for x in range(BLOCK_SIZE):
block_sum += data[row + x0 + x]
avgs.append(block_sum / (BLOCK_SIZE * BLOCK_SIZE))
# Use an adaptive threshold per frame so mild re-encoding or
# luma shifts do not corrupt the binary watermark.
lo = min(avgs)
hi = max(avgs)
mid = (lo + hi) / 2 if hi > lo else lo
idx = 0
for i, avg in enumerate(avgs):
if avg > mid:
idx |= (1 << i)
indices.append(idx)
return indices
def main():
if not os.path.exists(ARTIFACT):
fail('re_cut.mp4 not found in /logs/artifacts')
expected = parse_edl(EDL)
info = get_video_info(ARTIFACT)
width = int(info.get('width', 0))
height = int(info.get('height', 0))
if width != 480 or height != 270:
fail(f'wrong resolution: {width}x{height}, expected 480x270')
rate = info.get('avg_frame_rate', '')
if '/' in rate:
num, den = rate.split('/')
fps = float(num) / float(den)
else:
fps = float(rate)
if abs(fps - 30000.0 / 1001.0) > 0.001:
fail(f'wrong frame rate: {rate}')
actual = decode_frame_indices(ARTIFACT, width, height)
if len(actual) != len(expected):
fail(f'frame count mismatch: got {len(actual)}, expected {len(expected)}')
for i, (a, e) in enumerate(zip(actual, expected)):
if abs(a - e) > 2:
fail(f'frame {i} mismatch: decoded index {a}, expected {e}')
expected_dur = len(expected) * 1001.0 / 30000.0
expected_samples = int(round(expected_dur * 48000))
try:
actual_samples = get_audio_samples(ARTIFACT)
except subprocess.CalledProcessError:
fail('no audio stream or audio decoding failed')
if abs(actual_samples - expected_samples) > 1920:
fail(f'audio sample mismatch: {actual_samples} vs {expected_samples}')
print('PASS')
if __name__ == '__main__':
main()