diff --git a/sort_ym/analyze.py b/sort_ym/analyze.py index 3a1532a..0f83dad 100644 --- a/sort_ym/analyze.py +++ b/sort_ym/analyze.py @@ -252,16 +252,19 @@ def analyze_tracks( } try: data = call(settings, prompt) + entry = {**data, **stamp, "key_line_verified": verify_key_line(data["key_line"], text)} # Широкий except намеренно: трёхчасовой прогон не должен падать целиком из-за - # одного зависшего/битого ответа локальной модели. KeyboardInterrupt и SystemExit - # наследуются от BaseException и сюда не попадают - Ctrl+C прерывает прогон. + # одного зависшего/битого ответа локальной модели - в том числе синтаксически + # валидного JSON без обязательного поля (KeyError здесь же, не только в call()). + # KeyboardInterrupt и SystemExit наследуются от BaseException и сюда не попадают - + # Ctrl+C прерывает прогон. except Exception as e: cache[tid] = {**stamp, "error": f"{type(e).__name__}: {e}"} failed += 1 print(f" [{i}/{len(pending)}] {info['title']} - ошибка: {type(e).__name__}") else: - cache[tid] = {**data, **stamp, "key_line_verified": verify_key_line(data["key_line"], text)} - print(f" [{i}/{len(pending)}] {info['title']} - {', '.join(data['mood'])}") + cache[tid] = entry + print(f" [{i}/{len(pending)}] {info['title']} - {', '.join(entry.get('mood', []))}") _atomic_write_json(cache_file, cache) if failed: diff --git a/tests/test_analyze.py b/tests/test_analyze.py index a9b237c..d2f3f6d 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -246,6 +246,20 @@ def test_analyze_records_error_marker_and_continues(tmp_path: Path): assert result["200"]["summary"] == "Герой бродит по городу.", "таймаут одного трека не роняет батч" +def test_analyze_records_error_marker_for_schema_incomplete_response(tmp_path: Path): + # Ollama иногда возвращает синтаксически валидный JSON без обязательного поля - + # это не исключение внутри call(), падает на доступе к data[...] после успешного возврата. + def call(settings, prompt): + if "Тоска" in prompt: + return {k: v for k, v in GOOD_RESPONSE.items() if k != "key_line"} + return dict(GOOD_RESPONSE) + + result = analyze.analyze_tracks(TRACKS, LYRICS, tmp_path, _settings(), call=call) + + assert "KeyError" in result["100"]["error"] + assert result["200"]["summary"] == "Герой бродит по городу.", "битый ответ одного трека не роняет батч" + + def test_analyze_retries_error_entries_on_next_run(tmp_path: Path): def failing(settings, prompt): raise TimeoutError("модель зависла")