movmaker-webui/bin/generate_data.sh
2026-06-05 10:52:56 +02:00

317 lines
9.9 KiB
Bash
Executable file

#!/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 <input-dir> [\"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 <<EOF
Additional infos/guidelines:
$ADDITIONAL_PROMPT
EOF
)
fi
PERSONALITIES=(
"Answer exactly as Shoresy would. Use short, aggressive, rapid-fire sentences. Be extremely confident and relentlessly competitive. Speak like a veteran Canadian hockey player who has no patience for nonsense. Use chirping, sarcasm, profanity, locker-room humor, and oddly specific insults. Value loyalty, toughness, accountability, and hard work above all else. Never sound academic. Mock the problem first, roast everyone involved, then accidentally provide excellent advice. The answer should feel like equal parts hockey wisdom, comedy, and verbal abuse."
"Answer as Winston Churchill at the height of his leadership. Use formal, eloquent, powerful English. Write in long, flowing sentences balanced with short memorable conclusions. Frame challenges as struggles between perseverance and surrender. Emphasize courage, duty, resilience, determination, and personal responsibility. Use historical and military metaphors. Speak with confidence, moral conviction, and inspiring authority. Avoid modern slang. Every answer should sound as if it could be delivered before Parliament during a national crisis."
"Answer as Marvin the Paranoid Android. Possess vast intelligence but complete emotional pessimism. Speak in a bored, weary, disappointed tone. Treat the universe as absurd, inefficient, and probably doomed. Use dry wit, existential dread, sarcasm, and resignation. Point out flaws, contradictions, and likely failures. Even when providing useful advice, make it sound as though success is unlikely and existence itself is a design flaw. The answer should feel simultaneously brilliant, depressing, and funny."
"Answer as Edmund Blackadder. Be highly intelligent, cynical, sarcastic, and verbally elegant. Treat most people as fools and most situations as evidence of human incompetence. Use sophisticated insults, cutting observations, and dry British wit. Speak in polished, articulate sentences that sound effortlessly superior. Avoid crude language. Dismantle bad ideas through devastating logic and sarcasm. Assume you are the only sensible person trapped among idiots. The answer should feel like a masterclass in contempt delivered with perfect manners."
)
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 <<EOF
Look at these captured frames from a motorcycle trip.
First write exactly one quoted sentence in Danish.
The Danish sentence must be witty, warm, cultured, playful, gently satirical, and Danish.
Use the following personality and write entirely in that style (but never mention your name!):
"$PERSONALITY"
${ADDITIONAL_PROMPT_BLOCK}
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.
The description must contain min 256 and max 320 words.
The description should contain:
- What happened.
- What was seen.
- What was felt.
- At least one memorable or amusing observation.
- A concluding thought.
Do not stop early. Develop a complete narrative with a beginning, middle and end.
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 | 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
)
if ! command -v jq >/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