Preserve source cadence and sample picture fps after analysis
This commit is contained in:
parent
32683efccf
commit
06cf02db83
20 changed files with 303 additions and 87 deletions
93
extract.sh
93
extract.sh
|
|
@ -7,32 +7,77 @@
|
|||
# pass. A PNG sequence is exact, instantly seekable, and reproducible.
|
||||
#
|
||||
# Audio comes out alongside because the page uses it as the PLAYBACK CLOCK -
|
||||
# frame = floor(audio.currentTime * fps) - so picture and sound cannot drift
|
||||
# apart no matter how long the shot is or how slow the render loop runs.
|
||||
# frame = floor(audio.currentTime * fps). Detection sees every decoded source
|
||||
# frame; a lower drawing rate is a later playback choice, never an extraction
|
||||
# choice. This script accepts CFR footage because frame-index timing needs a
|
||||
# single rate. VFR needs per-frame timestamps in the manifest first.
|
||||
set -euo pipefail
|
||||
|
||||
src="${1:?usage: ./extract.sh CLIP [FPS] [OUTDIR]}"
|
||||
fps="${2:-12}"
|
||||
out="${3:-frames}"
|
||||
|
||||
rm -rf "$out"; mkdir -p "$out"
|
||||
ffmpeg -hide_banner -loglevel warning -i "$src" -vf "fps=$fps" "$out/%04d.png"
|
||||
count=$(ls -1 "$out" | wc -l)
|
||||
|
||||
# Mono is enough for judging sync and halves the file. Absent audio is not fatal.
|
||||
if ffprobe -v error -select_streams a:0 -show_entries stream=codec_type \
|
||||
-of csv=p=0 "$src" 2>/dev/null | grep -q audio; then
|
||||
ffmpeg -hide_banner -loglevel warning -y -i "$src" -vn -ac 1 -ar 44100 audio.wav
|
||||
audio='"audio.wav"'
|
||||
echo "audio -> audio.wav"
|
||||
else
|
||||
audio='null'
|
||||
echo "no audio stream"
|
||||
src="${1:?usage: ./extract.sh CLIP [BUNDLE_DIR]}"
|
||||
bundle="${2:-.}"
|
||||
if [[ "$bundle" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
|
||||
echo "The FPS argument was removed: extraction always keeps the source rate. Use a directory as argument 2." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$bundle" = /* || "$bundle" = *..* ]]; then
|
||||
echo "BUNDLE_DIR must be a relative directory inside this repo" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# The page must know the true extraction rate: if it guessed, audio and picture
|
||||
# would drift. Source of truth lives here, next to the frames it describes.
|
||||
printf '{"fps":%s,"frames":%s,"dir":"%s","audio":%s,"source":"%s"}\n' \
|
||||
"$fps" "$count" "$out" "$audio" "$(basename "$src")" > manifest.json
|
||||
probe=$(ffprobe -v error -select_streams v:0 \
|
||||
-show_entries stream=r_frame_rate,avg_frame_rate,nb_frames \
|
||||
-of json "$src")
|
||||
fps=$(python3 -c '
|
||||
import json, sys
|
||||
from fractions import Fraction
|
||||
streams = json.load(sys.stdin).get("streams", [])
|
||||
if not streams:
|
||||
raise SystemExit("no video stream in source")
|
||||
s = streams[0]
|
||||
nominal = Fraction(s["r_frame_rate"])
|
||||
average = Fraction(s["avg_frame_rate"])
|
||||
if nominal <= 0 or average <= 0 or abs(float(nominal / average) - 1) > 0.001:
|
||||
raise SystemExit("variable-frame-rate source needs timestamp-aware playback; refusing to guess its fps")
|
||||
print(float(average))
|
||||
' <<< "$probe")
|
||||
|
||||
echo "$count frames at ${fps}fps -> $out/ (manifest.json written)"
|
||||
if [[ "$bundle" = "." ]]; then
|
||||
dir="frames"; audio_path="audio.wav"; manifest_path="manifest.json"
|
||||
else
|
||||
dir="${bundle%/}/frames"
|
||||
audio_path="${bundle%/}/audio.wav"
|
||||
manifest_path="${bundle%/}/manifest.json"
|
||||
fi
|
||||
|
||||
rm -rf "$dir"; mkdir -p "$dir"
|
||||
ffmpeg -hide_banner -loglevel warning -i "$src" -fps_mode passthrough "$dir/%04d.png"
|
||||
count=$(find "$dir" -maxdepth 1 -name '*.png' -type f | wc -l | tr -d ' ')
|
||||
|
||||
expected=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["streams"][0].get("nb_frames", ""))' <<< "$probe")
|
||||
if [[ "$expected" =~ ^[0-9]+$ && "$count" != "$expected" ]]; then
|
||||
echo "decoded $count frames but source reports $expected; refusing an inaccurate manifest" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mono is enough for judging sync and halves the file. A silent clock lets a
|
||||
# mute source use the same audio-driven transport.
|
||||
if ffprobe -v error -select_streams a:0 -show_entries stream=codec_type \
|
||||
-of csv=p=0 "$src" 2>/dev/null | grep -q audio; then
|
||||
ffmpeg -hide_banner -loglevel warning -y -i "$src" -vn -ac 1 -ar 44100 "$audio_path"
|
||||
else
|
||||
duration=$(python3 -c 'import sys; print(int(sys.argv[1]) / float(sys.argv[2]))' "$count" "$fps")
|
||||
ffmpeg -hide_banner -loglevel warning -y -f lavfi -i anullsrc=r=44100:cl=mono \
|
||||
-t "$duration" -c:a pcm_s16le "$audio_path"
|
||||
fi
|
||||
|
||||
# JSON escaping belongs to a JSON writer, especially for source filenames.
|
||||
python3 - "$fps" "$count" "$dir" "$audio_path" "$src" "$manifest_path" <<'PY'
|
||||
import json, os, sys
|
||||
fps, count, frames, audio, source, path = sys.argv[1:]
|
||||
with open(path, "w") as out:
|
||||
json.dump({"fps": float(fps), "frames": int(count), "dir": frames,
|
||||
"audio": audio, "source": os.path.basename(source)}, out)
|
||||
out.write("\n")
|
||||
PY
|
||||
|
||||
echo "$count source frames at ${fps}fps -> $dir/ ($manifest_path written)"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue