@iamsamuelrodda/dictate 2026.5.18-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/assets/dictate-listening.ico +0 -0
- package/assets/dictate-listening.png +0 -0
- package/assets/dictate.ico +0 -0
- package/assets/dictate.png +0 -0
- package/config/default-config.yaml +17 -0
- package/install-windows-wizard.ps1 +392 -0
- package/install-windows.ps1 +330 -0
- package/install.ps1 +87 -0
- package/install.sh +233 -0
- package/npm/dictate-lifecycle.mjs +49 -0
- package/package.json +50 -0
- package/uninstall-windows.ps1 +83 -0
- package/uninstall.ps1 +51 -0
- package/uninstall.sh +92 -0
- package/update-windows.ps1 +70 -0
- package/update.ps1 +115 -0
- package/update.sh +34 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[switch]$NoVerify,
|
|
3
|
+
[switch]$NoPrepareTurbo,
|
|
4
|
+
[switch]$NoShortcut,
|
|
5
|
+
[switch]$NoStartup,
|
|
6
|
+
[switch]$RecreateVenv
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
$ErrorActionPreference = "Stop"
|
|
10
|
+
|
|
11
|
+
function Test-PythonVersion {
|
|
12
|
+
param(
|
|
13
|
+
[string]$Exe,
|
|
14
|
+
[string[]]$ArgumentList
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
& $Exe @ArgumentList -c "import sys; raise SystemExit(0 if (3, 11) <= sys.version_info < (3, 13) else 1)" *> $null
|
|
18
|
+
return ($LASTEXITCODE -eq 0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Resolve-Python {
|
|
22
|
+
$candidates = @()
|
|
23
|
+
|
|
24
|
+
if ($env:PYTHON) {
|
|
25
|
+
$candidates += [pscustomobject]@{ Exe = $env:PYTHON; ArgumentList = @() }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$python = Get-Command python -ErrorAction SilentlyContinue
|
|
29
|
+
if ($python) {
|
|
30
|
+
$candidates += [pscustomobject]@{ Exe = $python.Source; ArgumentList = @() }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$python3 = Get-Command python3 -ErrorAction SilentlyContinue
|
|
34
|
+
if ($python3) {
|
|
35
|
+
$candidates += [pscustomobject]@{ Exe = $python3.Source; ArgumentList = @() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
$py = Get-Command py -ErrorAction SilentlyContinue
|
|
39
|
+
if ($py) {
|
|
40
|
+
$candidates += [pscustomobject]@{ Exe = $py.Source; ArgumentList = @("-3.12") }
|
|
41
|
+
$candidates += [pscustomobject]@{ Exe = $py.Source; ArgumentList = @("-3.11") }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
foreach ($candidate in $candidates) {
|
|
45
|
+
if (Test-PythonVersion -Exe $candidate.Exe -ArgumentList $candidate.ArgumentList) {
|
|
46
|
+
return $candidate
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw "Python 3.11 or 3.12 was not found. Install Python from python.org or winget, then rerun this script."
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Invoke-Checked {
|
|
54
|
+
param(
|
|
55
|
+
[string]$Exe,
|
|
56
|
+
[string[]]$ArgumentList,
|
|
57
|
+
[string]$Description
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
Write-Host "==> $Description"
|
|
61
|
+
& $Exe @ArgumentList
|
|
62
|
+
if ($LASTEXITCODE -ne 0) {
|
|
63
|
+
throw "$Description failed with exit code $LASTEXITCODE."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Test-VcRuntime {
|
|
68
|
+
$system32 = Join-Path $env:WINDIR "System32"
|
|
69
|
+
$required = @("vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll")
|
|
70
|
+
foreach ($dll in $required) {
|
|
71
|
+
if (-not (Test-Path (Join-Path $system32 $dll))) {
|
|
72
|
+
return $false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return $true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function Ensure-VcRuntime {
|
|
79
|
+
if (Test-VcRuntime) {
|
|
80
|
+
Write-Host "==> Microsoft Visual C++ runtime already installed"
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
$installer = Join-Path $env:TEMP "vc_redist.x64.exe"
|
|
85
|
+
$url = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
|
86
|
+
Write-Host "==> Installing Microsoft Visual C++ runtime"
|
|
87
|
+
Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $installer
|
|
88
|
+
$process = Start-Process -FilePath $installer -ArgumentList "/install", "/quiet", "/norestart" -Wait -PassThru
|
|
89
|
+
if (($process.ExitCode -ne 0) -and ($process.ExitCode -ne 3010)) {
|
|
90
|
+
throw "Microsoft Visual C++ runtime install failed with exit code $($process.ExitCode)."
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Get-AppDataConfigPath {
|
|
95
|
+
$base = $env:APPDATA
|
|
96
|
+
if (-not $base) {
|
|
97
|
+
$base = Join-Path $HOME "AppData\Roaming"
|
|
98
|
+
}
|
|
99
|
+
return Join-Path $base "dictate\config.yaml"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function Seed-Config {
|
|
103
|
+
$configPath = Get-AppDataConfigPath
|
|
104
|
+
if (Test-Path $configPath) {
|
|
105
|
+
Write-Host "==> Config already exists: $configPath"
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
$defaultConfig = Join-Path $PSScriptRoot "config\default-config.yaml"
|
|
110
|
+
$configDir = Split-Path -Parent $configPath
|
|
111
|
+
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
|
112
|
+
Copy-Item -Path $defaultConfig -Destination $configPath
|
|
113
|
+
Write-Host "==> Seeded config: $configPath"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function Write-LauncherScripts {
|
|
117
|
+
param([string]$ScriptsDir)
|
|
118
|
+
|
|
119
|
+
$daemonPath = Join-Path $ScriptsDir "dictate-daemon.cmd"
|
|
120
|
+
$oncePath = Join-Path $ScriptsDir "dictate-once.cmd"
|
|
121
|
+
$controlsPath = Join-Path $ScriptsDir "dictate-controls.cmd"
|
|
122
|
+
$trayPath = Join-Path $ScriptsDir "dictate-tray.cmd"
|
|
123
|
+
$trayVbsPath = Join-Path $ScriptsDir "dictate-tray.vbs"
|
|
124
|
+
|
|
125
|
+
Set-Content -Path $trayPath -Encoding ASCII -Value @(
|
|
126
|
+
"@echo off",
|
|
127
|
+
'set "SCRIPT_DIR=%~dp0"',
|
|
128
|
+
'"%SCRIPT_DIR%dictate.exe" --type-backend pynput %*'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
Set-Content -Path $daemonPath -Encoding ASCII -Value @(
|
|
132
|
+
"@echo off",
|
|
133
|
+
'set "SCRIPT_DIR=%~dp0"',
|
|
134
|
+
'"%SCRIPT_DIR%dictate.exe" --no-tray --type-backend pynput %*'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
Set-Content -Path $oncePath -Encoding ASCII -Value @(
|
|
138
|
+
"@echo off",
|
|
139
|
+
'set "SCRIPT_DIR=%~dp0"',
|
|
140
|
+
'"%SCRIPT_DIR%dictate.exe" --once %*'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
Set-Content -Path $controlsPath -Encoding ASCII -Value @(
|
|
144
|
+
"@echo off",
|
|
145
|
+
'set "SCRIPT_DIR=%~dp0"',
|
|
146
|
+
'start "" "%SCRIPT_DIR%dictate-controls.exe" %*'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
Set-Content -Path $trayVbsPath -Encoding ASCII -Value @(
|
|
150
|
+
'Set shell = CreateObject("WScript.Shell")',
|
|
151
|
+
'Set fso = CreateObject("Scripting.FileSystemObject")',
|
|
152
|
+
'scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)',
|
|
153
|
+
'venvDir = fso.GetParentFolderName(scriptDir)',
|
|
154
|
+
'shell.CurrentDirectory = fso.GetParentFolderName(venvDir)',
|
|
155
|
+
'shell.Run """" & scriptDir & "\pythonw.exe"" -m dictate --type-backend pynput", 0, False'
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
Write-Host "==> Wrote launchers:"
|
|
159
|
+
Write-Host " $trayPath"
|
|
160
|
+
Write-Host " $trayVbsPath"
|
|
161
|
+
Write-Host " $daemonPath"
|
|
162
|
+
Write-Host " $oncePath"
|
|
163
|
+
Write-Host " $controlsPath"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function Get-StartMenuProgramsDir {
|
|
167
|
+
$programsDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"
|
|
168
|
+
if (-not $env:APPDATA) {
|
|
169
|
+
$programsDir = Join-Path $HOME "AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
|
|
170
|
+
}
|
|
171
|
+
return $programsDir
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Get-StartupDir {
|
|
175
|
+
return (Join-Path (Get-StartMenuProgramsDir) "Startup")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function New-DictateShortcut {
|
|
179
|
+
param(
|
|
180
|
+
[string]$ShortcutPath,
|
|
181
|
+
[string]$TargetPath,
|
|
182
|
+
[string]$Arguments = "",
|
|
183
|
+
[string]$WorkingDirectory,
|
|
184
|
+
[string]$Description
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
$iconPath = Join-Path $PSScriptRoot "assets\dictate.ico"
|
|
188
|
+
$shell = New-Object -ComObject WScript.Shell
|
|
189
|
+
$shortcut = $shell.CreateShortcut($ShortcutPath)
|
|
190
|
+
$shortcut.TargetPath = $TargetPath
|
|
191
|
+
$shortcut.Arguments = $Arguments
|
|
192
|
+
$shortcut.WorkingDirectory = $WorkingDirectory
|
|
193
|
+
$shortcut.Description = $Description
|
|
194
|
+
if (Test-Path $iconPath) {
|
|
195
|
+
$shortcut.IconLocation = $iconPath
|
|
196
|
+
}
|
|
197
|
+
$shortcut.Save()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function Install-StartMenuShortcut {
|
|
201
|
+
param(
|
|
202
|
+
[string]$TargetPath,
|
|
203
|
+
[string]$Arguments = "",
|
|
204
|
+
[string]$WorkingDirectory
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
$programsDir = Get-StartMenuProgramsDir
|
|
208
|
+
New-Item -ItemType Directory -Force -Path $programsDir | Out-Null
|
|
209
|
+
|
|
210
|
+
$shortcutPath = Join-Path $programsDir "Dictate.lnk"
|
|
211
|
+
$legacyShortcutPath = Join-Path $programsDir "Dictate Controls.lnk"
|
|
212
|
+
|
|
213
|
+
if ($NoShortcut) {
|
|
214
|
+
Remove-Item -Force -ErrorAction SilentlyContinue -Path $shortcutPath, $legacyShortcutPath
|
|
215
|
+
Write-Host "==> Removed Start Menu shortcuts"
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Remove-Item -Force -ErrorAction SilentlyContinue -Path $legacyShortcutPath
|
|
220
|
+
New-DictateShortcut -ShortcutPath $shortcutPath -TargetPath $TargetPath -Arguments $Arguments -WorkingDirectory $WorkingDirectory -Description "Start Dictate push-to-talk tray"
|
|
221
|
+
|
|
222
|
+
Write-Host "==> Installed Start Menu shortcut: $shortcutPath"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function Install-StartupShortcut {
|
|
226
|
+
param(
|
|
227
|
+
[string]$TargetPath,
|
|
228
|
+
[string]$Arguments = "",
|
|
229
|
+
[string]$WorkingDirectory
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if ($NoShortcut -or $NoStartup) {
|
|
233
|
+
$startupDir = Get-StartupDir
|
|
234
|
+
$shortcutPath = Join-Path $startupDir "Dictate.lnk"
|
|
235
|
+
if (Test-Path $shortcutPath) {
|
|
236
|
+
Remove-Item -Force $shortcutPath
|
|
237
|
+
Write-Host "==> Removed startup shortcut: $shortcutPath"
|
|
238
|
+
}
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
$startupDir = Get-StartupDir
|
|
243
|
+
New-Item -ItemType Directory -Force -Path $startupDir | Out-Null
|
|
244
|
+
|
|
245
|
+
$shortcutPath = Join-Path $startupDir "Dictate.lnk"
|
|
246
|
+
New-DictateShortcut -ShortcutPath $shortcutPath -TargetPath $TargetPath -Arguments $Arguments -WorkingDirectory $WorkingDirectory -Description "Start Dictate automatically at sign-in"
|
|
247
|
+
|
|
248
|
+
Write-Host "==> Installed startup shortcut: $shortcutPath"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function Register-InstalledApp {
|
|
252
|
+
param(
|
|
253
|
+
[string]$InstallLocation,
|
|
254
|
+
[string]$DisplayIcon
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
$keyPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Dictate"
|
|
258
|
+
New-Item -Force -Path $keyPath | Out-Null
|
|
259
|
+
New-ItemProperty -Force -Path $keyPath -Name "DisplayName" -Value "Dictate" -PropertyType String | Out-Null
|
|
260
|
+
New-ItemProperty -Force -Path $keyPath -Name "DisplayVersion" -Value "2026.5.18-1" -PropertyType String | Out-Null
|
|
261
|
+
New-ItemProperty -Force -Path $keyPath -Name "Publisher" -Value "Arc Forge Labs" -PropertyType String | Out-Null
|
|
262
|
+
New-ItemProperty -Force -Path $keyPath -Name "InstallLocation" -Value $InstallLocation -PropertyType String | Out-Null
|
|
263
|
+
if (Test-Path $DisplayIcon) {
|
|
264
|
+
New-ItemProperty -Force -Path $keyPath -Name "DisplayIcon" -Value $DisplayIcon -PropertyType String | Out-Null
|
|
265
|
+
}
|
|
266
|
+
$uninstallScript = Join-Path $InstallLocation "uninstall-windows.ps1"
|
|
267
|
+
if (Test-Path $uninstallScript) {
|
|
268
|
+
$uninstallCommand = "powershell -NoProfile -ExecutionPolicy Bypass -File `"$uninstallScript`""
|
|
269
|
+
New-ItemProperty -Force -Path $keyPath -Name "UninstallString" -Value $uninstallCommand -PropertyType String | Out-Null
|
|
270
|
+
New-ItemProperty -Force -Path $keyPath -Name "QuietUninstallString" -Value "$uninstallCommand -Quiet" -PropertyType String | Out-Null
|
|
271
|
+
}
|
|
272
|
+
New-ItemProperty -Force -Path $keyPath -Name "NoModify" -Value 1 -PropertyType DWord | Out-Null
|
|
273
|
+
New-ItemProperty -Force -Path $keyPath -Name "NoRepair" -Value 1 -PropertyType DWord | Out-Null
|
|
274
|
+
Write-Host "==> Registered Dictate in Windows Installed Apps"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
$pythonCommand = Resolve-Python
|
|
278
|
+
$venvDir = Join-Path $PSScriptRoot ".venv"
|
|
279
|
+
$scriptsDir = Join-Path $venvDir "Scripts"
|
|
280
|
+
$venvPython = Join-Path $scriptsDir "python.exe"
|
|
281
|
+
|
|
282
|
+
Ensure-VcRuntime
|
|
283
|
+
|
|
284
|
+
if ($RecreateVenv -and (Test-Path $venvDir)) {
|
|
285
|
+
Write-Host "==> Removing existing virtual environment: $venvDir"
|
|
286
|
+
Remove-Item -Recurse -Force $venvDir
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (-not (Test-Path $venvPython)) {
|
|
290
|
+
Invoke-Checked -Exe $pythonCommand.Exe -ArgumentList @($pythonCommand.ArgumentList + @("-m", "venv", $venvDir)) -Description "Creating virtual environment"
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "pip", "install", "--upgrade", "pip") -Description "Upgrading pip"
|
|
294
|
+
Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "pip", "install", "-e", "${PSScriptRoot}[windows]") -Description "Installing Dictate Windows package"
|
|
295
|
+
|
|
296
|
+
Seed-Config
|
|
297
|
+
Write-LauncherScripts -ScriptsDir $scriptsDir
|
|
298
|
+
$trayVbs = Join-Path $scriptsDir "dictate-tray.vbs"
|
|
299
|
+
$wscript = Join-Path $env:WINDIR "System32\wscript.exe"
|
|
300
|
+
$trayArgs = "`"$trayVbs`""
|
|
301
|
+
Install-StartMenuShortcut -TargetPath $wscript -Arguments $trayArgs -WorkingDirectory $PSScriptRoot
|
|
302
|
+
Install-StartupShortcut -TargetPath $wscript -Arguments $trayArgs -WorkingDirectory $PSScriptRoot
|
|
303
|
+
Register-InstalledApp -InstallLocation $PSScriptRoot -DisplayIcon (Join-Path $PSScriptRoot "assets\dictate.ico")
|
|
304
|
+
|
|
305
|
+
if (-not $NoPrepareTurbo) {
|
|
306
|
+
Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "dictate", "prepare-model", "--stt-backend", "faster-whisper", "--model", "turbo", "--device", "auto", "--compute-type", "int8") -Description "Preparing faster-whisper turbo model"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (-not $NoVerify) {
|
|
310
|
+
Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "dictate", "doctor", "--quick", "--type-backend", "pynput") -Description "Running Dictate doctor"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
Write-Host ""
|
|
314
|
+
Write-Host "Dictate is installed."
|
|
315
|
+
if ($NoShortcut -or $NoStartup) {
|
|
316
|
+
Write-Host "Dictate startup shortcut was not installed."
|
|
317
|
+
} else {
|
|
318
|
+
Write-Host "Dictate starts automatically when you sign in."
|
|
319
|
+
}
|
|
320
|
+
Write-Host "Start push-to-talk with a Windows tray icon from the Start Menu shortcut named 'Dictate', or run:"
|
|
321
|
+
Write-Host " .\.venv\Scripts\dictate-tray.cmd"
|
|
322
|
+
Write-Host ""
|
|
323
|
+
Write-Host "Headless push-to-talk daemon:"
|
|
324
|
+
Write-Host " .\.venv\Scripts\dictate-daemon.cmd"
|
|
325
|
+
Write-Host ""
|
|
326
|
+
Write-Host "Control panel:"
|
|
327
|
+
Write-Host " .\.venv\Scripts\dictate-controls.cmd"
|
|
328
|
+
Write-Host ""
|
|
329
|
+
Write-Host "One-shot mode:"
|
|
330
|
+
Write-Host " .\.venv\Scripts\dictate-once.cmd"
|
package/install.ps1
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$InstallRoot,
|
|
3
|
+
[string]$ArchiveUrl,
|
|
4
|
+
[switch]$NoVerify,
|
|
5
|
+
[switch]$NoPrepareTurbo,
|
|
6
|
+
[switch]$NoShortcut,
|
|
7
|
+
[switch]$NoStartup,
|
|
8
|
+
[switch]$Wizard,
|
|
9
|
+
[switch]$RecreateVenv
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
$ErrorActionPreference = "Stop"
|
|
13
|
+
$DictateVersion = "2026.5.18-1"
|
|
14
|
+
|
|
15
|
+
if (-not $ArchiveUrl) {
|
|
16
|
+
$ArchiveUrl = "https://github.com/arcforgelabs/dictate/archive/refs/tags/v$DictateVersion.zip"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (-not $InstallRoot) {
|
|
20
|
+
$base = $env:LOCALAPPDATA
|
|
21
|
+
if (-not $base) {
|
|
22
|
+
$base = Join-Path $HOME "AppData\Local"
|
|
23
|
+
}
|
|
24
|
+
$InstallRoot = Join-Path $base "Dictate"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$installRootPath = [System.IO.Path]::GetFullPath($InstallRoot)
|
|
28
|
+
$sourceDir = Join-Path $installRootPath "source"
|
|
29
|
+
$stagingRoot = Join-Path $env:TEMP ("dictate-install-" + [guid]::NewGuid().ToString("N"))
|
|
30
|
+
$archivePath = Join-Path $stagingRoot "dictate.zip"
|
|
31
|
+
|
|
32
|
+
Write-Host "==> Installing Dictate to $installRootPath"
|
|
33
|
+
New-Item -ItemType Directory -Force -Path $stagingRoot | Out-Null
|
|
34
|
+
New-Item -ItemType Directory -Force -Path $installRootPath | Out-Null
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
if (Test-Path -LiteralPath $ArchiveUrl) {
|
|
38
|
+
Write-Host "==> Copying local archive $ArchiveUrl"
|
|
39
|
+
Copy-Item -Force -LiteralPath $ArchiveUrl -Destination $archivePath
|
|
40
|
+
} else {
|
|
41
|
+
Write-Host "==> Downloading $ArchiveUrl"
|
|
42
|
+
Invoke-WebRequest -UseBasicParsing -Uri $ArchiveUrl -OutFile $archivePath
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Write-Host "==> Expanding source archive"
|
|
46
|
+
Expand-Archive -Force -Path $archivePath -DestinationPath $stagingRoot
|
|
47
|
+
$expanded = Get-ChildItem -Path $stagingRoot -Directory |
|
|
48
|
+
Where-Object { Test-Path (Join-Path $_.FullName "install-windows.ps1") } |
|
|
49
|
+
Select-Object -First 1
|
|
50
|
+
if (-not $expanded) {
|
|
51
|
+
throw "Downloaded archive did not contain install-windows.ps1."
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (Test-Path $sourceDir) {
|
|
55
|
+
Write-Host "==> Replacing existing managed source: $sourceDir"
|
|
56
|
+
Remove-Item -Recurse -Force $sourceDir
|
|
57
|
+
}
|
|
58
|
+
Move-Item -Path $expanded.FullName -Destination $sourceDir
|
|
59
|
+
|
|
60
|
+
$installerScript = if ($Wizard) {
|
|
61
|
+
Join-Path $sourceDir "install-windows-wizard.ps1"
|
|
62
|
+
} else {
|
|
63
|
+
Join-Path $sourceDir "install-windows.ps1"
|
|
64
|
+
}
|
|
65
|
+
$installerArgs = @("-ExecutionPolicy", "Bypass", "-File", $installerScript)
|
|
66
|
+
if (-not $Wizard) {
|
|
67
|
+
if ($NoVerify) { $installerArgs += "-NoVerify" }
|
|
68
|
+
if ($NoPrepareTurbo) { $installerArgs += "-NoPrepareTurbo" }
|
|
69
|
+
if ($NoShortcut) { $installerArgs += "-NoShortcut" }
|
|
70
|
+
if ($NoStartup) { $installerArgs += "-NoStartup" }
|
|
71
|
+
if ($RecreateVenv) { $installerArgs += "-RecreateVenv" }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Write-Host "==> Running Dictate Windows installer"
|
|
75
|
+
& powershell @installerArgs
|
|
76
|
+
if ($LASTEXITCODE -ne 0) {
|
|
77
|
+
throw "Dictate Windows installer failed with exit code $LASTEXITCODE."
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
if (Test-Path $stagingRoot) {
|
|
81
|
+
Remove-Item -Recurse -Force $stagingRoot
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Write-Host ""
|
|
86
|
+
Write-Host "Dictate install source: $sourceDir"
|
|
87
|
+
Write-Host "Start Dictate from the Start Menu shortcut named 'Dictate'."
|
package/install.sh
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install/update dictate into a standalone venv at ~/.local/share/dictate.
|
|
3
|
+
# Uses --system-site-packages so GTK/gi bindings are available.
|
|
4
|
+
# Re-run this script after pulling changes to update the installation.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
INSTALL_DIR="$HOME/.local/share/dictate"
|
|
9
|
+
BIN_DIR="$HOME/.local/bin"
|
|
10
|
+
DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
|
11
|
+
AUTOSTART_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/autostart"
|
|
12
|
+
ICON_DIR="$INSTALL_DIR/share/icons"
|
|
13
|
+
ICON_PATH="$ICON_DIR/dictate-simple.png"
|
|
14
|
+
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/dictate"
|
|
15
|
+
CONFIG_PATH="$CONFIG_DIR/config.yaml"
|
|
16
|
+
DEFAULT_CONFIG_SOURCE="$SCRIPT_DIR/config/default-config.yaml"
|
|
17
|
+
VERIFY=1
|
|
18
|
+
PREPARE_TURBO=1
|
|
19
|
+
SEED_DEFAULT_CONFIG=1
|
|
20
|
+
STARTUP=1
|
|
21
|
+
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
|
22
|
+
|
|
23
|
+
# Prefer the distro Python so --system-site-packages can see modules such as
|
|
24
|
+
# python3-gi from /usr/lib/python3/dist-packages.
|
|
25
|
+
if [ -x /usr/bin/python3 ]; then
|
|
26
|
+
PYTHON_BIN="/usr/bin/python3"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
usage() {
|
|
30
|
+
cat <<EOF
|
|
31
|
+
Usage: $0 [--no-verify] [--no-prepare-turbo] [--no-seed-default-config] [--no-startup] [--session-backend auto|x11|wayland]
|
|
32
|
+
|
|
33
|
+
Installs dictate into ~/.local/share/dictate, links ~/.local/bin/dictate,
|
|
34
|
+
seeds the default config on first install, creates app launcher/autostart entries,
|
|
35
|
+
and prepares the faster-whisper turbo model unless disabled.
|
|
36
|
+
EOF
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
detect_session_backend() {
|
|
40
|
+
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
|
|
41
|
+
printf 'wayland\n'
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
if [ -n "${DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "x11" ]; then
|
|
45
|
+
printf 'x11\n'
|
|
46
|
+
return
|
|
47
|
+
fi
|
|
48
|
+
if [ -n "${XDG_SESSION_ID:-}" ] && command -v loginctl >/dev/null 2>&1; then
|
|
49
|
+
local detected
|
|
50
|
+
detected="$(loginctl show-session "$XDG_SESSION_ID" -p Type --value 2>/dev/null || true)"
|
|
51
|
+
if [ "$detected" = "wayland" ] || [ "$detected" = "x11" ]; then
|
|
52
|
+
printf '%s\n' "$detected"
|
|
53
|
+
return
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
printf 'unknown\n'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
SESSION_BACKEND="auto"
|
|
60
|
+
|
|
61
|
+
while [ "$#" -gt 0 ]; do
|
|
62
|
+
case "$1" in
|
|
63
|
+
--no-verify)
|
|
64
|
+
VERIFY=0
|
|
65
|
+
;;
|
|
66
|
+
--no-prepare-turbo)
|
|
67
|
+
PREPARE_TURBO=0
|
|
68
|
+
;;
|
|
69
|
+
--no-seed-default-config)
|
|
70
|
+
SEED_DEFAULT_CONFIG=0
|
|
71
|
+
;;
|
|
72
|
+
--no-startup)
|
|
73
|
+
STARTUP=0
|
|
74
|
+
;;
|
|
75
|
+
--session-backend)
|
|
76
|
+
shift
|
|
77
|
+
SESSION_BACKEND="${1:-}"
|
|
78
|
+
;;
|
|
79
|
+
--session-backend=*)
|
|
80
|
+
SESSION_BACKEND="${1#*=}"
|
|
81
|
+
;;
|
|
82
|
+
-h|--help)
|
|
83
|
+
usage
|
|
84
|
+
exit 0
|
|
85
|
+
;;
|
|
86
|
+
*)
|
|
87
|
+
usage
|
|
88
|
+
exit 1
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
shift
|
|
92
|
+
done
|
|
93
|
+
|
|
94
|
+
if [ "$SESSION_BACKEND" = "auto" ]; then
|
|
95
|
+
SESSION_BACKEND="$(detect_session_backend)"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
PIP_TARGET="$SCRIPT_DIR"
|
|
99
|
+
if [ "$SESSION_BACKEND" = "x11" ]; then
|
|
100
|
+
PIP_TARGET="${SCRIPT_DIR}[x11]"
|
|
101
|
+
elif [ "$SESSION_BACKEND" = "wayland" ]; then
|
|
102
|
+
PIP_TARGET="${SCRIPT_DIR}[wayland]"
|
|
103
|
+
elif [ "$SESSION_BACKEND" = "unknown" ]; then
|
|
104
|
+
PIP_TARGET="${SCRIPT_DIR}[x11,wayland]"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
echo "Detected install session backend: $SESSION_BACKEND"
|
|
108
|
+
|
|
109
|
+
echo "Creating venv at $INSTALL_DIR ..."
|
|
110
|
+
uv venv "$INSTALL_DIR/venv" --python "$PYTHON_BIN" --system-site-packages --quiet
|
|
111
|
+
|
|
112
|
+
echo "Installing dictate from $PIP_TARGET ..."
|
|
113
|
+
uv pip install "$PIP_TARGET" --python "$INSTALL_DIR/venv/bin/python" --quiet
|
|
114
|
+
|
|
115
|
+
echo "Linking binary ..."
|
|
116
|
+
mkdir -p "$BIN_DIR"
|
|
117
|
+
ln -sf "$INSTALL_DIR/venv/bin/dictate" "$BIN_DIR/dictate"
|
|
118
|
+
|
|
119
|
+
echo "Installing icon ..."
|
|
120
|
+
mkdir -p "$ICON_DIR"
|
|
121
|
+
rm -f "$ICON_DIR/dictate-controls.png" "$ICON_DIR/dictate.png"
|
|
122
|
+
install -m 644 "$SCRIPT_DIR/assets/dictate.png" "$ICON_PATH"
|
|
123
|
+
|
|
124
|
+
echo "Installing desktop entry ..."
|
|
125
|
+
mkdir -p "$DESKTOP_DIR"
|
|
126
|
+
rm -f "$DESKTOP_DIR/dictate-settings.desktop"
|
|
127
|
+
cat > "$DESKTOP_DIR/dictate.desktop" <<EOF
|
|
128
|
+
[Desktop Entry]
|
|
129
|
+
Name=Dictate
|
|
130
|
+
Comment=Dictate into the focused app
|
|
131
|
+
Exec=$HOME/.local/bin/dictate
|
|
132
|
+
Icon=$ICON_PATH
|
|
133
|
+
Type=Application
|
|
134
|
+
Categories=AudioVideo;Audio;
|
|
135
|
+
Keywords=voice;speech;transcription;dictation;asr;whisper;canary;
|
|
136
|
+
Terminal=false
|
|
137
|
+
EOF
|
|
138
|
+
|
|
139
|
+
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
|
|
140
|
+
|
|
141
|
+
if [ "$STARTUP" -eq 1 ]; then
|
|
142
|
+
echo "Installing autostart entry ..."
|
|
143
|
+
mkdir -p "$AUTOSTART_DIR"
|
|
144
|
+
cat > "$AUTOSTART_DIR/dictate.desktop" <<EOF
|
|
145
|
+
[Desktop Entry]
|
|
146
|
+
Name=Dictate
|
|
147
|
+
Comment=Dictate into the focused app
|
|
148
|
+
Exec=$HOME/.local/bin/dictate
|
|
149
|
+
Icon=$ICON_PATH
|
|
150
|
+
Type=Application
|
|
151
|
+
Categories=AudioVideo;Audio;
|
|
152
|
+
Keywords=voice;speech;transcription;dictation;asr;whisper;canary;
|
|
153
|
+
Terminal=false
|
|
154
|
+
X-GNOME-Autostart-enabled=true
|
|
155
|
+
EOF
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [ "$SEED_DEFAULT_CONFIG" -eq 1 ]; then
|
|
159
|
+
if [ ! -f "$CONFIG_PATH" ]; then
|
|
160
|
+
if [ ! -f "$DEFAULT_CONFIG_SOURCE" ]; then
|
|
161
|
+
echo "Default config template not found: $DEFAULT_CONFIG_SOURCE"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
echo "Seeding default config at $CONFIG_PATH ..."
|
|
165
|
+
mkdir -p "$CONFIG_DIR"
|
|
166
|
+
install -m 600 "$DEFAULT_CONFIG_SOURCE" "$CONFIG_PATH"
|
|
167
|
+
if [ "$SESSION_BACKEND" = "wayland" ]; then
|
|
168
|
+
sed -i 's/^push_to_talk_combo: .*/push_to_talk_combo: ctrl+space/' "$CONFIG_PATH"
|
|
169
|
+
fi
|
|
170
|
+
else
|
|
171
|
+
echo "Existing config found at $CONFIG_PATH; leaving it unchanged."
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
DICTATE_BIN="$INSTALL_DIR/venv/bin/dictate"
|
|
176
|
+
|
|
177
|
+
run_logged_check() {
|
|
178
|
+
local label="$1"
|
|
179
|
+
local log_path="$2"
|
|
180
|
+
local timeout_seconds="$3"
|
|
181
|
+
shift 3
|
|
182
|
+
echo "Running: $label ..."
|
|
183
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
184
|
+
if ! timeout "${timeout_seconds}s" "$@" >"$log_path" 2>&1; then
|
|
185
|
+
echo "Command failed: $label"
|
|
186
|
+
echo "See: $log_path"
|
|
187
|
+
tail -n 120 "$log_path" || true
|
|
188
|
+
exit 1
|
|
189
|
+
fi
|
|
190
|
+
else
|
|
191
|
+
if ! "$@" >"$log_path" 2>&1; then
|
|
192
|
+
echo "Command failed: $label"
|
|
193
|
+
echo "See: $log_path"
|
|
194
|
+
tail -n 120 "$log_path" || true
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if [ "$PREPARE_TURBO" -eq 1 ]; then
|
|
201
|
+
PREPARE_LOG="/tmp/dictate-install-prepare.log"
|
|
202
|
+
run_logged_check \
|
|
203
|
+
"dictate prepare-model --stt-backend faster-whisper --model turbo --device auto --compute-type int8" \
|
|
204
|
+
"$PREPARE_LOG" \
|
|
205
|
+
1800 \
|
|
206
|
+
"$DICTATE_BIN" \
|
|
207
|
+
prepare-model \
|
|
208
|
+
--stt-backend faster-whisper \
|
|
209
|
+
--model turbo \
|
|
210
|
+
--device auto \
|
|
211
|
+
--compute-type int8
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
if [ "$VERIFY" -eq 1 ]; then
|
|
215
|
+
VERIFY_LOG="/tmp/dictate-install-verify.log"
|
|
216
|
+
run_logged_check "dictate --help" "$VERIFY_LOG" 20 "$DICTATE_BIN" --help
|
|
217
|
+
run_logged_check "dictate benchmark --help" "$VERIFY_LOG" 20 "$DICTATE_BIN" benchmark --help
|
|
218
|
+
run_logged_check \
|
|
219
|
+
"dictate doctor --quick --stt-backend faster-whisper --model turbo" \
|
|
220
|
+
"$VERIFY_LOG" \
|
|
221
|
+
20 \
|
|
222
|
+
"$DICTATE_BIN" \
|
|
223
|
+
doctor \
|
|
224
|
+
--quick \
|
|
225
|
+
--stt-backend faster-whisper \
|
|
226
|
+
--model turbo
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
if [ "$STARTUP" -eq 1 ]; then
|
|
230
|
+
echo "Done. 'dictate' is now available on your PATH, in the app launcher, and starts when you sign in."
|
|
231
|
+
else
|
|
232
|
+
echo "Done. 'dictate' is now available on your PATH and in the app launcher."
|
|
233
|
+
fi
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const packageRoot = dirname(scriptDir);
|
|
8
|
+
const invokedAs = basename(process.argv[1] || "dictate-install");
|
|
9
|
+
const firstArg = process.argv[2] || "";
|
|
10
|
+
|
|
11
|
+
let command = "install";
|
|
12
|
+
let passthrough = process.argv.slice(2);
|
|
13
|
+
if (invokedAs.includes("update")) {
|
|
14
|
+
command = "update";
|
|
15
|
+
} else if (invokedAs.includes("uninstall")) {
|
|
16
|
+
command = "uninstall";
|
|
17
|
+
} else if (["install", "update", "uninstall", "wizard"].includes(firstArg)) {
|
|
18
|
+
command = firstArg;
|
|
19
|
+
passthrough = process.argv.slice(3);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isWindows = process.platform === "win32";
|
|
23
|
+
const scripts = isWindows
|
|
24
|
+
? {
|
|
25
|
+
install: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "install.ps1")]],
|
|
26
|
+
update: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "update.ps1")]],
|
|
27
|
+
uninstall: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "uninstall.ps1")]],
|
|
28
|
+
wizard: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "install.ps1"), "-Wizard"]],
|
|
29
|
+
}
|
|
30
|
+
: {
|
|
31
|
+
install: ["bash", [join(packageRoot, "install.sh")]],
|
|
32
|
+
update: ["bash", [join(packageRoot, "update.sh")]],
|
|
33
|
+
uninstall: ["bash", [join(packageRoot, "uninstall.sh")]],
|
|
34
|
+
wizard: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const selected = scripts[command];
|
|
38
|
+
if (!selected) {
|
|
39
|
+
console.error(`dictate ${command} is not supported on ${process.platform}`);
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [exe, args] = selected;
|
|
44
|
+
const result = spawnSync(exe, [...args, ...passthrough], { stdio: "inherit" });
|
|
45
|
+
if (result.error) {
|
|
46
|
+
console.error(result.error.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
process.exit(result.status ?? 1);
|