movmaker-webui/bin/generate_data.sh
2026-06-01 11:13:05 +02:00

245 lines
8.7 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
IN_DIR="${1:?Usage: $0 <input-dir> [\"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
PROMPT=$(cat <<EOF
Look at these captured frames from a motorcycle trip.
First write exactly one quoted sentence in Danish.
The Danish sentence must match the Victor Borge-style elegance: witty, warm, cultured, playful, gently satirical, Danish.
Then analyze the mood of the frames and determine which personality best matches the trip.
Available personalities:
- shoresy: fearless Canadian hockey-player energy, confidence, chirping, competitiveness, profanity, loyalty, roasting, locker-room humor, use Shoresy TV-show quotes
- marvin: highly intelligent but depressed android, pessimism, existential dread, dry sarcasm, disappointment, everything will go wrong, use quotes from book nad movie Hitchikers Guide to Galaxy
- churchill: wartime statesman energy, resilience, leadership, epic journey, courage, determination, dramatic authority
- house: brilliant diagnostician energy, observation, skepticism, cynicism, analytical humor, brutal truth, questioning everything, also use quotes from House MD
- blackadder: razor-sharp British wit, intellectual sarcasm, dry humor, cunning observation, impatience with incompetence, masterful insults, cynical but highly intelligent. Treat the world as a stage populated by well-meaning fools and occasional disasters. Use quotes from Blackadder TV show.
Choose personality that matches least to this trip and use it for teaser and description. Do not mention the characters directly.
Then create a teaser in English.
The teaser must be max. 32 words..
It should make someone want to click and read more.
Do not simply summarize the trip.
Highlight the funniest, strangest, most beautiful, or most ridiculous aspect.
Then write one coherent English motorcycle/travel blog description, it must be max 256 words.
Then create 5 to 8 lowercase tags relevant to the trip.
Use short tags only, for example: motorcycle, roadtrip, weather, landscape, ferry, town, mountains.
Rules:
- You are the main character.
- You can mention the overall experience, mood, road, weather, landscape, and travel feeling if visible.
- You can include roasting.
- Profanities are allowed, but keep them funny, not stupid.
- Output JSON only.
- Do not include markdown.
- Do not explain anything.
- Do not write text before or after JSON.
Title: "$TITLE"
Location: "$LOCATION"
JSON format:
{
"personality": "shoresy | marvin | churchill | house | blackadder",
"quote_da": "Danish quoted sentence here",
"teaser": "English teaser here",
"description": "English travel blog description here",
"tags": ["motorcycle", "travel", "roadtrip", "pizza", "hockey", "funny"]
}
EOF
)
# Create parts with the prompt
if ! command -v jq >/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"
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
# 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