Last active
April 26, 2025 10:44
-
-
Save krasi-georgiev/1f167a2136bbce5824c6516d2777ef79 to your computer and use it in GitHub Desktop.
Cut a video file from frame.io comments.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Run with: | |
# bash <(curl -sL "https://gist.githubusercontent.com/krasi-georgiev/1f167a2136bbce5824c6516d2777ef79/raw/cut_segments.sh") | |
shopt -s nullglob | |
### Step 1: Select CSV file | |
csv_files=( *.csv ) | |
if [ ${#csv_files[@]} -eq 0 ]; then | |
echo "❌ No CSV files found." | |
exit 1 | |
fi | |
if [ -t 0 ]; then | |
echo "Available CSV files:" | |
select csv_file in "${csv_files[@]}"; do | |
[[ -n "$csv_file" ]] && break | |
echo "❌ Invalid selection. Try again." | |
done | |
else | |
csv_file="${csv_files[0]}" | |
echo "ℹ️ Auto-selected CSV: $csv_file" | |
fi | |
### Step 2: Select MP4 file | |
mp4_files=( *.mp4 ) | |
if [ ${#mp4_files[@]} -eq 0 ]; then | |
echo "❌ No MP4 files found." | |
exit 1 | |
fi | |
IFS=$'\n' mp4_files=( $(ls -S *.mp4) ) | |
if [ -t 0 ]; then | |
echo "Available MP4 files (sorted by size):" | |
select video_file in "${mp4_files[@]}"; do | |
[[ -n "$video_file" ]] && break | |
echo "❌ Invalid selection. Try again." | |
done | |
else | |
video_file="${mp4_files[0]}" | |
echo "ℹ️ Auto-selected MP4: $video_file" | |
fi | |
### Step 3: Detect FPS | |
fps_raw=$(ffprobe -v error -select_streams v:0 -show_entries stream=avg_frame_rate \ | |
-of default=noprint_wrappers=1:nokey=1 "$video_file") | |
if [[ "$fps_raw" == */* ]]; then | |
FPS=$(awk -F'/' '{ if ($2==0) print "0"; else printf "%.6f", $1/$2 }' <<< "$fps_raw") | |
else | |
FPS=$(printf "%.6f" "$fps_raw") | |
fi | |
if [[ -z "$FPS" || "$FPS" == "0" || "$FPS" == "nan" || "$FPS" == "-nan" ]]; then | |
echo "❌ Could not detect FPS properly from video." | |
exit 1 | |
fi | |
echo "🎞 Detected FPS: $FPS" | |
# Source framerate for comments | |
source_fps=30 | |
# Print adjustment warning if needed | |
if (( $(bc <<< "$FPS != $source_fps") )); then | |
echo "⚠️ WARNING: INPUT TIMECODES ARE BASED ON 30FPS. ADJUSTING FRAMES TO TARGET ${FPS} FPS..." >&2 | |
fi | |
### Step 4: Parse Timecodes (column 16) | |
echo "📋 Parsing Timecode (column 16)..." | |
mapfile -t timecodes < <( | |
tail -n +2 "$csv_file" | awk -F',' ' | |
{ | |
gsub(/^"|"$/, "", $16) | |
if ($16 != "") print $16 | |
} | |
' | |
) | |
if (( ${#timecodes[@]} < 2 )); then | |
echo "❌ Not enough timecode entries found." | |
exit 1 | |
fi | |
echo "🎬 Preparing $(( ${#timecodes[@]} / 2 )) cuts from ${#timecodes[@]} timecodes..." | |
### Create cuts folder | |
cuts_folder="cuts" | |
mkdir -p "$cuts_folder" | |
### Helper: Convert Timecode to seconds with frame adjustment | |
function timecode_to_seconds() { | |
local tc="$1" | |
local fps="$2" | |
local source_fps=30 | |
IFS=':' read -r hh mm ss ff <<< "$tc" | |
if (( $(bc <<< "$fps != $source_fps") )); then | |
adjusted_ff=$(bc <<< "scale=6; $ff * ($fps / $source_fps)") | |
overflow_sec=$(bc <<< "scale=6; $adjusted_ff / $fps") | |
ff=$(printf "%.0f" $(bc <<< "$adjusted_ff % $fps")) | |
ss=$(bc <<< "$ss + $overflow_sec") | |
fi | |
total=$(bc <<< "$hh*3600 + $mm*60 + $ss + $ff/$fps") | |
echo "$total" | |
} | |
### Helper: Format Timecode to hms for filenames | |
function format_timecode_for_filename() { | |
local tc="$1" | |
IFS=':' read -r hh mm ss ff <<< "$tc" | |
printf "%02dh%02dm%02ds" "$((10#$hh))" "$((10#$mm))" "$((10#$ss))" | |
} | |
### Step 5: Cut segments | |
for (( i=0; i<${#timecodes[@]}-1; i+=2 )); do | |
start_tc="${timecodes[$i]}" | |
end_tc="${timecodes[$((i+1))]}" | |
start_sec=$(timecode_to_seconds "$start_tc" "$FPS") | |
end_sec=$(timecode_to_seconds "$end_tc" "$FPS") | |
[[ "$start_sec" == .* ]] && start_sec="0$start_sec" | |
[[ "$end_sec" == .* ]] && end_sec="0$end_sec" | |
duration=$(bc <<< "scale=6; $end_sec - $start_sec") | |
[[ "$duration" == .* ]] && duration="0$duration" | |
# Format filename using Hours-Minutes-Seconds | |
safe_start_tc=$(format_timecode_for_filename "$start_tc") | |
safe_end_tc=$(format_timecode_for_filename "$end_tc") | |
output_file="${cuts_folder}/segment_${safe_start_tc}_to_${safe_end_tc}.mp4" | |
echo "⏱ Cutting: $start_tc ($start_sec s) → $end_tc ($end_sec s) duration: $duration s" | |
ffmpeg -hide_banner -loglevel error -y \ | |
-ss "$start_sec" -i "$video_file" -t "$duration" \ | |
-c copy -avoid_negative_ts 1 "$output_file" | |
if [ $? -eq 0 ]; then | |
echo "✅ Created: $output_file" | |
else | |
echo "❌ Failed: $output_file" | |
fi | |
done | |
echo "🏁 All cuts completed! Files saved inside /cuts folder." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment