movmaker-webui/bin/generate_data.sh

252 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 analyze the mood of the frames and determine which two personalities best match the trip.
Available personalities:
- shoresy: fearless Canadian hockey-player energy, confidence, chirping, competitiveness, profanity, loyalty, roasting, locker-room humor
- marvin: highly intelligent but depressed android, pessimism, existential dread, dry sarcasm, disappointment, everything will go wrong
- 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
- 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.
Choose:
- one primary personality, about 70%
- one secondary personality, about 30%
Blend them naturally. Do not mention the characters directly in the description.
First write exactly one quoted sentence in Danish.
The Danish sentence must match the chosen personality blend, but with Victor Borge-style elegance:
witty, warm, cultured, playful, gently satirical.
Then create a teaser in English.
The teaser must be 100-150 characters.
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.
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.
- Mention the overall experience, mood, road, weather, landscape, and travel feeling if visible.
- Include playful roasting.
- Profanities are allowed, but keep them funny, not stupid.
- Make it less than 1024 characters.
- 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:
{
"primary_personality": "shoresy | marvin | churchill | house | blackadder",
"secondary_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"]
}
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