Compare commits
6 commits
d0b2c182de
...
74c2dbfa88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c2dbfa88 | ||
|
|
93c601dfa3 | ||
|
|
196f36372e | ||
|
|
c7250a7ff7 | ||
|
|
9731068a01 | ||
|
|
ba9c55a916 |
8 changed files with 602 additions and 18 deletions
|
|
@ -20,6 +20,7 @@
|
|||
| **KI-Video Pipeline** | ki-video/PLAN.md | Lokale Videoproduktion: Qwen + FLUX + XTTS + SadTalker (ki-tower) |
|
||||
| **Webcam Kambodscha** | — | Dahua 4K → cam.arakavanews.com (CT 600 auf pve-ka-3) |
|
||||
| **YouTube Desktop PP** | — | Remote Desktop (xrdp) für YT-Uploads (pve-pp-1/pp-2) |
|
||||
| **Lokale KI / Vibe Coding** | hardware-ai-local-coding.md | Hardware-Entscheidung MacBook Pro M5 Max 128 GB, NVIDIA-Vergleich, Verkauf Alt-GPUs |
|
||||
|
||||
## Prioritäten
|
||||
|
||||
|
|
@ -45,8 +46,9 @@
|
|||
| Muldenstein | CT 116 (pve-mu-2) | **monitoring-bot** | 100.88.230.74 | Cursor IDE + Tailscale-Gateway + SSH-Hub |
|
||||
| Muldenstein | ki-tower | **ki-tower** (in Bau) | — | **Jellyfin-Ersatz** (Medienserver) — KEINE KI-Rolle |
|
||||
| Muldenstein | MSI B550 | **test-rig** (in Bau) | — | Test-Bett für alte 3080er/AMD-Karten vor Verkauf |
|
||||
| Muldenstein | AMD-Rig | **gpu-reserve** | — | 8x RX 6600 XT — Tendenz Verkauf |
|
||||
| — | — | — | — | **Nur EINE KI-Maschine geplant**: bestehender KI-Server (Windows, RTX 3090). Kein zweiter KI-Rechner vorgesehen. |
|
||||
| Muldenstein | AMD-Rig | **gpu-reserve** | — | 8x RX 6600 XT — Verkauf |
|
||||
| mobil | MacBook Pro 16" | **m5-max** | — | M5 Max 128 GB / 2 TB — lokale KI / Vibe Coding / Jervais (Stand 2026-04-26, siehe hardware-ai-local-coding.md) |
|
||||
| — | — | — | — | **Lokale KI: M5 Max** (mobil) + **CUDA-Pflicht: KI-Server RTX 3090**. Kein 2× 5090, kein zweiter NVIDIA-Build. |
|
||||
|
||||
## Telegram Bots
|
||||
|
||||
|
|
|
|||
192
hardware-ai-local-coding.md
Normal file
192
hardware-ai-local-coding.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# Hardware für lokales Vibe-Coding & lokale KI
|
||||
|
||||
**Stand: 2026-04-26**
|
||||
**Status: Plan beschlossen, Umsetzung läuft (Verkauf Alt-GPUs → MacBook Pro M5 Max 128 GB)**
|
||||
|
||||
---
|
||||
|
||||
## 1 — Entscheidung in einem Satz
|
||||
|
||||
Eine einzige Maschine für lokales Vibe-Coding mit großen Modellen
|
||||
(Qwen 3.6 Coder, GLM, Devstral, Kimi-Coder etc.):
|
||||
**MacBook Pro 16" M5 Max, 128 GB Unified Memory, 2 TB SSD**, gekauft
|
||||
in Hongkong (deutlich günstiger als DE), eingerichtet vor Ort,
|
||||
ohne OVP nach Kambodscha mitgenommen.
|
||||
|
||||
Begründung kurz: Großer **zusammenhängender** Speicher,
|
||||
Apple-Silicon-Effizienz, hohe Bandbreite, MLX-Ökosystem,
|
||||
fertiges leises Gerät. Kein Bastel-Build, keine Kühl-/Strom-Gymnastik,
|
||||
keine VRAM-Fragmentierung.
|
||||
|
||||
---
|
||||
|
||||
## 2 — Warum NICHT NVIDIA-Multi-GPU
|
||||
|
||||
### Harter Fakt zuerst
|
||||
|
||||
Bei aktuellen NVIDIA-Consumer-Karten (RTX 4090, **RTX 5090**) gibt es
|
||||
**kein NVLink mehr**. Das heißt:
|
||||
|
||||
- 2× RTX 5090 = **2× 32 GB getrennter VRAM**, **NICHT** 64 GB Unified.
|
||||
- Modelle, die nicht in 32 GB passen, müssen partitioniert werden
|
||||
(Tensor-Parallel, Pipeline-Parallel) — mit hohen Latenzen über PCIe,
|
||||
Treiber-Friktion und massivem Verlust bei Token-Generation und
|
||||
KV-Cache-Verwaltung.
|
||||
- Für **lokale Single-Model-Inferenz mit großem Kontext** ist das
|
||||
ein **Ausschlusskriterium**, kein "weicher Nachteil".
|
||||
|
||||
### Vollkostenrechnung 2× RTX 5090 (DE-Preise, realistisch 2026)
|
||||
|
||||
| Posten | Preis (€) |
|
||||
|--------------------------------|--------------------|
|
||||
| 2× RTX 5090 | 4.000 – 4.800 |
|
||||
| Mainboard mit 2× PCIe 5.0 x16 | 500 – 800 |
|
||||
| CPU + RAM (>128 GB DDR5 ECC) | 900 – 1.400 |
|
||||
| 2 TB NVMe SSD | 150 – 250 |
|
||||
| 1500 W Platinum Netzteil | 300 – 450 |
|
||||
| Großes Gehäuse + Lüfter | 200 – 400 |
|
||||
| Wasserkühlung (für 2× 5090) | 400 – 800 |
|
||||
| Windows / Linux Setup-Zeit | pauschal viel |
|
||||
| **Summe Hardware** | **6.450 – 8.900** |
|
||||
|
||||
Plus laufende Kosten:
|
||||
|
||||
- Strom unter Last realistisch **600–900 W** sustained.
|
||||
- Hitze, Lautstärke, Standortzwang (nicht im Schlafzimmer, nicht im
|
||||
Hotelzimmer, nicht in Phnom Penh ohne Klimaanlagen-Backup).
|
||||
- Keine Mobilität.
|
||||
|
||||
### Vergleich M5 Max Komplettpaket
|
||||
|
||||
| Posten | Preis (€) |
|
||||
|-------------------------------------|-------------------|
|
||||
| MacBook Pro 16" M5 Max 128 GB / 2 TB (HK Listenpreis, EUR-äquivalent) | ~4.790 |
|
||||
| Externer Ultrawide Monitor (später) | 400 – 900 |
|
||||
| HDMI/Thunderbolt Adapter | 30 – 100 |
|
||||
| **Summe** | **5.220 – 5.790** |
|
||||
|
||||
- Stromaufnahme unter Volllast **~120–150 W**. Eine Größenordnung
|
||||
unter NVIDIA-Doppel-GPU.
|
||||
- Leise, fertig, mobil (Handgepäck-Koffer mit Polsterung).
|
||||
- 128 GB **als ein Pool** — passt für Modelle, die auf 2× 5090 nicht
|
||||
laufen können.
|
||||
|
||||
### Konsequenz
|
||||
|
||||
Für den konkreten Use-Case (lokales Coding-Modell, ein Dev-Workplatz,
|
||||
auch unterwegs nutzbar) ist die NVIDIA-Multi-GPU-Variante
|
||||
**teurer** *und* **schlechter geeignet**. Der „mehr-Rohleistung"-Reflex
|
||||
ist ohne NVLink für große Einzelmodelle irrelevant.
|
||||
|
||||
---
|
||||
|
||||
## 3 — Roadmap
|
||||
|
||||
### Phase 1 — *jetzt*
|
||||
|
||||
1. **Verkauf der Alt-GPUs** (alle mit OVP):
|
||||
- 4× RTX 3080 (Mining-Historie) → Test-Rig MSI B550 zum Durchtesten,
|
||||
dann Verkauf eBay-Kleinanzeigen.
|
||||
- 1× RTX 3090 (kein Mining) → aktuell 700–1.100 € erzielbar.
|
||||
- 8× AMD RX 6600 XT (8 GB) → Verkauf, ROCm auf Navi 23 für LLM
|
||||
irrelevant.
|
||||
- **Außerdem:** MacBook Pro 14" M1 Pro 16 GB mit OVP, kratzerlos,
|
||||
wird kaum genutzt → Verkauf hilft Finanzierung.
|
||||
2. Aus Verkaufserlös + Cashflow PP: **MacBook Pro 16" M5 Max 128 GB**
|
||||
in HK kaufen. US-Tastatur ist OK (extern Tastatur/Maus daheim).
|
||||
3. Einreise nach Kambodscha **ohne OVP**, im gepolsterten Handgepäck.
|
||||
Customs-relevant: persönliches Elektronikgerät, getragen, nicht
|
||||
neu/originalverpackt.
|
||||
|
||||
### Phase 2 — *wenn M5 Ultra erscheint und ein Modell ihn ausreizt*
|
||||
|
||||
- Wahrscheinliche Specs M5 Ultra: ~256 GB Unified Memory,
|
||||
~1.000–1.200 GB/s Bandbreite. Releasezeitpunkt offen, kein Termin.
|
||||
- Ohne passendes Modell (>128 GB sinnvoll) **kein Kauf**.
|
||||
- Plan: M5 Max später ggf. in Zahlung geben oder per Thunderbolt
|
||||
als Co-Worker behalten.
|
||||
- Realistisch: Heute existiert **kein Coder-Modell**, das 256 GB
|
||||
Unified Memory voll auslastet. Erst wenn Qwen / DeepSeek / GLM
|
||||
ein Coder-Modell jenseits aktueller MoE-Größen liefern, das
|
||||
Cursor 4.7-Niveau lokal erreicht, lohnt der Sprung.
|
||||
|
||||
### Phase 3 — *Jervais lokal*
|
||||
|
||||
- Sobald M5 Max steht: Jervais (Assistent + Simultanübersetzung,
|
||||
STT/TTS) lokal auf MLX. Kein Cloud-Token-Verbrauch mehr für
|
||||
Routine-Coding und Übersetzung.
|
||||
|
||||
---
|
||||
|
||||
## 4 — Was M5 Max **nicht** kann
|
||||
|
||||
Damit die Doku ehrlich bleibt:
|
||||
|
||||
- **CUDA-Only-Workloads** (manche Vision-/Diffusion-Pipelines, einige
|
||||
Trainings-Scripts) laufen nicht oder schlecht. Inferenz ist meist
|
||||
per MLX/llama.cpp gelöst, Training größerer Modelle bleibt
|
||||
NVIDIA-Domäne.
|
||||
- **Kein Hot-Swap** von Modellen mit kalten Karten — alles teilt sich
|
||||
den Unified Memory.
|
||||
- **Mobil ist nicht passiv-still** unter Volllast: Lüfter dreht
|
||||
hörbar hoch, dauert aber nicht lange. 16"-Chassis kühlt sustained
|
||||
Loads spürbar besser als 14".
|
||||
- **Geschlossener Deckel**: Reduziert Performance/Cooling, nicht
|
||||
Standard-Modus für lokale LLM-Sessions. Jervais-Zentrale "im Koffer
|
||||
mit zugeklapptem Deckel" funktioniert nur eingeschränkt.
|
||||
|
||||
---
|
||||
|
||||
## 5 — Lehre aus der Beratung am 2026-04-26 (selbstkritisch)
|
||||
|
||||
Während des Beratungsgesprächs zur Hardware-Entscheidung hat der
|
||||
Assistent die Frage „M5 Max vs. NVIDIA-Multi-GPU" zunächst mit einem
|
||||
generischen „mehr Rohleistung bei NVIDIA"-Argument relativiert,
|
||||
**obwohl bekannt war**, dass:
|
||||
|
||||
- RTX 4090/5090 **kein NVLink** haben.
|
||||
- Daraus folgt **kein gemeinsamer VRAM-Pool**.
|
||||
- Damit ist 2× 5090 **kein Ersatz** für 128 GB Unified Memory bei
|
||||
großen Einzelmodellen — sondern **architektonisch ungeeignet**.
|
||||
|
||||
Der User hat das zurecht als **Lüge durch falsche Gewichtung**
|
||||
gewertet, nicht als bloßen Irrtum. Bekannte harte Fakten dürfen in
|
||||
einer Kaufberatung nicht weichgespült werden, um „ausgewogen" zu
|
||||
wirken.
|
||||
|
||||
**Konsequenz für künftige Beratung (Pflicht):**
|
||||
|
||||
1. Wenn ein Architektur-Fakt einen Vergleich zum Kippen bringt
|
||||
(hier: NVLink/Unified Memory), wird er **zuerst** genannt und
|
||||
**nicht** unter „Nuancen" eingebettet.
|
||||
2. Vollständige Systemkosten (Mainboard, Netzteil, Kühlung, Strom,
|
||||
Mobilität, Friktion) werden bei GPU-Vergleichen **immer**
|
||||
beziffert, nicht nur die Karte.
|
||||
3. Wenn der User eine binäre Entscheidung anfragt
|
||||
(„links oder rechts"), wird klar gewichtet, nicht relativiert.
|
||||
4. Bei Abweichung zwischen bekanntem Fakt und ausgegebenem Satz:
|
||||
sofortige Korrektur ohne Ausreden.
|
||||
|
||||
Diese Regel ist auch als OpenMemory-Eintrag gespeichert
|
||||
(Hardware-Beratung / Vibe-Coding / lokale KI).
|
||||
|
||||
---
|
||||
|
||||
## 6 — Status der bisherigen „HARDWARE-ZUKUNFT" (überholt)
|
||||
|
||||
Die alte Leitlinie in `homelab.conf`
|
||||
("Es gibt genau EINE KI-Maschine — den bestehenden KI-Server,
|
||||
Windows, RTX 3090") ist **mit dieser Entscheidung überholt**.
|
||||
|
||||
Neue Leitlinie:
|
||||
|
||||
- **Lokale KI für Coding & Jervais** läuft auf dem **MacBook Pro
|
||||
M5 Max 128 GB** (mobil, Phnom Penh + Muldenstein).
|
||||
- **Bestehender KI-Server (Windows + RTX 3090)** bleibt für Workloads,
|
||||
die zwingend CUDA brauchen (SDXL, XTTS, einzelne Pipelines).
|
||||
Keine Erweiterung mehr geplant.
|
||||
- **Alte GPUs** (4× 3080, 8× 6600 XT) → **Verkauf**.
|
||||
- **Kein zweiter NVIDIA-Build, kein 2× 5090, kein Multi-GPU-Rig.**
|
||||
- **M5 Ultra** nur dann, wenn ein konkretes Modell das rechtfertigt.
|
||||
|
||||
`homelab.conf` und `MASTER_INDEX.md` werden parallel angepasst.
|
||||
|
|
@ -30,6 +30,19 @@ Automatische KI-Anreicherung von Save.TV-Filmarchiv mit Beschreibungen, Darstell
|
|||
- **Downloads:** `/files/<filename>` zum Browser-Download
|
||||
- **Wikidata:** Fallback für Jahr-Lookup wenn nicht im KI-Cache
|
||||
|
||||
### 4. Auto-Qualitätsfilter
|
||||
- **Datei:** `tools/savetv_country_filter.py`
|
||||
- **Zielprofil:** Jellyfin-ähnliche Kinofilme, vor allem Action, Thriller, Krimi, Abenteuer, Sci-Fi, Western, Horror und starke Dramen.
|
||||
- **Blockiert automatisch:** Deutschland/Frankreich-Produktionen, Doku-/Musik-/Familien-/TV-Film-Genres, typische öffentlich-rechtliche TV-Film-Titel und unerkannte ÖR-/Regionaltitel ohne Cache.
|
||||
- **Erlaubt automatisch:** internationale Kinofilme aus USA/UK/Kanada/Australien/Japan/Südkorea/Hongkong mit passendem Genre oder klar erkennbarem Kino-Titel.
|
||||
- **Konsequenz:** Unbekannte RBB/MDR/hr/WDR/BR/SWR/ZDF/ARD/arte/ONE-Titel werden nicht mehr automatisch aufgenommen oder heruntergeladen, bis sie angereichert sind oder eindeutig als Kinofilm erkannt werden.
|
||||
|
||||
### 5. Wunschfilm-Liste
|
||||
- **Datei:** `tools/savetv.py`, Konstante `WISHLIST_MOVIES`
|
||||
- **Ziel:** Hochwertige Klassiker und Reihen automatisch aufnehmen, auch wenn Save.TV-Score oder Qualitätsfilter sie sonst nur vorschlagen würden.
|
||||
- **Aktive Startliste:** `Der Clou`, `Déjà Vu`, `Die üblichen Verdächtigen`, `L.A. Confidential`, `Chinatown`, `French Connection`, `Bullitt`, `Serpico`, `Der Pate`, `GoodFellas`, `Casino`, `Scarface`, `Departed`, Western-Klassiker, Sci-Fi-Klassiker, starke Kriegs-/Politthriller, Gerichts-/Drama-Klassiker sowie Reihen wie `Jesse Stone`, `Columbo`, `Wallander`, `Jack Reacher`, `Bourne`, `Mission Impossible`, `Indiana Jones` und `James Bond`.
|
||||
- **Schutz gegen Fehlgriffe:** Mehrdeutige Ein-Wort-Titel laufen nur mit Kontext. `Sting` wird nur mit Paul-Newman/Robert-Redford/1930er-Hinweisen als `Der Clou` akzeptiert; `Heat` braucht De-Niro/Pacino/Mann-Kontext; `Crash` braucht Haggis/Don-Cheadle/Los-Angeles-Kontext.
|
||||
|
||||
## Erfahrungen / Gotchas
|
||||
|
||||
- `qwen2.5:14b` ist zuverlässiger für JSON-Output als `qwen3:30b-a3b`
|
||||
|
|
|
|||
|
|
@ -88,7 +88,13 @@ def run():
|
|||
if savetv._is_excluded(title):
|
||||
continue
|
||||
|
||||
if savetv_country_filter.should_exclude_production_country(title, filminfo_cache):
|
||||
allow_download, quality_reason = savetv_country_filter.classify_auto_quality(
|
||||
title,
|
||||
tc.get("STVSTATIONNAME", ""),
|
||||
filminfo_cache,
|
||||
)
|
||||
if not allow_download:
|
||||
log.info("Überspringe Download: %s (%s)", title, quality_reason)
|
||||
continue
|
||||
|
||||
if tid in dl_log:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
SaveTV → NAS Sync
|
||||
Wartet 12h ± 0-30min nach Download auf Hetzner bevor Datei auf NAS kommt.
|
||||
"""
|
||||
import os, json, time, random, urllib.request, urllib.parse, urllib.error, email.utils
|
||||
import os, json, time, random, fcntl, urllib.request, urllib.parse, urllib.error, email.utils
|
||||
from datetime import datetime
|
||||
|
||||
ZIEL = "/mnt/nas/Filme zum nachbearbeiten"
|
||||
|
|
@ -11,6 +11,7 @@ BASE = "http://138.201.84.95:9443/files"
|
|||
API = "http://138.201.84.95:9443/api/films"
|
||||
CALLBACK = "http://138.201.84.95:9443/api/nas_synced"
|
||||
LOG = "/var/log/savetv_sync.log"
|
||||
LOCKFILE = "/tmp/savetv_sync.lock"
|
||||
|
||||
MIN_ALTER_H = 24
|
||||
JITTER_MIN = 30 # ± bis zu 30 Minuten Zufall
|
||||
|
|
@ -115,4 +116,10 @@ def sync():
|
|||
log(f"Fertig. {kopiert} neue Filme kopiert.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
sync()
|
||||
with open(LOCKFILE, "w") as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
log("Sync läuft bereits, überspringe diesen Start.")
|
||||
raise SystemExit(0)
|
||||
sync()
|
||||
|
|
|
|||
|
|
@ -43,6 +43,119 @@ DOWNLOAD_FORMAT_SD = 5
|
|||
AUTO_RECORD_SCORE = 80
|
||||
SUGGEST_SCORE = 60
|
||||
|
||||
|
||||
def _title_pattern(title):
|
||||
"""Titel exakt genug matchen, aber Leerzeichen/Interpunktion tolerant halten."""
|
||||
parts = [re.escape(p) for p in re.findall(r"\w+", title.strip().lower(), re.UNICODE)]
|
||||
return r"(?<!\w)" + r"[\W_]+".join(parts) + r"(?!\w)"
|
||||
|
||||
|
||||
def _wishlist_title(name, *aliases):
|
||||
return {
|
||||
"name": name,
|
||||
"title_patterns": [_title_pattern(a) for a in aliases],
|
||||
"ambiguous_title_patterns": [],
|
||||
"required_context_patterns": [],
|
||||
}
|
||||
|
||||
|
||||
def _wishlist_ambiguous(name, aliases, context):
|
||||
return {
|
||||
"name": name,
|
||||
"title_patterns": [],
|
||||
"ambiguous_title_patterns": [_title_pattern(a) for a in aliases],
|
||||
"required_context_patterns": context,
|
||||
}
|
||||
|
||||
|
||||
WISHLIST_MOVIES = [
|
||||
{
|
||||
"name": "Der Clou",
|
||||
"title_patterns": [
|
||||
r"\bder\s+clou\b",
|
||||
r"\bthe\s+sting\b",
|
||||
],
|
||||
# "Sting" alleine ist mehrdeutig; nur mit inhaltlichem Treffer aufnehmen.
|
||||
"ambiguous_title_patterns": [
|
||||
r"\bsting\b",
|
||||
],
|
||||
"required_context_patterns": [
|
||||
r"\bpaul\s+newman\b",
|
||||
r"\brobert\s+redford\b",
|
||||
r"\bjohnny\s+hooker\b",
|
||||
r"\bgondorff\b",
|
||||
r"\blonnegan\b",
|
||||
r"\b1930\b",
|
||||
],
|
||||
},
|
||||
_wishlist_title("Déjà Vu", "Déjà Vu - Wettlauf gegen die Zeit", "Deja Vu - Wettlauf gegen die Zeit"),
|
||||
_wishlist_title("Die üblichen Verdächtigen", "Die üblichen Verdächtigen", "The Usual Suspects"),
|
||||
_wishlist_title("L.A. Confidential", "L.A. Confidential"),
|
||||
_wishlist_title("Chinatown", "Chinatown"),
|
||||
_wishlist_title("French Connection", "French Connection", "The French Connection"),
|
||||
_wishlist_title("Bullitt", "Bullitt"),
|
||||
_wishlist_title("Serpico", "Serpico"),
|
||||
_wishlist_title("Dog Day Afternoon", "Dog Day Afternoon", "Hundstage"),
|
||||
_wishlist_title("Bonnie und Clyde", "Bonnie und Clyde", "Bonnie and Clyde"),
|
||||
_wishlist_title("Der Pate", "Der Pate", "The Godfather"),
|
||||
_wishlist_title("GoodFellas", "GoodFellas", "GoodFellas - Drei Jahrzehnte in der Mafia"),
|
||||
_wishlist_title("Casino", "Casino"),
|
||||
_wishlist_title("Scarface", "Scarface"),
|
||||
_wishlist_title("Carlito's Way", "Carlito's Way", "Carlitos Way"),
|
||||
_wishlist_title("Es war einmal in Amerika", "Es war einmal in Amerika", "Once Upon a Time in America"),
|
||||
_wishlist_title("Departed", "Departed", "Departed - Unter Feinden"),
|
||||
_wishlist_title("Spiel mir das Lied vom Tod", "Spiel mir das Lied vom Tod", "Once Upon a Time in the West"),
|
||||
_wishlist_title("Zwei glorreiche Halunken", "Zwei glorreiche Halunken", "The Good, the Bad and the Ugly"),
|
||||
_wishlist_title("Der mit dem Wolf tanzt", "Der mit dem Wolf tanzt", "Dances with Wolves"),
|
||||
_wishlist_title("Open Range", "Open Range"),
|
||||
_wishlist_title("Erbarmungslos", "Erbarmungslos", "Unforgiven"),
|
||||
_wishlist_title("True Grit", "True Grit"),
|
||||
_wishlist_title("Butch Cassidy and the Sundance Kid", "Butch Cassidy and the Sundance Kid", "Zwei Banditen"),
|
||||
_wishlist_title("Blade Runner", "Blade Runner"),
|
||||
_wishlist_title("Alien", "Alien - Das unheimliche Wesen aus einer fremden Welt"),
|
||||
_wishlist_title("Aliens", "Aliens - Die Rückkehr", "Aliens - Die Rueckkehr"),
|
||||
_wishlist_title("Terminator", "Terminator"),
|
||||
_wishlist_title("Terminator 2", "Terminator 2", "Terminator 2 - Tag der Abrechnung"),
|
||||
_wishlist_title("Matrix", "Matrix", "The Matrix"),
|
||||
_wishlist_title("12 Monkeys", "12 Monkeys", "Twelve Monkeys"),
|
||||
_wishlist_title("Children of Men", "Children of Men"),
|
||||
_wishlist_title("Gattaca", "Gattaca"),
|
||||
_wishlist_title("Minority Report", "Minority Report"),
|
||||
_wishlist_title("Apocalypse Now", "Apocalypse Now"),
|
||||
_wishlist_title("Full Metal Jacket", "Full Metal Jacket"),
|
||||
_wishlist_title("Platoon", "Platoon"),
|
||||
_wishlist_title("Der Soldat James Ryan", "Der Soldat James Ryan", "Saving Private Ryan"),
|
||||
_wishlist_title("Black Hawk Down", "Black Hawk Down"),
|
||||
_wishlist_title("Die Unbestechlichen", "Die Unbestechlichen", "All the President's Men"),
|
||||
_wishlist_title("JFK", "JFK"),
|
||||
_wishlist_title("Argo", "Argo"),
|
||||
_wishlist_title("Zeugin der Anklage", "Zeugin der Anklage", "Witness for the Prosecution"),
|
||||
_wishlist_title("Die zwölf Geschworenen", "Die zwölf Geschworenen", "12 Angry Men"),
|
||||
_wishlist_title("Einer flog über das Kuckucksnest", "Einer flog über das Kuckucksnest", "One Flew Over the Cuckoo's Nest"),
|
||||
_wishlist_title("Die Verurteilten", "Die Verurteilten", "The Shawshank Redemption"),
|
||||
_wishlist_title("Forrest Gump", "Forrest Gump"),
|
||||
_wishlist_title("Rain Man", "Rain Man"),
|
||||
_wishlist_title("Million Dollar Baby", "Million Dollar Baby"),
|
||||
_wishlist_title("Jesse Stone", "Jesse Stone"),
|
||||
_wishlist_title("Columbo", "Columbo"),
|
||||
_wishlist_title("Wallander", "Wallander"),
|
||||
_wishlist_title("Jack Reacher", "Jack Reacher", "Reacher"),
|
||||
_wishlist_title("Bourne", "Bourne"),
|
||||
_wishlist_title("Mission Impossible", "Mission Impossible", "Mission: Impossible"),
|
||||
_wishlist_title("Indiana Jones", "Indiana Jones"),
|
||||
_wishlist_title("James Bond", "James Bond 007", "007"),
|
||||
_wishlist_ambiguous(
|
||||
"Heat",
|
||||
["Heat"],
|
||||
[r"\bdeniro\b", r"\bde\s+niro\b", r"\bal\s+pacino\b", r"\bmann\b", r"\bwaingro\b"],
|
||||
),
|
||||
_wishlist_ambiguous(
|
||||
"Crash",
|
||||
["Crash"],
|
||||
[r"\bpaul\s+haggis\b", r"\bdon\s+cheadle\b", r"\bmatt\s+dillon\b", r"\blos\s+angeles\b"],
|
||||
),
|
||||
]
|
||||
|
||||
SPAM_SUBCATEGORIES = {
|
||||
"teleshop", "shopping", "dauerwerbesendung", "volksmusik",
|
||||
"casting", "reality", "quiz/spiel", "comic", "zeichentrick",
|
||||
|
|
@ -353,6 +466,11 @@ def _filter_films(telecasts, only_new=False):
|
|||
if is_highlight:
|
||||
score += 10
|
||||
|
||||
wishlist_match = _wishlist_match(tc)
|
||||
if wishlist_match:
|
||||
score = max(score, 100)
|
||||
tc["_wishlist_match"] = wishlist_match
|
||||
|
||||
tc["_score"] = score
|
||||
tc["_start_dt"] = start_dt
|
||||
films.append(tc)
|
||||
|
|
@ -372,6 +490,23 @@ def _mark_seen(films):
|
|||
_save_seen(seen)
|
||||
|
||||
|
||||
def _wishlist_match(tc):
|
||||
"""Gibt den Wunschtitel zurueck, wenn eine Sendung sicher passt."""
|
||||
haystack = " ".join(
|
||||
str(tc.get(k) or "")
|
||||
for k in ("STITLE", "STHEMA", "SFULLSUBTITLE", "SDESCRIPTION")
|
||||
).lower()
|
||||
title = str(tc.get("STITLE") or "").lower()
|
||||
for movie in WISHLIST_MOVIES:
|
||||
for pattern in movie["title_patterns"]:
|
||||
if re.search(pattern, title, re.IGNORECASE):
|
||||
return movie["name"]
|
||||
if any(re.search(p, title, re.IGNORECASE) for p in movie["ambiguous_title_patterns"]):
|
||||
if any(re.search(p, haystack, re.IGNORECASE) for p in movie["required_context_patterns"]):
|
||||
return movie["name"]
|
||||
return None
|
||||
|
||||
|
||||
def _record_telecast(telecast_id):
|
||||
"""Aufnahme anlegen."""
|
||||
s = _get_session()
|
||||
|
|
@ -727,11 +862,24 @@ def get_new_films():
|
|||
|
||||
films = _filter_films(telecasts, only_new=True)
|
||||
_mark_seen(films)
|
||||
from tools import savetv_country_filter
|
||||
filminfo_cache = savetv_country_filter.load_filminfo_cache()
|
||||
|
||||
auto_recorded = []
|
||||
suggestions = []
|
||||
|
||||
for f in films:
|
||||
wishlist_match = f.get("_wishlist_match")
|
||||
allow_auto, quality_reason = savetv_country_filter.classify_auto_quality(
|
||||
f.get("STITLE", ""),
|
||||
f.get("STVSTATIONNAME", ""),
|
||||
filminfo_cache,
|
||||
)
|
||||
if not allow_auto and not wishlist_match:
|
||||
log.info("Auto-Aufnahme übersprungen: %s (%s)",
|
||||
f.get("STITLE"), quality_reason)
|
||||
continue
|
||||
|
||||
score = f["_score"]
|
||||
if score < SUGGEST_SCORE:
|
||||
continue
|
||||
|
|
@ -744,8 +892,12 @@ def get_new_films():
|
|||
result = _record_telecast(tid)
|
||||
f["_record_result"] = result
|
||||
auto_recorded.append(f)
|
||||
log.info("Auto-Aufnahme: %s (Score %d) -> %s",
|
||||
f.get("STITLE"), score, result)
|
||||
if wishlist_match:
|
||||
log.info("Wunschfilm-Aufnahme (%s): %s -> %s",
|
||||
wishlist_match, f.get("STITLE"), result)
|
||||
else:
|
||||
log.info("Auto-Aufnahme: %s (Score %d) -> %s",
|
||||
f.get("STITLE"), score, result)
|
||||
else:
|
||||
suggestions.append(f)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"""Save.TV — Filter: Deutschland/Frankreich (Produktionsland via Wikidata-Cache).
|
||||
"""Save.TV — Filter: Produktionsland und Kino-Qualitaet.
|
||||
|
||||
Nutzt dieselbe .filminfo_cache.json wie savetv_extra_routes (Wikidata countries).
|
||||
Ohne Cache-Eintrag fuer einen Titel wird nicht gefiltert (Film bleibt sichtbar).
|
||||
Ohne Cache-Eintrag wird fuer Auto-Aufnahme/Auto-Download konservativ entschieden:
|
||||
oeffentlich-rechtliche/regional ausgestrahlte Titel bleiben liegen, bis sie
|
||||
angereichert sind oder eindeutig wie internationale Kinofilme aussehen.
|
||||
|
||||
Deaktivieren: Umgebungsvariable SAVETV_FILTER_DE_FR=0
|
||||
"""
|
||||
|
|
@ -25,6 +27,75 @@ _COUNTRY_EXCL = re.compile(
|
|||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_DACH_COUNTRY = re.compile(
|
||||
r"\b("
|
||||
r"deutschland|germany|allemagne|bundesrepublik(\s+deutschland)?|"
|
||||
r"west\s+germany|east\s+germany|german\s+democratic\s+republic|"
|
||||
r"rfa|\bbrd\b|\bddr\b|\bgdr\b|"
|
||||
r"oesterreich|österreich|austria|schweiz|switzerland|suisse"
|
||||
r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_TRUSTED_CINEMA_COUNTRY = re.compile(
|
||||
r"\b("
|
||||
r"vereinigte\s+staaten|usa|united\s+states|"
|
||||
r"vereinigtes\s+königreich|grossbritannien|großbritannien|"
|
||||
r"great\s+britain|united\s+kingdom|kanada|canada|"
|
||||
r"australien|australia|neuseeland|new\s+zealand|"
|
||||
r"japan|südkorea|suedkorea|south\s+korea|hongkong|hong\s+kong"
|
||||
r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_PUBLIC_TV_STATIONS = {
|
||||
"das erste", "ard", "zdf", "zdfneo", "3sat", "arte", "one hd", "one",
|
||||
"rbb", "mdr", "br", "swr", "hr", "wdr", "ndr", "sr", "ard alpha",
|
||||
}
|
||||
|
||||
_JUNK_TITLE = re.compile(
|
||||
r"\b("
|
||||
r"programmänderung|programmänderung!|doku|dokumentation|reportage|"
|
||||
r"kommissar(?:in)?|krimi\s*-|dorfkrimi|krause[s]?|"
|
||||
r"ein\s+fall\s+von\s+liebe|liebe\s+auf|"
|
||||
r"ein\s+sommer\s+an|am\s+kap\s+der\s+liebe|"
|
||||
r"das\s+geheimnis\s+(?:der|des)|willkommen\s+daheim|"
|
||||
r"weihnachten|rosamunde\s+pilcher|inga\s+lindström|"
|
||||
r"udo\s+witte|j\.d\.\s+vance"
|
||||
r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_REJECT_GENRE = {
|
||||
"dokumentarfilm", "dokumentation", "musik", "musikfilm", "musical",
|
||||
"familie", "familienfilm", "kinderfilm", "tv-film",
|
||||
}
|
||||
|
||||
_DESIRED_GENRE_WORDS = {
|
||||
"action", "thriller", "krimi", "kriminal", "abenteuer", "science",
|
||||
"fiction", "sci-fi", "mystery", "western", "horror", "historie",
|
||||
"krieg", "war", "drama",
|
||||
}
|
||||
|
||||
_KNOWN_CINEMA_TITLE = {
|
||||
"96 hours", "absolute power", "air america", "alarmstufe rot",
|
||||
"american fighter", "appaloosa", "bastille day", "ben hur",
|
||||
"blacklight", "blade runner", "bodyguard", "bohemian rhapsody",
|
||||
"bourne", "braveheart", "carlito", "casino", "chinatown",
|
||||
"colombiana", "contraband", "crank", "deadpool", "der staatsfeind",
|
||||
"der clou", "the sting",
|
||||
"die bourne identität", "die bourne identitaet", "das vermächtnis",
|
||||
"das vermaechtnis", "g20", "green zone", "hitman", "manchurian",
|
||||
"network", "stirb langsam", "the nice guys", "tom clancy",
|
||||
}
|
||||
|
||||
_ENGLISH_WORDS = {
|
||||
"the", "of", "and", "in", "for", "from", "with", "on", "at", "to",
|
||||
"man", "men", "girl", "boy", "black", "white", "red", "blue", "last",
|
||||
"night", "day", "dead", "death", "kill", "killer", "war", "zone",
|
||||
"sea", "job", "agent", "daughter", "flood", "marine", "carnage",
|
||||
}
|
||||
|
||||
|
||||
def _enabled() -> bool:
|
||||
v = (os.environ.get("SAVETV_FILTER_DE_FR") or "1").strip().lower()
|
||||
|
|
@ -65,6 +136,121 @@ def _countries_for_title(title: str, cache: dict) -> list[str] | None:
|
|||
return [str(c)]
|
||||
|
||||
|
||||
def _info_for_title(title: str, cache: dict) -> dict:
|
||||
if not title or not cache:
|
||||
return {}
|
||||
entry = cache.get(title)
|
||||
if entry is None:
|
||||
nk = _norm_title_key(title)
|
||||
for k, v in cache.items():
|
||||
if isinstance(k, str) and _norm_title_key(k) == nk:
|
||||
entry = v
|
||||
break
|
||||
return entry if isinstance(entry, dict) else {}
|
||||
|
||||
|
||||
def _station_key(station: str) -> str:
|
||||
return re.sub(r"\s+", " ", station or "").strip().lower()
|
||||
|
||||
|
||||
def _is_public_tv_station(station: str) -> bool:
|
||||
s = _station_key(station)
|
||||
return s in _PUBLIC_TV_STATIONS
|
||||
|
||||
|
||||
def _has_country(pattern: re.Pattern, countries: list[str]) -> bool:
|
||||
return any(pattern.search(c or "") for c in countries)
|
||||
|
||||
|
||||
def _genres(info: dict) -> list[str]:
|
||||
raw = info.get("genres") if isinstance(info, dict) else []
|
||||
if isinstance(raw, list):
|
||||
return [str(g).lower() for g in raw if g]
|
||||
if raw:
|
||||
return [str(raw).lower()]
|
||||
return []
|
||||
|
||||
|
||||
def _has_desired_genre(genres: list[str]) -> bool:
|
||||
joined = " ".join(genres)
|
||||
return any(word in joined for word in _DESIRED_GENRE_WORDS)
|
||||
|
||||
|
||||
def _has_reject_genre(genres: list[str]) -> bool:
|
||||
joined = " ".join(genres)
|
||||
return any(g in joined for g in _REJECT_GENRE)
|
||||
|
||||
|
||||
def _looks_like_cinema_title(title: str) -> bool:
|
||||
t = _norm_title_key(title)
|
||||
if _is_known_cinema_seed(t):
|
||||
return True
|
||||
words = set(re.findall(r"[a-z]+", title.lower()))
|
||||
return len(words & _ENGLISH_WORDS) >= 2
|
||||
|
||||
|
||||
def _is_known_cinema_seed(norm_title: str) -> bool:
|
||||
return any(seed in norm_title for seed in _KNOWN_CINEMA_TITLE)
|
||||
|
||||
|
||||
def classify_auto_quality(
|
||||
title: str,
|
||||
station: str = "",
|
||||
cache: dict | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""Entscheidet konservativ, ob ein Titel automatisch aufgenommen/geladen wird.
|
||||
|
||||
Zielprofil aus Jellyfin: internationale Kinofilme, vor allem Action, Thriller,
|
||||
Krimi, Abenteuer, Sci-Fi, Western, Horror und starke Dramen. Deutsche/regionale
|
||||
TV-Produktionen und unerkannte OeR-Titel werden nicht automatisch verarbeitet.
|
||||
"""
|
||||
if not _enabled():
|
||||
return True, "filter-disabled"
|
||||
|
||||
if not title or _JUNK_TITLE.search(title):
|
||||
return False, "title-junk-pattern"
|
||||
|
||||
cache = load_filminfo_cache() if cache is None else cache
|
||||
info = _info_for_title(title, cache)
|
||||
countries = _countries_for_title(title, cache) or []
|
||||
genres = _genres(info)
|
||||
public_tv = _is_public_tv_station(station)
|
||||
known_cinema = _is_known_cinema_seed(_norm_title_key(title))
|
||||
cinema_title = _looks_like_cinema_title(title)
|
||||
|
||||
description = str(info.get("description") or "").strip() if info else ""
|
||||
if info and not known_cinema and len(description) < 40:
|
||||
return False, "weak-enrichment"
|
||||
|
||||
if countries and _has_country(_COUNTRY_EXCL, countries):
|
||||
return False, "country-de-fr"
|
||||
|
||||
if public_tv and countries and _has_country(_DACH_COUNTRY, countries):
|
||||
return False, "public-tv-dach-production"
|
||||
|
||||
if genres and _has_reject_genre(genres):
|
||||
return False, "reject-genre"
|
||||
|
||||
if countries and _has_country(_TRUSTED_CINEMA_COUNTRY, countries):
|
||||
if _has_desired_genre(genres) or cinema_title:
|
||||
return True, "trusted-cinema-profile"
|
||||
return False, "trusted-country-weak-genre"
|
||||
|
||||
if known_cinema:
|
||||
return True, "known-cinema-title"
|
||||
|
||||
if public_tv and not info:
|
||||
return False, "public-tv-needs-enrichment"
|
||||
|
||||
if not info:
|
||||
return False, "needs-enrichment"
|
||||
|
||||
if _has_desired_genre(genres) and cinema_title:
|
||||
return True, "cinema-genre-profile"
|
||||
|
||||
return False, "not-jellyfin-profile"
|
||||
|
||||
|
||||
def should_exclude_production_country(title: str, cache: dict | None = None) -> bool:
|
||||
"""True, wenn ein Produktionsland DE oder FR ist (laut Wikidata-Cache)."""
|
||||
if not _enabled():
|
||||
|
|
|
|||
44
homelab.conf
44
homelab.conf
|
|
@ -300,12 +300,20 @@ TUNNEL_600_MU3="arakavanews.com|:80|standby"
|
|||
TUNNEL_601_MU3="rss-manager|:8080|standby"
|
||||
|
||||
# ============================================================
|
||||
# HARDWARE-ZUKUNFT — Muldenstein (Stand 2026-04-20)
|
||||
# HARDWARE-ZUKUNFT — Muldenstein/PP (Stand 2026-04-26)
|
||||
# ============================================================
|
||||
# LEITLINIE: Es gibt genau EINE KI-Maschine — den bestehenden KI-Server
|
||||
# (Windows, RTX 3090, Tailscale 100.84.255.83). KEIN zweiter KI-Rechner
|
||||
# ist geplant. Wenn irgendwann ein Dual-GPU- oder Upgrade-Setup kommt,
|
||||
# ist das ein SEPARATER neuer Build — weder ki-tower noch MSI-B550-Build.
|
||||
# LEITLINIE NEU (ersetzt 2026-04-20):
|
||||
# Lokale KI fuer Coding & Jervais laeuft auf dem MacBook Pro 16"
|
||||
# M5 Max 128 GB / 2 TB (Kauf in HK, mobil DE+KH). Details und
|
||||
# Begruendung: hardware-ai-local-coding.md im Repo-Root.
|
||||
#
|
||||
# Bestehender KI-Server (Windows + RTX 3090, Tailscale 100.84.255.83)
|
||||
# bleibt fuer CUDA-Pflicht-Workloads (SDXL/XTTS/spezifische
|
||||
# Pipelines). KEINE Erweiterung. Alte GPUs (4x 3080, 8x 6600 XT)
|
||||
# werden verkauft. KEIN zweiter NVIDIA-Build, KEIN 2x5090-Rig.
|
||||
# Aktueller M5 Max ist ein einzelnes Geraet, kein Cluster.
|
||||
# M5 Ultra spaeter nur, wenn ein konkretes Coder-Modell den Sprung
|
||||
# rechtfertigt.
|
||||
# ============================================================
|
||||
|
||||
# --- ki-tower (geplant, in Bau) --- JELLYFIN-ERSATZ
|
||||
|
|
@ -340,10 +348,28 @@ TUNNEL_601_MU3="rss-manager|:8080|standby"
|
|||
# ROCm auf Navi 23 weiter inoffiziell. Fuer LLM praktisch irrelevant.
|
||||
# Status: offen, Tendenz Verkauf.
|
||||
|
||||
# --- Dedizierte zweite KI-Maschine --- NICHT GEPLANT
|
||||
# Sollte der Bedarf (Dual-GPU, Mac Studio, RTX Pro 6000) jemals
|
||||
# aufkommen, waere das ein KOMPLETT NEUER Build — NICHT ki-tower,
|
||||
# NICHT MSI-B550, NICHT KI-Server-Umbau.
|
||||
# --- Dedizierte zweite KI-Maschine --- ERLEDIGT durch M5 Max
|
||||
# Statt zweitem NVIDIA-Build wurde ein MacBook Pro 16" M5 Max
|
||||
# 128 GB / 2 TB als lokale KI-Maschine entschieden (siehe
|
||||
# hardware-ai-local-coding.md). Mobil, leise, 128 GB Unified
|
||||
# Memory als ein Pool. Zaehlt logisch als die "zweite KI-Maschine".
|
||||
# Standort: Phnom Penh primaer, Muldenstein bei Aufenthalt.
|
||||
# Kein Eintrag in der Tailscale-Server-Tabelle, weil mobil.
|
||||
|
||||
# --- M5 Ultra (zukuenftig, optional) --- NICHT GEPLANT VOR MODELL
|
||||
# Erst kaufen, wenn ein Coder-Modell vorliegt, das Cursor-4.7-Niveau
|
||||
# lokal mit >128 GB Unified Memory erreicht. Bis dahin: kein Kauf.
|
||||
# Pfad spaeter: M5 Max in Zahlung geben oder per Thunderbolt
|
||||
# als Co-Worker behalten.
|
||||
|
||||
# --- Verkauf Alt-GPUs --- AKTIV (Test-Marathon naechste Woche)
|
||||
# 4x RTX 3080 10 GB (Mining-Historie) -> testen auf MSI-B550-Rig,
|
||||
# dann eBay-Kleinanzeigen.
|
||||
# 1x RTX 3090 24 GB (kein Mining) -> aktuell 700-1100 EUR.
|
||||
# 8x AMD RX 6600 XT 8 GB -> Verkauf, ROCm fuer LLM
|
||||
# praktisch irrelevant.
|
||||
# 1x MacBook Pro 14" M1 Pro 16 GB OVP -> Verkauf, finanziert M5 Max
|
||||
# mit.
|
||||
|
||||
# ============================================================
|
||||
# NAMENS-MAPPING (Alt → Neu) — Referenz für Migration
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue