Skip to content

Instantly share code, notes, and snippets.

@krasi-georgiev
Last active April 26, 2025 10:44
Show Gist options
  • Save krasi-georgiev/1f167a2136bbce5824c6516d2777ef79 to your computer and use it in GitHub Desktop.
Save krasi-georgiev/1f167a2136bbce5824c6516d2777ef79 to your computer and use it in GitHub Desktop.
Cut a video file from frame.io comments.
#!/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