Skip to content
Merged
15 changes: 9 additions & 6 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ The ZIP root should contain the project files directly. Do not wrap them inside
an extra nested `Parroty` directory.

Required source/runtime files include `app/`, `tools/`, `requirements.txt`, the
tracked launcher helpers, `parroty.ico`, documentation, and `LICENSE`.
tracked launcher helpers, `parroty.ico`, `app/static/parroty.ico`, documentation,
and `LICENSE`.

The voice package must include all eight `builtin_*.wav` files and
`app/assets/voices/ATTRIBUTION.md`.
Expand Down Expand Up @@ -57,11 +58,13 @@ py -3.12 -m py_compile launch_parroty.pyw app/server.py app/tts.py app/narrate_w
py -3.12 -c "from app.tts import ENGINE_CATALOG; v=ENGINE_CATALOG['chatterbox']['builtin_voices']; assert len(v)==8; print(list(v))"
```

After installation, verify the monitor identifies CUDA/NVIDIA, the app opens in
a dedicated maximized window without a visible console, and closing that app
window with X stops the Flask listener and releases port 5000. Also verify the
desktop shortcut uses `parroty.ico`, all eight voices appear, previews work, and
`stop.bat` still shuts down port 5000 as a fallback.
After installation, verify the monitor identifies CUDA/NVIDIA and the app opens in
a dedicated maximized window without a visible console. Confirm the desktop
shortcut, native app window, and taskbar button all use `parroty.ico`. Start a
short narration, close the app window with X, and verify the Flask listener and
every `app.narrate_worker` process exit. Also verify the generated `stop.bat` can
find and stop Parroty workers even when port 5000 is no longer listening. Finally,
confirm all eight voices appear and previews work.

Before publishing, review `README.md`, `Quick Start Readme.txt`, `BUILD.md`,
`RELEASE_NOTES.md`, and the voice attribution. No version bump or GitHub Release
Expand Down
37 changes: 26 additions & 11 deletions Quick Start Readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ launch_parroty.pyw, and the app folder:
5. Save with the exact .bat filename shown.

The eight bundled Chatterbox voices already live under app\assets\voices.
No API key or separate voice download is required. Closing the dedicated
Parroty Chrome/Edge app window with X automatically stops the hidden backend.
No API key or separate voice download is required. The dedicated Parroty
Chrome/Edge app window uses the bird icon in its title bar and taskbar. Closing
it with X stops Flask and any active narration workers.

========================================================================
FILE: install_all.bat
Expand Down Expand Up @@ -98,13 +99,27 @@ exit /b 0
========================================================================

@echo off
setlocal enabledelayedexpansion
set "FOUND=0"
for /f "tokens=5" %%P in ('netstat -ano ^| findstr ":5000" ^| findstr LISTENING') do (
taskkill /F /PID %%P >nul 2>&1
set "FOUND=1"
)
if "!FOUND!"=="0" (echo Parroty was not running.) else (echo Parroty stopped.)
setlocal
cd /d "%~dp0"
title Stop Parroty

powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command ^
"$root=[IO.Path]::GetFullPath('%CD%');" ^
"$targets=New-Object 'System.Collections.Generic.HashSet[int]';" ^
"try { Get-NetTCPConnection -LocalPort 5000 -State Listen -ErrorAction Stop | ForEach-Object { if ($_.OwningProcess -gt 0) { [void]$targets.Add([int]$_.OwningProcess) } } } catch {};" ^
"Get-CimInstance Win32_Process | Where-Object {" ^
" $_.Name -match '^pythonw?\.exe$' -and $_.CommandLine -and (" ^
" ($_.CommandLine -like ('*' + $root + '*launch_parroty.pyw*')) -or" ^
" ($_.CommandLine -like ('*' + $root + '*-m app.narrate_worker*')) -or" ^
" ($_.CommandLine -like ('*' + $root + '*-m app.server*'))" ^
" )" ^
"} | ForEach-Object { [void]$targets.Add([int]$_.ProcessId) };" ^
"if ($targets.Count -eq 0) { Write-Host 'Parroty was not running.'; exit 0 };" ^
"$ordered=@($targets) | Sort-Object -Descending;" ^
"foreach ($id in $ordered) { & taskkill.exe /PID $id /T /F 2>$null | Out-Null };" ^
"Remove-Item -LiteralPath (Join-Path $root '.parroty.browser.pid') -Force -ErrorAction SilentlyContinue;" ^
"Write-Host 'Parroty stopped, including active narration workers.'"

timeout /t 2 >nul
exit /b 0

Expand Down Expand Up @@ -141,8 +156,8 @@ exit /b 0

1. Run install_all.bat once.
2. Start Parroty with run.bat or the desktop shortcut.
3. Close the Parroty app window with X to stop the hidden backend automatically.
4. Use stop.bat only if the window is already gone or a forced stop is needed.
3. Close the Parroty app window with X to stop Flask and active narration workers.
4. Use stop.bat if the window is already gone or a forced full stop is needed.
5. Use run_debug.bat only for troubleshooting.

Do not commit the generated BAT files. They are local/package conveniences.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ timestamps. Everything runs on your own machine and opens in Chrome.
off — finishing 100+ chapter books on a 16 GB machine without crashes.
- **Runs entirely on your own machine** — your files and (for the local engine)
your audio never leave your computer.
- **Windows desktop-app behavior:** opens maximized with the Parroty bird icon in
the window and taskbar; closing X stops Flask and active narration workers.

## Screenshots

Expand Down Expand Up @@ -417,12 +419,13 @@ installed). Press **Ctrl+C** to stop.
**Optional Windows launchers:** create the local `.bat` files from
`Quick Start Readme.txt`, or use a packaged Windows ZIP that already includes
them. `run.bat` starts the backend windowlessly through `pythonw.exe`, opens
Parroty in a dedicated maximized Chrome/Edge app window, and silently creates or
refreshes the desktop shortcut. Closing that Parroty app window with **X** now
automatically stops the hidden Flask backend, the same result as running
`stop.bat`. Output goes to `parroty.log`; `stop.bat` remains available as a
fallback if the window is already gone or the backend needs to be forced closed.
These `.bat` files are deliberately ignored by Git.
Parroty in a dedicated maximized Chrome/Edge app window, applies the Parroty bird
icon to the native window and taskbar button, and silently creates or refreshes
the desktop shortcut. Closing that app window with **X** terminates the complete
Parroty process tree—including an active `app.narrate_worker`—rather than only
closing the port-5000 Flask parent. Output goes to `parroty.log`; `stop.bat` uses
the same full-tree behavior and can also find orphaned Parroty workers if the
listener is already gone. These `.bat` files are deliberately ignored by Git.

Starting again later needs no reinstall — setup is one-time. Open a terminal,
`cd` into the Parroty folder, activate the venv, and run the server:
Expand Down
12 changes: 7 additions & 5 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ No version number is assigned to this update.
- Added the bottom-left CPU/GPU/VRAM system monitor.
- Added hidden background launching with persistent `parroty.log` diagnostics.
- Added a dedicated maximized Chrome/Edge app window using its own browser profile.
- Closing the dedicated Parroty app window now automatically stops the hidden
Flask backend, matching `stop.bat` behavior without affecting normal browser windows.
- Corrected that shutdown detection to monitor the exact native Parroty window
handle rather than waiting for the Chromium process, which may remain alive
after its app window is closed.
- Closing the dedicated Parroty app window now monitors the exact native window
handle and terminates the complete Parroty process tree, including active
`app.narrate_worker` children that Windows would otherwise leave orphaned.
- Updated the local `stop.bat` template to detect both the port-5000 listener and
Parroty launcher/server/narration workers, preventing false “not running” reports.
- Added the Parroty bird favicon and applies `parroty.ico` directly to the native
Chrome/Edge app window so the title bar and Windows taskbar show the app icon.
- Added desktop-shortcut creation using the Parroty icon.
- Corrected hidden-launch template/static resolution.
- Kept narration workers GPU-enabled when the app window is hidden or inactive.
Expand Down
Binary file added app/static/parroty.ico
Binary file not shown.
5 changes: 4 additions & 1 deletion app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Parroty — EPUB to Audiobook</title>
<meta name="application-name" content="Parroty">
<link rel="icon" type="image/x-icon" href="/static/parroty.ico?v=20260723">
<link rel="shortcut icon" type="image/x-icon" href="/static/parroty.ico?v=20260723">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,900&family=Spline+Sans+Mono:wght@400;500;600&family=Spline+Sans:wght@400;500;600&display=swap" rel="stylesheet">
Expand Down Expand Up @@ -156,7 +159,7 @@ <h2>Choose the voice</h2>
<div class="field hidden" id="builtinField">
<label>Built-in voice <span class="muted">(free — no sample, no key needed)</span></label>
<select id="builtinSelect"></select>
<p class="muted" id="builtinNote">Bundled male/female voices that run fully offline. Click Preview below to hear them. Or upload your own sample to clone a different voice.</p>
<p class="muted" id="builtinNote">Eight reviewed audiobook voices that run fully offline. Click Preview below to hear them. Or upload your own sample to clone a different voice.</p>
</div>

<div class="field hidden" id="keyField">
Expand Down
Binary file added parroty.ico
Binary file not shown.
67 changes: 64 additions & 3 deletions parroty_window.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,56 @@ function Stop-ParrotyBackend {

foreach ($backendProcessId in $ProcessIds) {
if ($backendProcessId -le 0 -or $backendProcessId -eq $PID) { continue }

# Stop-Process kills only the Flask parent on Windows and can leave active
# app.narrate_worker children orphaned. taskkill /T terminates the complete
# Parroty process tree while the parent relationship still exists.
try {
& taskkill.exe /PID $backendProcessId /T /F *> $null
} catch {}

try {
Get-Process -Id $backendProcessId -ErrorAction Stop | Out-Null
Stop-Process -Id $backendProcessId -Force -ErrorAction Stop
try { Wait-Process -Id $backendProcessId -Timeout 5 -ErrorAction SilentlyContinue } catch {}
Wait-Process -Id $backendProcessId -Timeout 5 -ErrorAction SilentlyContinue
} catch {}
}
}

function Set-ParrotyWindowIcon {
param(
[IntPtr]$WindowHandle,
[string]$IconPath
)

if ($WindowHandle -eq [IntPtr]::Zero) { return }
if (-not (Test-Path -LiteralPath $IconPath)) { return }

try {
$IMAGE_ICON = 1
$LR_LOADFROMFILE = 0x10
$WM_SETICON = 0x0080
$ICON_SMALL = 0
$ICON_BIG = 1

$smallIcon = [ParrotyNativeWindow]::LoadImage(
[IntPtr]::Zero, $IconPath, $IMAGE_ICON, 16, 16, $LR_LOADFROMFILE
)
$largeIcon = [ParrotyNativeWindow]::LoadImage(
[IntPtr]::Zero, $IconPath, $IMAGE_ICON, 48, 48, $LR_LOADFROMFILE
)

if ($smallIcon -ne [IntPtr]::Zero) {
[ParrotyNativeWindow]::SendMessage(
$WindowHandle, $WM_SETICON, [IntPtr]$ICON_SMALL, $smallIcon
) | Out-Null
}
if ($largeIcon -ne [IntPtr]::Zero) {
[ParrotyNativeWindow]::SendMessage(
$WindowHandle, $WM_SETICON, [IntPtr]$ICON_BIG, $largeIcon
) | Out-Null
}
} catch {}
}

function Maximize-ParrotyWindow {
param(
[System.Diagnostics.Process]$InitialProcess,
Expand All @@ -88,6 +130,24 @@ public static class ParrotyNativeWindow {

[DllImport("user32.dll")]
public static extern bool IsWindow(IntPtr hWnd);

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LoadImage(
IntPtr hInst,
string name,
uint type,
int cx,
int cy,
uint loadFlags
);

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr SendMessage(
IntPtr hWnd,
uint message,
IntPtr wParam,
IntPtr lParam
);
}
"@
} catch {}
Expand Down Expand Up @@ -130,6 +190,7 @@ public static class ParrotyNativeWindow {
for ($pass = 0; $pass -lt 5; $pass++) {
[ParrotyNativeWindow]::ShowWindowAsync($chosen.MainWindowHandle, 3) | Out-Null
[ParrotyNativeWindow]::SetForegroundWindow($chosen.MainWindowHandle) | Out-Null
Set-ParrotyWindowIcon -WindowHandle $chosen.MainWindowHandle -IconPath (Join-Path $Root "parroty.ico")
Start-Sleep -Milliseconds 300
try { $chosen.Refresh() } catch {}
}
Expand Down