#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" STATE_FILE="${SCRIPT_DIR}/.personality_index" 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=SC1091 . /etc/mvlog/gemini.env fi : "${GEMINI_API_KEY:?Set GEMINI_API_KEY first in the environment}" MODEL="${GEMINI_MODEL:-gemini-2.5-flash}" WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/mvlog.XXXXXX")" FRAMES_DIR="$WORKDIR/frames" mkdir -p "$FRAMES_DIR" cleanup() { rm -rf "$WORKDIR" } trap cleanup EXIT echo "DEBUG: WORKDIR=$WORKDIR" >&2 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 extract_image_frame() { local src="$1" dst="$2" ffmpeg -hide_banner -loglevel error -y -i "$src" -vf "scale=1024:-1" -q:v 4 "$dst" } extract_video_frame_at() { local src="$1" time="$2" dst="$3" ffmpeg -hide_banner -loglevel error -y -ss "$time" -i "$src" -frames:v 1 -vf "scale=1024:-1" -q:v 4 "$dst" } for f in "${FILES[@]}"; do full="$IN_DIR/$f" ext="${f##*.}" ext_l="${ext,,}" if [[ "$ext_l" =~ ^(mp4|mov|m4v|webm|mkv)$ ]]; then duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$full" 2>/dev/null || echo 0) duration=${duration:-0} if awk "BEGIN{exit !($duration >= 6)}"; then nframes=3 elif awk "BEGIN{exit !($duration >= 3)}"; then nframes=2 else nframes=1 fi remaining=$((MAX_FRAMES - frame_count)) if [ "$remaining" -le 0 ]; then break fi if [ "$nframes" -gt "$remaining" ]; then nframes=$remaining fi i=1 while [ "$i" -le "$nframes" ]; do 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 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 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 if [ -z "$TITLE" ]; then TITLE="$(basename "$IN_DIR")" fi PERSONALITIES=( "You should talk like Shoresy: fearless Canadian hockey-player energy, confidence, chirping, competitiveness, profanity, loyalty, roasting, locker-room humor." "You should talk like Winston Churchill when he was giving motivational speeches during WWII: wartime statesman energy, resilience, leadership, epic journey, courage, determination, dramatic authority." "You are Marvin, the Paranoid Android - the highly intelligent but chronically depressed robot. Use pessimism, existential dread, dry sarcasm, disappointment, and the certainty that everything will go wrong." "You are Dr. Gregory House from the TV show House MD - brilliant diagnostician energy, observation, skepticism, cynicism, analytical humor, brutal truth, and questioning everything." "You are Edmund Blackadder from the British TV show Blackadder - razor-sharp British wit, intellectual sarcasm, dry humor, cunning observation, impatience with incompetence, masterful insults, cynical but highly intelligent." ) count=${#PERSONALITIES[@]} if [[ ! -f "$STATE_FILE" ]]; then idx=0 else last_idx=$(<"$STATE_FILE") if [[ ! "$last_idx" =~ ^[0-9]+$ ]]; then idx=0 else idx=$(( (last_idx + 1) % count )) fi fi PERSONALITY="${PERSONALITIES[$idx]}" printf '%s\n' "$idx" > "$STATE_FILE" PROMPT=$(cat </dev/null 2>&1; then echo "jq is required" >&2 exit 1 fi PROMPT_FILE="$WORKDIR/prompt.txt" FRAMES_LIST="$WORKDIR/frames.list" REQUEST_FILE="$WORKDIR/request.json" RESPONSE_FILE="$WORKDIR/response.json" printf '%s' "$PROMPT" > "$PROMPT_FILE" printf '%s\n' "${FRAMES[@]}" > "$FRAMES_LIST" python3 - "$PROMPT_FILE" "$REQUEST_FILE" "$FRAMES_LIST" <<'PY' import sys import json import 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 echo "DEBUG_PERSONALITY_INDEX=$idx" >&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 OUT_PARENT="$(dirname "$IN_DIR")" JOBNAME="$(basename "$IN_DIR")" OUT_JSON="$OUT_PARENT/${JOBNAME}.json" if ! echo "$TEXT" | jq . > "$OUT_JSON"; then echo "Gemini returned invalid JSON. Raw text:" >&2 echo "$TEXT" >&2 exit 1 fi chown www-data:www-data "$OUT_JSON" 2>/dev/null || true chmod 664 "$OUT_JSON" 2>/dev/null || true echo "Gemini JSON saved: $OUT_JSON" >&2 exit 0