161 lines
6.8 KiB
Bash
161 lines
6.8 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Rauchtest fuer den laufenden Stack.
|
||
|
|
#
|
||
|
|
# Prueft nicht "laeuft der Container", sondern genau die Dinge, die bei diesem
|
||
|
|
# Deployment schon einmal kaputt waren — inklusive der Annahmen, auf denen unsere
|
||
|
|
# Anpassungen beruhen. Genau die koennen ein Image-Update stillschweigend brechen:
|
||
|
|
# der Bind-Mount ueber prompts/podcast/ verdeckt neue Dateien, config/content_core.yaml
|
||
|
|
# friert alte Defaults ein (CCORE_CONFIG_PATH ersetzt, statt zu mergen), und die
|
||
|
|
# Env-Var-Workarounds haengen an Interna von content-core/esperanto/podcast_creator.
|
||
|
|
#
|
||
|
|
# ./scripts/smoke_test.sh # alles
|
||
|
|
# SKIP_NET=1 ./scripts/smoke_test.sh # ohne Tests, die ins Internet muessen
|
||
|
|
#
|
||
|
|
# Exit 0 = alles gruen. Exit 1 = mindestens ein Test rot.
|
||
|
|
set -uo pipefail
|
||
|
|
|
||
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
|
cd "$REPO"
|
||
|
|
|
||
|
|
API="http://127.0.0.1:5055"
|
||
|
|
IMAGE="$(grep -oP '(?<=image: )lfnovo/open_notebook\S+' docker-compose.yml | head -1)"
|
||
|
|
PY=/app/.venv/bin/python
|
||
|
|
fails=0
|
||
|
|
|
||
|
|
ok() { printf ' \033[32mOK\033[0m %s\n' "$1"; }
|
||
|
|
fail() { printf ' \033[31mFEHL\033[0m %s\n' "$1"; fails=$((fails + 1)); }
|
||
|
|
sec() { printf '\n%s\n' "$1"; }
|
||
|
|
|
||
|
|
inapp() { docker compose exec -T open_notebook "$@" 2>/dev/null; }
|
||
|
|
|
||
|
|
sec "Grundfunktion"
|
||
|
|
|
||
|
|
if curl -sf -m 10 "$API/api/models/providers" -o /tmp/.st_prov 2>/dev/null; then
|
||
|
|
if grep -q ollama /tmp/.st_prov && grep -q openrouter /tmp/.st_prov; then
|
||
|
|
ok "API erreichbar, Provider ollama + openrouter verfuegbar"
|
||
|
|
else
|
||
|
|
fail "API erreichbar, aber Provider fehlen: $(cat /tmp/.st_prov)"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
fail "API $API nicht erreichbar"
|
||
|
|
fi
|
||
|
|
rm -f /tmp/.st_prov
|
||
|
|
|
||
|
|
sec "Lokale Sprachdienste (vom Container aus)"
|
||
|
|
|
||
|
|
if inapp "$PY" -c "
|
||
|
|
import httpx,sys
|
||
|
|
r = httpx.post('http://host.docker.internal:8901/audio/speech',
|
||
|
|
json={'model':'chatterbox','voice':'default','input':'Test.','response_format':'wav'}, timeout=180)
|
||
|
|
sys.exit(0 if r.status_code == 200 and len(r.content) > 1000 else 1)"; then
|
||
|
|
ok "TTS antwortet mit Audio (host.docker.internal:8901)"
|
||
|
|
else
|
||
|
|
fail "TTS liefert kein Audio — Dienst tot (ConnectError) oder VRAM voll (HTTP 500)?
|
||
|
|
systemctl --user status open-notebook-tts; journalctl --user -u open-notebook-tts -n 30"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if inapp "$PY" -c "
|
||
|
|
import httpx,sys
|
||
|
|
r = httpx.get('http://host.docker.internal:8902/health', timeout=30)
|
||
|
|
sys.exit(0 if r.status_code == 200 else 1)"; then
|
||
|
|
ok "STT erreichbar (host.docker.internal:8902)"
|
||
|
|
else
|
||
|
|
fail "STT nicht erreichbar — systemctl --user status open-notebook-stt"
|
||
|
|
fi
|
||
|
|
|
||
|
|
sec "Annahmen unserer Anpassungen (brechen leise bei Image-Updates)"
|
||
|
|
|
||
|
|
# 1) prompts/podcast: Verzeichnis-Mount verdeckt das Image-Verzeichnis komplett.
|
||
|
|
# Kommt upstream eine dritte Vorlage dazu, waere sie unsichtbar.
|
||
|
|
img_tpl="$(docker run --rm --entrypoint ls "$IMAGE" /app/prompts/podcast 2>/dev/null | sort | tr '\n' ' ')"
|
||
|
|
our_tpl="$(ls prompts/podcast | sort | tr '\n' ' ')"
|
||
|
|
if [ "$img_tpl" = "$our_tpl" ]; then
|
||
|
|
ok "prompts/podcast: Dateiliste deckt sich mit dem Image ($our_tpl)"
|
||
|
|
else
|
||
|
|
fail "prompts/podcast weicht ab — unser Mount verdeckt Image-Dateien!
|
||
|
|
Image: $img_tpl
|
||
|
|
wir : $our_tpl"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 2) Unsere Patches in den Vorlagen sind noch drin.
|
||
|
|
if grep -q '{{ language }}' prompts/podcast/outline.jinja prompts/podcast/transcript.jinja &&
|
||
|
|
head -1 prompts/podcast/outline.jinja | grep -q '/no_think'; then
|
||
|
|
ok "Vorlagen enthalten Sprachanweisung ({{ language }}) und /no_think"
|
||
|
|
else
|
||
|
|
fail "Vorlagen-Patches fehlen — Podcasts kaemen wieder auf Englisch / mit kaputtem JSON"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 3) podcast_creator reicht `language` ueberhaupt noch in die Vorlagen durch.
|
||
|
|
if inapp grep -q "language" /app/.venv/lib/python3.12/site-packages/podcast_creator/nodes.py; then
|
||
|
|
ok "podcast_creator kennt weiterhin ein language-Feld"
|
||
|
|
else
|
||
|
|
fail "podcast_creator reicht kein language mehr durch — Vorlagen-Patch wirkungslos"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 4) TTS_BATCH_SIZE existiert noch als Stellschraube (sonst wieder 5 parallele Clips -> OOM).
|
||
|
|
if inapp grep -q "TTS_BATCH_SIZE" /app/.venv/lib/python3.12/site-packages/podcast_creator/nodes.py; then
|
||
|
|
ok "podcast_creator liest weiterhin TTS_BATCH_SIZE"
|
||
|
|
else
|
||
|
|
fail "TTS_BATCH_SIZE wird nicht mehr gelesen — Gefahr paralleler Clips (CUDA OOM)"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 5) content-core sucht YouTube-Untertitel weiterhin ueber preferred_languages.
|
||
|
|
if inapp grep -q "preferred_languages" /app/.venv/lib/python3.12/site-packages/content_core/processors/youtube.py; then
|
||
|
|
ok "content-core nutzt weiterhin preferred_languages (deutsche Untertitel)"
|
||
|
|
else
|
||
|
|
fail "content-core sucht Untertitel anders — config/content_core.yaml pruefen"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 6) content-core baut das STT-Modell weiterhin ohne base_url (deshalb die Env-Var).
|
||
|
|
if inapp grep -q "OPENAI_COMPATIBLE_BASE_URL" /app/.venv/lib/python3.12/site-packages/esperanto/providers/stt/openai_compatible.py; then
|
||
|
|
ok "esperanto akzeptiert weiterhin OPENAI_COMPATIBLE_BASE_URL_STT (Audio-Transkription)"
|
||
|
|
else
|
||
|
|
fail "esperanto liest die Env-Var nicht mehr — Audio-Quellen fielen auf OpenAI zurueck"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 7) CCORE_CONFIG_PATH ersetzt die Config komplett. Bringt ein Update neue
|
||
|
|
# Top-Level-Schluessel mit, frieren wir die alten ein — hier faellt das auf.
|
||
|
|
img_keys="$(docker run --rm --entrypoint "$PY" -e CCORE_CONFIG_PATH= "$IMAGE" \
|
||
|
|
-c "from content_core.config import load_config; print(' '.join(sorted(load_config())))" 2>/dev/null)"
|
||
|
|
our_keys="$(inapp "$PY" -c "
|
||
|
|
import yaml; print(' '.join(sorted(yaml.safe_load(open('/app/config/content_core.yaml')))))")"
|
||
|
|
missing="$(comm -23 <(tr ' ' '\n' <<<"$img_keys" | sort -u) <(tr ' ' '\n' <<<"$our_keys" | sort -u) | tr '\n' ' ')"
|
||
|
|
if [ -z "${missing// /}" ] && [ -n "$img_keys" ]; then
|
||
|
|
ok "config/content_core.yaml enthaelt alle Default-Schluessel des Images"
|
||
|
|
else
|
||
|
|
fail "config/content_core.yaml fehlen Schluessel aus dem Image: ${missing:-(Image-Defaults nicht lesbar)}
|
||
|
|
Neu erzeugen — siehe Kopfkommentar in config/content_core.yaml"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 8) Episode-Profile haben eine Sprache (sonst wieder englische Podcasts).
|
||
|
|
if curl -sf -m 10 "$API/api/episode-profiles" | python3 -c "
|
||
|
|
import json,sys
|
||
|
|
p=json.load(sys.stdin)
|
||
|
|
sys.exit(0 if p and all(x.get('language') for x in p) else 1)" 2>/dev/null; then
|
||
|
|
ok "alle Episode-Profile haben ein language-Feld gesetzt"
|
||
|
|
else
|
||
|
|
fail "mindestens ein Episode-Profil ohne language — Podcast kaeme auf Englisch"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "${SKIP_NET:-0}" != "1" ]; then
|
||
|
|
sec "Ende-zu-Ende (braucht Internet)"
|
||
|
|
if inapp "$PY" -c "
|
||
|
|
import asyncio,sys
|
||
|
|
from content_core import extract_content
|
||
|
|
async def m():
|
||
|
|
r = await extract_content({'url':'https://www.youtube.com/watch?v=hzxiegk9QAg'})
|
||
|
|
sys.exit(0 if len((r.content or '')) > 1000 else 1)
|
||
|
|
asyncio.run(m())"; then
|
||
|
|
ok "YouTube-Quelle liefert Transkript (deutsche Untertitel werden gefunden)"
|
||
|
|
else
|
||
|
|
fail "YouTube-Extraktion liefert keinen Text — CCORE_CONFIG_PATH/Sprachen pruefen"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
sec "Ergebnis"
|
||
|
|
if [ "$fails" -eq 0 ]; then
|
||
|
|
echo " Alles gruen."
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
echo " $fails Test(s) fehlgeschlagen."
|
||
|
|
exit 1
|