refactor reviews for extra info reporting even on happy path

condor-scripts
Mike Lang 3 years ago
parent c1465f49a6
commit e4700e6e4f

@ -10,7 +10,7 @@ import signal
import gevent
import gevent.backdoor
import prometheus_client as prom
from flask import Flask, url_for, request, abort, redirect, Response
from flask import Flask, url_for, request, abort, Response
from gevent.pywsgi import WSGIServer
from common import dateutil, get_best_segments, rough_cut_segments, smart_cut_segments, fast_cut_segments, full_cut_segments, PromLogCountsHandler, install_stacksampler
@ -373,8 +373,12 @@ def review_race(match_id, race_number):
finish_range = map(float, request.args.get('finish_range', '-5,10').split(','))
racer1_start = float(request.args['racer1_start']) if 'racer1_start' in request.args else None
racer2_start = float(request.args['racer2_start']) if 'racer2_start' in request.args else None
path_to_url = lambda path: os.path.join(app.static_url_path, os.path.relpath(path, app.static_folder))
link = lambda url, text: '<a href="{}">{}</a>'.format(url, text)
try:
review_path, suspect_starts = review(
info = review(
match_id, race_number, app.static_folder, app.condor_db, start_range, finish_range,
racer1_start, racer2_start,
)
@ -389,32 +393,29 @@ def review_race(match_id, race_number):
"Please check start video and adjust start_range or set racer{}_start: {}\n"
"Note timestamps in that video are only valid for the current start_range.\n"
).format(
e, e.racer_number, os.path.join(app.static_url_path, os.path.relpath(e.path, app.static_folder))
e, e.racer_number, path_to_url(e.path),
), 400
relative_path = os.path.relpath(review_path, app.static_folder)
review_url = os.path.join(app.static_url_path, relative_path)
if suspect_starts:
# warn and link via html
return "\n".join([
"<html>",
"<body>",
"Review succeeded, but start times are uncertain. Please check start videos:</br>",
"\n".join('<a href="{}">{}</a></br>'.format(
os.path.join(app.static_url_path, os.path.relpath(e.path, app.static_folder)),
e,
) for e in suspect_starts),
"If all is well, the review is available",
'<a href="{}">HERE</a>'.format(review_url),
"</body>",
"</html>",
])
else:
# happy path, redirect
response = redirect(review_url)
response.autocorrect_location_header = False
return response
body = []
for racer_info in info["racers"]:
if "start_path" in racer_info:
body.append("Detected {info[name]} start at {info[offset]} of {link}".format(
info=racer_info,
link=link(path_to_url(racer_info["start_path"]), "start video"),
))
if racer_info["start_holes"]:
body.append("WARNING: Start video for {} was missing some data and may be inaccurate".format(racer_info["name"]))
if len(racer_info["starts"]) > 1:
body.append("WARNING: Detected multiple possible start times for {}: {}".format(
racer_info["name"], ", ".join(racer_info["starts"])
))
else:
body.append("Using given start of {info[offset]} for {info[name]}".format(info=racer_info))
if racer_info["finish_holes"]:
body.append("WARNING: Result video for {} was missing some data and may be inaccurate".format(racer_info["name"]))
body.append(link(path_to_url(info["result_path"]), "Result video available here"))
return "<html><body>{}</body></html>".format("</br>\n".join(body))
def main(host='0.0.0.0', port=8000, base_dir='.', backdoor_port=0, condor_db=None):

@ -1,5 +1,6 @@
import datetime
import json
import logging
import os
import re
@ -24,16 +25,12 @@ class RaceNotFound(Exception):
class CantFindStart(Exception):
def __init__(self, racer, racer_number, found, path):
def __init__(self, racer, racer_number, path):
self.racer = racer
self.racer_number = racer_number
self.found = found
self.path = path
def __str__(self):
if self.found > 0:
return "Found multiple ({}) possible start points for racer {} ({})".format(self.found, self.racer_number, self.racer)
else:
return "Failed to find start point for racer {} ({})".format(self.racer_number, self.racer)
return "Failed to find start point for racer {} ({})".format(self.racer_number, self.racer)
def ts(dt):
@ -41,12 +38,14 @@ def ts(dt):
def cut_to_file(logger, filename, base_dir, stream, start, end, variant='source', frame_counter=False):
"""Returns boolean of whether cut video contained holes"""
logger.info("Cutting {}".format(filename))
segments = get_best_segments(
os.path.join(base_dir, stream, variant).lower(),
start, end,
)
if None in segments:
contains_holes = None in segments
if contains_holes:
logger.warning("Cutting {} ({} to {}) but it contains holes".format(filename, ts(start), ts(end)))
if not segments or set(segments) == {None}:
raise NoSegments("Can't cut {} ({} to {}): No segments".format(filename, ts(start), ts(end)))
@ -66,6 +65,7 @@ def cut_to_file(logger, filename, base_dir, stream, start, end, variant='source'
with open(filename, 'w') as f:
for chunk in full_cut_segments(segments, start, end, filter_args + encoding_args):
f.write(chunk)
return contains_holes
def add_range(base, range):
@ -86,6 +86,21 @@ def review(
match_id, race_number, base_dir, db_url, start_range=(0, 10), finish_range=(-5, 10),
racer1_start=None, racer2_start=None,
):
"""Cuts a review, returning the following structure:
{
racers: [
{
name: racer name
start_path: path to start video, omitted if start given
start_holes: bool, whether the start video contained holes, omitted if start given
starts: [start times within video], omitted if start given
offset: final time offset used
finish_holes: bool, whether the finish video contained holes
} for each racer
]
result_path: path to result video
}
"""
logger = logging.getLogger("review").getChild("{}-{}".format(match_id, race_number))
conn = conn_from_url(db_url)
@ -129,22 +144,29 @@ def review(
os.makedirs(output_dir)
result_name = "review_{}.mp4".format(cache_str)
result_path = os.path.join(output_dir, result_name)
if os.path.exists(result_path):
cache_path = os.path.join(output_dir, "cache_{}.json".format(cache_str))
if os.path.exists(result_path) and os.path.exists(cache_path):
logger.info("Result already exists for {}, reusing".format(result_path))
return result_path, []
with open(cache_path) as f:
return json.load(f)
finish_paths = []
suspect_starts = []
result_info = {
"result_path": result_path
}
for racer_index, (racer, time_offset) in enumerate(((racer1, racer1_start), (racer2, racer2_start))):
nonce = str(uuid4())
racer_number = racer_index + 1
racer_info = {"name": racer}
result_info.setdefault("racers", []).append(racer_info)
if time_offset is None:
start_path = os.path.join(output_dir, "start-{}-{}.mp4".format(racer_number, nonce))
start_path = os.path.join(output_dir, "start-{}-{}-{}.mp4".format(racer_number, cache_str, nonce))
racer_info["start_path"] = start_path
logger.info("Cutting start for racer {} ({})".format(racer_number, racer))
start_start, start_end = add_range(start, start_range)
cut_to_file(logger, start_path, base_dir, racer, start_start, start_end)
racer_info["start_holes"] = cut_to_file(logger, start_path, base_dir, racer, start_start, start_end)
logger.info("Running blackdetect")
args = [
@ -161,19 +183,25 @@ def review(
line for line in re.split('[\r\n]', err.strip())
if line.startswith('[blackdetect @ ')
]
if len(lines) != 1:
found = len(lines)
logger.warning("Unable to detect start (expected 1 black interval, but found {}), re-cutting with timestamps".format(found))
cut_to_file(logger, start_path, base_dir, racer, start_start, start_end, frame_counter=True)
error = CantFindStart(racer, racer_number, found, start_path)
if not lines:
raise error
# try to continue by picking first
suspect_starts.append(error)
line = lines[0]
black_end = line.split(' ')[4]
assert black_end.startswith('black_end:')
time_offset = float(black_end.split(':')[1])
starts = []
racer_info["starts"] = starts
for line in lines:
black_end = line.split(' ')[4]
assert black_end.startswith('black_end:')
starts.append(float(black_end.split(':')[1]))
# unconditionally re-cut a start, this time with frame counter.
# TODO avoid the repeated work and do cut + blackdetect + frame counter all in one pass
cut_to_file(logger, start_path, base_dir, racer, start_start, start_end, frame_counter=True)
if not starts:
raise CantFindStart(racer, racer_number, start_path)
if len(starts) > 1:
logging.warning("Found multiple starts, picking first: {}".format(starts))
time_offset = starts[0]
racer_info["offset"] = time_offset
time_offset = datetime.timedelta(seconds=start_range[0] + time_offset)
# start each racer's finish video at TIME_OFFSET later, so they are the same
@ -183,7 +211,7 @@ def review(
finish_path = os.path.join(output_dir, "finish-{}-{}.mp4".format(racer_number, nonce))
finish_paths.append(finish_path)
logger.info("Got time offset of {}, cutting finish at finish_base {}".format(time_offset, finish_base))
cut_to_file(logger, finish_path, base_dir, racer, finish_start, finish_end)
racer_info["finish_holes"] = cut_to_file(logger, finish_path, base_dir, racer, finish_start, finish_end)
temp_path = "{}.{}.mp4".format(result_path, str(uuid4()))
args = ['ffmpeg']
@ -195,13 +223,14 @@ def review(
'-y', temp_path,
]
cache_temp = "{}.{}.json".format(cache_path, str(uuid4()))
with open(cache_temp, 'w') as f:
f.write(json.dumps(result_info))
os.rename(cache_temp, cache_path)
logger.info("Cutting final result")
subprocess.check_call(args)
# atomic rename so that if result_path exists at all, we know it is complete and correct
# don't do this if we have a suspect start though, as the cached result wouldn't know.
if suspect_starts:
result_path = temp_path
else:
os.rename(temp_path, result_path)
logger.info("Review done, suspect starts = {}".format(len(suspect_starts)))
return result_path, suspect_starts
os.rename(temp_path, result_path)
logger.info("Review done: {}".format(result_info))
return result_info

Loading…
Cancel
Save