From 1c6e62375336ddaa4240fba1d2aaa55350da54d2 Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:01:27 -0400 Subject: [PATCH 01/12] Add temporary finalization updater --- tools/finalize_shutdown_icon_docs.py | 315 +++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 tools/finalize_shutdown_icon_docs.py diff --git a/tools/finalize_shutdown_icon_docs.py b/tools/finalize_shutdown_icon_docs.py new file mode 100644 index 0000000..68f5acd --- /dev/null +++ b/tools/finalize_shutdown_icon_docs.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +import shutil +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def write(path: str, text: str, *, crlf: bool = False) -> None: + target = ROOT / path + if crlf: + normalized = text.replace("\r\n", "\n") + with target.open("w", encoding="utf-8", newline="") as file: + file.write(normalized.replace("\n", "\r\n")) + else: + target.write_text(text, encoding="utf-8") + + +def replace_once(text: str, old: str, new: str, label: str) -> str: + if new in text: + return text + if old not in text: + raise RuntimeError(f"Expected text not found while updating {label}") + return text.replace(old, new, 1) + + +def update_window_launcher() -> None: + path = "parroty_window.ps1" + text = read(path) + + old_stop = '''function Stop-ParrotyBackend { + param([int[]]$ProcessIds) + + foreach ($backendProcessId in $ProcessIds) { + if ($backendProcessId -le 0 -or $backendProcessId -eq $PID) { continue } + 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 {} + } catch {} + } +}''' + new_stop = '''function Stop-ParrotyBackend { + param([int[]]$ProcessIds) + + 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 { + Wait-Process -Id $backendProcessId -Timeout 5 -ErrorAction SilentlyContinue + } catch {} + } +}''' + text = replace_once(text, old_stop, new_stop, path) + + old_native = ''' [DllImport("user32.dll")] + public static extern bool IsWindow(IntPtr hWnd); +}''' + new_native = ''' [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 + ); +}''' + text = replace_once(text, old_native, new_native, path) + + helper = '''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 {} +} + +''' + marker = "function Maximize-ParrotyWindow {" + if "function Set-ParrotyWindowIcon" not in text: + if marker not in text: + raise RuntimeError("Maximize-ParrotyWindow marker not found") + text = text.replace(marker, helper + marker, 1) + + old_loop = ''' [ParrotyNativeWindow]::ShowWindowAsync($chosen.MainWindowHandle, 3) | Out-Null + [ParrotyNativeWindow]::SetForegroundWindow($chosen.MainWindowHandle) | Out-Null + Start-Sleep -Milliseconds 300''' + new_loop = ''' [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''' + text = replace_once(text, old_loop, new_loop, path) + write(path, text) + + +def update_template_and_icon() -> None: + path = "app/templates/index.html" + text = read(path) + old = "Parroty — EPUB to Audiobook" + new = '''Parroty — EPUB to Audiobook + + +''' + text = replace_once(text, old, new, path) + text = text.replace( + "Bundled male/female voices that run fully offline. Click Preview below to hear them.", + "Eight reviewed audiobook voices that run fully offline. Click Preview below to hear them.", + ) + write(path, text) + + static_icon = ROOT / "app/static/parroty.ico" + static_icon.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / "parroty.ico", static_icon) + + +def stop_bat_template() -> str: + return r'''@echo off +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''' + + +def update_quick_start() -> None: + path = "Quick Start Readme.txt" + text = read(path) + text = replace_once( + text, + "No API key or separate voice download is required. Closing the dedicated\nParroty Chrome/Edge app window with X automatically stops the hidden backend.", + "No API key or separate voice download is required. The dedicated Parroty\nChrome/Edge app window uses the bird icon in its title bar and taskbar. Closing\nit with X stops Flask and any active narration workers.", + path, + ) + + start = text.index("@echo off", text.index("FILE: stop.bat")) + end_marker = "\n\n========================================================================\n FILE: run_debug.bat" + end = text.index(end_marker, start) + text = text[:start] + stop_bat_template() + text[end:] + + text = text.replace( + "3. Close the Parroty app window with X to stop the hidden backend automatically.\n" + "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.\n" + "4. Use stop.bat if the window is already gone or a forced full stop is needed.", + ) + write(path, text, crlf=True) + + +def update_readme() -> None: + path = "README.md" + text = read(path) + highlight = ( + "- **Runs entirely on your own machine** — your files and (for the local engine)\n" + " your audio never leave your computer." + ) + replacement = highlight + ( + "\n- **Windows desktop-app behavior:** opens maximized with the Parroty bird icon in\n" + " the window and taskbar; closing X stops Flask and active narration workers." + ) + text = replace_once(text, highlight, replacement, path) + + old_running = ( + "Parroty in a dedicated maximized Chrome/Edge app window, and silently creates or\n" + "refreshes the desktop shortcut. Closing that Parroty app window with **X** now\n" + "automatically stops the hidden Flask backend, the same result as running\n" + "`stop.bat`. Output goes to `parroty.log`; `stop.bat` remains available as a\n" + "fallback if the window is already gone or the backend needs to be forced closed.\n" + "These `.bat` files are deliberately ignored by Git." + ) + new_running = ( + "Parroty in a dedicated maximized Chrome/Edge app window, applies the Parroty bird\n" + "icon to the native window and taskbar button, and silently creates or refreshes\n" + "the desktop shortcut. Closing that app window with **X** terminates the complete\n" + "Parroty process tree—including an active `app.narrate_worker`—rather than only\n" + "closing the port-5000 Flask parent. Output goes to `parroty.log`; `stop.bat` uses\n" + "the same full-tree behavior and can also find orphaned Parroty workers if the\n" + "listener is already gone. These `.bat` files are deliberately ignored by Git." + ) + text = replace_once(text, old_running, new_running, path) + write(path, text) + + +def update_build() -> None: + path = "BUILD.md" + text = read(path) + text = replace_once( + text, + "tracked launcher helpers, `parroty.ico`, documentation, and `LICENSE`.", + "tracked launcher helpers, `parroty.ico`, `app/static/parroty.ico`, documentation,\n" + "and `LICENSE`.", + path, + ) + text = replace_once( + text, + "After installation, verify the monitor identifies CUDA/NVIDIA, the app opens in\n" + "a dedicated maximized window without a visible console, and closing that app\n" + "window with X stops the Flask listener and releases port 5000. Also verify the\n" + "desktop shortcut uses `parroty.ico`, all eight voices appear, previews work, and\n" + "`stop.bat` still shuts down port 5000 as a fallback.", + "After installation, verify the monitor identifies CUDA/NVIDIA and the app opens in\n" + "a dedicated maximized window without a visible console. Confirm the desktop\n" + "shortcut, native app window, and taskbar button all use `parroty.ico`. Start a\n" + "short narration, close the app window with X, and verify the Flask listener and\n" + "every `app.narrate_worker` process exit. Also verify the generated `stop.bat` can\n" + "find and stop Parroty workers even when port 5000 is no longer listening. Finally,\n" + "confirm all eight voices appear and previews work.", + path, + ) + write(path, text) + + +def update_release_notes() -> None: + path = "RELEASE_NOTES.md" + text = read(path) + old = ( + "- Closing the dedicated Parroty app window now automatically stops the hidden\n" + " Flask backend, matching `stop.bat` behavior without affecting normal browser windows.\n" + "- Corrected that shutdown detection to monitor the exact native Parroty window\n" + " handle rather than waiting for the Chromium process, which may remain alive\n" + " after its app window is closed." + ) + new = ( + "- Closing the dedicated Parroty app window now monitors the exact native window\n" + " handle and terminates the complete Parroty process tree, including active\n" + " `app.narrate_worker` children that Windows would otherwise leave orphaned.\n" + "- Updated the local `stop.bat` template to detect both the port-5000 listener and\n" + " Parroty launcher/server/narration workers, preventing false “not running” reports.\n" + "- Added the Parroty bird favicon and applies `parroty.ico` directly to the native\n" + " Chrome/Edge app window so the title bar and Windows taskbar show the app icon." + ) + text = replace_once(text, old, new, path) + write(path, text) + + +def main() -> None: + update_window_launcher() + update_template_and_icon() + update_quick_start() + update_readme() + update_build() + update_release_notes() + print("Applied confirmed shutdown, icon, and documentation updates") + + +if __name__ == "__main__": + main() From 44481187aa9b2c036ddc802e4849e46ec5d06784 Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:02:00 -0400 Subject: [PATCH 02/12] Add temporary Windows finalization test --- .github/workflows/finalize-shutdown-icon.yml | 146 +++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 .github/workflows/finalize-shutdown-icon.yml diff --git a/.github/workflows/finalize-shutdown-icon.yml b/.github/workflows/finalize-shutdown-icon.yml new file mode 100644 index 0000000..aea5c28 --- /dev/null +++ b/.github/workflows/finalize-shutdown-icon.yml @@ -0,0 +1,146 @@ +name: Finalize shutdown and window icon + +on: + pull_request: + branches: [main] + +permissions: + contents: write + +jobs: + verify: + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + ref: finalize-shutdown-and-window-icon + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Apply confirmed local changes + run: python tools/finalize_shutdown_icon_docs.py + + - name: Parse PowerShell and Python + shell: pwsh + run: | + $tokens = $null + $errors = $null + [void][System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path "parroty_window.ps1"), + [ref]$tokens, + [ref]$errors + ) + if ($errors.Count -gt 0) { + $errors | Format-List | Out-String | Write-Error + exit 1 + } + python -m py_compile launch_parroty.pyw app/server.py app/narrate_worker.py + + - name: Verify full process-tree shutdown + shell: pwsh + run: | + @' + import pathlib + import subprocess + import sys + import time + + child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(120)"]) + pathlib.Path("child.pid").write_text(str(child.pid), encoding="utf-8") + time.sleep(120) + '@ | Set-Content -LiteralPath tree_parent.py -Encoding UTF8 + + $parent = Start-Process python -ArgumentList "tree_parent.py" -PassThru -WindowStyle Hidden + try { + $childPid = $null + for ($i = 0; $i -lt 40; $i++) { + Start-Sleep -Milliseconds 250 + if (Test-Path child.pid) { + $childPid = [int](Get-Content child.pid -Raw) + break + } + } + if (-not $childPid) { throw "Child PID was not created" } + if (-not (Get-Process -Id $parent.Id -ErrorAction SilentlyContinue)) { throw "Parent exited too soon" } + if (-not (Get-Process -Id $childPid -ErrorAction SilentlyContinue)) { throw "Child exited too soon" } + + $tokens = $null + $errors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path "parroty_window.ps1"), + [ref]$tokens, + [ref]$errors + ) + $functions = $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] + }, $true) + foreach ($function in $functions) { + Invoke-Expression $function.Extent.Text + } + + Stop-ParrotyBackend -ProcessIds @($parent.Id) + Start-Sleep -Milliseconds 750 + + if (Get-Process -Id $parent.Id -ErrorAction SilentlyContinue) { + throw "Parent process survived full-tree shutdown" + } + if (Get-Process -Id $childPid -ErrorAction SilentlyContinue) { + throw "Child narration-worker stand-in survived full-tree shutdown" + } + Write-Host "Verified taskkill /T stops both parent and child." + } + finally { + Stop-Process -Id $parent.Id -Force -ErrorAction SilentlyContinue + if ($childPid) { Stop-Process -Id $childPid -Force -ErrorAction SilentlyContinue } + Remove-Item tree_parent.py,child.pid -Force -ErrorAction SilentlyContinue + } + + - name: Verify icon and documentation assets + shell: pwsh + run: | + if (-not (Test-Path app/static/parroty.ico)) { throw "Static Parroty icon missing" } + $rootHash = (Get-FileHash parroty.ico -Algorithm SHA256).Hash + $staticHash = (Get-FileHash app/static/parroty.ico -Algorithm SHA256).Hash + if ($rootHash -ne $staticHash) { throw "Static icon does not match root parroty.ico" } + + $html = Get-Content app/templates/index.html -Raw + if ($html -notmatch '/static/parroty\.ico') { throw "Favicon link missing" } + + $launcher = Get-Content parroty_window.ps1 -Raw + foreach ($required in @('taskkill.exe /PID $backendProcessId /T /F','Set-ParrotyWindowIcon','WM_SETICON')) { + if (-not $launcher.Contains($required)) { throw "Launcher is missing: $required" } + } + + $quick = Get-Content "Quick Start Readme.txt" -Raw + if (-not $quick.Contains('app.narrate_worker')) { throw "Quick Start stop.bat does not detect workers" } + if (-not $quick.Contains('/T /F')) { throw "Quick Start stop.bat does not terminate process trees" } + + - name: Enforce repository policy + shell: pwsh + run: | + $trackedBat = @(git ls-files "*.bat") + if ($trackedBat.Count -gt 0) { + throw "Tracked BAT files found: $($trackedBat -join ', ')" + } + $ignore = Get-Content .gitignore -Raw + if ($ignore -notmatch '(?m)^\*\.bat\s*$') { + throw ".gitignore does not block *.bat" + } + + - name: Commit generated final files + shell: pwsh + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add parroty_window.ps1 app/templates/index.html app/static/parroty.ico README.md "Quick Start Readme.txt" BUILD.md RELEASE_NOTES.md + if (git diff --cached --quiet) { + Write-Host "Final files already current." + } else { + git commit -m "Finalize process-tree shutdown and window icon" + git push origin HEAD:finalize-shutdown-and-window-icon + } From f7c7fce8546ebaa2e14c46390c12b84f8f71193f Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:03:43 -0400 Subject: [PATCH 03/12] Capture finalization updater diagnostics --- .github/workflows/finalize-shutdown-icon.yml | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/finalize-shutdown-icon.yml b/.github/workflows/finalize-shutdown-icon.yml index aea5c28..d94383d 100644 --- a/.github/workflows/finalize-shutdown-icon.yml +++ b/.github/workflows/finalize-shutdown-icon.yml @@ -22,9 +22,21 @@ jobs: python-version: "3.12" - name: Apply confirmed local changes - run: python tools/finalize_shutdown_icon_docs.py + id: apply + continue-on-error: true + shell: pwsh + run: python tools/finalize_shutdown_icon_docs.py *> updater.log + + - name: Upload updater diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: finalization-updater-diagnostics + path: updater.log + if-no-files-found: warn - name: Parse PowerShell and Python + if: steps.apply.outcome == 'success' shell: pwsh run: | $tokens = $null @@ -41,6 +53,7 @@ jobs: python -m py_compile launch_parroty.pyw app/server.py app/narrate_worker.py - name: Verify full process-tree shutdown + if: steps.apply.outcome == 'success' shell: pwsh run: | @' @@ -101,6 +114,7 @@ jobs: } - name: Verify icon and documentation assets + if: steps.apply.outcome == 'success' shell: pwsh run: | if (-not (Test-Path app/static/parroty.ico)) { throw "Static Parroty icon missing" } @@ -121,6 +135,7 @@ jobs: if (-not $quick.Contains('/T /F')) { throw "Quick Start stop.bat does not terminate process trees" } - name: Enforce repository policy + if: steps.apply.outcome == 'success' shell: pwsh run: | $trackedBat = @(git ls-files "*.bat") @@ -133,6 +148,7 @@ jobs: } - name: Commit generated final files + if: steps.apply.outcome == 'success' shell: pwsh run: | git config user.name "github-actions[bot]" @@ -144,3 +160,7 @@ jobs: git commit -m "Finalize process-tree shutdown and window icon" git push origin HEAD:finalize-shutdown-and-window-icon } + + - name: Enforce updater result + if: steps.apply.outcome != 'success' + run: exit 1 From c6ba36d135381bd108798b33b781bee10862c848 Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:14:57 -0400 Subject: [PATCH 04/12] Add temporary Parroty icon payload --- tools/parroty_icon.b64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 tools/parroty_icon.b64 diff --git a/tools/parroty_icon.b64 b/tools/parroty_icon.b64 new file mode 100644 index 0000000..8c44532 --- /dev/null +++ b/tools/parroty_icon.b64 @@ -0,0 +1 @@ +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 \ No newline at end of file From 7bf52afd0b2792389c63895398a1528def3e5a65 Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:15:39 -0400 Subject: [PATCH 05/12] Restore Parroty icon before finalization --- .github/workflows/finalize-shutdown-icon.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/finalize-shutdown-icon.yml b/.github/workflows/finalize-shutdown-icon.yml index d94383d..6e328c5 100644 --- a/.github/workflows/finalize-shutdown-icon.yml +++ b/.github/workflows/finalize-shutdown-icon.yml @@ -21,6 +21,15 @@ jobs: with: python-version: "3.12" + - name: Restore Parroty icon payload + shell: pwsh + run: | + $payload = (Get-Content tools/parroty_icon.b64 -Raw).Trim() + [IO.File]::WriteAllBytes( + (Join-Path $PWD "parroty.ico"), + [Convert]::FromBase64String($payload) + ) + - name: Apply confirmed local changes id: apply continue-on-error: true @@ -153,7 +162,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add parroty_window.ps1 app/templates/index.html app/static/parroty.ico README.md "Quick Start Readme.txt" BUILD.md RELEASE_NOTES.md + git add parroty.ico parroty_window.ps1 app/templates/index.html app/static/parroty.ico README.md "Quick Start Readme.txt" BUILD.md RELEASE_NOTES.md if (git diff --cached --quiet) { Write-Host "Final files already current." } else { From 86dc7f867606c968a7ab365ee854976f732bf8d9 Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:16:56 -0400 Subject: [PATCH 06/12] Decode icon payload with Python --- .github/workflows/finalize-shutdown-icon.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/finalize-shutdown-icon.yml b/.github/workflows/finalize-shutdown-icon.yml index 6e328c5..341ce22 100644 --- a/.github/workflows/finalize-shutdown-icon.yml +++ b/.github/workflows/finalize-shutdown-icon.yml @@ -22,13 +22,11 @@ jobs: python-version: "3.12" - name: Restore Parroty icon payload - shell: pwsh - run: | - $payload = (Get-Content tools/parroty_icon.b64 -Raw).Trim() - [IO.File]::WriteAllBytes( - (Join-Path $PWD "parroty.ico"), - [Convert]::FromBase64String($payload) - ) + run: >- + python -c "import base64,pathlib; + data=base64.b64decode(pathlib.Path('tools/parroty_icon.b64').read_text(encoding='utf-8').strip(),validate=True); + pathlib.Path('parroty.ico').write_bytes(data); + print('Restored parroty.ico:',len(data),'bytes')" - name: Apply confirmed local changes id: apply From 85425fc792d10f1373b34f3d068fe013c4709f08 Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:20:26 -0400 Subject: [PATCH 07/12] Add Parroty window icon assets --- app/static/parroty.ico | Bin 0 -> 2550 bytes parroty.ico | Bin 0 -> 2550 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/static/parroty.ico create mode 100644 parroty.ico diff --git a/app/static/parroty.ico b/app/static/parroty.ico new file mode 100644 index 0000000000000000000000000000000000000000..5d827759405a01a357971239f52ccbd75a052d5c GIT binary patch literal 2550 zcmVTrc&^l;Q1VYQmwnpG0DzldqK7yLm9OvQuo~Ok)BSQ$? z&4qjJx%c}xUs+%KxP$---F?5cTNe=(5kXANIcLZByRS<3Ws%-l!yptz zs}5-ojqf~GSJ(LdZHb8!$LK#$q|jf$iE#eRDK`IX(cEeB;r%&UqMXFd}4Gt|Ewtsk*)}GaNw!g0Y4uiVnCqis%1{A^cCY z#fYi@1rH1bx*W5q(*OVf07*qoM6N<$f{9R0M-2)Z3IG5A4M|8uQUCw|AOHXWAP5Ek z0047(dh`GQ2M0++K~#90jh9V~99I#?fAwB>&*#qUhj+a;#$T`|&WD78K!QV(6G1pp zh%A#3Ktd6hz<~%PkT@U^LXjdAPKW~>A`8L|5VDOTq(~tajxDep1FIvOp{HkhX4esK4&6PkUe&+qRsCN**}Qf2cK`^00d@4h)c`L2U&zfFR2?D) zl-x+?RZOZ5m(!6AgW|B(3dXKTV|88!24VPpNP9XVnxWk;Bia8wnS1C_Reay49YxDt z7={TSFf`LUiY13hwGZio_2fRIC>CQ((*x{4m!P5p>7HUiP;yrCk;)z;AVmbtArs94 zBsg^{f{0+eG*YPnMG=)jghhuGLK;4iRKAZ#Mr6>w@l1C%mpPgZ4^0;ghfDi{8L+%a zHt)12(@RbU7r#{EpI5H%?mL&T)*D1XK{Vay{+9LLGc&69 z*n&*K(5F+ZHH4jrldt}o8|TiU-3WH=p%C1pzUvVC56w|3TJpP)PedIl7XiG{n1~KC zKJSgHBT9r{y!0~5r%!Qk-(D(4h&EQa6t#HrTVJM93g|=;p66|=rv7uJ9?ih^NG0dI zNRouH+87tkz0Ja3-s1D$`z~Y0pW*84hq0v!pYSWZ{f9r27=vfcAOiW)+>u9Q0AMuJ z^TV@-<<)imeCBmz|2`gn{&|)bmneVq2!#h8A}~tl<^r#td=2Y+s3s(d+fkp8<$wYX zSy{NO{fr+SJ@RO@B7XI=AF#i4i=~YYX8M!7@Xc>S;B)dPFQMnoa$P*uZ?Ez2b6?^c z-+2*|F0FP4Rlyh;%?0&6U#v0nrtkeWgn$UnDZaH#P48hTtZ??+8ujuxQY>)ct+Q-h z{2LUDY<9XldhkPBJ9Ua5{^-ZNdGLJm~_T_imh#0V--t(2IVtn%#FzCz*PAr`M)<6mKiOB)Sl4crbptarPV zjA61;=h27v^O4{xzx??tw7YStzE=Sm#jAqphd;-7{fg+3i7^&qEg}YE4bu~KK0YW~s;TasI+ZN*Pl!TyZ8(kr`#GYCkXX=YkkZ zDJU>~-~cOOo1^tA9c#GJis%6C#BptFi%&iA9O1-c{ORIdDqe@RTMM{cIi~!g2%6)8 zj3#%;g?x&1S}i{P*dr`e>(B~$WOj-NcTKRT6wqumnEmYIJp09`IdS3{j(zp}T-lhV z*=VMfFr-v^Nw_db!l82PJ)Am>7_4XUt%q|CYb|fQ`4+GJ^cDQg4U7n8a*EHKIL;T2 zKaCZL;~3w!EZ$nD9flk@un*@HBL;^{%g~znK^2U)WPlMv5cq^)i_OhVZm+Jfy1L5R z>MHG4NTU<+_e<|mDi%31cbKW#7=@s~Z}c9*eQohOfd4nHtM4l2S>N0e=Ya}MA4nVPaI?8&N{yD zrKzLPjbmaJw(d3viUq{@?B25%-&%|a;E3axYHgg~{Py=OFRyU;@ByN3yhBGMx-AM> z+kIzallK=EQ5^MJjZ&#dAqc<-m*1@`XQ&E+fq=H}uu&1T5* zt=qIa5pj?83Vwhe_}qW=0j8%X>Be!tIq9c;{xBM@0>vc>p6`*w2~pIg6Lko~7GX2w zZlgh?*<@>T3u7#`+8EVpg=)1*rBbF`DiIU{0?)@-LpO=BVj(@4)c1Tb#-vHOQ?p2q zc%mo5$$;Mk*&Kn_dm?-C-f3sw$`^UTtj5-a~9)xoFo?+hNUXHk&UGt<8J2Bl`bCCXZvsIZ?D_J+E2Piggfy8r+H M07*qoM6N<$f|;qn+5i9m literal 0 HcmV?d00001 diff --git a/parroty.ico b/parroty.ico new file mode 100644 index 0000000000000000000000000000000000000000..5d827759405a01a357971239f52ccbd75a052d5c GIT binary patch literal 2550 zcmVTrc&^l;Q1VYQmwnpG0DzldqK7yLm9OvQuo~Ok)BSQ$? z&4qjJx%c}xUs+%KxP$---F?5cTNe=(5kXANIcLZByRS<3Ws%-l!yptz zs}5-ojqf~GSJ(LdZHb8!$LK#$q|jf$iE#eRDK`IX(cEeB;r%&UqMXFd}4Gt|Ewtsk*)}GaNw!g0Y4uiVnCqis%1{A^cCY z#fYi@1rH1bx*W5q(*OVf07*qoM6N<$f{9R0M-2)Z3IG5A4M|8uQUCw|AOHXWAP5Ek z0047(dh`GQ2M0++K~#90jh9V~99I#?fAwB>&*#qUhj+a;#$T`|&WD78K!QV(6G1pp zh%A#3Ktd6hz<~%PkT@U^LXjdAPKW~>A`8L|5VDOTq(~tajxDep1FIvOp{HkhX4esK4&6PkUe&+qRsCN**}Qf2cK`^00d@4h)c`L2U&zfFR2?D) zl-x+?RZOZ5m(!6AgW|B(3dXKTV|88!24VPpNP9XVnxWk;Bia8wnS1C_Reay49YxDt z7={TSFf`LUiY13hwGZio_2fRIC>CQ((*x{4m!P5p>7HUiP;yrCk;)z;AVmbtArs94 zBsg^{f{0+eG*YPnMG=)jghhuGLK;4iRKAZ#Mr6>w@l1C%mpPgZ4^0;ghfDi{8L+%a zHt)12(@RbU7r#{EpI5H%?mL&T)*D1XK{Vay{+9LLGc&69 z*n&*K(5F+ZHH4jrldt}o8|TiU-3WH=p%C1pzUvVC56w|3TJpP)PedIl7XiG{n1~KC zKJSgHBT9r{y!0~5r%!Qk-(D(4h&EQa6t#HrTVJM93g|=;p66|=rv7uJ9?ih^NG0dI zNRouH+87tkz0Ja3-s1D$`z~Y0pW*84hq0v!pYSWZ{f9r27=vfcAOiW)+>u9Q0AMuJ z^TV@-<<)imeCBmz|2`gn{&|)bmneVq2!#h8A}~tl<^r#td=2Y+s3s(d+fkp8<$wYX zSy{NO{fr+SJ@RO@B7XI=AF#i4i=~YYX8M!7@Xc>S;B)dPFQMnoa$P*uZ?Ez2b6?^c z-+2*|F0FP4Rlyh;%?0&6U#v0nrtkeWgn$UnDZaH#P48hTtZ??+8ujuxQY>)ct+Q-h z{2LUDY<9XldhkPBJ9Ua5{^-ZNdGLJm~_T_imh#0V--t(2IVtn%#FzCz*PAr`M)<6mKiOB)Sl4crbptarPV zjA61;=h27v^O4{xzx??tw7YStzE=Sm#jAqphd;-7{fg+3i7^&qEg}YE4bu~KK0YW~s;TasI+ZN*Pl!TyZ8(kr`#GYCkXX=YkkZ zDJU>~-~cOOo1^tA9c#GJis%6C#BptFi%&iA9O1-c{ORIdDqe@RTMM{cIi~!g2%6)8 zj3#%;g?x&1S}i{P*dr`e>(B~$WOj-NcTKRT6wqumnEmYIJp09`IdS3{j(zp}T-lhV z*=VMfFr-v^Nw_db!l82PJ)Am>7_4XUt%q|CYb|fQ`4+GJ^cDQg4U7n8a*EHKIL;T2 zKaCZL;~3w!EZ$nD9flk@un*@HBL;^{%g~znK^2U)WPlMv5cq^)i_OhVZm+Jfy1L5R z>MHG4NTU<+_e<|mDi%31cbKW#7=@s~Z}c9*eQohOfd4nHtM4l2S>N0e=Ya}MA4nVPaI?8&N{yD zrKzLPjbmaJw(d3viUq{@?B25%-&%|a;E3axYHgg~{Py=OFRyU;@ByN3yhBGMx-AM> z+kIzallK=EQ5^MJjZ&#dAqc<-m*1@`XQ&E+fq=H}uu&1T5* zt=qIa5pj?83Vwhe_}qW=0j8%X>Be!tIq9c;{xBM@0>vc>p6`*w2~pIg6Lko~7GX2w zZlgh?*<@>T3u7#`+8EVpg=)1*rBbF`DiIU{0?)@-LpO=BVj(@4)c1Tb#-vHOQ?p2q zc%mo5$$;Mk*&Kn_dm?-C-f3sw$`^UTtj5-a~9)xoFo?+hNUXHk&UGt<8J2Bl`bCCXZvsIZ?D_J+E2Piggfy8r+H M07*qoM6N<$f|;qn+5i9m literal 0 HcmV?d00001 From c54934ebeee9c3536cce5344ff094e1ee660376c Mon Sep 17 00:00:00 2001 From: pgotta <168211683+pgotta@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:21:08 -0400 Subject: [PATCH 08/12] Use committed icon assets in finalization test --- .github/workflows/finalize-shutdown-icon.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/finalize-shutdown-icon.yml b/.github/workflows/finalize-shutdown-icon.yml index 341ce22..1df8b18 100644 --- a/.github/workflows/finalize-shutdown-icon.yml +++ b/.github/workflows/finalize-shutdown-icon.yml @@ -21,13 +21,6 @@ jobs: with: python-version: "3.12" - - name: Restore Parroty icon payload - run: >- - python -c "import base64,pathlib; - data=base64.b64decode(pathlib.Path('tools/parroty_icon.b64').read_text(encoding='utf-8').strip(),validate=True); - pathlib.Path('parroty.ico').write_bytes(data); - print('Restored parroty.ico:',len(data),'bytes')" - - name: Apply confirmed local changes id: apply continue-on-error: true @@ -124,6 +117,7 @@ jobs: if: steps.apply.outcome == 'success' shell: pwsh run: | + if (-not (Test-Path parroty.ico)) { throw "Root Parroty icon missing" } if (-not (Test-Path app/static/parroty.ico)) { throw "Static Parroty icon missing" } $rootHash = (Get-FileHash parroty.ico -Algorithm SHA256).Hash $staticHash = (Get-FileHash app/static/parroty.ico -Algorithm SHA256).Hash From e65849666af76172587a16b38827022e79cacf39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:21:52 +0000 Subject: [PATCH 09/12] Finalize process-tree shutdown and window icon --- BUILD.md | 15 +++++---- Quick Start Readme.txt | 37 +++++++++++++++------- README.md | 15 +++++---- RELEASE_NOTES.md | 12 ++++--- app/templates/index.html | 5 ++- parroty_window.ps1 | 67 ++++++++++++++++++++++++++++++++++++++-- 6 files changed, 119 insertions(+), 32 deletions(-) diff --git a/BUILD.md b/BUILD.md index 6a8008d..d02db27 100644 --- a/BUILD.md +++ b/BUILD.md @@ -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`. @@ -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 diff --git a/Quick Start Readme.txt b/Quick Start Readme.txt index 07cc6d6..4c754d3 100644 --- a/Quick Start Readme.txt +++ b/Quick Start Readme.txt @@ -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 @@ -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 @@ -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. diff --git a/README.md b/README.md index 1a308fd..93852cb 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7a7eb7e..331d6d3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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. diff --git a/app/templates/index.html b/app/templates/index.html index c12f649..dac98c3 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -4,6 +4,9 @@ Parroty — EPUB to Audiobook + + + @@ -156,7 +159,7 @@

Choose the voice