Skip to content

Instantly share code, notes, and snippets.

@coderobe
Last active May 18, 2026 11:26
Show Gist options
  • Select an option

  • Save coderobe/6b52be04e6122756ee3f453824b812c2 to your computer and use it in GitHub Desktop.

Select an option

Save coderobe/6b52be04e6122756ee3f453824b812c2 to your computer and use it in GitHub Desktop.
Experiments on whether sparse FFmpeg HLS/fMP4 transcodes preserve enough information to be able to splice segments from separate runs together cleanly.
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# ffmpeg_sparse_transcode_test.rb input_audio [--verbose]
#
# Check whether sparse FFmpeg HLS/fMP4 transcodes preserve enough timing
# information to splice segments from separate runs together.
#
# Use --verbose for ffmpeg logging.
#
require "fileutils"
require "json"
require "open3"
require "pathname"
require "tempfile"
VERBOSE = ARGV.delete("--verbose")
INPUT = ARGV[0]
abort("usage: ruby #{$PROGRAM_NAME} input_audio [--verbose]") unless INPUT
abort("input file not found: #{INPUT}") unless File.exist?(INPUT)
SEGMENT_DURATION = 6
SEAM_WINDOW_SECONDS = 0.05
BASELINE_WINDOW_SECONDS = 0.10
BASELINE_GAP_SECONDS = 0.025
TRANSIENT_BUCKETS_MS = [
[-10, 0],
[0, 5],
[5, 10],
[10, 20],
[20, 40]
].freeze
OUTPUT_ROOT = Pathname.new("ffmpeg_sparse_test_output")
FULL_DIR = OUTPUT_ROOT.join("full")
VARIANTS_DIR = OUTPUT_ROOT.join("variants")
FileUtils.rm_rf(OUTPUT_ROOT)
FileUtils.mkdir_p(FULL_DIR)
FileUtils.mkdir_p(VARIANTS_DIR)
def monotonic
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
def parse_playlist(path)
segments = []
current_duration = nil
File.readlines(path).each do |line|
line = line.strip
if line.start_with?("#EXTINF:")
current_duration =
line
.sub("#EXTINF:", "")
.sub(",", "")
.to_f
elsif !line.start_with?("#") && !line.empty?
segments << {
file: line,
duration: current_duration
}
current_duration = nil
end
end
segments
end
def ffprobe_json(path, entries: nil)
cmd = ["ffprobe", "-v", "quiet", "-of", "json"]
cmd += ["-show_entries", entries] if entries
cmd << path.to_s
stdout, stderr, status = Open3.capture3(*cmd)
abort("ffprobe failed:\n#{stderr}") unless status.success?
JSON.parse(stdout)
end
def monitor_ffmpeg(command, output_dir)
puts(command.join(" ")) if VERBOSE
first_write = nil
known_files = {}
start_time = monotonic
_stdin, _stdout, stderr, wait_thr = Open3.popen3(*command.map(&:to_s))
monitor_thread = Thread.new do
until wait_thr.join(0.05)
Dir.glob("#{output_dir}/**/*").each do |path|
next unless File.file?(path)
next if known_files[path]
known_files[path] = true
first_write ||= monotonic
end
end
end
ffmpeg_log = +""
stderr.each_line do |line|
ffmpeg_log << line
puts line if VERBOSE
end
status = wait_thr.value
monitor_thread.join
finish_time = monotonic
unless status.success?
puts ffmpeg_log unless VERBOSE
abort("ffmpeg failed")
end
{
runtime: finish_time - start_time,
first_write: first_write ? first_write - start_time : nil
}
end
def build_ffmpeg_command(input:, output_dir:, seek_time: nil, copyts: false)
playlist = output_dir.join("stream.m3u8")
init_seg = output_dir.join("init.mp4")
seg_pattern = output_dir.join("seg_%05d.m4s")
cmd = [
"ffmpeg",
"-y",
"-fflags", "+bitexact",
"-flags:a", "+bitexact",
"-avoid_negative_ts", "disabled"
]
cmd << "-copyts" if copyts
if seek_time
cmd += ["-ss", format("%.6f", seek_time)]
end
cmd += [
"-i", input.to_s,
"-map", "0:a:0",
"-c:a", "libopus",
"-b:a", "192k",
"-vbr", "on",
"-compression_level", "10",
"-application", "audio",
"-f", "hls",
"-hls_time", SEGMENT_DURATION.to_s,
"-hls_playlist_type", "vod",
"-hls_segment_type", "fmp4",
"-hls_flags", "independent_segments",
"-hls_fmp4_init_filename", init_seg.basename.to_s,
"-hls_segment_filename", seg_pattern.to_s,
"-movflags", "+frag_keyframe+empty_moov+default_base_moof",
playlist.to_s
]
cmd
end
def audio_sample_rate(path)
json = ffprobe_json(path, entries: "stream=sample_rate")
stream = json.fetch("streams").fetch(0)
Integer(stream.fetch("sample_rate"))
end
def audio_stream_info(path)
json = ffprobe_json(path, entries: "stream=sample_rate,channels")
stream = json.fetch("streams").fetch(0)
{
sample_rate: Integer(stream.fetch("sample_rate")),
channels: Integer(stream.fetch("channels"))
}
end
def ffprobe_packets(path, playlist: false)
cmd = ["ffprobe", "-v", "quiet"]
if playlist
cmd += [
"-allowed_extensions", "ALL",
"-protocol_whitelist", "file,crypto,data"
]
end
cmd += [
"-select_streams", "a:0",
"-show_packets",
"-show_entries", "packet=pts_time,duration_time",
"-of", "json",
path.to_s
]
stdout, stderr, status = Open3.capture3(*cmd)
abort("ffprobe failed:\n#{stderr}") unless status.success?
JSON.parse(stdout).fetch("packets", [])
end
def decode_pcm_samples(path, sample_rate:, channels:, playlist: false)
cmd = ["ffmpeg", "-v", "error"]
if playlist
cmd += [
"-allowed_extensions", "ALL",
"-protocol_whitelist", "file,crypto,data"
]
end
cmd += [
"-i", path.to_s,
"-map", "0:a:0",
"-f", "s16le",
"-acodec", "pcm_s16le",
"-ar", sample_rate.to_s,
"-ac", channels.to_s,
"-"
]
stdout, stderr, status = Open3.capture3(*cmd)
abort("ffmpeg decode failed:\n#{stderr}") unless status.success?
stdout.unpack("s<*")
end
def decoded_audio_md5(init_path, segment_path)
Tempfile.create(["ffmpeg_sparse_decode", ".mp4"]) do |file|
file.binmode
file.write(File.binread(init_path))
file.write(File.binread(segment_path))
file.flush
stdout, stderr, status = Open3.capture3(
"ffmpeg",
"-v", "error",
"-i", file.path,
"-map", "0:a:0",
"-f", "md5",
"-"
)
abort("ffmpeg decode failed:\n#{stderr}") unless status.success?
stdout.strip.sub(/\AMD5=/, "")
end
end
def rewrite_tfdt(segment_path, output_path, base_decode_time)
data = File.binread(segment_path)
bytes = data.bytes
patched = false
parse_boxes = lambda do |start_pos, end_pos, recurse|
pos = start_pos
while pos + 8 <= end_pos
size = bytes[pos, 4].pack("C*").unpack1("N")
type = bytes[pos + 4, 4].pack("C*")
header_size = 8
if size == 1
size = bytes[pos + 8, 8].pack("C*").unpack1("Q>")
header_size = 16
elsif size == 0
size = end_pos - pos
end
box_end = pos + size
if type == "tfdt"
version = bytes[pos + header_size]
value_offset = pos + header_size + 4
replacement =
if version == 1
[base_decode_time].pack("Q>").bytes
else
[base_decode_time].pack("N").bytes
end
replacement.each_with_index do |byte, index|
bytes[value_offset + index] = byte
end
patched = true
elsif %w[moof traf].include?(type)
recurse.call(pos + header_size, box_end, recurse)
end
pos = box_end
end
end
parse_boxes.call(0, bytes.length, parse_boxes)
abort("tfdt box not found in #{segment_path}") unless patched
output_path.binwrite(bytes.pack("C*"))
end
def region_diff_metrics(reference_samples, test_samples, channels:, start_frame:, frame_count:)
start_index = start_frame * channels
sample_count = frame_count * channels
reference_slice = reference_samples.slice(start_index, sample_count)
test_slice = test_samples.slice(start_index, sample_count)
abort("sample window too short for seam analysis") unless reference_slice && test_slice
abort("sample window size mismatch for seam analysis") unless reference_slice.length == test_slice.length
sum_abs = 0.0
sum_sq = 0.0
max_abs = 0
reference_slice.zip(test_slice) do |ref_sample, test_sample|
delta = (test_sample - ref_sample).abs
sum_abs += delta
sum_sq += delta * delta
max_abs = delta if delta > max_abs
end
count = reference_slice.length
{
mean_abs: sum_abs / count,
rms: Math.sqrt(sum_sq / count),
max_abs: max_abs
}
end
def seam_jump(samples, channels:, seam_frame:)
previous_index = (seam_frame - 1) * channels
current_index = seam_frame * channels
channels.times.sum do |channel|
(samples[current_index + channel] - samples[previous_index + channel]).abs
end.fdiv(channels)
end
def analyze_seam(reference_playlist:, merged_playlist:, seam_frame:, sample_rate:, channels:)
reference_samples =
decode_pcm_samples(
reference_playlist,
sample_rate: sample_rate,
channels: channels,
playlist: true
)
merged_samples =
decode_pcm_samples(
merged_playlist,
sample_rate: sample_rate,
channels: channels,
playlist: true
)
total_frames = [
reference_samples.length / channels,
merged_samples.length / channels
].min
reference_samples = reference_samples.first(total_frames * channels)
merged_samples = merged_samples.first(total_frames * channels)
seam_window_frames = (sample_rate * SEAM_WINDOW_SECONDS).round
baseline_window_frames = (sample_rate * BASELINE_WINDOW_SECONDS).round
baseline_gap_frames = (sample_rate * BASELINE_GAP_SECONDS).round
seam_half_frames = seam_window_frames / 2
pre_start = seam_frame - baseline_gap_frames - baseline_window_frames
seam_start = seam_frame - seam_half_frames
post_start = seam_frame + baseline_gap_frames
abort("not enough decoded audio for seam analysis") if pre_start.negative?
abort("not enough decoded audio after seam for analysis") if post_start + baseline_window_frames > total_frames
abort("seam window exceeds decoded audio") if seam_start.negative? || seam_start + seam_window_frames > total_frames
pre_metrics =
region_diff_metrics(
reference_samples,
merged_samples,
channels: channels,
start_frame: pre_start,
frame_count: baseline_window_frames
)
seam_metrics =
region_diff_metrics(
reference_samples,
merged_samples,
channels: channels,
start_frame: seam_start,
frame_count: seam_window_frames
)
post_metrics =
region_diff_metrics(
reference_samples,
merged_samples,
channels: channels,
start_frame: post_start,
frame_count: baseline_window_frames
)
baseline_rms = (pre_metrics[:rms] + post_metrics[:rms]) / 2.0
baseline_mean_abs = (pre_metrics[:mean_abs] + post_metrics[:mean_abs]) / 2.0
reference_jump = seam_jump(reference_samples, channels: channels, seam_frame: seam_frame)
merged_jump = seam_jump(merged_samples, channels: channels, seam_frame: seam_frame)
buckets =
TRANSIENT_BUCKETS_MS.map do |from_ms, to_ms|
start_frame = seam_frame + (sample_rate * from_ms / 1000.0).round
end_frame = seam_frame + (sample_rate * to_ms / 1000.0).round
bucket_frames = end_frame - start_frame
next if start_frame.negative? || end_frame > total_frames || bucket_frames <= 0
[
"#{from_ms}..#{to_ms}ms",
region_diff_metrics(
reference_samples,
merged_samples,
channels: channels,
start_frame: start_frame,
frame_count: bucket_frames
)
]
end.compact.to_h
early_transient_rms = ((buckets["0..5ms"] || {})[:rms].to_f + (buckets["5..10ms"] || {})[:rms].to_f) / 2.0
late_transient_rms = (buckets["20..40ms"] || {})[:rms].to_f
{
pre: pre_metrics,
seam: seam_metrics,
post: post_metrics,
buckets: buckets,
baseline_rms: baseline_rms,
baseline_mean_abs: baseline_mean_abs,
seam_rms_ratio: baseline_rms.zero? ? 1.0 : seam_metrics[:rms] / baseline_rms,
seam_mean_abs_ratio: baseline_mean_abs.zero? ? 1.0 : seam_metrics[:mean_abs] / baseline_mean_abs,
reference_jump: reference_jump,
merged_jump: merged_jump,
jump_ratio: reference_jump.zero? ? 1.0 : merged_jump / reference_jump,
early_transient_rms_ratio: baseline_rms.zero? ? 1.0 : early_transient_rms / baseline_rms,
transient_decay_ratio: late_transient_rms.zero? ? 1.0 : early_transient_rms / late_transient_rms
}
end
def classify_seam(seam_analysis)
if seam_analysis[:seam_rms_ratio] <= 1.25 &&
seam_analysis[:seam_mean_abs_ratio] <= 1.25 &&
seam_analysis[:jump_ratio] <= 1.5
"no seam-specific artifact detected"
elsif seam_analysis[:jump_ratio] >= 2.0
"hard discontinuity / click-like jump at the seam"
elsif seam_analysis[:early_transient_rms_ratio] >= 2.0 &&
seam_analysis[:transient_decay_ratio] >= 1.5 &&
seam_analysis[:jump_ratio] < 1.5
"short seam-local transient (likely encoder-state mismatch), not a single-sample click"
else
"seam-region mismatch detected"
end
end
def mode_target_segment_index(mode, suffix)
mode.fetch(:"#{suffix}_selected_segment_index", 0)
end
def mode_preroll_segments(mode, suffix)
mode.fetch(:"#{suffix}_preroll_segments", 0)
end
def fragment_packet_stats(init_path, segment_path, tmp_path)
File.binwrite(
tmp_path,
File.binread(init_path) + File.binread(segment_path)
)
packets = ffprobe_packets(tmp_path)
packet_stats(packets)
end
def packet_stats(packets)
abort("no packets found") if packets.empty?
first_pts = packets.first.fetch("pts_time").to_f
last_pts = packets.last.fetch("pts_time").to_f
last_duration = packets.last.fetch("duration_time").to_f
deltas =
packets.each_cons(2).map do |left, right|
right.fetch("pts_time").to_f -
(left.fetch("pts_time").to_f + left.fetch("duration_time").to_f)
end
{
first_pts: first_pts,
end_pts: last_pts + last_duration,
packet_count: packets.length,
min_delta: deltas.min || 0.0,
max_delta: deltas.max || 0.0
}
end
def write_merge_playlist(path, init_path:, segment_paths:)
lines = [
"#EXTM3U",
"#EXT-X-VERSION:7",
"#EXT-X-TARGETDURATION:#{SEGMENT_DURATION}",
"#EXT-X-PLAYLIST-TYPE:VOD",
"#EXT-X-MAP:URI=\"#{init_path.expand_path}\""
]
segment_paths.each do |segment_path|
lines << "#EXTINF:#{format('%.6f', SEGMENT_DURATION.to_f)},"
lines << segment_path.expand_path.to_s
end
lines << "#EXT-X-ENDLIST"
path.write(lines.join("\n") + "\n")
end
def reference_offset_for(segments, index)
segments
.take(index)
.sum { |segment| segment[:duration] }
end
reference_metrics = monitor_ffmpeg(
build_ffmpeg_command(
input: INPUT,
output_dir: FULL_DIR
),
FULL_DIR
)
reference_segments = parse_playlist(FULL_DIR.join("stream.m3u8"))
abort("not enough segments generated") if reference_segments.size < 4
first_index = reference_segments.size / 2
second_index = first_index + 1
abort("not enough trailing segments for merge test") if second_index >= reference_segments.size
reference_a_start = reference_offset_for(reference_segments, first_index)
reference_b_start = reference_offset_for(reference_segments, second_index)
full_tmp_a = OUTPUT_ROOT.join("full_segment_a.mp4")
full_tmp_b = OUTPUT_ROOT.join("full_segment_b.mp4")
reference_a_stats =
fragment_packet_stats(
FULL_DIR.join("init.mp4"),
FULL_DIR.join(reference_segments[first_index][:file]),
full_tmp_a
)
reference_b_stats =
fragment_packet_stats(
FULL_DIR.join("init.mp4"),
FULL_DIR.join(reference_segments[second_index][:file]),
full_tmp_b
)
sample_rate = audio_sample_rate(FULL_DIR.join("init.mp4"))
stream_info = audio_stream_info(FULL_DIR.join("init.mp4"))
abort("not enough leading segments for preroll test") if first_index < 1
modes = [
{ key: "rebased", label: "rebased", copyts: false },
{ key: "copyts", label: "copyts", copyts: true },
{ key: "copyts_patched", label: "copyts+tfdt", copyts: true, patch_tfdt: true },
{
key: "preroll_discard",
label: "preroll+discard",
copyts: true,
patch_tfdt: true,
a_preroll_segments: 1,
b_preroll_segments: 1,
a_selected_segment_index: 1,
b_selected_segment_index: 1
}
]
results =
modes.map do |mode|
segment_results = []
[
["a", first_index, reference_a_start],
["b", second_index, reference_b_start]
].each do |suffix, segment_index, reference_start|
preroll_segments = mode_preroll_segments(mode, suffix)
selected_segment_index = mode_target_segment_index(mode, suffix)
sparse_start_index = segment_index - preroll_segments
output_dir = VARIANTS_DIR.join("#{mode[:key]}_#{suffix}")
FileUtils.mkdir_p(output_dir)
seek_time = reference_offset_for(reference_segments, sparse_start_index)
metrics = monitor_ffmpeg(
build_ffmpeg_command(
input: INPUT,
output_dir: output_dir,
seek_time: seek_time,
copyts: mode[:copyts]
),
output_dir
)
sparse_segments = parse_playlist(output_dir.join("stream.m3u8"))
selected_sparse_segment = sparse_segments.fetch(selected_segment_index)
tmp_path = OUTPUT_ROOT.join("#{mode[:key]}_#{suffix}.mp4")
packet_summary =
fragment_packet_stats(
output_dir.join("init.mp4"),
output_dir.join(selected_sparse_segment[:file]),
tmp_path
)
segment_results << {
suffix: suffix,
output_dir: output_dir,
metrics: metrics,
stats: packet_summary,
reference_start: reference_start,
sparse_segments: sparse_segments,
selected_segment_index: selected_segment_index,
selected_segment_path: output_dir.join(selected_sparse_segment[:file]),
reference_segment_index: segment_index
}
end
merge_playlist_path = OUTPUT_ROOT.join("#{mode[:key]}_merge.m3u8")
first_segment_path = segment_results[0][:selected_segment_path]
second_segment_path = segment_results[1][:selected_segment_path]
if mode[:patch_tfdt]
patched_first_segment_path = OUTPUT_ROOT.join("#{mode[:key]}_seg_a.m4s")
rewrite_tfdt(first_segment_path, patched_first_segment_path, 0)
first_segment_path = patched_first_segment_path
patched_segment_path = OUTPUT_ROOT.join("#{mode[:key]}_seg_b.m4s")
tfdt_offset =
((reference_b_stats[:first_pts] - reference_a_stats[:first_pts]) * sample_rate).round
rewrite_tfdt(second_segment_path, patched_segment_path, tfdt_offset)
second_segment_path = patched_segment_path
end
write_merge_playlist(
merge_playlist_path,
init_path: segment_results[0][:output_dir].join("init.mp4"),
segment_paths: [
first_segment_path,
second_segment_path
]
)
merged_packets = ffprobe_packets(merge_playlist_path, playlist: true)
{
label: mode[:label],
copyts: mode[:copyts],
a: segment_results[0],
b: segment_results[1],
merged: packet_stats(merged_packets),
merge_playlist_path: merge_playlist_path
}
end
reference_merge_playlist_path = OUTPUT_ROOT.join("reference_merge.m3u8")
write_merge_playlist(
reference_merge_playlist_path,
init_path: FULL_DIR.join("init.mp4"),
segment_paths: [
FULL_DIR.join(reference_segments[first_index][:file]),
FULL_DIR.join(reference_segments[second_index][:file])
]
)
patched = results.find { |result| result[:label] == "copyts+tfdt" }
preroll = results.find { |result| result[:label] == "preroll+discard" }
seam_frames = ((reference_a_stats[:end_pts] - reference_a_stats[:first_pts]) * sample_rate).round
[patched, preroll].compact.each do |result|
result[:seam_analysis] =
analyze_seam(
reference_playlist: reference_merge_playlist_path,
merged_playlist: result[:merge_playlist_path],
seam_frame: seam_frames,
sample_rate: stream_info[:sample_rate],
channels: stream_info[:channels]
)
end
if preroll
warmup_next_run = preroll[:b]
warmup_init = warmup_next_run[:output_dir].join("init.mp4")
warmup_first_segment = warmup_next_run[:output_dir].join(
warmup_next_run[:sparse_segments].fetch(0)[:file]
)
warmup_second_segment = warmup_next_run[:selected_segment_path]
preroll[:warmup_segment_checks] = {
first_segment_matches_reference:
decoded_audio_md5(
warmup_init,
warmup_first_segment
) == decoded_audio_md5(
FULL_DIR.join("init.mp4"),
FULL_DIR.join(reference_segments[first_index][:file])
),
second_segment_matches_reference:
decoded_audio_md5(
warmup_init,
warmup_second_segment
) == decoded_audio_md5(
FULL_DIR.join("init.mp4"),
FULL_DIR.join(reference_segments[second_index][:file])
)
}
end
puts "Boundary under test: seg #{first_index} -> seg #{second_index}"
puts(
"Reference timeline: " \
"#{format('%.3f', reference_a_stats[:first_pts])}-#{format('%.3f', reference_a_stats[:end_pts])}s, " \
"#{format('%.3f', reference_b_stats[:first_pts])}-#{format('%.3f', reference_b_stats[:end_pts])}s"
)
puts "Reference runtime: #{format('%.3fs', reference_metrics[:runtime])}"
puts
puts "Timing continuity"
puts "-----------------"
puts [
"Mode".ljust(16),
"Seg A".ljust(21),
"Seg B".ljust(21),
"A->B".rjust(9),
"Merged".rjust(9)
].join(" ")
puts "-" * 79
results.each do |result|
a_stats = result[:a][:stats]
b_stats = result[:b][:stats]
a_window = "#{format('%.3f', a_stats[:first_pts])}->#{format('%.3f', a_stats[:end_pts])}"
b_window = "#{format('%.3f', b_stats[:first_pts])}->#{format('%.3f', b_stats[:end_pts])}"
standalone_delta = b_stats[:first_pts] - a_stats[:end_pts]
row = [
result[:label].ljust(16),
a_window.ljust(21),
b_window.ljust(21),
format("%+.3fs", standalone_delta).rjust(9),
format("%+.3fs", result[:merged][:min_delta]).rjust(9)
]
puts row.join(" ")
end
puts
rebased = results.find { |result| result[:label] == "rebased" }
copyts = results.find { |result| result[:label] == "copyts" }
if preroll && preroll[:merged][:min_delta] > -0.001 &&
preroll[:seam_analysis] &&
classify_seam(preroll[:seam_analysis]) == "no seam-specific artifact detected"
puts "Conclusion: sparse merging is reliable with a 1-segment preroll, discarding the first encoded segment, and rewriting tfdt onto the shared timeline."
elsif patched && patched[:merged][:min_delta] > -0.001
puts "Conclusion: sparse merging is timing-correct with tfdt rewriting, but seam artifacts remain unless the encoder has enough warm-up."
elsif copyts
puts(
"Conclusion: -copyts fixes per-segment timing, but merged HLS/fMP4 output still resets at the stitch point " \
"(merged delta #{format('%+.3fs', copyts[:merged][:min_delta])})."
)
else
puts "Conclusion: sparse segment timing did not stay mergeable."
end
puts
puts "Seam artifact check"
puts "-------------------"
puts [
"Mode".ljust(16),
"Seam RMS".rjust(10),
"Post RMS".rjust(10),
"Jump".rjust(8),
"Classification"
].join(" ")
puts "-" * 88
[patched, preroll].compact.each do |result|
seam_analysis = result[:seam_analysis]
puts [
result[:label].ljust(16),
format("%9.2f", seam_analysis[:seam][:rms]),
format("%9.2f", seam_analysis[:post][:rms]),
format("%7.3fx", seam_analysis[:jump_ratio]),
classify_seam(seam_analysis)
].join(" ")
end
if preroll && preroll[:warmup_segment_checks]
puts
puts "Warm-up segment check"
puts "---------------------"
puts(
"First segment from the warmed sparse run matches reference current segment: " \
"#{preroll[:warmup_segment_checks][:first_segment_matches_reference] ? 'yes' : 'no'}"
)
puts(
"Second segment from the warmed sparse run matches reference next segment: " \
"#{preroll[:warmup_segment_checks][:second_segment_matches_reference] ? 'yes' : 'no'}"
)
end
puts "Artifacts: #{OUTPUT_ROOT}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment