diff options
| -rw-r--r-- | prompts/skills/fujifilm-video-stabilization/SKILL.md | 52 | ||||
| -rwxr-xr-x | scripts/usbimport | 108 |
2 files changed, 133 insertions, 27 deletions
diff --git a/prompts/skills/fujifilm-video-stabilization/SKILL.md b/prompts/skills/fujifilm-video-stabilization/SKILL.md index 330be1a..026b4e9 100644 --- a/prompts/skills/fujifilm-video-stabilization/SKILL.md +++ b/prompts/skills/fujifilm-video-stabilization/SKILL.md @@ -1,12 +1,12 @@ --- name: fujifilm-video-stabilization -description: "Stabilize shaky Fujifilm X100V videos (DSCF*.MOV) using ffmpeg's 2-pass vidstab filter. Handles pass 1 (motion detection → .trf), pass 2 (transform + unsharp), cleanup of temp files, and batch processing of multiple clips. Use when asked to stabilize video, reduce camera shake, smooth handheld footage, or process Fujifilm X100V MOV files. Triggers on: stabilize video, reduce camera shake, vidstab, ffmpeg stabilization, Fujifilm X100V, smooth footage, handheld video." +description: "Stabilize shaky Fujifilm X100V videos (DSCF*.MOV) using ffmpeg's 2-pass vidstab filter and encode compact 4K HEVC MP4 output. Handles motion detection, transform and sharpening, Intel VAAPI acceleration with a software fallback, cleanup, verification, and batch processing. Use when asked to stabilize video, reduce camera shake, smooth handheld footage, compress stabilized video, or process Fujifilm X100V MOV files. Triggers on: stabilize video, reduce camera shake, vidstab, ffmpeg stabilization, Fujifilm X100V, compress video, smooth footage, handheld video." --- # Fujifilm X100V Video Stabilization 2-pass ffmpeg vidstab workflow to remove camera shake from Fujifilm X100V `.MOV` clips. -Outputs stabilized `.mkv` files; cleans up temp `.trf` files when done. +Outputs compact 4K HEVC `.mp4` files; cleans up temp `.trf` files when done. ## When to Use @@ -18,7 +18,8 @@ Outputs stabilized `.mkv` files; cleans up temp `.trf` files when done. ## Required Tools - `ffmpeg` with `libvidstab` enabled (verify: `ffmpeg -filters 2>/dev/null | grep vidstab`) -- Input location: `/home/paul/syncthing/Documents/Inbox/Fujifilm.Videos/` (typical) +- Intel VAAPI HEVC support through `/dev/dri/renderD128` (preferred) +- Input location: `/home/paul/syncthing/Documents/Inbox/Fujifilm/` (typical) ## 2-Pass Workflow @@ -34,18 +35,32 @@ ffmpeg -y -i DSCF7259.MOV -vf vidstabdetect=result=DSCF7259.trf -f null - - `-f null -` discards output; only the `.trf` sidecar is produced. - Run pass 1 for all clips **before** starting pass 2 (they can run in parallel if CPU allows). -### Pass 2 — Stabilize & Sharpen +### Pass 2 — Stabilize, Sharpen, and Compress -Apply transforms and add a mild unsharp pass (FFmpeg's own recommendation): +Apply transforms, add a mild unsharp pass, and encode directly to a compact HEVC MP4: ```bash -ffmpeg -y -i DSCF7259.MOV \ - -vf "vidstabtransform=input=DSCF7259.trf,unsharp=5:5:0.8:3:3:0.4" \ - stabilised-DSCF7259.mkv +ffmpeg -y -vaapi_device /dev/dri/renderD128 -i DSCF7259.MOV \ + -map 0:v:0 -map "0:a:0?" \ + -vf "vidstabtransform=input=DSCF7259.trf,unsharp=5:5:0.8:3:3:0.4,format=nv12,hwupload" \ + -c:v hevc_vaapi -rc_mode CQP -global_quality 34 -tag:v hvc1 \ + -c:a aac -b:a 128k -movflags +faststart stabilised-DSCF7259.mp4 ``` - `unsharp=5:5:0.8:3:3:0.4` — luma and chroma unsharp with mild strength (0.8 / 0.4); compensates for the slight softening vidstab introduces. -- Output is named `stabilised-<original>.mkv` to keep it alongside the source. +- VAAPI performs the expensive 4K HEVC encode on the Intel GPU. +- Quality 34 reduces typical stabilized videos by roughly 80% while retaining 4K resolution. Use 30–32 when higher quality is more important than size. +- AAC audio and `hvc1` tagging provide broad MP4 compatibility. `faststart` makes remote playback begin sooner. +- Output is named `stabilised-<original>.mp4` to keep it alongside the source. + +If VAAPI is unavailable, use software HEVC encoding instead (this is substantially slower): + +```bash +ffmpeg -y -i DSCF7259.MOV -map 0:v:0 -map "0:a:0?" \ + -vf "vidstabtransform=input=DSCF7259.trf,unsharp=5:5:0.8:3:3:0.4" \ + -c:v libx265 -preset medium -crf 28 -tag:v hvc1 \ + -c:a aac -b:a 128k -movflags +faststart stabilised-DSCF7259.mp4 +``` ### Cleanup @@ -67,12 +82,15 @@ for f in *.MOV; do done wait -# Pass 2 — serial (CPU-heavy; one at a time is fine) +# Pass 2 — serial to avoid overloading the GPU for f in *.MOV; do base="${f%.MOV}" - ffmpeg -y -i "$f" \ - -vf "vidstabtransform=input=${base}.trf,unsharp=5:5:0.8:3:3:0.4" \ - "stabilised-${base}.mkv" + ffmpeg -y -vaapi_device /dev/dri/renderD128 -i "$f" \ + -map 0:v:0 -map "0:a:0?" \ + -vf "vidstabtransform=input=${base}.trf,unsharp=5:5:0.8:3:3:0.4,format=nv12,hwupload" \ + -c:v hevc_vaapi -rc_mode CQP -global_quality 34 -tag:v hvc1 \ + -c:a aac -b:a 128k -movflags +faststart \ + "stabilised-${base}.mp4" done rm -f *.trf @@ -83,14 +101,16 @@ rm -f *.trf Check duration and file size of stabilized clips: ```bash -for f in stabilised-*.mkv; do +for f in stabilised-*.mp4; do echo -n "$f: " ffprobe -v error -show_entries format=duration,size -of csv=p=0 "$f" + ffmpeg -v error -i "$f" -f null - done ``` ## Notes -- **Syncthing caveat**: the `Fujifilm.Videos` folder is synced. If files vanish mid-session, Syncthing may have moved them after detecting completion. Check `~/.local/share/syncthing/` or other sync peers. +- **Syncthing caveat**: the `Fujifilm` folder is synced. If files vanish mid-session, Syncthing may have moved them after detecting completion. Check `~/.local/share/syncthing/` or other sync peers. - **Quality**: default vidstab params produce good results for typical X100V handheld footage. For extremely shaky video, add `shakiness=10:accuracy=15` to the pass 1 filter. -- **Output size**: stabilized `.mkv` files are re-encoded; expect ~10–15 MB/s for X100V clips at the default encoder quality (crf 23). +- **Output size**: HEVC quality 34 reduced a 334 MB stabilized test clip to 58 MB without changing its 3840×2160 resolution or duration. +- **Avoid intermediate encodes**: encode the stabilized MP4 directly from the camera `.MOV`. Do not first create a VP9 MKV and then transcode it, because that wastes time and adds generation loss. diff --git a/scripts/usbimport b/scripts/usbimport index 6942e9a..4be74c2 100755 --- a/scripts/usbimport +++ b/scripts/usbimport @@ -3,7 +3,9 @@ set -euo pipefail declare -r PROGRAM=${0##*/} declare -r GVFS_ROOT=${GVFS_ROOT:-"/run/user/$(id -u)/gvfs"} -declare -r FUJIFILM_DEST=${FUJIFILM_DEST:-"$HOME/Documents/Inbox/Fujifilm"} +declare -r FUJIFILM_DEST=${FUJIFILM_DEST:-"$HOME/Pictures/Fujifilm.Inbox"} +declare -r FUJIFILM_RAW_DEST=${FUJIFILM_RAW_DEST:-"$HOME/Pictures/Fujifilm.RAW"} +declare -r GPX_DIR=${GPX_DIR:-"$HOME/Documents/GPX"} declare -r SUPERNOTE_DEST=${SUPERNOTE_DEST:-"$HOME/Documents/Inbox/Supernote"} declare -r CONVERT_PARALLELISM=${CONVERT_PARALLELISM:-3} @@ -14,6 +16,7 @@ declare -i TOTAL_FAILED=0 declare -i TOTAL_CONVERTED=0 declare -i TOTAL_CONVERT_SKIPPED=0 declare -a CLEANUP_PATHS=() +declare -a IMPORTED_IMAGE_PATHS=() log() { printf '%s\n' "$*" @@ -49,11 +52,15 @@ Modes: supernote Copy the Supernote Nomad Note folder, then convert .note files to PDF. Default destinations: - Fujifilm media: $FUJIFILM_DEST + Fujifilm JPEG/video: $FUJIFILM_DEST + Fujifilm RAW: $FUJIFILM_RAW_DEST + GPX tracks: $GPX_DIR Supernote files: $SUPERNOTE_DEST Environment overrides: FUJIFILM_DEST=/path/to/dir + FUJIFILM_RAW_DEST=/path/to/dir + GPX_DIR=/path/to/dir SUPERNOTE_DEST=/path/to/dir CONVERT_PARALLELISM=3 GVFS_ROOT=/run/user/UID/gvfs @@ -114,18 +121,50 @@ find_supernote_root() { return 1 } +is_raw_media_name() { + local name=$1 + + case "$name" in + *.[3][Ff][Rr]|*.[Aa][Rr][Ww]|*.[Cc][Rr]2|*.[Cc][Rr]3|\ + *.[Dd][Cc][Rr]|*.[Dd][Nn][Gg]|*.[Ee][Rr][Ff]|*.[Gg][Pp][Rr]|\ + *.[Ii][Ii][Qq]|*.[Kk][Dd][Cc]|*.[Mm][Ee][Ff]|*.[Mm][Oo][Ss]|\ + *.[Mm][Rr][Ww]|*.[Nn][Ee][Ff]|*.[Nn][Rr][Ww]|*.[Oo][Rr][Ff]|\ + *.[Pp][Ee][Ff]|*.[Rr][Aa][Ff]|*.[Rr][Aa][Ww]|*.[Rr][Ww]2|\ + *.[Rr][Ww][Ll]|*.[Ss][Rr]2|*.[Ss][Rr][Ff]|*.[Ss][Rr][Ww]|\ + *.[Xx]3[Ff]) return 0 ;; + *) return 1 ;; + esac +} + is_fujifilm_media_name() { local name=$1 + if is_raw_media_name "$name"; then + return 0 + fi + case "$name" in - *.[Jj][Pp][Gg]|*.[Jj][Pp][Ee][Gg]|*.[Rr][Aa][Ff]|\ + *.[Jj][Pp][Gg]|*.[Jj][Pp][Ee][Gg]|\ *.[Mm][Oo][Vv]|*.[Mm][Pp]4|*.[Aa][Vv][Ii]) return 0 ;; *) return 1 ;; esac } +is_fujifilm_image_name() { + local name=$1 + + if is_raw_media_name "$name"; then + return 0 + fi + + case "$name" in + *.[Jj][Pp][Gg]|*.[Jj][Pp][Ee][Gg]) return 0 ;; + *) return 1 ;; + esac +} + copy_fujifilm_media_from_dir() { - local source_dir=$1 dest_root=$2 + local source_dir=$1 dest_root=$2 raw_dest_root=$3 local name _ignored type source_path dest_path while IFS=$'\t' read -r name _ignored type _ignored; do @@ -134,12 +173,16 @@ copy_fujifilm_media_from_dir() { case "$type" in *directory*) - copy_fujifilm_media_from_dir "$source_path" "$dest_root" + copy_fujifilm_media_from_dir "$source_path" "$dest_root" "$raw_dest_root" ;; *regular*) is_fujifilm_media_name "$name" || continue increment TOTAL_FOUND - dest_path="$dest_root/$name" + if is_raw_media_name "$name"; then + dest_path="$raw_dest_root/$name" + else + dest_path="$dest_root/$name" + fi if [[ -e $dest_path ]]; then log "skip existing: $name" @@ -150,6 +193,9 @@ copy_fujifilm_media_from_dir() { log "copy: $source_path -> $dest_path" if gio copy -- "$source_path" "$dest_path"; then increment TOTAL_COPIED + if is_fujifilm_image_name "$name"; then + IMPORTED_IMAGE_PATHS+=("$dest_path") + fi else increment TOTAL_FAILED fi @@ -158,6 +204,43 @@ copy_fujifilm_media_from_dir() { done < <(gio list -a standard::name,standard::type "$source_dir") } +geotag_imported_images() { + local -a gpx_files=() geotag_args=() + local gpx_file + + if [[ ${#IMPORTED_IMAGE_PATHS[@]} -eq 0 ]]; then + log "No new images to GPS tag." + return 0 + fi + + if [[ -d $GPX_DIR ]]; then + mapfile -d '' -t gpx_files \ + < <(find "$GPX_DIR" -type f -iname '*.gpx' -print0 | sort -z) + fi + + if [[ ${#gpx_files[@]} -eq 0 ]]; then + log "No GPX files found under $GPX_DIR; skipping GPS tagging." + return 0 + fi + + require_command exiftool || { + increment TOTAL_FAILED + return 1 + } + + for gpx_file in "${gpx_files[@]}"; do + geotag_args+=(-geotag "$gpx_file") + done + + log "GPS tagging ${#IMPORTED_IMAGE_PATHS[@]} image(s) from ${#gpx_files[@]} GPX file(s)." + if ! exiftool -overwrite_original -P "${geotag_args[@]}" \ + -- "${IMPORTED_IMAGE_PATHS[@]}"; then + error "GPS tagging failed." + increment TOTAL_FAILED + return 1 + fi +} + gio_modified_time() { local source_path=$1 @@ -350,7 +433,7 @@ finish_import() { import_fujifilm() { local dest=${1:-$FUJIFILM_DEST} - local root + local raw_dest=$FUJIFILM_RAW_DEST root root=$(find_fujifilm_root) || { error "No GVFS Fuji/Fujifilm camera mount found under $GVFS_ROOT" @@ -358,13 +441,16 @@ import_fujifilm() { return 1 } - mkdir -p "$dest" + mkdir -p "$dest" "$raw_dest" log "Device: Fujifilm camera" log "Source: $root" - log "Destination: $dest" + log "JPEG/video destination: $dest" + log "RAW destination: $raw_dest" + log "GPX track directory: $GPX_DIR" log "Importing JPEG, RAW, and video files." - copy_fujifilm_media_from_dir "$root" "$dest" - finish_import "$dest" + copy_fujifilm_media_from_dir "$root" "$dest" "$raw_dest" + geotag_imported_images || true + finish_import "$dest (JPEG/video), $raw_dest (RAW)" } import_supernote() { |
