#!/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] [additional_prompt]}" TITLE="${2:-}" LOCATION="${3:-}" MAX_FRAMES="${4:-12}" ADDITIONAL_PROMPT="${5:-}" if [ "$ADDITIONAL_PROMPT" = "no-append" ]; then ADDITIONAL_PROMPT="" fi # Source system-wide OpenRouter API key if present so the webserver can run this as www-data. if [ -f /etc/mvlog/openrouter.env ]; then # shellcheck disable=SC1091 . /etc/mvlog/openrouter.env fi : "${OPENROUTER_API_KEY:?Set OPENROUTER_API_KEY first in the environment}" MODEL="${OPENROUTER_MODEL:-openrouter/auto}" OPENROUTER_SITE_URL="${OPENROUTER_SITE_URL:-https://bubulescu.org}" OPENROUTER_SITE_NAME="${OPENROUTER_SITE_NAME:-MVLog}" 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 ADDITIONAL_PROMPT_BLOCK="" if [ -n "$ADDITIONAL_PROMPT" ]; then ADDITIONAL_PROMPT_BLOCK=$(cat < "$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" "$MODEL" <<'PY' import sys import json import base64 prompt_file = sys.argv[1] request_file = sys.argv[2] frames_list = sys.argv[3] model = sys.argv[4] with open(prompt_file, "r", encoding="utf-8") as f: prompt = f.read() content = [{"type": "text", "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") content.append({ "type": "image_url", "image_url": { "url": f"data:image/jpeg;base64,{b64}" } }) req = { "model": model, "messages": [ { "role": "user", "content": content } ], "temperature": 0.7, "response_format": { "type": "json_object" } } with open(request_file, "w", encoding="utf-8") as rf: json.dump(req, rf, ensure_ascii=False) PY echo "Calling OpenRouter API..." >&2 echo "DEBUG_MODEL=$MODEL" >&2 echo "DEBUG_PERSONALITY_INDEX=$idx" >&2 curl -sS \ "https://openrouter.ai/api/v1/chat/completions" \ -H "Authorization: Bearer ${OPENROUTER_API_KEY}" \ -H "Content-Type: application/json" \ -H "HTTP-Referer: ${OPENROUTER_SITE_URL}" \ -H "X-Title: ${OPENROUTER_SITE_NAME}" \ -X POST \ -d @"$REQUEST_FILE" \ > "$RESPONSE_FILE" TEXT=$(jq -r '.choices[0].message.content // empty' "$RESPONSE_FILE") if [ -z "$TEXT" ]; then echo "OpenRouter 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 "OpenRouter 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 "OpenRouter JSON saved: $OUT_JSON" >&2 exit 0