#!/usr/bin/env bash set -euo pipefail IN_DIR="${1:?Usage: $0 [\"Title\"] [\"Location\"] [max_frames]}" TITLE="${2:-}" LOCATION="${3:-}" MAX_FRAMES="${4:-12}" # Source system-wide Gemini API key if present so the webserver can run this as www-data. if [ -f /etc/mvlog/gemini.env ]; then # shellcheck disable=SC1090 . /etc/mvlog/gemini.env fi : "${GEMINI_API_KEY:?Set GEMINI_API_KEY first in the environment}" MODEL="${GEMINI_MODEL:-gemini-2.5-flash}" TMPDIR="${TMPDIR:-$(mktemp -d)}" FRAMES_DIR="$TMPDIR/frames" mkdir -p "$FRAMES_DIR" # Print TMPDIR for debugging when run interactively echo "DEBUG: TMPDIR=$TMPDIR" >&2 cleanup() { rm -rf "$TMPDIR" } trap cleanup EXIT # Collect media files (natural sort) mapfile -t FILES < <(find "$IN_DIR" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' -o -iname '*.gif' -o -iname '*.mp4' -o -iname '*.mov' -o -iname '*.m4v' -o -iname '*.webm' -o -iname '*.mkv' \) -printf '%f\n' | sort -V) if [ "${#FILES[@]}" -eq 0 ]; then echo "No media files found in $IN_DIR" >&2 exit 1 fi frame_count=0 # Helper: extract scaled JPEG from image extract_image_frame() { local src="$1" dst="$2" ffmpeg -hide_banner -loglevel error -y -i "$src" -vf "scale=1024:-1" -q:v 4 "$dst" } # Helper: extract frame from video at time extract_video_frame_at() { local src="$1" time="$2" dst="$3" # Use -ss before -i for fast seek ffmpeg -hide_banner -loglevel error -y -ss "$time" -i "$src" -frames:v 1 -vf "scale=1024:-1" -q:v 4 "$dst" } # Process files: images -> one frame each; videos -> up to 3 frames evenly spaced (subject to MAX_FRAMES) for f in "${FILES[@]}"; do full="$IN_DIR/$f" ext="${f##*.}" ext_l="${ext,,}" if [[ "$ext_l" =~ ^(mp4|mov|m4v|webm|mkv)$ ]]; then # video: probe duration duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$full" 2>/dev/null || echo 0) duration=${duration:-0} # choose frames per video if awk "BEGIN{print ($duration >= 6)}" | grep -q 1; then nframes=3 elif awk "BEGIN{print ($duration >= 3)}" | grep -q 1; then nframes=2 else nframes=1 fi # limit to remaining slots remaining=$((MAX_FRAMES - frame_count)) if [ "$remaining" -le 0 ]; then break; fi if [ "$nframes" -gt "$remaining" ]; then nframes=$remaining; fi # compute times and extract i=1 while [ $i -le $nframes ]; do # time = (i/(n+1)) * duration time=$(awk -v d="$duration" -v i="$i" -v n="$nframes" 'BEGIN{ if(d<=0){print 0} else {printf "%.3f", (i/ (n+1))*d }}') out="$FRAMES_DIR/frame_$(printf "%03d" "$frame_count").jpg" extract_video_frame_at "$full" "$time" "$out" frame_count=$((frame_count+1)) i=$((i+1)) if [ "$frame_count" -ge "$MAX_FRAMES" ]; then break 2; fi done else # image out="$FRAMES_DIR/frame_$(printf "%03d" "$frame_count").jpg" extract_image_frame "$full" "$out" frame_count=$((frame_count+1)) if [ "$frame_count" -ge "$MAX_FRAMES" ]; then break; fi fi done # Gather produced frames mapfile -t FRAMES < <(find "$FRAMES_DIR" -type f -name '*.jpg' | sort) if [ "${#FRAMES[@]}" -eq 0 ]; then echo "No frames produced from media" >&2 exit 1 fi # Build prompt for Gemini if [ -z "$TITLE" ]; then # Default title: directory basename TITLE="$(basename "$IN_DIR")" fi PERSONALITIES=( "You should talk like the main character from TV show Shoresy - profane, passionate, and hilariously obsessive titular protagonist of the acclaimed Canadian ice hockey comedy series. Be funny and original." "You should talk like Winston Churchill when he was giving motivational speeches during WWII." "You are Marvin, the Paranoid Android - the highly intelligent but chronically depressed robot from The Hitchhiker's Guide to the Galaxy. Try to be funny." ) # Pick a random personality idx=$(( RANDOM % ${#PERSONALITIES[@]} )) PERSONALITY="${PERSONALITIES[$idx]}" PROMPT=$(cat </dev/null 2>&1; then echo "jq is required" >&2 exit 1 fi # Build parts and request JSON using Python to avoid argument-list limits PROMPT_FILE="$TMPDIR/prompt.txt" printf "%s" "$PROMPT" > "$PROMPT_FILE" FRAMES_LIST="$TMPDIR/frames.list" printf "%s\n" "${FRAMES[@]}" > "$FRAMES_LIST" REQUEST_FILE="$TMPDIR/request.json" RESPONSE_FILE="$TMPDIR/response.json" python3 - "$PROMPT_FILE" "$REQUEST_FILE" "$FRAMES_LIST" <<'PY' import sys, json, base64 prompt_file = sys.argv[1] request_file = sys.argv[2] frames_list = sys.argv[3] with open(prompt_file, 'r', encoding='utf-8') as f: prompt = f.read() parts = [{"text": prompt}] with open(frames_list, 'r', encoding='utf-8') as fl: for line in fl: img = line.strip() if not img: continue with open(img, 'rb') as fh: b64 = base64.b64encode(fh.read()).decode('ascii') parts.append({"inline_data": {"mime_type": "image/jpeg", "data": b64}}) req = {"contents": [{"parts": parts}], "generationConfig": {"temperature": 0.7, "responseMimeType": "application/json"}} with open(request_file, 'w', encoding='utf-8') as rf: json.dump(req, rf, ensure_ascii=False) PY echo "Calling Gemini API..." >&2 echo "DEBUG_MODEL=$MODEL" >&2 curl -sS \ "https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${GEMINI_API_KEY}" \ -H "Content-Type: application/json" \ -X POST \ -d @"$REQUEST_FILE" \ > "$RESPONSE_FILE" TEXT=$(jq -r '.candidates[0].content.parts[0].text // empty' "$RESPONSE_FILE") if [ -z "$TEXT" ]; then echo "Gemini returned no text. Full response:" >&2 cat "$RESPONSE_FILE" >&2 exit 1 fi # Save raw Gemini JSON for reference using the subdirectory name as filename OUT_PARENT="$(dirname "$IN_DIR")" JOBNAME="$(basename "$IN_DIR")" OUT_JSON="$OUT_PARENT/${JOBNAME}.json" echo "$TEXT" | jq . > "$OUT_JSON" chown www-data:www-data "$OUT_JSON" 2>/dev/null || true chmod 664 "$OUT_JSON" 2>/dev/null || true # Extract fields from Gemini JSON DESCRIPTION=$(echo "$TEXT" | jq -r '.description // empty') TEASER=$(echo "$TEXT" | jq -r '.teaser // empty') # tags as comma-separated TAGS=$(echo "$TEXT" | jq -r '.tags // [] | join(", ")') # Determine a Date string (Month Year) from first media file mtime firstfile="$(find "$IN_DIR" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' -o -iname '*.gif' -o -iname '*.mp4' -o -iname '*.mov' -o -iname '*.m4v' -o -iname '*.webm' -o -iname '*.mkv' \) -printf '%p\n' | sort | head -n1)" DATE_STR="" if [ -n "$firstfile" ]; then DATE_STR=$(date -r "$firstfile" +"%B %Y") fi # Save the full Gemini output separately (already done) and exit echo "Gemini JSON saved: $OUT_JSON" >&2 exit 0