Last active
December 8, 2025 19:45
-
-
Save donnaken15/98e665de860e95521f81c1fdb3907a8c to your computer and use it in GitHub Desktop.
speed up videos slightly and cut silence audacity-style for skimming commentary slop
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/zsh | |
| #set -e # exits early without pressing anything, POS | |
| ((# == 0)) && { # LOL # $((?)) also works, amazing | |
| echo "No files entered"; exit 1 | |
| } | |
| hms=(00 00 00); (( speed=1.18, limit=9999999, delay=((hms[1]*3600)+(hms[2]*60)+hms[3]) )) # seek to | |
| unset hms; #limit=6000 # seconds to shorten for skimming (testing) | |
| fargs=( # tfw draft preset | |
| -stats -stats_period 0.01 -c:v libvpx-vp9 -pix_fmt yuv444p -aspect 16:9 | |
| -crf 43 -qmin 28 -qmax 45 -vb 1100k -minrate 40k -maxrate 2500k -g 900 -bf 64 -refs 8 | |
| -threads 16 -tile-columns 6 -tile-rows 2 -row-mt 1 -deadline best -speed -16 -cpu-used -8 | |
| -tune ssim -tune-content screen -corpus-complexity 10000 -enable-tpl 1 -rc_lookahead 25 | |
| -frame-parallel 0 -aq-mode 2 -arnr-strength 4 -arnr-maxframes 7 -arnr-type 3 -sharpness 4 | |
| -static-thresh 0 -color_primaries bt709 -color_trc bt709 -colorspace bt709 -auto-alt-ref 1 | |
| -color_range pc -bufsize 512M -lag-in-frames 25 -fflags +genpts+bitexact+nobuffer | |
| -flags +qpel+mv4+aic+global_header -map_metadata -1 -ar 24k -c:a libopus -ab 56k | |
| -frame_duration 120 -vbr:a on -flags:a +bitexact -compression_level:a 10 -quality best | |
| -application voip -flush_packets 1 -max_muxing_queue_size 1 -blocksize 2048 | |
| ); cr=$'\r'; lf=$'\n'; imark='\[(silencedetect|Parsed_blackframe_[0-9]+) @ [0-9a-f]{16}\]'; kv='( ([A-Za-z0-9_]+): ?([^ ]+))?'$cr?; god="( \|)?$kv"; | |
| comp='volume=-2dB,dynaudnorm' # broken (clips hardcore): acompressor=threshold=-9dB:ratio=5.1:attack=0.2:release=1.0:makeup=1 | |
| timecode='([0-9]{2}):([0-9]{2}):([0-9]{2}\.[0-9]+)'; sthres='-19.7dB'; ((sdur=0.13,srem=0.13,nick=0)); # figure this out like audacity duration and truncate to | |
| ((m_notice = 10.0)) # heads up warning (not using yet) | |
| ((m_lead = 0.3)) # seconds before/after detected frames | |
| ((m_trail = 1.9)) # minimal time skip (from last frame) because fatty has to get in that stupid word | |
| ((m_dist = 4.0)) # allowable distance between seconds before creating new cut, this should be a little more than reasonable | |
| ((cut_res = 30.0)) # trying hard to avoid video drifting, unironically doing more damage with more "bandaids" # bandages on my legs and my arms for you bandages bandages bandages | |
| # ive never had such strong contempt in my life for something as (ironically) insignificant as a freaking adjective | |
| # and its all because of you zoomer ******s, im on team flam now, oreo should neck | |
| fontparams='fontfile=e\\\:/Gossix.ttf:fontsize=32:box=1:boxcolor=black@0.6' | |
| afix='pan=mono|c0=c0+c1,aresample=32k:async=0'; bthres='blend=difference,blackframe=94:38' # trailing 200ms visibility left with this | |
| # cut processing literally zooms when video is disabled, like a zoomer......... | |
| echo "\x1b[91mKILL FFMPEG IF IT GETS STUCK, BECAUSE IT'S BAD SOFTWARE!!!!!!\x1b[0m" | |
| alias readinto="read -r -d ''" | |
| function timefmt() { | |
| local h m s ms time="$(($1))" | |
| (( h = ((m = (s = (ms = ${time#-})) / 60) / 60) % 60, m %= 60, s %= 60, ms %= 1000, time < 0 )) && sign=- | |
| # wish i could cram this in one statement | |
| printf '%s%s%02d:%02d:%02d.%03d' "$2" "$sign" $h $m $s $ms | |
| } | |
| #trap 'exit 1' INT TERM # leaves ffmpeg open # use kill -9 `jobs` if seriously | |
| trap 'wait; exit 1;' TERM # don't have cuts print past termination | |
| for a in "$@"; do | |
| [ ! -f "$a" ] && { | |
| echo "Invalid or non-existent file: $a" | |
| continue | |
| } | |
| date +'%F_%H-%M-%S.%3N' | readinto salt; salt="${salt//$cr/}" | |
| base="`ffprobe -hide_banner -i "$a" -show_streams -print_format json | jq -c --arg delay "$delay" --arg limit "$limit" --arg "fname" "$a" '{ off: (\$delay | tonumber), len: (\$limit | tonumber), sil: [], mas: [], str: .streams, fname: \$fname }'`" | |
| jq -nr --argjson base "$base" 'first($base.str.[] | select(.codec_type == "video")).index' | readinto v; v="${v//$cr/}" | |
| jq -ne --argjson base "$base" --argjson i "$v" '$base.str[$i].codec_type != "video"' >/dev/null && { | |
| echo 'This file does not have a video stream' | |
| jq -n --indent 4 --argjson base "$base" '$base.str' | |
| continue | |
| } | |
| cut_res="$(jq -nr --argjson base "$base" --argjson i "$v" '$base.str[$i].r_frame_rate' | tr -d '\r')" && \ | |
| echo "FPS eval: $cut_res" && cut_res=$(($cut_res)) || { | |
| echo 'Something went wrong when parsing frame rate' | |
| jq -n --indent 4 --argjson base "$base" '$base.str' | |
| continue | |
| } | |
| echo "offset `timefmt ${delay}`:skim `timefmt ${limit}`:res 1/${cut_res}" | |
| scr="[0:a]$afix,$comp,silencedetect=n=$sthres:d=$sdur;"; sil=(); mas=(); tc=0.0 # total time cut | |
| # disable if not skimming nick's vods | |
| # 999999iq amogus: regex check to determine whose title is who's | |
| # now that i think about it, graph time shouldnt be noticeable if printing is | |
| # nonblocking, so it'll keep going while the read loop takes its sweet @$$ time....maybe... | |
| # ...just going off the fact that cpu usage doesn't pause when marking | |
| if ((nick!=0)); then | |
| scr+="[1][0:v]scale2ref[why1][trash1];[trash1][why1]$bthres" | |
| why=( -t "$limit" -i "$a" -loop 1 -t "$limit" -i 'C:/dummy_target.jpg') | |
| else | |
| #scr+="[1]nullsink" | |
| why=(-vn -t "$limit" -i "$a") | |
| fi | |
| ffmpeg -hide_banner -hwaccel dxva2 -v info -nostats -ss "$delay" "${why[@]}" -lavfi "$scr" \ | |
| -threads 13 -f null nul 2>&1 | while read -r i; do | |
| # old: 96:32 | |
| if [[ "${i//$cr/}" =~ ${imark}${kv}${god}${god}${god}${god}${god} ]]; then | |
| filt="${match[1]}" | |
| #echo "--- $filt ---" | |
| case "$filt" in | |
| Parsed_blackframe_[0-9]*) ;& | |
| silencedetect) | |
| for ((y=0;y<6;y++)); do | |
| ((x=3+(y*4))); k="${match[x]}"; v="${match[++x]}" | |
| [ -z "$k" ] && break; [ "$k" = 'silence_start' -o "$k" = 'frame' ] && temp=() | |
| [[ "$k" = silence_* ]] && { # redundant | |
| temp+=("[\"${k:8}\",$v]") | |
| # inb4 associative array and fromentries to save time from subprocesses when building up item | |
| # or entries | |
| [ "$k" = 'silence_duration' ] && { | |
| jq -nc --argjson r "$srem" '$ARGS.positional | map({key: .[0], value: .[1]}) | from_entries | . + {cut: ([$r,(.duration - $r)] | max)}' --jsonargs "${temp[@]}" | readinto test | |
| [[ "$test" =~ '"cut":'([0-9]+(\.[0-9]+)?) ]] && ((tc+=(match[1]))) && ( | |
| cut=($(jq -ncr --argjson r "$srem" --argjson j "$test" '$j | [.start, .end, .cut] | join(" ")' | tr -d '\r')) | |
| printf 'silence \x1b[92m%9.3f\x1b[97m ==> \x1b[92m%9.3f\x1b[0m, \x1b[97mcut \x1b[96m%6.3fs\x1b[0m, total: \x1b[95m%8.3f\x1b[0m\n' "${cut[@]}" "$tc" | |
| ) & | |
| sil+=("${test//$cr/}") | |
| } | |
| continue | |
| } | |
| [[ "$filt" = Parsed_blackframe_[0-9]* ]] && { | |
| temp+=("[\"$k\",$v]") | |
| [ "$k" = 'last_keyframe' ] && { | |
| jq -nc '$ARGS.positional | from_entries' --jsonargs "${temp[@]}" | readinto test | |
| jq -ncr --argjson hate "$hate" --argjson j "$test" '$j | | |
| "\u001b[97mBAD: \u001b[91m"+ | |
| (.t | strftime("%H:%M:%S.")+((.*$hate%$hate+$hate)|tostring|.[1:]))+ | |
| "\u001b[0m"' & | |
| mas+=("${test//$cr/}") | |
| } | |
| continue | |
| } | |
| echo "?????: $k = $v" | |
| done | |
| ;; | |
| *) echo "Uncaught $filt: $MATCH" | |
| ;; | |
| esac | |
| else | |
| if ! [[ "${i:l}" =~ ^(metadata|m(aj|in)or_|encoder|output \#|stream \#[0-9]:[0-9](\[[0-9]x[0-9]\([a-z]*\)\])?: [av]|duration: |handler_|vendor|compati) ]]; then | |
| printf "\x1b[90m%s\x1b[0m\n" "${i//$cr/}" | |
| fi | |
| fi | |
| done | |
| wait | |
| why=(); ((start=-1,end=-1,hate=cut_res*100000,m_dist*=hate)) | |
| jq -nc -r '$ARGS.positional | .[].t' --jsonargs "${mas[@]}" | tr -d $'\r' | while read -r t; do | |
| ((start<(t*=hate)-m_dist)) && { | |
| ((start!=-1)) && why+=("$start $end"); ((start=t)) | |
| }; ((end=t)) | |
| done; ((start!=-1)) && why+=("$start $end") | |
| jq -nc --argjson hate "$hate" --argjson r "$srem" --argjson j "$base" \ | |
| '$j|.sil = ($ARGS.positional | map([.start, .end, ([$r,.duration] | min/2)]))' \ | |
| --jsonargs "${sil[@]}" | \ | |
| jq -c --argjson hate "$hate" '.mas = ($ARGS.positional | map(. | split(" ")))' --args "${why[@]}" | readinto base; base="${base//$cr/}" | |
| echo 'writing detected data'; jq -nc --argjson j "$base" '$j' > "c:/skim_$salt.json" | |
| ((nick!=0)) && block=",drawbox=x=iw-w:y=ih-h+2:w=(iw*(1-0.66)):h=(ih*(1-0.58))+2:t=fill:c=red" | |
| echo 'generating cut points'; jq -ncr \ | |
| --argjson hate "$((hate/cut_res))" --argjson ml "$m_lead" --argjson tb "$cut_res" \ | |
| --argjson mx "$limit" --argjson j "$base" --argjson mt "$m_trail" \ | |
| '($j | .sil | map( | |
| [ (.[0] + .[2]), (.[1] - .[2]) ] | map((. * $tb | round / $tb) * $hate | floor / $hate) | |
| ) | . + ( $j | .mas | map( [ (.[0] - $ml), (.[1] + $mt) ] ) ) ) | flatten | [0,0,0]+.+[$mx] | | |
| [ range(0; (length/2)) as $i | [ .[(2*$i)+0], .[(2*$i)+1], .[(2*$i)]-.[(2*$i)-1] ] ][1:] | |
| # i hate this language now, why cant this just be like actual javascript | |
| ' | readinto trim # halve silence buffer time and distribute (forgot what to write for why...probably something about preserving a little more of the end sound and start of a new one) | |
| # need to fix inverse ranges when srem < sdur | |
| splits="$(jq -nr --argjson t "$trim" '$t | length' | tr -d '\r')" | |
| blocks="$(jq -nr --argjson t "$trim" '$t | to_entries | map("[v"+(.key | tostring)+"s]") | join("")')" | |
| echo 'assembling graph, rendering (no printing yet = seeking, maybe...)' # crop=480:130:0:0 | |
| #range="trim='$delay':'$((delay+limit))'" | |
| tee "c:/skim_$salt.filt.txt" <<!! | ffmpeg -hide_banner -hwaccel dxva2 -v warning -stats -ss "$delay" -t "$limit" -i "$a" -filter_complex_script pipe: "${fargs[@]}" -map '[right]' -map '[now]' "c:/skim_$salt.webm" -y && echo "--------- $a done (${pipestatus[@]})" | |
| [0:v]scale=-1:360,drawtext=text='%{pts\:hms}':${fontparams}:x=8:y=8:fontcolor=white${block}, | |
| split=$splits${blocks}; | |
| [0:a]aformat=f=flt,asplit=$splits${blocks//v/a}; | |
| $(jq -nr --arg fp "$fontparams" --argjson t "$trim" ' | |
| $t | to_entries | map( | |
| ("trim="+(.value | .[:2] | map(. | tostring) | join(":"))+",") as $trim | (.key | tostring) as $i | | |
| "[v"+$i+"s]"+$trim+"setpts=PTS-STARTPTS,drawtext=text='$'\''' >> "+ | |
| ((.value[2] * 1000 | floor) | tostring | (((.|tonumber/1000|floor|tostring|length)-3) as $pad | " " | | |
| .[$pad:])+(("0"+.[0:-3]) | tonumber | tostring)+"."+(.[-3:]+"000" | .[:3]))+"s'$'\''':"+$fp+ | |
| ":x=162:y=8:fontcolor=lime[v"+$i+"c];"+ | |
| "[a"+$i+"s]a"+$trim+"asetpts=PTS-STARTPTS[a"+$i+"c];\n" | |
| ) | join("")' | tr -d '\r')$(jq -nr --argjson t "$trim" ' | |
| $t | to_entries | map((.key | tostring) as $i | ("[v"+$i+"c][a"+$i+"c]")) | | |
| join("")' | tr -d '\r')concat=n=$splits:v=1:a=1[kys][die]; | |
| [kys]setpts='PTS/$speed'[right]; | |
| [die]$afix,$comp,atempo='$speed'[now]; | |
| !! | |
| # KILLS ITSELF AROUND 8 MINUTES (THE CRINGE IS THAT BAD), NO CRASH DUMP?! | |
| # offset pts | |
| done | |
| #-ss "$delay" -t "$limit" -i "$a" -loop 1 \ | |
| # -i 'C:\dummy_target.jpg' -i 'C:\dummy_target2.jpg' -lavfi " | |
| # [0:a]$comp,silencedetect=n=-25dB:d=0.16,anullsink; | |
| # [1][0:v]scale2ref[why1][trash1]; | |
| # [2][trash1]scale2ref[why2][trash2]; | |
| # [trash1][why1]$bthresh,nullsink; | |
| # [trash2][why2]$bthresh | |
| #" -f null nul 2>&1 | while read -r i; do | |
| # USES 7 GB COMMIT JUST FOR A SECOND COMPARISON FRAME WTFFFFFFF | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment