From b599c9eb8a4a5d1526c14ebdb02dd431a4ca99af Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 17 Feb 2026 22:56:02 +0100 Subject: [PATCH 1/2] Fix default model, increase timeout, improve multi-column prompt - Change default text-LLM from llama3 (not installed) to gemma3:12b - Increase LLM timeout from 120s to 300s (large models need longer) - Add explicit multi-column layout instruction to vision prompt to prevent skipping columns on dense CD back-cover tracklists Co-Authored-By: Claude Sonnet 4.5 --- src/musiksammlung/cli.py | 4 ++-- src/musiksammlung/llm_parser.py | 4 ++-- src/musiksammlung/vision_llm.py | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/musiksammlung/cli.py b/src/musiksammlung/cli.py index afab349..820d078 100644 --- a/src/musiksammlung/cli.py +++ b/src/musiksammlung/cli.py @@ -90,7 +90,7 @@ def scan( ), languages: str = typer.Option("deu+eng", "--lang", "-l", help="OCR-Sprachen"), backend: str = typer.Option("ollama", "--backend", "-b", help="LLM-Backend"), - model: str = typer.Option("llama3", "--model", "-m", help="Text-LLM-Modell"), + model: str = typer.Option("gemma3:12b", "--model", "-m", help="Text-LLM-Modell"), base_url: str = typer.Option( "http://localhost:11434", "--url", help="LLM-API-URL" ), @@ -262,7 +262,7 @@ def process( ), languages: str = typer.Option("deu+eng", "--lang", "-l"), backend: str = typer.Option("ollama", "--backend", "-b"), - model: str = typer.Option("llama3", "--model", "-m"), + model: str = typer.Option("gemma3:12b", "--model", "-m"), base_url: str = typer.Option("http://localhost:11434", "--url"), dry_run: bool = typer.Option(False, "--dry-run"), ) -> None: diff --git a/src/musiksammlung/llm_parser.py b/src/musiksammlung/llm_parser.py index 1b0a98e..c69118c 100644 --- a/src/musiksammlung/llm_parser.py +++ b/src/musiksammlung/llm_parser.py @@ -47,7 +47,7 @@ def _call_ollama(text: str, model: str, base_url: str) -> str: ], "stream": False, }, - timeout=120.0, + timeout=300.0, ) response.raise_for_status() return response.json()["message"]["content"] @@ -71,7 +71,7 @@ def _call_openai_compatible( {"role": "user", "content": text}, ], }, - timeout=120.0, + timeout=300.0, ) response.raise_for_status() return response.json()["choices"][0]["message"]["content"] diff --git a/src/musiksammlung/vision_llm.py b/src/musiksammlung/vision_llm.py index 171d23e..1ac84a4 100644 --- a/src/musiksammlung/vision_llm.py +++ b/src/musiksammlung/vision_llm.py @@ -28,6 +28,9 @@ WICHTIG: - Wenn "CD 1", "CD 2", "Disc 1" etc. sichtbar sind, erstelle mehrere Einträge in "discs". - Ohne Disc-Angabe: eine Disc mit disc_number=1. - Lasse Zeitangaben (z.B. "3:12") und Interpretenangaben pro Track weg. +- MEHRSPALTIGE LAYOUTS: CD-Rückseiten haben oft 2, 3 oder 4 Spalten nebeneinander. + Lies ALLE Spalten vollständig von oben nach unten, bevor du zur nächsten Spalte gehst. + Überspringen oder Auslassen von Spalten ist ein häufiger Fehler — lies jede Spalte komplett. Antworte NUR mit dem JSON, ohne Erklärung. Beispiel: From f4e49a3df6e2fc5be4b980aa783e60dd3f1ec9f2 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 17 Feb 2026 23:08:24 +0100 Subject: [PATCH 2/2] Add disc count validation before apply Check audio file count vs JSON track count per disc before processing. Aborts with a clear error showing which discs have discrepancies and whether tracks are missing from the JSON or audio files are missing from the directory. Co-Authored-By: Claude Sonnet 4.5 --- src/musiksammlung/cli.py | 32 +++++++++++++++++++++- src/musiksammlung/organizer.py | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/musiksammlung/cli.py b/src/musiksammlung/cli.py index 820d078..6b2cc32 100644 --- a/src/musiksammlung/cli.py +++ b/src/musiksammlung/cli.py @@ -13,7 +13,7 @@ from musiksammlung.cover import copy_covers from musiksammlung.llm_parser import parse_tracklist from musiksammlung.models import Album from musiksammlung.ocr import ocr_images -from musiksammlung.organizer import apply_mapping, build_mapping +from musiksammlung.organizer import apply_mapping, build_mapping, check_disc_counts from musiksammlung.playlist import generate_playlist from musiksammlung.ripper import RipperConfig, interactive_rip from musiksammlung.tagger import tag_album @@ -146,6 +146,36 @@ def apply( raw = json.loads(album_json.read_text(encoding="utf-8")) album = Album.model_validate(raw) + # Prüfe Track-Anzahl pro Disc + checks = check_disc_counts(album, input_dir) + problems = [c for c in checks if not c.ok] + if problems: + typer.echo( + "\nFEHLER: Track-Diskrepanz zwischen gerippten Dateien und album.json:\n", + err=True, + ) + for c in checks: + status = "OK" if c.ok else "!!" + typer.echo( + f" [{status}] Disc {c.disc_number}: " + f"{c.audio_file_count} Datei(en), {c.json_track_count} JSON-Track(s)", + err=True, + ) + if c.surplus_files: + typer.echo( + f" → {c.surplus_files} Track(s) fehlen im JSON " + f"(Tracks {c.json_track_count + 1}–{c.audio_file_count} eintragen)", + err=True, + ) + elif c.surplus_json: + typer.echo( + f" → {c.surplus_json} JSON-Eintrag/Einträge ohne Audiodatei " + f"(Tracks {c.audio_file_count + 1}–{c.json_track_count} prüfen)", + err=True, + ) + typer.echo(f"\nBitte {album_json} korrigieren und erneut aufrufen.", err=True) + raise typer.Exit(1) + mapping = build_mapping(album, input_dir, output_dir) typer.echo(f"Mapping: {len(mapping)} Dateien") for src, dst in mapping.items(): diff --git a/src/musiksammlung/organizer.py b/src/musiksammlung/organizer.py index 22a6a3e..1c00419 100644 --- a/src/musiksammlung/organizer.py +++ b/src/musiksammlung/organizer.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging import re import shutil +from dataclasses import dataclass from pathlib import Path from musiksammlung.config import AUDIO_EXTENSIONS @@ -13,6 +14,29 @@ from musiksammlung.models import Album logger = logging.getLogger(__name__) +@dataclass +class DiscCheck: + """Ergebnis der Track-Zählung für eine einzelne Disc.""" + + disc_number: int + audio_file_count: int + json_track_count: int + + @property + def ok(self) -> bool: + return self.audio_file_count == self.json_track_count + + @property + def surplus_files(self) -> int: + """Dateien ohne JSON-Eintrag (Tracks fehlen im JSON).""" + return max(0, self.audio_file_count - self.json_track_count) + + @property + def surplus_json(self) -> int: + """JSON-Einträge ohne Datei (Dateien fehlen im Verzeichnis).""" + return max(0, self.json_track_count - self.audio_file_count) + + def _sanitize_filename(name: str) -> str: """Entfernt problematische Zeichen aus Dateinamen.""" return re.sub(r'[<>:"/\\|?*]', "_", name).strip() @@ -28,6 +52,31 @@ def discover_audio_files(directory: Path) -> list[Path]: return sorted(files, key=extract_number) +def check_disc_counts(album: Album, input_dir: Path) -> list[DiscCheck]: + """Vergleicht Dateianzahl und JSON-Track-Anzahl pro Disc. + + Args: + album: Validiertes Album-Modell + input_dir: Verzeichnis mit gerippten Dateien (enthält CD1/, CD2/, ... bei Multi-Disc) + + Returns: + Liste von DiscCheck-Objekten — auch für korrekte Discs (ok=True). + """ + multi_disc = len(album.discs) > 1 + results: list[DiscCheck] = [] + + for disc in album.discs: + source_dir = input_dir / f"CD{disc.disc_number}" if multi_disc else input_dir + file_count = len(discover_audio_files(source_dir)) if source_dir.exists() else 0 + results.append(DiscCheck( + disc_number=disc.disc_number, + audio_file_count=file_count, + json_track_count=len(disc.tracks), + )) + + return results + + def build_mapping( album: Album, input_dir: Path,