Skip to content

Commit 1ba0f72

Browse files
committed
feat: v1.3.2 - Windows robustness, professional metadata, branding footer, and responsive UI
1 parent f9f97dc commit 1ba0f72

7 files changed

Lines changed: 122 additions & 19 deletions

File tree

app/config.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,36 @@
1212

1313
# Detect if the app is run as a bundle (PyInstaller)
1414
if getattr(sys, 'frozen', False):
15-
# Running in a frozen bundle (.exe)
1615
BASE_DIR = os.path.dirname(sys.executable)
1716
else:
18-
# Running in a normal Python environment
1917
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2018

21-
MODELS_DIR = os.path.join(BASE_DIR, "models")
22-
OUTPUT_DIR = os.path.join(BASE_DIR, "exports")
23-
HISTORY_FILE = os.path.join(BASE_DIR, "history.json")
19+
# Windows Robustness: Check if BASE_DIR is writable.
20+
# If not (e.g. C:\Program Files), fallback to AppData for models/settings.
21+
def is_writable(path):
22+
try:
23+
if not os.path.exists(path):
24+
os.makedirs(path, exist_ok=True)
25+
test_file = os.path.join(path, ".write_test")
26+
with open(test_file, "w") as f:
27+
f.write("test")
28+
os.remove(test_file)
29+
return True
30+
except Exception:
31+
return False
32+
33+
DATA_ROOT = BASE_DIR
34+
if sys.platform == "win32":
35+
if not is_writable(BASE_DIR):
36+
appdata = os.environ.get("APPDATA")
37+
if appdata:
38+
DATA_ROOT = os.path.join(appdata, "clipperr")
39+
os.makedirs(DATA_ROOT, exist_ok=True)
40+
41+
MODELS_DIR = os.path.join(DATA_ROOT, "models")
42+
OUTPUT_DIR = os.path.join(DATA_ROOT, "exports")
43+
HISTORY_FILE = os.path.join(DATA_ROOT, "history.json")
44+
USER_SETTINGS_FILE = os.path.join(DATA_ROOT, "user_settings.json")
2445

2546
WHISPER_MODEL_PATH = os.path.join(MODELS_DIR, "whisper", "base")
2647
LLM_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
@@ -74,7 +95,7 @@
7495
WINDOW_DEFAULT_HEIGHT = 700
7596

7697
# ── User Preferences ──────────────────────────────────
77-
USER_SETTINGS_FILE = os.path.join(BASE_DIR, "user_settings.json")
98+
# USER_SETTINGS_FILE is now defined above with DATA_ROOT fallback
7899

79100
class UserSettings:
80101
"""Simple JSON-based persistent dynamic settings."""

app/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ def _build_sidebar(self) -> QFrame:
117117
self._nav_btns.append(btn)
118118

119119
layout.addStretch()
120+
121+
# Branding Footer
122+
footer = QLabel("Created by crediblemark.com")
123+
footer.setStyleSheet("color: #475569; font-size: 10px; font-weight: bold; margin-bottom: 15px;")
124+
footer.setAlignment(Qt.AlignCenter)
125+
layout.addWidget(footer)
126+
120127
return sidebar
121128

122129
def _switch_page(self, index: int):

app/pages/settings_page.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,15 +455,18 @@ def _build_model_section(self, layout: QVBoxLayout):
455455
from config import prefs
456456
desc = QLabel("Manage your local AI models and application preferences.")
457457
desc.setStyleSheet("color: #94a3b8; margin-bottom: 20px;")
458+
desc.setWordWrap(True)
458459
layout.addWidget(desc)
459460

460461
section_title = QLabel("AI Model Manager")
461462
section_title.setStyleSheet("font-size: 18px; font-weight: 700; color: #38bdf8;")
463+
section_title.setWordWrap(True)
462464
layout.addWidget(section_title)
463465

464466
# Informational note about models
465467
info_label = QLabel("Note: AI models will be downloaded automatically from public repositories (HuggingFace/GitHub).")
466468
info_label.setStyleSheet("color: #64748b; font-size: 11px; font-style: italic; margin-top: 10px;")
469+
info_label.setWordWrap(True)
467470
layout.addWidget(info_label)
468471

469472
# Model cards
@@ -475,8 +478,10 @@ def _build_model_section(self, layout: QVBoxLayout):
475478
v_info = QVBoxLayout()
476479
name = QLabel(model_id.upper().replace("-", " "))
477480
name.setStyleSheet("font-weight: 800; font-size: 14px;")
481+
name.setWordWrap(True)
478482
repo_label = QLabel(info.get("repo", info.get("url", "")))
479483
repo_label.setStyleSheet("color: #64748b; font-size: 11px;")
484+
repo_label.setWordWrap(True)
480485
v_info.addWidget(name)
481486
v_info.addWidget(repo_label)
482487

app/services/downloader.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,18 @@ class DownloadWorker(QObject):
6969

7070
def download_model(self, repo_id: str, filenames: list[str], local_dir: str, token: str | None = None):
7171
try:
72+
# 1. Check Directory Permissions early
73+
if not config.is_writable(local_dir):
74+
self.progress_signal.emit("Error: Access Denied! Run as Admin or choose different path.", 0)
75+
self.finished_signal.emit(repo_id, False)
76+
return
77+
7278
os.makedirs(local_dir, exist_ok=True)
7379
log.info("Starting granular download for %s into %s", repo_id, local_dir)
7480

81+
# Robustness: Increase ETAG timeout for large files on restricted networks
82+
os.environ["HF_HUB_ETAG_TIMEOUT"] = "100"
83+
7584
# If no files specified, use snapshot_download (likely a complex repo like Qwen)
7685
if not filenames:
7786
self.progress_signal.emit(f"Verifying {repo_id} (Snapshot)...", 30)
@@ -97,14 +106,27 @@ def download_model(self, repo_id: str, filenames: list[str], local_dir: str, tok
97106
# Create a bridge for tqdm with multi-file context and byte-weighting
98107
bridge = ProgressBridge(fname, self.progress_signal, i, len(filenames), weights)
99108

100-
hf_hub_download(
101-
repo_id=repo_id,
102-
filename=fname,
103-
local_dir=local_dir,
104-
token=None,
105-
local_dir_use_symlinks=False,
106-
tqdm_class=lambda **kwargs: bridge # Inject our bridge as the tqdm class
107-
)
109+
# Retry loop (5 attempts) for resilience against ConnectionReset/Firewall
110+
max_retries = 5
111+
for attempt in range(max_retries):
112+
try:
113+
hf_hub_download(
114+
repo_id=repo_id,
115+
filename=fname,
116+
local_dir=local_dir,
117+
token=None,
118+
local_dir_use_symlinks=False,
119+
tqdm_class=lambda **kwargs: bridge # Inject our bridge as the tqdm class
120+
)
121+
break # Success!
122+
except Exception as e:
123+
if attempt < max_retries - 1:
124+
log.warning("Download attempt %d failed for %s: %s", attempt+1, fname, e)
125+
self.progress_signal.emit(f"Retrying {fname} ({attempt+2}/{max_retries})...", 5)
126+
import time
127+
time.sleep(2) # Exponential backoff would be better but fixed sleep is OK for now
128+
else:
129+
raise # Final attempt failed
108130

109131
self.progress_signal.emit("Setup complete!", 100)
110132
self.finished_signal.emit(repo_id, True)
@@ -114,14 +136,17 @@ def download_model(self, repo_id: str, filenames: list[str], local_dir: str, tok
114136
error_msg = str(exc)
115137
log.error("Download failed for %s: %s", repo_id, error_msg)
116138

117-
# DO NOT clean up directory anymore—we want to support resuming large files.
118-
# hf_hub_download handles its own .incomplete files.
119-
120-
if "401" in error_msg or "Unauthorized" in error_msg:
139+
# Diagnostic for common Windows failures
140+
if "SSL" in error_msg:
141+
display_msg = "Error: SSL/Certificate Mismatch. Check VPN/Firewall."
142+
elif "Connection reset" in error_msg or "10054" in error_msg:
143+
display_msg = "Error: Connection Reset by Firewall."
144+
elif "401" in error_msg or "Unauthorized" in error_msg:
121145
display_msg = "Error: Auth Required (Gated Model?)"
122146
elif "disk" in error_msg.lower() or "space" in error_msg.lower():
123147
display_msg = "Error: Disk Full!"
124148
else:
149+
# Show more of the error to help debug
125150
display_msg = f"Error: {error_msg[:100]}" if len(error_msg) > 100 else f"Error: {error_msg}"
126151

127152
self.progress_signal.emit(display_msg, 0)

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: 1.3.1.{build}
1+
version: 1.3.2.{build}
22
image: Visual Studio 2022
33

44
# Hanya build jika ada TAG (seperti v1.0.0)

clipperr.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ exe = EXE(
7676
codesign_identity=None,
7777
entitlements_file=None,
7878
icon=['app/assets/icon.png'],
79+
version='version_info.txt',
7980
)
8081
coll = COLLECT(
8182
exe,

version_info.txt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
# UTF-8
3+
#
4+
# For more details about fixed file info 'ffi' see:
5+
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
6+
VSVersionInfo(
7+
ffi=FixedFileInfo(
8+
# filevers and prodvers should be (6, 1, 7, 1)
9+
# i.e. a tuple of four int, or a sentinel object
10+
filevers=(1, 3, 2, 0),
11+
prodvers=(1, 3, 2, 0),
12+
# Contains a bitmask that specifies the valid bits 'flags'r
13+
mask=0x3f,
14+
# Contains a bitmask that specifies the attributes of the file.
15+
flags=0x0,
16+
# The operating system for which this file was designed.
17+
# 0x4 - NT and Windows
18+
OS=0x40004,
19+
# The general type of file.
20+
# 0x1 - the file is an application.
21+
fileType=0x1,
22+
# The function of the file.
23+
# 0x0 - the function is not defined.
24+
subtype=0x0,
25+
# Creation date and time stamp.
26+
date=(0, 0)
27+
),
28+
kids=[
29+
StringFileInfo(
30+
[
31+
StringTable(
32+
'040904b0',
33+
[StringStruct('CompanyName', 'crediblemark.com'),
34+
StringStruct('FileDescription', 'Professional AI Video Clipping Tool'),
35+
StringStruct('FileVersion', '1.3.2'),
36+
StringStruct('InternalName', 'clipperr'),
37+
StringStruct('LegalCopyright', 'Copyright (c) 2026 crediblemark.com'),
38+
StringStruct('OriginalFilename', 'clipperr.exe'),
39+
StringStruct('ProductName', 'Clipperr AI'),
40+
StringStruct('ProductVersion', '1.3.2')])
41+
]),
42+
VarFileInfo([VarStruct('Translation', [1033, 1200])])
43+
]
44+
)

0 commit comments

Comments
 (0)