Kapitel 13/Tutorial.md aktualisiert
This commit is contained in:
@@ -825,10 +825,22 @@ Das folgende Schaubild zeigt dir die konkrete Verkabelung
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Cleanup bei CTRL+C, SIGTERM, etc.
|
||||
cleanup() {
|
||||
echo "[WARN] Analyse abgebrochen – prüfe auf ffmpeg-Leichen..."
|
||||
pkill -P $$ ffmpeg || true
|
||||
pkill -f "ffmpeg.*$VOD_ID" || true
|
||||
# FIFO säubern (nur wenn gesetzt & vorhanden)
|
||||
if [ -n "${FIFO_PATH-}" ] && [ -p "$FIFO_PATH" ]; then rm -f "$FIFO_PATH"; fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
# Umgebungsvariablen laden
|
||||
if [ -f "/etc/clipper/clipper.env" ]; then
|
||||
set -a
|
||||
@@ -861,7 +873,7 @@ echo "[INFO] VOD gefunden: $VOD_PATH"
|
||||
echo "[INFO] Extrahiere WAV aus $VOD_PATH → $TMP_AUDIO"
|
||||
|
||||
# Audio extrahieren mit Logging
|
||||
ffmpeg -v warning -i "$VOD_PATH" -ac 1 -ar 16000 -vn "$TMP_AUDIO" 2> "$TMP_LOG_AUDIO"
|
||||
nice -n 10 ffmpeg -v warning -threads 1 -i "$VOD_PATH" -ac 1 -ar 16000 -vn "$TMP_AUDIO" 2> "$TMP_LOG_AUDIO"
|
||||
echo "[OK] Audio extrahiert: $TMP_AUDIO ($(du -h "$TMP_AUDIO" | cut -f1))"
|
||||
|
||||
echo "[INFO] Verwende Schwelle: $CLIPPER_PEAK_THRESHOLD"
|
||||
@@ -931,6 +943,208 @@ with open(outfile, 'w') as f:
|
||||
json.dump(candidates, f)
|
||||
print(f"[DONE] {outfile} geschrieben mit {len(candidates)} Clip(s)")
|
||||
EOF
|
||||
|
||||
echo "[INFO] Starte visuelle Analyse (alle 3 Sekunden ein Frame)..."
|
||||
|
||||
TMP_FRAME_DIR="$TMP_DIR/frames"
|
||||
mkdir -p "$TMP_FRAME_DIR"
|
||||
|
||||
# Videodauer ermitteln (in Sekunden)
|
||||
DURATION=$(ffprobe -v error -show_entries format=duration \
|
||||
-of default=noprint_wrappers=1:nokey=1 "$VOD_PATH")
|
||||
DURATION=${DURATION%.*}
|
||||
|
||||
INTERVAL=3
|
||||
EXPECTED_FRAMES=$(printf "%.0f" "$(echo "$DURATION / $INTERVAL" | bc -l)")
|
||||
|
||||
echo "[INFO] VOD-Dauer: ${DURATION}s → erwarte ca. ${EXPECTED_FRAMES} Bilder bei fps=1/${INTERVAL}"
|
||||
|
||||
# ffmpeg im Hintergrund
|
||||
START_TIME=$(date +%s)
|
||||
nice -n 10 ffmpeg -v warning -i "$VOD_PATH" \
|
||||
-vf "fps=1/${INTERVAL},scale=160:90" \
|
||||
"$TMP_FRAME_DIR/frame_%06d.bmp" &
|
||||
FFMPEG_PID=$!
|
||||
|
||||
EXTRACTED_FRAMES=0
|
||||
LAST_LOGGED_FRAMES=0
|
||||
|
||||
while kill -0 $FFMPEG_PID 2>/dev/null; do
|
||||
EXTRACTED_FRAMES=$(find "$TMP_FRAME_DIR" -name 'frame_*.bmp' | wc -l)
|
||||
|
||||
if (( EXTRACTED_FRAMES > LAST_LOGGED_FRAMES )); then
|
||||
NOW=$(date +%s)
|
||||
ELAPSED=$((NOW - START_TIME))
|
||||
ELAPSED_FMT=$(printf "%02d:%02d:%02d" $((ELAPSED/3600)) $((ELAPSED%3600/60)) $((ELAPSED%60)))
|
||||
|
||||
if (( EXTRACTED_FRAMES > 0 )); then
|
||||
ESTIMATED_TOTAL=$((ELAPSED * EXPECTED_FRAMES / EXTRACTED_FRAMES))
|
||||
ETA_SECONDS=$((ESTIMATED_TOTAL - ELAPSED))
|
||||
ETA_FMT=$(printf "%02d:%02d:%02d" $((ETA_SECONDS/3600)) $((ETA_SECONDS%3600/60)) $((ETA_SECONDS%60)))
|
||||
else
|
||||
ETA_FMT="??:??:??"
|
||||
fi
|
||||
|
||||
PERCENT=$(awk "BEGIN { printf \"%.2f\", $EXTRACTED_FRAMES * 100 / $EXPECTED_FRAMES }")
|
||||
echo "[INFO] Lade Frame $EXTRACTED_FRAMES / $EXPECTED_FRAMES (${PERCENT}%) | ETA: $ETA_FMT | Elapsed: $ELAPSED_FMT"
|
||||
|
||||
|
||||
LAST_LOGGED_FRAMES=$EXTRACTED_FRAMES
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Final-Log
|
||||
EXTRACTED_FRAMES=$(find "$TMP_FRAME_DIR" -name 'frame_*.bmp' | wc -l)
|
||||
END_TIME=$(date +%s)
|
||||
TOTAL_TIME=$((END_TIME - START_TIME))
|
||||
TOTAL_FMT=$(printf "%02d:%02d:%02d" $((TOTAL_TIME/3600)) $((TOTAL_TIME%3600/60)) $((TOTAL_TIME%60)))
|
||||
|
||||
echo "[INFO] Frame-Export abgeschlossen mit $EXTRACTED_FRAMES Bildern. Dauer: $TOTAL_FMT"
|
||||
|
||||
echo "[INFO] Starte visuelle Analyse basierend auf Bewegung zwischen Frames..."
|
||||
|
||||
TMP_JSON_VISUAL="$TMP_DIR/candidates.visual.json"
|
||||
|
||||
/srv/clipper/.venv/bin/python3 <<EOF
|
||||
import os
|
||||
import json
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
frame_dir = "$TMP_FRAME_DIR"
|
||||
output_json = "$TMP_JSON_VISUAL"
|
||||
|
||||
frame_files = sorted([
|
||||
os.path.join(frame_dir, f) for f in os.listdir(frame_dir)
|
||||
if f.endswith(".bmp")
|
||||
])
|
||||
|
||||
movement_threshold = 12000
|
||||
min_duration = 5
|
||||
candidates = []
|
||||
|
||||
prev_frame = None
|
||||
active_start = None
|
||||
|
||||
for idx, frame_path in enumerate(frame_files):
|
||||
frame = cv2.imread(frame_path, cv2.IMREAD_GRAYSCALE)
|
||||
if frame is None:
|
||||
continue
|
||||
|
||||
if prev_frame is not None:
|
||||
diff = cv2.absdiff(prev_frame, frame)
|
||||
score = int(np.sum(diff))
|
||||
print(f"[DEBUG] Frame {idx}: Bewegungsscore = {score}")
|
||||
|
||||
t = idx * 3
|
||||
|
||||
if score > movement_threshold:
|
||||
if active_start is None:
|
||||
active_start = t
|
||||
else:
|
||||
if active_start is not None and (t - active_start) >= min_duration:
|
||||
# Pre/Post-Puffer hinzufügen
|
||||
start_time = max(0, active_start - 2)
|
||||
end_time = min(t + 2, len(frame_files) * 3)
|
||||
candidates.append({
|
||||
"start": round(start_time, 2),
|
||||
"end": round(end_time, 2)
|
||||
})
|
||||
active_start = None
|
||||
|
||||
prev_frame = frame
|
||||
|
||||
# Letzter offener Clip am Ende
|
||||
if active_start is not None:
|
||||
t = len(frame_files) * 3
|
||||
if t - active_start >= min_duration:
|
||||
start_time = max(0, active_start - 2)
|
||||
end_time = min(t + 2, len(frame_files) * 3)
|
||||
candidates.append({
|
||||
"start": round(start_time, 2),
|
||||
"end": round(end_time, 2)
|
||||
})
|
||||
|
||||
with open(output_json, "w") as f:
|
||||
json.dump(candidates, f)
|
||||
|
||||
print(f"[DONE] Bewegungserkennung abgeschlossen → {len(candidates)} Clip(s) geschrieben in {output_json}")
|
||||
EOF
|
||||
|
||||
|
||||
echo "[INFO] Vergleiche Audio- und visuelle Kandidaten..."
|
||||
|
||||
TMP_JSON_FINAL="$TMP_DIR/candidates.final.json"
|
||||
CLIPPER_MATCH_TOLERANCE="${CLIPPER_MATCH_TOLERANCE:-4.0}"
|
||||
|
||||
/srv/clipper/.venv/bin/python3 <<EOF
|
||||
import json
|
||||
import os
|
||||
|
||||
audio_file = "$TMP_JSON"
|
||||
visual_file = "$TMP_JSON_VISUAL"
|
||||
output_file = "$TMP_JSON_FINAL"
|
||||
tolerance = float("${CLIPPER_MATCH_TOLERANCE}")
|
||||
|
||||
# Overlap mit Toleranz
|
||||
def overlap(a, b):
|
||||
return not (a["end"] + tolerance < b["start"] or b["end"] + tolerance < a["start"])
|
||||
|
||||
try:
|
||||
with open(audio_file) as f:
|
||||
audio = json.load(f)
|
||||
except:
|
||||
print("[WARN] Audio-Kandidaten fehlen oder leer.")
|
||||
audio = []
|
||||
|
||||
try:
|
||||
with open(visual_file) as f:
|
||||
visual = json.load(f)
|
||||
except:
|
||||
print("[WARN] Visuelle Kandidaten fehlen oder leer.")
|
||||
visual = []
|
||||
|
||||
combined = []
|
||||
only_audio = []
|
||||
only_video = visual.copy()
|
||||
matched_visual_indices = set()
|
||||
|
||||
for a in audio:
|
||||
matched = False
|
||||
for idx, v in enumerate(visual):
|
||||
if overlap(a, v):
|
||||
combined.append({
|
||||
"start": round(min(a["start"], v["start"]), 2),
|
||||
"end": round(max(a["end"], v["end"]), 2)
|
||||
})
|
||||
matched_visual_indices.add(idx)
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
only_audio.append(a)
|
||||
|
||||
only_video = [
|
||||
v for idx, v in enumerate(visual)
|
||||
if idx not in matched_visual_indices
|
||||
]
|
||||
|
||||
result = {
|
||||
"only_audio": only_audio,
|
||||
"only_video": only_video,
|
||||
"combined": combined
|
||||
}
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
json.dump(result, f)
|
||||
|
||||
print(f"[DONE] Kandidaten klassifiziert → combined={len(combined)}, only_audio={len(only_audio)}, only_video={len(only_video)}")
|
||||
EOF
|
||||
|
||||
echo "[INFO] Ersetze ursprüngliches JSON mit finaler Version..."
|
||||
mv "$TMP_JSON_FINAL" "$TMP_JSON"
|
||||
|
||||
```
|
||||
1. **SSH Node – Analyze VOD** (Node-Name: Analyze VOD)
|
||||
- Node-Typ: SSH
|
||||
|
||||
Reference in New Issue
Block a user