Skip to content

Commit 8c274c7

Browse files
author
33
committed
feat: add HF Transformers auto-conversion with bundled converter support
1 parent 3ad0fc4 commit 8c274c7

6 files changed

Lines changed: 909 additions & 15 deletions

File tree

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@ logs/
55
__pycache__
66
faster-whisper-xxl-gui.spec
77
*.json
8-
*.txt
8+
*.txt
9+
bundle/
10+
conv_env/
11+
fwxxl-converter-win-x64.zip
12+
fwxxl-converter-win-x64.sha256
13+
tools/

src/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11

2-
APP_VERSION = "1.13.0"
2+
APP_VERSION = "1.14.0"
33
SUPPORTED_EXTENSIONS = ('.mp3', '.wav', '.mp4', '.m4a', '.flac', '.aac', '.ogg', '.webm', '.mkv', '.avi', '.mov')
44
YTDLP_UPDATE_FAILURE_COOLDOWN_HOURS = 6
55
YTDLP_DEBUG_LOG_NAME = "ytdlp_update_debug.log"
66
EXTERNAL_MODULE_DIR_NAME = "python_modules"
77
PYTHON_PROBE_LOG_MAX = 25
8+
CONVERTER_BUNDLE_REPO = "cbro33/Faster-Whisper-XXL-GUI"
9+
CONVERTER_BUNDLE_TAG = "converter-bundle"
10+
CONVERTER_BUNDLE_ASSET = "fwxxl-converter-win-x64.zip"
11+
CONVERTER_BUNDLE_SHA256_ASSET = "fwxxl-converter-win-x64.sha256"
12+
CONVERTER_BUNDLE_DIR_NAME = "converter_bundle"
813

914
PYTHON_INFO_SCRIPT = """import json
1015
import sys

src/converter_utils.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import os
2+
import sys
3+
import json
4+
import shutil
5+
import zipfile
6+
import tempfile
7+
import hashlib
8+
import logging
9+
import requests
10+
11+
from config import (
12+
APP_VERSION,
13+
CONVERTER_BUNDLE_REPO,
14+
CONVERTER_BUNDLE_TAG,
15+
CONVERTER_BUNDLE_ASSET,
16+
CONVERTER_BUNDLE_SHA256_ASSET,
17+
CONVERTER_BUNDLE_DIR_NAME,
18+
)
19+
from utils import get_settings_directory
20+
21+
GITHUB_HEADERS = {"User-Agent": f"Faster-Whisper-XXL-GUI/{APP_VERSION}"}
22+
23+
_WEIGHT_FILENAMES = {"model.safetensors", "pytorch_model.bin"}
24+
25+
def find_transformers_weight_files(filenames):
26+
if not filenames:
27+
return []
28+
matches = []
29+
for name in filenames:
30+
if not name:
31+
continue
32+
lowered = name.lower()
33+
if lowered in _WEIGHT_FILENAMES:
34+
matches.append(name)
35+
elif lowered.startswith("model-") and lowered.endswith(".safetensors"):
36+
matches.append(name)
37+
elif lowered.startswith("pytorch_model-") and lowered.endswith(".bin"):
38+
matches.append(name)
39+
return matches
40+
41+
def scan_transformers_weights(model_dir):
42+
if not model_dir or not os.path.isdir(model_dir):
43+
return []
44+
try:
45+
filenames = os.listdir(model_dir)
46+
except Exception:
47+
return []
48+
return find_transformers_weight_files(filenames)
49+
50+
def get_converter_bundle_cache_dir():
51+
settings_dir = get_settings_directory()
52+
return os.path.join(settings_dir, CONVERTER_BUNDLE_DIR_NAME)
53+
54+
def get_converter_bundle_dir():
55+
base_dir = get_converter_bundle_cache_dir()
56+
return os.path.join(base_dir, CONVERTER_BUNDLE_TAG)
57+
58+
def get_converter_python_path(bundle_dir):
59+
if not bundle_dir:
60+
return None
61+
candidates = [
62+
os.path.join(bundle_dir, "python", "Scripts", "python.exe"),
63+
os.path.join(bundle_dir, "Scripts", "python.exe"),
64+
os.path.join(bundle_dir, "python", "python.exe"),
65+
os.path.join(bundle_dir, "python.exe"),
66+
]
67+
for path in candidates:
68+
if os.path.isfile(path):
69+
return path
70+
return None
71+
72+
def _fetch_release_info():
73+
api_url = f"https://api.github.com/repos/{CONVERTER_BUNDLE_REPO}/releases/tags/{CONVERTER_BUNDLE_TAG}"
74+
response = requests.get(api_url, timeout=12, headers=GITHUB_HEADERS)
75+
if response.status_code != 200:
76+
raise RuntimeError(f"Release lookup failed (status {response.status_code}).")
77+
try:
78+
return response.json()
79+
except json.JSONDecodeError as exc:
80+
raise RuntimeError(f"Release lookup failed: {exc}") from exc
81+
82+
def _find_release_asset(release, asset_name):
83+
assets = release.get("assets") or []
84+
for asset in assets:
85+
if asset.get("name") == asset_name:
86+
return asset
87+
return None
88+
89+
def _download_file(url, target_path, progress_cb=None, cancel_cb=None):
90+
response = requests.get(url, stream=True, timeout=30, headers=GITHUB_HEADERS)
91+
response.raise_for_status()
92+
total = int(response.headers.get("content-length", 0))
93+
downloaded = 0
94+
with open(target_path, "wb") as handle:
95+
for chunk in response.iter_content(chunk_size=1024 * 1024):
96+
if cancel_cb and cancel_cb():
97+
return False, total, downloaded
98+
if not chunk:
99+
continue
100+
handle.write(chunk)
101+
downloaded += len(chunk)
102+
if progress_cb:
103+
progress_cb(downloaded, total)
104+
return True, total, downloaded
105+
106+
def _read_sha256_file(path):
107+
try:
108+
with open(path, "r", encoding="utf-8") as handle:
109+
contents = handle.read().strip()
110+
if not contents:
111+
return None
112+
token = contents.split()[0].strip()
113+
if len(token) != 64:
114+
return None
115+
return token.lower()
116+
except Exception:
117+
return None
118+
119+
def _compute_sha256(path):
120+
digest = hashlib.sha256()
121+
with open(path, "rb") as handle:
122+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
123+
digest.update(chunk)
124+
return digest.hexdigest().lower()
125+
126+
def ensure_converter_bundle(progress_cb=None, cancel_cb=None):
127+
bundle_dir = get_converter_bundle_dir()
128+
python_path = get_converter_python_path(bundle_dir)
129+
if python_path and os.path.isfile(python_path):
130+
return python_path
131+
132+
os.makedirs(bundle_dir, exist_ok=True)
133+
temp_dir = tempfile.mkdtemp(prefix="fw-converter-")
134+
try:
135+
release = _fetch_release_info()
136+
asset = _find_release_asset(release, CONVERTER_BUNDLE_ASSET)
137+
if not asset or not asset.get("browser_download_url"):
138+
raise RuntimeError(f"Converter bundle asset not found: {CONVERTER_BUNDLE_ASSET}")
139+
zip_url = asset.get("browser_download_url")
140+
141+
sha_asset = _find_release_asset(release, CONVERTER_BUNDLE_SHA256_ASSET)
142+
sha_url = sha_asset.get("browser_download_url") if sha_asset else None
143+
144+
zip_path = os.path.join(temp_dir, CONVERTER_BUNDLE_ASSET)
145+
if progress_cb:
146+
progress_cb("Downloading converter bundle...", 0, 0)
147+
148+
def download_progress(downloaded, total):
149+
if progress_cb:
150+
progress_cb("Downloading converter bundle...", downloaded, total)
151+
152+
ok, _total, _downloaded = _download_file(zip_url, zip_path, progress_cb=download_progress, cancel_cb=cancel_cb)
153+
if not ok:
154+
raise RuntimeError("Converter download cancelled.")
155+
156+
sha_expected = None
157+
if sha_url:
158+
sha_path = os.path.join(temp_dir, CONVERTER_BUNDLE_SHA256_ASSET)
159+
_download_file(sha_url, sha_path, cancel_cb=cancel_cb)
160+
sha_expected = _read_sha256_file(sha_path)
161+
162+
if sha_expected:
163+
sha_actual = _compute_sha256(zip_path)
164+
if sha_actual != sha_expected:
165+
raise RuntimeError("Converter bundle checksum mismatch.")
166+
167+
if progress_cb:
168+
progress_cb("Extracting converter bundle...", 0, 0)
169+
extract_dir = os.path.join(temp_dir, "extract")
170+
os.makedirs(extract_dir, exist_ok=True)
171+
with zipfile.ZipFile(zip_path, "r") as archive:
172+
archive.extractall(extract_dir)
173+
174+
extracted_root = extract_dir
175+
entries = os.listdir(extract_dir)
176+
if len(entries) == 1:
177+
candidate = os.path.join(extract_dir, entries[0])
178+
if os.path.isdir(candidate):
179+
extracted_root = candidate
180+
181+
if os.path.exists(bundle_dir):
182+
shutil.rmtree(bundle_dir, ignore_errors=True)
183+
shutil.move(extracted_root, bundle_dir)
184+
185+
python_path = get_converter_python_path(bundle_dir)
186+
if not python_path or not os.path.isfile(python_path):
187+
raise RuntimeError("Converter bundle missing python.exe.")
188+
return python_path
189+
finally:
190+
shutil.rmtree(temp_dir, ignore_errors=True)
191+
192+
def get_fallback_python():
193+
override = (os.environ.get("FWHISPER_CONVERTER_PYTHON") or "").strip()
194+
if override:
195+
if os.path.isdir(override):
196+
candidate = os.path.join(override, "python.exe")
197+
if os.path.isfile(candidate):
198+
return candidate
199+
if os.path.isfile(override):
200+
return override
201+
return sys.executable

0 commit comments

Comments
 (0)