v1.0.4 - compatibility update

This commit is contained in:
2026-01-25 20:28:01 +02:00
parent 1318c72210
commit 6a98142c1d
5 changed files with 105 additions and 21 deletions

View File

@@ -43,6 +43,18 @@ Whisper Voice operates directly on the metal. It is not an API wrapper; it is an
| **Sensory Gate** | **Silero VAD** | Enterprise-grade Voice Activity Detection filters out the noise, ensuring only pure intent is processed. | | **Sensory Gate** | **Silero VAD** | Enterprise-grade Voice Activity Detection filters out the noise, ensuring only pure intent is processed. |
| **Interface** | **Qt 6 / QML** | Hardware-accelerated, glassmorphic UI that is fluid, responsive, and sovereign. | | **Interface** | **Qt 6 / QML** | Hardware-accelerated, glassmorphic UI that is fluid, responsive, and sovereign. |
### 🛑 Compatibility Matrix (Windows)
The core engine (`CTranslate2`) is heavily optimized for Nvidia tensor cores.
| Manufacturer | Hardware | Status | Notes |
| :--- | :--- | :--- | :--- |
| **Nvidia** | GTX 900+ / RTX | ✅ **Supported** | Full heavy-metal acceleration. |
| **AMD** | Radeon RX | ⚠️ **CPU Fallback** | Runs on CPU. Valid for `Small/Medium`, slow for `Large`. |
| **Intel** | Arc / Iris | ⚠️ **CPU Fallback** | Runs on CPU. Valid for `Small/Medium`, slow for `Large`. |
| **Apple** | M1 / M2 / M3 | ❌ **Unsupported** | Release is strictly Windows x64. |
> **AMD Users**: v1.0.3 auto-detects GPU failures and silently falls back to CPU.
<br> <br>
## 🖋️ Universal Transcription ## 🖋️ Universal Transcription

BIN
dist/WhisperVoice.exe vendored

Binary file not shown.

25
main.py
View File

@@ -9,6 +9,31 @@ app_dir = os.path.dirname(os.path.abspath(__file__))
if app_dir not in sys.path: if app_dir not in sys.path:
sys.path.insert(0, app_dir) sys.path.insert(0, app_dir)
# -----------------------------------------------------------------------------
# WINDOWS DLL FIX (CRITICAL for Portable CUDA)
# Python 3.8+ on Windows requires explicit DLL directory addition.
# -----------------------------------------------------------------------------
if os.name == 'nt' and hasattr(os, 'add_dll_directory'):
try:
from pathlib import Path
# Scan sys.path for site-packages
for p in sys.path:
path_obj = Path(p)
if path_obj.name == 'site-packages' and path_obj.exists():
nvidia_path = path_obj / "nvidia"
if nvidia_path.exists():
for subdir in nvidia_path.iterdir():
# Add 'bin' folder from each nvidia stub (cublas, cudnn, etc.)
bin_path = subdir / "bin"
if bin_path.exists():
os.add_dll_directory(str(bin_path))
# Also try adding site-packages itself just in case
# os.add_dll_directory(str(path_obj))
break
except Exception:
pass
# -----------------------------------------------------------------------------
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox
from PySide6.QtCore import QObject, Slot, Signal, QThread, Qt, QUrl from PySide6.QtCore import QObject, Slot, Signal, QThread, Qt, QUrl
from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQml import QQmlApplicationEngine

View File

@@ -21,7 +21,7 @@ except ImportError:
torch = None torch = None
# Import directly - valid since we are now running in the full environment # Import directly - valid since we are now running in the full environment
from faster_whisper import WhisperModel
class WhisperTranscriber: class WhisperTranscriber:
""" """
@@ -62,13 +62,32 @@ class WhisperTranscriber:
# Force offline if path exists to avoid HF errors # Force offline if path exists to avoid HF errors
local_only = new_path.exists() local_only = new_path.exists()
self.model = WhisperModel( try:
model_input, from faster_whisper import WhisperModel
device=device, self.model = WhisperModel(
compute_type=compute, model_input,
download_root=str(get_models_path()), device=device,
local_files_only=local_only compute_type=compute,
) download_root=str(get_models_path()),
local_files_only=local_only
)
except Exception as load_err:
# CRITICAL FALLBACK: If CUDA/cublas fails (AMD/Intel users), fallback to CPU
err_str = str(load_err).lower()
if "cublas" in err_str or "cudnn" in err_str or "library" in err_str or "device" in err_str:
logging.warning(f"CUDA Init Failed ({load_err}). Falling back to CPU...")
self.config.set("compute_device", "cpu") # Update config for persistence/UI
self.current_compute_device = "cpu"
self.model = WhisperModel(
model_input,
device="cpu",
compute_type="int8", # CPU usually handles int8 well with newer extensions, or standard
download_root=str(get_models_path()),
local_files_only=local_only
)
else:
raise load_err
self.current_model_size = size self.current_model_size = size
self.current_compute_device = device self.current_compute_device = device
@@ -79,6 +98,32 @@ class WhisperTranscriber:
logging.error(f"Failed to load model: {e}") logging.error(f"Failed to load model: {e}")
self.model = None self.model = None
# Auto-Repair: Detect vocabulary/corrupt errors
err_str = str(e).lower()
if "vocabulary" in err_str or "tokenizer" in err_str or "config.json" in err_str:
# ... existing auto-repair logic ...
logging.warning("Corrupt model detected on load. Attempting to delete and reset...")
try:
import shutil
# Differentiate between simple path and HF path
new_path = get_models_path() / f"faster-whisper-{size}"
if new_path.exists():
shutil.rmtree(new_path)
logging.info(f"Deleted corrupt model at {new_path}")
else:
# Try legacy HF path
hf_path = get_models_path() / f"models--Systran--faster-whisper-{size}"
if hf_path.exists():
shutil.rmtree(hf_path)
logging.info(f"Deleted corrupt HF model at {hf_path}")
# Notify UI to refresh state (will show 'Download' button now)
# We can't reach bridge easily here without passing it in,
# but the UI polls or listens to logs.
# The user will simply see "Model Missing" in settings after this.
except Exception as del_err:
logging.error(f"Failed to delete corrupt model: {del_err}")
def transcribe(self, audio_data, is_file: bool = False, task: Optional[str] = None) -> str: def transcribe(self, audio_data, is_file: bool = False, task: Optional[str] = None) -> str:
""" """
Transcribe audio data. Transcribe audio data.
@@ -89,7 +134,7 @@ class WhisperTranscriber:
if not self.model: if not self.model:
self.load_model() self.load_model()
if not self.model: if not self.model:
return "Error: Model failed to load." return "Error: Model failed to load. Please check Settings -> Model Info."
try: try:
# Config # Config
@@ -174,8 +219,11 @@ class WhisperTranscriber:
def model_exists(self, size: str) -> bool: def model_exists(self, size: str) -> bool:
"""Checks if a model size is already downloaded.""" """Checks if a model size is already downloaded."""
new_path = get_models_path() / f"faster-whisper-{size}" new_path = get_models_path() / f"faster-whisper-{size}"
if (new_path / "config.json").exists(): if new_path.exists():
return True # Strict check
required = ["config.json", "model.bin", "vocabulary.json"]
if all((new_path / f).exists() for f in required):
return True
# Legacy HF cache check # Legacy HF cache check
folder_name = f"models--Systran--faster-whisper-{size}" folder_name = f"models--Systran--faster-whisper-{size}"

View File

@@ -381,25 +381,24 @@ class UIBridge(QObject):
# Check new simple format used by DownloadWorker # Check new simple format used by DownloadWorker
path_simple = get_models_path() / f"faster-whisper-{size}" path_simple = get_models_path() / f"faster-whisper-{size}"
if path_simple.exists() and any(path_simple.iterdir()): if path_simple.exists():
return True # Strict check: Ensure all critical files exist
required = ["config.json", "model.bin", "vocabulary.json"]
if all((path_simple / f).exists() for f in required):
return True
# Check HF Cache format (legacy/default) # Check HF Cache format (legacy/default)
folder_name = f"models--Systran--faster-whisper-{size}" folder_name = f"models--Systran--faster-whisper-{size}"
path_hf = get_models_path() / folder_name path_hf = get_models_path() / folder_name
snapshots = path_hf / "snapshots" snapshots = path_hf / "snapshots"
if snapshots.exists() and any(snapshots.iterdir()): if snapshots.exists() and any(snapshots.iterdir()):
return True return True # Legacy cache structure is complex, assume valid if present
# Check direct folder (simple) return False
path_direct = get_models_path() / size
if (path_direct / "config.json").exists():
return True
except Exception as e: except Exception as e:
logging.error(f"Error checking model status: {e}") logging.error(f"Error checking model status: {e}")
return False
return False
@Slot(str) @Slot(str)
def downloadModel(self, size): def downloadModel(self, size):