From abe35d6dfb275378aa2824c83dfeb0051ea9dff5 Mon Sep 17 00:00:00 2001 From: Marcel Hofer <49859266+goodguy1963@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:34:52 +0200 Subject: [PATCH] Release 2.3.7 Whisper offline cache fix --- AILab_WhisperASR.py | 36 +++++++++++++- __init__.py | 2 +- custom_nodes.json | 2 +- pyproject.toml | 2 +- tests/test_new_features.py | 97 +++++++++++++++++++++++++++++++++++++- 5 files changed, 133 insertions(+), 6 deletions(-) diff --git a/AILab_WhisperASR.py b/AILab_WhisperASR.py index 00ef29a..a900008 100644 --- a/AILab_WhisperASR.py +++ b/AILab_WhisperASR.py @@ -160,6 +160,19 @@ def _missing_audio_result(notes: list[str] | None = None) -> tuple[str, str, str return message, "[]", "\n".join(trace_lines) +def _is_local_whisper_cache_miss(exc: Exception) -> bool: + try: + from huggingface_hub.errors import LocalEntryNotFoundError + + if isinstance(exc, LocalEntryNotFoundError): + return True + except Exception: + pass + + message = str(exc or "").lower() + return "local_files_only" in message and "cached" in message + + class ThinkingLLM_Whisper_ASR: @classmethod def INPUT_TYPES(cls): @@ -219,7 +232,25 @@ class ThinkingLLM_Whisper_ASR: resolved_device = _resolve_whisper_device(str(device)) resolved_compute_type = _resolve_compute_type(resolved_device, str(compute_type)) - model = WhisperModel(str(model_size), device=resolved_device, compute_type=resolved_compute_type) + local_files_only = True + try: + model = WhisperModel( + str(model_size), + device=resolved_device, + compute_type=resolved_compute_type, + local_files_only=True, + ) + except Exception as exc: + if not _is_local_whisper_cache_miss(exc): + raise + notes.append("Local Whisper cache miss; downloaded model from Hugging Face once.") + local_files_only = False + model = WhisperModel( + str(model_size), + device=resolved_device, + compute_type=resolved_compute_type, + local_files_only=False, + ) segments_iter, info = model.transcribe( str(wav_path), language=None if language == "auto" else str(language), @@ -245,9 +276,10 @@ class ThinkingLLM_Whisper_ASR: [ "[WHISPER]", f"source={source_label}", - f"model_size={model_size}; device={resolved_device}; compute_type={resolved_compute_type}; task={task}; vad_filter={bool(vad_filter)}", + f"model_size={model_size}; device={resolved_device}; compute_type={resolved_compute_type}; local_files_only={local_files_only}; task={task}; vad_filter={bool(vad_filter)}", f"language={detected_language}; language_probability={detected_probability}; duration={duration}", f"segments={len(segments)}; transcript_chars={len(transcript)}", + *notes, ] ) return transcript, segments_json, trace diff --git a/__init__.py b/__init__.py index b8fdd9c..cc6340f 100644 --- a/__init__.py +++ b/__init__.py @@ -3,7 +3,7 @@ "name": "ComfyUI-ThinkingLLM", "description": "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time.", "author": "goodguy1963", - "version": "2.3.6", + "version": "2.3.7", "url": "https://github.com/goodguy1963/ComfyUI-ThinkingLLM", "category": "image" } diff --git a/custom_nodes.json b/custom_nodes.json index 884e352..25ebb56 100644 --- a/custom_nodes.json +++ b/custom_nodes.json @@ -9,5 +9,5 @@ "description": "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time.", "category": "image", "tags": ["vision", "language", "multimodal", "thinkingllm", "qwen", "gguf", "flash_attention", "performance", "wan2.2", "video", "i2v", "t2v", "storyboard", "caching", "fork", "gpl-3.0"], - "version": "2.3.6" + "version": "2.3.7" } diff --git a/pyproject.toml b/pyproject.toml index dfefd0d..a385486 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI-ThinkingLLM" description = "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time." -version = "2.3.6" +version = "2.3.7" license = {file = "LICENSE"} dependencies = ["transformers>=5.2.0", "torch", "huggingface-hub", "hf_xet", "psutil", "numpy", "Pillow", "opencv-python", "bitsandbytes", "accelerate", "sentencepiece", "tiktoken", "faster-whisper", "llama-cpp-python"] diff --git a/tests/test_new_features.py b/tests/test_new_features.py index 972da16..024fd3a 100644 --- a/tests/test_new_features.py +++ b/tests/test_new_features.py @@ -1049,7 +1049,7 @@ class TestGGUFAdvancedWorkflowCompatibility(unittest.TestCase): captured = {} class FakeWhisperModel: - def __init__(self, model_size, device, compute_type): + def __init__(self, model_size, device, compute_type, local_files_only=False): captured["init"] = (model_size, device, compute_type) def transcribe(self, path, language, task, beam_size, vad_filter): @@ -1083,6 +1083,101 @@ class TestGGUFAdvancedWorkflowCompatibility(unittest.TestCase): self.assertEqual(captured["transcribe"], (".wav", "de", "transcribe", 3, True)) self.assertIn("language=de", raw_trace) + def test_whisper_asr_prefers_cached_model_without_hf_ping(self): + module = load_module_from_file( + "AILab_WhisperASR.py", + "thinkingllm_whisper_cached_model_test", + ) + + class FakeSegment: + start = 0.0 + end = 1.0 + text = " cached transcript " + + fake_info = types.SimpleNamespace(language="en", language_probability=0.99, duration=1.0) + captured = {} + + class FakeWhisperModel: + def __init__(self, model_size, device, compute_type, local_files_only=False): + captured["init"] = (model_size, device, compute_type, local_files_only) + + def transcribe(self, path, language, task, beam_size, vad_filter): + return [FakeSegment()], fake_info + + fake_backend = build_stub_module("faster_whisper", WhisperModel=FakeWhisperModel) + with tempfile.TemporaryDirectory() as temp_dir: + source = Path(temp_dir) / "input.wav" + with wave.open(str(source), "wb") as wav: + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(16000) + wav.writeframes(b"\0\0" * 1600) + + with mock.patch.dict(sys.modules, {"faster_whisper": fake_backend}, clear=False): + transcript, _, raw_trace = module.ThinkingLLM_Whisper_ASR().process( + model_size="small", + language="auto", + task="transcribe", + device="cpu", + compute_type="int8", + beam_size=5, + vad_filter=True, + audio_file_path=str(source), + ) + + self.assertEqual(transcript, "cached transcript") + self.assertEqual(captured["init"], ("small", "cpu", "int8", True)) + self.assertIn("local_files_only=True", raw_trace) + + def test_whisper_asr_downloads_once_when_cache_is_missing(self): + module = load_module_from_file( + "AILab_WhisperASR.py", + "thinkingllm_whisper_cache_miss_test", + ) + + class FakeSegment: + start = 0.0 + end = 1.0 + text = " downloaded transcript " + + fake_info = types.SimpleNamespace(language="en", language_probability=0.99, duration=1.0) + attempts = [] + + class FakeWhisperModel: + def __init__(self, model_size, device, compute_type, local_files_only=False): + attempts.append(local_files_only) + if local_files_only: + raise RuntimeError("local_files_only=True and no cached snapshot was found") + + def transcribe(self, path, language, task, beam_size, vad_filter): + return [FakeSegment()], fake_info + + fake_backend = build_stub_module("faster_whisper", WhisperModel=FakeWhisperModel) + with tempfile.TemporaryDirectory() as temp_dir: + source = Path(temp_dir) / "input.wav" + with wave.open(str(source), "wb") as wav: + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(16000) + wav.writeframes(b"\0\0" * 1600) + + with mock.patch.dict(sys.modules, {"faster_whisper": fake_backend}, clear=False): + transcript, _, raw_trace = module.ThinkingLLM_Whisper_ASR().process( + model_size="small", + language="auto", + task="transcribe", + device="cpu", + compute_type="int8", + beam_size=5, + vad_filter=True, + audio_file_path=str(source), + ) + + self.assertEqual(transcript, "downloaded transcript") + self.assertEqual(attempts, [True, False]) + self.assertIn("local_files_only=False", raw_trace) + self.assertIn("Local Whisper cache miss", raw_trace) + def test_audio_to_wav_base64_resamples_to_16khz_mono(self): import numpy as real_numpy