Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
df25ac5
initial
TheSecurityDev Feb 18, 2026
760b337
put chunks in subfolder
TheSecurityDev Feb 18, 2026
479b20a
enhance compute_chunks to allow configurable tolerance for identical …
TheSecurityDev Feb 18, 2026
ed8e853
cleanup
TheSecurityDev Feb 19, 2026
5157313
enhance UI diff template with accordion sections for chunk display an…
TheSecurityDev Feb 19, 2026
0d987f9
h3
TheSecurityDev Feb 19, 2026
64313d8
refactor UI diff template to streamline video playback and navigation
TheSecurityDev Feb 19, 2026
c6bbd99
chunk id in name
TheSecurityDev Feb 19, 2026
293a59e
single folder
TheSecurityDev Feb 19, 2026
9c0b8b7
custom padding before/after
TheSecurityDev Feb 19, 2026
33ba597
refactor compute_chunks to remove max_same_frames parameter and adjus…
TheSecurityDev Feb 19, 2026
c97ee10
remove background preloading of video scenes from UI diff template
TheSecurityDev Feb 19, 2026
513432b
refactor padding parameters and improve frame extraction in compute_c…
TheSecurityDev Feb 19, 2026
bf91329
enhance scene switching by updating URL parameters without page reload
TheSecurityDev Feb 19, 2026
e447ff8
update scene switching to use ?selected query param instead of ?chunk
TheSecurityDev Feb 19, 2026
5b4ac7a
hide nav when no diff segments are present in UI diff template
TheSecurityDev Feb 19, 2026
53333d0
update clip padding parameters and adjust identical frame gap toleran…
TheSecurityDev Feb 19, 2026
9ed8cab
increase clip padding
TheSecurityDev Feb 19, 2026
1a0be4c
rename Section to Diff Group
TheSecurityDev Feb 19, 2026
b6a1dd8
label
TheSecurityDev Feb 19, 2026
d6f2ae3
add thumbnail generation for diff video chunks and update UI to displ…
TheSecurityDev Feb 19, 2026
541936b
fix: update print statements to include quotes around file paths in c…
TheSecurityDev Feb 19, 2026
e129d08
improve layout
TheSecurityDev Feb 19, 2026
3b3b0ff
smaller body margin
TheSecurityDev Feb 19, 2026
ceefa03
refactor
TheSecurityDev Feb 19, 2026
8464282
cleanup
TheSecurityDev Feb 19, 2026
ddb56e1
fix
TheSecurityDev Feb 19, 2026
b57aa49
simplify
TheSecurityDev Feb 19, 2026
e2aec33
formatting
TheSecurityDev Feb 19, 2026
35c741e
formatting
TheSecurityDev Feb 19, 2026
1d2b9ef
DIFF
TheSecurityDev Feb 19, 2026
1f399a0
refactor: improve layout and styling of video badges in UI diff template
TheSecurityDev Feb 19, 2026
3c146f4
fix
TheSecurityDev Feb 19, 2026
eaaa6a6
loop main
TheSecurityDev Feb 19, 2026
d0f7e46
feat: add 'none' badge style for explicit '(none)' labels in UI diff
TheSecurityDev Feb 19, 2026
f9fe3f3
fix: adjust frame range in UI diff description to be 1-based
TheSecurityDev Feb 19, 2026
585d418
improve
TheSecurityDev Feb 19, 2026
c12fbed
update
TheSecurityDev Feb 19, 2026
ce53424
lowercase
TheSecurityDev Feb 19, 2026
0f44a8a
fix: remove duration from UI diff description for clarity
TheSecurityDev Feb 19, 2026
43a3b41
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Feb 21, 2026
baf2b47
clean
TheSecurityDev Feb 21, 2026
b53bf3f
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Feb 21, 2026
673c25f
replace '(none)' with 'none'
TheSecurityDev Feb 21, 2026
40ccb19
update
TheSecurityDev Feb 21, 2026
60b4e27
frames label
TheSecurityDev Feb 21, 2026
8f50920
bolder selected border
TheSecurityDev Feb 21, 2026
fe34a5a
remove CHUNK_FRAME_GAP_TOLERANCE and merging code
TheSecurityDev Feb 21, 2026
c9807f0
clarify
TheSecurityDev Feb 21, 2026
4bbeb25
docs
TheSecurityDev Feb 21, 2026
f4f47eb
reduce diff
TheSecurityDev Feb 21, 2026
74250fb
create Chunk dataclass for types
TheSecurityDev Feb 21, 2026
13e4a0f
add divider
TheSecurityDev Feb 21, 2026
5e317cd
don't extract black clips or copy diffs
TheSecurityDev Feb 21, 2026
a756d8e
refactor
TheSecurityDev Feb 21, 2026
4dcd6a1
remove badges
TheSecurityDev Feb 21, 2026
589dab4
rename TYPE_NAV to TYPE_NAV_CLASSES for clarity
TheSecurityDev Feb 21, 2026
4b1d2af
cleanup styles
TheSecurityDev Feb 21, 2026
4de0b76
show explicit frame ranges for clarity
TheSecurityDev Feb 21, 2026
4a6ae7b
use jpg for normal video thumbnails
TheSecurityDev Feb 21, 2026
0e46bd5
formatting
TheSecurityDev Feb 21, 2026
0f459de
refactor
TheSecurityDev Feb 21, 2026
fcc4841
reduce diff
TheSecurityDev Feb 21, 2026
5ba208a
cleanup
TheSecurityDev Feb 21, 2026
7275e2d
reuse create diff video and cleanup
TheSecurityDev Feb 21, 2026
60f8bf2
refactor: use paths
TheSecurityDev Feb 21, 2026
763263e
comment
TheSecurityDev Feb 21, 2026
049bfeb
use basedir for chunk clips
TheSecurityDev Feb 21, 2026
e6b8f13
no need to log path again
TheSecurityDev Feb 21, 2026
f10ac07
log frame better
TheSecurityDev Feb 21, 2026
5fe3dc3
simplify
TheSecurityDev Feb 21, 2026
66e6d83
simplify again
TheSecurityDev Feb 21, 2026
d4373b3
rename
TheSecurityDev Feb 21, 2026
f9a2ae3
fix: don't double add basedir
TheSecurityDev Feb 21, 2026
f73cf72
correct
TheSecurityDev Feb 21, 2026
c29977c
cleanup
TheSecurityDev Feb 21, 2026
d804beb
cleanup
TheSecurityDev Feb 21, 2026
d1d89d6
remove display block
TheSecurityDev Feb 21, 2026
4045ca5
clean styles
TheSecurityDev Feb 21, 2026
6d5fadd
more cleanup styles
TheSecurityDev Feb 21, 2026
d76a6f4
refactor
TheSecurityDev Feb 21, 2026
a56a2fb
fix
TheSecurityDev Feb 21, 2026
dd0f331
fix mobile
TheSecurityDev Feb 21, 2026
8891fd6
remove nav-row
TheSecurityDev Feb 21, 2026
f5f1a77
cleanup
TheSecurityDev Feb 21, 2026
08d593b
clean
TheSecurityDev Feb 21, 2026
ff4a9a5
more improvements and cleanup
TheSecurityDev Feb 22, 2026
c900e5e
align top
TheSecurityDev Feb 22, 2026
e46415d
fix no video placeholder size
TheSecurityDev Feb 22, 2026
49a089e
fix flashing
TheSecurityDev Feb 22, 2026
f93b464
show loading
TheSecurityDev Feb 22, 2026
eca7c60
simplify
TheSecurityDev Feb 22, 2026
c112bd5
enhance video loading experience with placeholders and smooth scrolling
TheSecurityDev Feb 22, 2026
8a45615
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Feb 22, 2026
e881838
copy diff chunks in workflow
TheSecurityDev Feb 22, 2026
f7e3197
fix bug
TheSecurityDev Feb 22, 2026
5e4fea1
cleanup and refactor pass
TheSecurityDev Feb 22, 2026
9221870
parallelize video frame hashing
TheSecurityDev Feb 22, 2026
6a29d05
update styles
TheSecurityDev Feb 22, 2026
5c7eeb0
use Path instead
TheSecurityDev Feb 22, 2026
a5add3c
do diff in parallel
TheSecurityDev Feb 22, 2026
0c31cd3
process chunks in parallel
TheSecurityDev Feb 22, 2026
8eb625d
more
TheSecurityDev Feb 22, 2026
b539b24
parallelize within chunks
TheSecurityDev Feb 22, 2026
b1f4f3a
should have done this way
TheSecurityDev Feb 22, 2026
3cf5dd7
comment
TheSecurityDev Feb 22, 2026
a8b947d
rename Chunk to DiffChunk
TheSecurityDev Feb 22, 2026
ffa9e30
update comment
TheSecurityDev Feb 22, 2026
0f81204
cleanup
TheSecurityDev Feb 22, 2026
e9449c8
clean
TheSecurityDev Feb 22, 2026
cdb00c7
make selected nav index black
TheSecurityDev Feb 22, 2026
0ddcf3d
rename
TheSecurityDev Feb 22, 2026
f9ebe53
align
TheSecurityDev Feb 22, 2026
bb58c12
log found chunks count
TheSecurityDev Feb 22, 2026
1db3049
log more
TheSecurityDev Feb 22, 2026
10ef9f4
add chunks progress bar
TheSecurityDev Feb 22, 2026
aec975f
cleanup and better progress
TheSecurityDev Feb 22, 2026
385ac67
move
TheSecurityDev Feb 22, 2026
a332949
print results
TheSecurityDev Feb 23, 2026
77b3715
add todo
TheSecurityDev Feb 23, 2026
83fcc93
add note about chunk padding
TheSecurityDev Feb 23, 2026
19912d2
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Feb 23, 2026
6c8226e
fix lint
TheSecurityDev Feb 23, 2026
375660b
remove parentheses
TheSecurityDev Feb 23, 2026
daaaebd
add semicolon
TheSecurityDev Feb 23, 2026
5b2fa63
only copy if exists
TheSecurityDev Feb 23, 2026
0b58a4f
remove
TheSecurityDev Feb 23, 2026
b48fb31
remove check, swallow error
TheSecurityDev Feb 23, 2026
3063808
sort by original index
TheSecurityDev Feb 23, 2026
d2ac36c
fail if fps don't match
TheSecurityDev Feb 23, 2026
415b7c0
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Feb 23, 2026
19c782a
Merge branch 'master' of ssh://github.com/commaai/openpilot into repl…
adeebshihadeh Feb 24, 2026
b537719
no more xset
adeebshihadeh Feb 24, 2026
7427eba
simplify frame range if equal
TheSecurityDev Feb 24, 2026
e1c6234
upload everything under report
TheSecurityDev Feb 25, 2026
66c11e2
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Feb 25, 2026
0315492
update progress bar
TheSecurityDev Feb 28, 2026
a35a893
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Feb 28, 2026
4aff78c
simplify
TheSecurityDev Feb 28, 2026
61a1668
use threadpoolexecutor to handle exceptions
TheSecurityDev Feb 28, 2026
af0cc84
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Mar 11, 2026
7f59993
improve
TheSecurityDev Mar 11, 2026
d4216da
str cast not needed
TheSecurityDev Mar 11, 2026
a41c2f0
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Mar 11, 2026
a031f90
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Apr 30, 2026
0dc72e7
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev May 13, 2026
051dd94
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Jun 13, 2026
a0c535e
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Jun 17, 2026
3e9eb27
fix: ensure video paths are converted to string for ffmpeg commands
TheSecurityDev Jun 17, 2026
b72382d
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Jun 17, 2026
c88bccc
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Jun 23, 2026
5501e1c
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Jun 26, 2026
f16ed95
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Jul 25, 2026
38e5e6d
Merge branch 'master' into replay-chunks-diff-thumbnails
TheSecurityDev Aug 7, 2026
e52d8e2
refactor: remove unnecessary source
TheSecurityDev Aug 7, 2026
7210e53
remove unnecessary type
TheSecurityDev Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/ui_preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ jobs:
"${{ github.workspace }}/pr_ui/${video}_proposed.mp4" \
"${diff_name}.html" --basedir "$baseurl" --no-open || diff_exit_code=$?

cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.html" "${{ github.workspace }}/pr_ui/"
cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.mp4" "${{ github.workspace }}/pr_ui/"
cp -r "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/"* "${{ github.workspace }}/pr_ui/"

REPORT_URL="https://commaai.github.io/ci-artifacts/${diff_name}_pr_${{ github.event.number }}.html"
if [ $diff_exit_code -eq 0 ]; then
Expand Down
191 changes: 165 additions & 26 deletions openpilot/selfdrive/ui/tests/diff/diff.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
#!/usr/bin/env python3
import difflib
import json
import os
import sys
import subprocess
import webbrowser
import argparse
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm
from pathlib import Path
from dataclasses import dataclass
from typing import Literal
from openpilot.common.basedir import BASEDIR

DIFF_OUT_DIR = Path(BASEDIR) / "openpilot" / "selfdrive" / "ui" / "tests" / "diff" / "report"
HTML_TEMPLATE_PATH = Path(__file__).with_name("diff_template.html")

# extra frames of context to include before/after each diff chunk
# NOTE: adding this can make it difficult to view the exact frames that changed if they are small,
# but it could be useful for added context or to reduce any video flickering.
CHUNK_PADDING_BEFORE = 0
CHUNK_PADDING_AFTER = 0


def extract_framehashes(video_path: Path) -> list[str]:
cmd = ['ffmpeg', '-nostdin', '-i', video_path, '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-']
cmd = ['ffmpeg', '-nostdin', '-i', str(video_path), '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-']
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
hashes = []
for line in result.stdout.splitlines():
Expand All @@ -37,30 +48,142 @@ def get_video_frame_hashes(video1: Path, video2: Path) -> tuple[list[str], list[
return hashes1, hashes2


@dataclass
class DiffChunk:
"""Represents a contiguous chunk of differences between the two videos. Ranges (start-end) are inclusive."""
type: Literal['replace', 'insert', 'delete']
v1_start: int
v1_end: int
v1_count: int
v2_start: int
v2_end: int
v2_count: int


def compute_diff_chunks(hashes1: list[str], hashes2: list[str]) -> list[DiffChunk]:
"""Use difflib to compute diff chunks from the two hash lists. Returns a list of DiffChunk objects."""
matcher = difflib.SequenceMatcher(a=hashes1, b=hashes2, autojunk=False)
diff_ops = [op for op in matcher.get_opcodes() if op[0] != 'equal'] # filter out equal chunks
chunks: list[DiffChunk] = []
for tag, i1, i2, j1, j2 in diff_ops:
chunks.append(DiffChunk(
type=tag,
v1_start=i1, v1_end=i2 - 1, v1_count=i2 - i1,
v2_start=j1, v2_end=j2 - 1, v2_count=j2 - j1,
))
return chunks


def create_diff_video(video1: Path, video2: Path, output: Path) -> None:
"""Create a diff video of two clips using ffmpeg blend filter with difference mode."""
cmd = ['ffmpeg', '-nostdin', '-i', video1, '-i', video2, '-filter_complex', 'blend=all_mode=difference', '-vsync', '0', '-y', output]
cmd = ['ffmpeg', '-nostdin', '-i', str(video1), '-i', str(video2), '-filter_complex', 'blend=all_mode=difference', '-vsync', '0', '-y', str(output)]
subprocess.run(cmd, capture_output=True, check=True)


def find_frame_differences(hashes1: list[str], hashes2: list[str]) -> list[int]:
"""Compare two lists of frame hashes and return the indices of different frames."""
different_frames = []
for i, (h1, h2) in enumerate(zip(hashes1, hashes2, strict=False)):
if h1 != h2:
different_frames.append(i)
return different_frames
def get_video_fps(video_path: Path) -> float:
"""Return fps for a video file."""
cmd = ['ffprobe', '-select_streams', 'v:0', '-show_entries', 'stream=r_frame_rate', '-of', 'json', str(video_path)]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
info = json.loads(result.stdout)['streams'][0]
num, den = info['r_frame_rate'].split('/')
return int(num) / int(den)
Comment thread
TheSecurityDev marked this conversation as resolved.


def extract_clip(video_path: Path, start_frame: int, end_frame: int, output_path: Path, fps: float) -> int:
"""Extract [start_frame, end_frame] plus padding before/after into *output_path*. Returns the actual number of frames written."""
padded_start = max(0, start_frame - CHUNK_PADDING_BEFORE)
padding_before = start_frame - padded_start
total_frames = (end_frame - start_frame + 1) + padding_before + CHUNK_PADDING_AFTER
start_time = padded_start / fps
cmd = ['ffmpeg', '-nostdin', '-i', str(video_path), '-ss', f"{start_time:.6f}", '-frames:v', str(total_frames), '-vsync', '0', '-y', str(output_path)]
subprocess.run(cmd, capture_output=True, check=True)
return total_frames


def generate_html_report(videos: tuple[Path, Path], basedir: str, different_frames: list[int], frame_counts: tuple[int, int], diff_video_name: str) -> str:
def generate_thumbnail(video_path: Path, frame: int, out_path: Path, fps: float) -> None:
"""Create a single-frame thumbnail at the given frame index. File format is determined by the output extension (e.g. .jpg or .png)."""
t = frame / fps
cmd = ['ffmpeg', '-nostdin', '-i', str(video_path), '-ss', f"{t:.6f}", '-frames:v', '1', '-vsync', '0', '-y', str(out_path)]
subprocess.run(cmd, capture_output=True, check=True)


def extract_chunk_clips(video1: Path, video2: Path, chunks: list[DiffChunk], fps: float, basedir: str, folder_name: str) -> list[dict]:
"""For each diff chunk, extract clips from video1, video2, a diff video (if both are available), and a thumbnail image."""
output_dir = DIFF_OUT_DIR / folder_name
os.makedirs(output_dir, exist_ok=True)

def get_rel_path(p: Path) -> str:
""" Return path relative to the basedir."""
return os.path.join(basedir, folder_name, p.name)

def process_chunk(i: int, chunk: DiffChunk) -> dict:
chunk_type = chunk.type
v1_start, v1_end, v1_count = chunk.v1_start, chunk.v1_end, chunk.v1_count
v2_start, v2_end, v2_count = chunk.v2_start, chunk.v2_end, chunk.v2_count
clips: dict[str, str | None] = {'video1': None, 'video2': None, 'diff': None}

v1_clip = output_dir / f"{i:03d}_video1.mp4"
v2_clip = output_dir / f"{i:03d}_video2.mp4"

# Parallelize video1/video2 clip extractions within each chunk
with ThreadPoolExecutor(max_workers=2) as executor:
futures = []
if chunk_type != 'insert':
# video 1 clip
futures.append(executor.submit(extract_clip, video1, v1_start, v1_end, v1_clip, fps))
clips['video1'] = get_rel_path(v1_clip)
if chunk_type != 'delete':
# video 2 clip
futures.append(executor.submit(extract_clip, video2, v2_start, v2_end, v2_clip, fps))
clips['video2'] = get_rel_path(v2_clip)
for future in futures:
future.result()

# diff clip
diff_clip = output_dir / f"{i:03d}_diff.mp4"
if chunk_type == 'replace':
create_diff_video(v1_clip, v2_clip, diff_clip)
clips['diff'] = get_rel_path(diff_clip)

# thumbnail (middle frame of the diff content inside the clip)
padding_used = min((v1_start if chunk_type != 'insert' else v2_start), CHUNK_PADDING_BEFORE)
content_count = v1_count if chunk_type != 'insert' else v2_count
thumb_frame = padding_used + content_count // 2
thumb_ext = 'png' if chunk_type == 'replace' else 'jpg' # Use PNG for the diff thumbnails for clarity; JPG is smaller for the other thumbnails
thumb_path = output_dir / f"{i:03d}_thumb.{thumb_ext}"
thumb_source = diff_clip if chunk_type == 'replace' else (v1_clip if chunk_type == 'delete' else v2_clip)
generate_thumbnail(thumb_source, thumb_frame, thumb_path, fps)

return {
'index': i, 'type': chunk_type, 'clips': clips, 'thumb': get_rel_path(thumb_path),
'v1_start': v1_start, 'v1_end': v1_end, 'v1_count': v1_count,
'v2_start': v2_start, 'v2_end': v2_end, 'v2_count': v2_count,
}

# Process chunks in parallel with a thread pool
max_workers = min(8, len(chunks))
print(f" Processing {len(chunks)} chunks with {max_workers} threads...")
results = []
with ThreadPoolExecutor(max_workers) as executor:
futures = [executor.submit(process_chunk, i, chunk) for i, chunk in enumerate(chunks)]
for future in tqdm(as_completed(futures), total=len(futures), desc="Processing chunks", unit="chunk", disable=bool(os.getenv("CI"))):
results.append(future.result())
# results will be out of order due to parallel processing, so sort them back to the original order
clip_sets = sorted(results, key=lambda x: x['index'])

return clip_sets


def generate_html_report(
videos: tuple[Path, Path], basedir: str, diff_frame_count: int, frame_counts: tuple[int, int], diff_video_name: str, clip_sets: list[dict]
) -> str:
total_frames = max(frame_counts)
frame_delta = frame_counts[1] - frame_counts[0]
different_total = len(different_frames) + abs(frame_delta)

result_text = (
f"✅ Videos are identical! ({total_frames} frames)"
if different_total == 0
else f"❌ Found {different_total} different frames out of {total_frames} total ({different_total / total_frames * 100:.1f}%)."
if diff_frame_count == 0
else f"❌ Found {diff_frame_count} different frames out of {total_frames} total ({diff_frame_count / total_frames * 100:.1f}%)."
+ (f" Video {'2' if frame_delta > 0 else '1'} is longer by {abs(frame_delta)} frames." if frame_delta != 0 else "")
)
print(f" Results: {result_text}")
Expand All @@ -72,6 +195,7 @@ def generate_html_report(videos: tuple[Path, Path], basedir: str, different_fram
"VIDEO2_SRC": os.path.join(basedir, videos[1].name),
"DIFF_SRC": os.path.join(basedir, diff_video_name),
"RESULT_TEXT": result_text,
"CHUNKS_JSON": json.dumps(clip_sets),
}
for key, value in placeholders.items():
html = html.replace(f"${key}", value)
Expand All @@ -97,34 +221,50 @@ def main():
if missing:
parser.error(f"Video file(s) not found: {', '.join(missing)}")

diff_video_name = f"{Path(args.output).stem}.mp4" # diff video name derived from output HTML name
output_stem = Path(args.output).stem
diff_video_name = f"{output_stem}.mp4"
chunks_folder_name = f"{output_stem}-chunks"

os.makedirs(DIFF_OUT_DIR, exist_ok=True)

print("=" * 60)
print("UI VIDEO DIFF - HTML REPORT")
print("UI VIDEO DIFF REPORT")
print("=" * 60)
print(f"Video 1: {video1}")
print(f"Video 2: {video2}")
print(f"HTML output: {args.output}")
print(f"Diff video: {diff_video_name}")
print(f"Chunks dir: {chunks_folder_name}")
print()

print("[1/4] Starting full video diff generation in background thread...")
print("[1/5] Starting full video diff generation in background thread...")
diff_executor = ThreadPoolExecutor(max_workers=1)
diff_future = diff_executor.submit(create_diff_video, video1, video2, DIFF_OUT_DIR / diff_video_name)

print("[2/4] Hashing frames...")
print("[2/5] Hashing frames...")
hashes1, hashes2 = get_video_frame_hashes(video1, video2)
frame_counts = (len(hashes1), len(hashes2))
print(f" Found {frame_counts[0]} frames in video 1 and {frame_counts[1]} frames in video 2.")

print("[3/4] Finding different frames...")
different_frames = find_frame_differences(hashes1, hashes2)
print(f" Found {len(different_frames)} different frames.")

print("[4/4] Generating HTML report...")
html = generate_html_report((video1, video2), args.basedir, different_frames, frame_counts, diff_video_name)
print("[3/5] Computing diff chunks...")
chunks = compute_diff_chunks(hashes1, hashes2)
diff_frame_count = sum(max(c.v1_count, c.v2_count) for c in chunks) # TODO: We could be more precise by reporting changes, inserts, and deletions separately.
print(f" Found {len(chunks)} diff chunk(s) with a total of {diff_frame_count} different frames.")

clip_sets = []
if chunks:
print(f"[4/5] Extracting {len(chunks)} diff chunk(s)...")
print(" Getting video fps...", end=' ')
fps1, fps2 = get_video_fps(video1), get_video_fps(video2)
if abs(fps1 - fps2) > 0.01:
raise ValueError(f"Videos have different fps values: {fps1:.2f} vs {fps2:.2f}")
print(f"{fps1:.2f} fps")
clip_sets = extract_chunk_clips(video1, video2, chunks, fps1, args.basedir, chunks_folder_name)
else:
print("[4/5] No diff chunks found, skipping clip extraction.")

print("[5/5] Generating HTML report...")
html = generate_html_report((video1, video2), args.basedir, diff_frame_count, frame_counts, diff_video_name, clip_sets)

output_path = DIFF_OUT_DIR / args.output
with open(output_path, 'w') as f:
Expand All @@ -143,8 +283,7 @@ def main():
diff_future.result()
diff_executor.shutdown()

extra_frames = abs(frame_counts[0] - frame_counts[1])
return 0 if (len(different_frames) + extra_frames) == 0 else 1
return 0 if diff_frame_count == 0 else 1


if __name__ == "__main__":
Expand Down
Loading
Loading