Skip to content

Commit 76c729e

Browse files
authored
release: v1.3.2 - Linux CUDA and paste fixes
release: v1.3.2 - Linux CUDA and paste fixes
2 parents 50a0048 + 5a97f46 commit 76c729e

20 files changed

Lines changed: 231 additions & 117 deletions

installer/voiceflow.iss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
; Creates a Windows installer from the PyInstaller --onedir output
33

44
#define MyAppName "VoiceFlow"
5-
#define MyAppVersion "1.3.1"
5+
#define MyAppVersion "1.3.2"
66
#define MyAppPublisher "VoiceFlow"
77
#define MyAppURL "https://github.com/your-repo/voiceflow"
88
#define MyAppExeName "VoiceFlow.exe"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "voiceflow",
33
"private": true,
4-
"version": "1.3.1",
4+
"version": "1.3.2",
55
"type": "module",
66
"scripts": {
77
"dev": "npm-run-all --parallel vite pyloid",

src-pyloid/app_controller.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ def transcribe():
174174
info(f"Transcription result: '{text}'")
175175

176176
if text:
177+
# Prepend space if enabled (useful for continuous dictation)
178+
if settings.prepend_space:
179+
text = " " + text
180+
177181
# Paste at cursor
178182
info("Pasting text at cursor...")
179183
self.clipboard_service.paste_at_cursor(text)
@@ -235,6 +239,7 @@ def get_settings(self) -> dict:
235239
"holdHotkeyEnabled": settings.hold_hotkey_enabled,
236240
"toggleHotkey": settings.toggle_hotkey,
237241
"toggleHotkeyEnabled": settings.toggle_hotkey_enabled,
242+
"prependSpace": settings.prepend_space,
238243
}
239244

240245
def update_settings(self, **kwargs) -> dict:
@@ -249,6 +254,8 @@ def update_settings(self, **kwargs) -> dict:
249254
mapped["save_audio_to_history"] = kwargs["saveAudioToHistory"]
250255
if "showPopup" in kwargs:
251256
mapped["show_popup"] = kwargs["showPopup"]
257+
if "prependSpace" in kwargs:
258+
mapped["prepend_space"] = kwargs["prependSpace"]
252259
# Hotkey settings (camelCase to snake_case)
253260
if "holdHotkey" in kwargs:
254261
mapped["hold_hotkey"] = kwargs["holdHotkey"]

src-pyloid/services/clipboard.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,32 @@ def _wl_copy(self, text: str):
6767
import pyperclip
6868
pyperclip.copy(text)
6969

70+
def _type_text_directly(self, text: str) -> bool:
71+
"""Type text directly using Wayland input tool. Returns True on success."""
72+
try:
73+
if self._paste_tool == 'wtype':
74+
subprocess.run(['wtype', '--', text], check=True, timeout=10)
75+
elif self._paste_tool == 'dotool':
76+
# dotool type command types text directly
77+
subprocess.run(['dotool'], input=f'type {text}\n'.encode(),
78+
check=True, timeout=10)
79+
elif self._paste_tool == 'ydotool':
80+
subprocess.run(['ydotool', 'type', '--', text],
81+
check=True, timeout=10)
82+
else:
83+
return False
84+
log.debug("Text typed directly via", tool=self._paste_tool)
85+
return True
86+
except (subprocess.SubprocessError, OSError) as e:
87+
log.warning("Direct type failed", tool=self._paste_tool, error=str(e))
88+
return False
89+
7090
def _simulate_paste_keystroke(self):
7191
"""Send Ctrl+V using the best available tool."""
7292
if IS_WAYLAND and self._paste_tool:
7393
try:
7494
if self._paste_tool == 'wtype':
75-
subprocess.run(['wtype', '-M', 'ctrl', '-P', 'v', '-m', 'ctrl'],
95+
subprocess.run(['wtype', '-M', 'ctrl', '-k', 'v', '-m', 'ctrl'],
7696
check=True, timeout=5)
7797
elif self._paste_tool == 'dotool':
7898
subprocess.run(['dotool'], input=b'key ctrl+v\n',
@@ -93,19 +113,26 @@ def paste_at_cursor(self, text: str):
93113
"""Copy text to clipboard and paste at current cursor position."""
94114
log.debug("Paste at cursor called", text_length=len(text))
95115

96-
# Copy our text to clipboard
116+
# Always copy to clipboard so user can re-paste manually if needed
97117
self.copy_to_clipboard(text)
98118
log.debug("Text copied to clipboard")
99119

100-
# Small delay to ensure clipboard is updated
120+
# On Wayland, type text directly — works in terminals (Ctrl+V doesn't)
121+
# and all other apps, regardless of their paste shortcut.
122+
if IS_WAYLAND and self._paste_tool:
123+
if self._type_text_directly(text):
124+
log.debug("Paste complete via direct type")
125+
return
126+
# Direct type failed — fall through to Ctrl+V
127+
log.warning("Direct type failed, falling back to Ctrl+V")
128+
129+
# Small delay to ensure clipboard is ready
101130
time.sleep(0.1)
102131

103-
# Simulate Ctrl+V
104132
log.debug("Simulating Ctrl+V")
105133
self._simulate_paste_keystroke()
106134
log.debug("Paste command sent")
107135

108-
# Small delay after paste
109136
time.sleep(0.1)
110137

111138
def get_clipboard(self) -> str:

src-pyloid/services/gpu.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,18 @@ def _check_cudnn_available() -> tuple[bool, Optional[str]]:
7676
Tuple of (is_available, error_message)
7777
"""
7878
if sys.platform != "win32":
79-
# On Linux, cuDNN is either bundled into ctranslate2 or not required.
80-
# The nvidia cublas pip packages (preloaded at startup) provide what's needed.
81-
return True, None
79+
# On Linux, verify that libcublas is actually loadable at runtime.
80+
# ctranslate2 can detect CUDA device presence without libcublas, but
81+
# inference will fail if the library isn't available.
82+
import ctypes
83+
for lib_name in ("libcublas.so.12", "libcublas.so.11", "libcublas.so"):
84+
try:
85+
ctypes.CDLL(lib_name)
86+
return True, None
87+
except OSError:
88+
continue
89+
log.warning("libcublas not loadable on Linux, CUDA disabled")
90+
return False, "CUDA libraries not found. Install nvidia-cuda-toolkit or use CPU mode."
8291

8392
# First, add local cuDNN to PATH if it exists
8493
_add_local_cudnn_to_path()

src-pyloid/services/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ class Settings:
5555
hold_hotkey_enabled: bool = True
5656
toggle_hotkey: str = "ctrl+shift+win"
5757
toggle_hotkey_enabled: bool = False
58+
# Transcription settings
59+
prepend_space: bool = False # Add leading space before pasted text
5860

5961

6062
class SettingsService:
@@ -83,6 +85,8 @@ def get_settings(self) -> Settings:
8385
hold_hotkey_enabled=self.db.get_setting("hold_hotkey_enabled", "true") == "true",
8486
toggle_hotkey=self.db.get_setting("toggle_hotkey", "ctrl+shift+win"),
8587
toggle_hotkey_enabled=self.db.get_setting("toggle_hotkey_enabled", "false") == "true",
88+
# Transcription settings
89+
prepend_space=self.db.get_setting("prepend_space", "false") == "true",
8690
)
8791
self._cache = settings
8892
return settings
@@ -104,6 +108,7 @@ def update_settings(
104108
toggle_hotkey: Optional[str] = None,
105109
toggle_hotkey_enabled: Optional[bool] = None,
106110
show_popup: Optional[bool] = None,
111+
prepend_space: Optional[bool] = None,
107112
) -> Settings:
108113
if language is not None:
109114
self.db.set_setting("language", language)
@@ -125,6 +130,8 @@ def update_settings(
125130
self.db.set_setting("save_audio_to_history", "true" if save_audio_to_history else "false")
126131
if show_popup is not None:
127132
self.db.set_setting("show_popup", "true" if show_popup else "false")
133+
if prepend_space is not None:
134+
self.db.set_setting("prepend_space", "true" if prepend_space else "false")
128135
# Hotkey settings - normalize before storing for consistent format
129136
if hold_hotkey is not None:
130137
self.db.set_setting("hold_hotkey", normalize_hotkey(hold_hotkey))

src-pyloid/services/transcription.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,38 @@ def transcribe(
118118

119119
log.debug("Audio stats", length=len(audio), max_amplitude=float(np.abs(audio).max()), mean_amplitude=float(np.abs(audio).mean()))
120120

121-
segments, info = self._model.transcribe(
122-
audio,
123-
language=language_arg,
124-
beam_size=5,
125-
vad_filter=True,
126-
vad_parameters=dict(
127-
min_silence_duration_ms=500, # Less aggressive silence detection
128-
speech_pad_ms=400, # More padding around speech
129-
),
130-
)
121+
try:
122+
segments, info = self._model.transcribe(
123+
audio,
124+
language=language_arg,
125+
beam_size=5,
126+
vad_filter=True,
127+
vad_parameters=dict(
128+
min_silence_duration_ms=500, # Less aggressive silence detection
129+
speech_pad_ms=400, # More padding around speech
130+
),
131+
)
132+
except RuntimeError as e:
133+
if "not found or cannot be loaded" in str(e) and self._current_device == "cuda":
134+
log.warning("CUDA runtime error during transcription, reloading on CPU", error=str(e))
135+
model_name = self._current_model_name
136+
repo_id = _get_repo_id(model_name)
137+
self._model = WhisperModel(repo_id, device="cpu", compute_type="int8")
138+
self._current_device = "cpu"
139+
self._current_compute_type = "int8"
140+
log.info("Model reloaded on CPU fallback")
141+
segments, info = self._model.transcribe(
142+
audio,
143+
language=language_arg,
144+
beam_size=5,
145+
vad_filter=True,
146+
vad_parameters=dict(
147+
min_silence_duration_ms=500,
148+
speech_pad_ms=400,
149+
),
150+
)
151+
else:
152+
raise
131153

132154
# Combine all segments
133155
segments_list = list(segments)

src/App.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import type { Settings } from "@/lib/types";
1212
// Qt WebEngine on Linux often falls back to software rendering,
1313
// making backdrop-filter/blur extremely expensive
1414
// Skip for popup route - it needs transparent background
15-
if (navigator.platform.startsWith("Linux") && window.location.hash !== "#/popup") {
15+
const isLinux =
16+
(navigator.userAgentData?.platform ?? navigator.userAgent).includes("Linux");
17+
if (isLinux && window.location.hash !== "#/popup") {
1618
document.documentElement.classList.add("reduced-effects");
1719
}
1820

@@ -99,7 +101,7 @@ function AppRouter() {
99101
if (loading) {
100102
return (
101103
<div className="min-h-screen flex items-center justify-center">
102-
<div className="text-muted-foreground">Loading...</div>
104+
<div role="status" aria-live="polite" className="text-muted-foreground">Loading...</div>
103105
</div>
104106
);
105107
}

src/components/HistoryPage.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function HistoryPage() {
5959
try {
6060
await api.copyToClipboard(text);
6161
toast.success("Copied to clipboard");
62-
} catch (error) {
62+
} catch {
6363
try {
6464
await navigator.clipboard.writeText(text);
6565
toast.success("Copied to clipboard");
@@ -124,7 +124,7 @@ export function HistoryPage() {
124124
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
125125
<div>
126126
<h1 className="text-4xl md:text-5xl font-bold tracking-tighter text-foreground mb-2">
127-
Full <span className="headline-serif text-primary">History</span>
127+
Full History
128128
</h1>
129129
<p className="text-lg text-muted-foreground/80 font-light max-w-2xl">
130130
A complete archive of your voice notes and dictations.
@@ -200,12 +200,13 @@ export function HistoryPage() {
200200
</Badge>
201201
)}
202202
</div>
203-
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
203+
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity flex gap-1">
204204
<Button
205205
variant="ghost"
206206
size="icon-sm"
207207
className="h-7 w-7 text-muted-foreground hover:text-primary hover:bg-primary/10"
208208
onClick={() => handleCopy(entry.text)}
209+
aria-label="Copy transcription"
209210
>
210211
<Copy className="h-3.5 w-3.5" />
211212
</Button>
@@ -216,6 +217,7 @@ export function HistoryPage() {
216217
className="h-7 w-7 text-muted-foreground hover:text-primary hover:bg-primary/10"
217218
onClick={() => handlePlayAudio(entry.id)}
218219
disabled={loadingAudioFor === entry.id}
220+
aria-label="Play audio recording"
219221
>
220222
<FileAudio className="h-3.5 w-3.5" />
221223
</Button>
@@ -225,6 +227,7 @@ export function HistoryPage() {
225227
size="icon-sm"
226228
className="h-7 w-7 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
227229
onClick={() => handleDelete(entry.id)}
230+
aria-label="Delete transcription"
228231
>
229232
<Trash2 className="h-3.5 w-3.5" />
230233
</Button>

src/components/HistoryTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function HistoryTab() {
4141
try {
4242
await api.copyToClipboard(text);
4343
toast.success("Copied to clipboard");
44-
} catch (error) {
44+
} catch {
4545
// Fallback
4646
try {
4747
await navigator.clipboard.writeText(text);

0 commit comments

Comments
 (0)