Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions extras/windows/Install-TTS_ka-Windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<#
.SYNOPSIS
One-step Windows setup for TTS_ka: context menu + global hotkeys, with
prerequisite checks so you know what (if anything) is missing.

.DESCRIPTION
This is a thin orchestrator over the two existing installers:
- extras\windows\context_menu\Install-TTS_ka-ContextMenu.ps1 (right-click "Read with TTS_ka")
- extras\autohotkey\Install-TTS_ka-Hotkeys.ps1 (Alt+E/R/X global hotkeys)

It first verifies that TTS_ka is runnable, then checks for AutoHotkey v2
(needed only for the hotkeys + in-app selection menu), then runs both
installers and prints a "what you can do now" summary.

Run from the repository root:
powershell -ExecutionPolicy Bypass -File .\extras\windows\Install-TTS_ka-Windows.ps1

.PARAMETER PythonPath
Path to python.exe, or "python" / "py" if on PATH. Passed to the context-menu installer.

.PARAMETER Languages
Languages for the context menu (default: en, ru, ka, ka-m). Passed through.

.PARAMETER SkipHotkeys
Install only the context menu; do not touch the AutoHotkey startup script.

.PARAMETER SkipContextMenu
Install only the hotkeys; do not register the right-click menu.

.PARAMETER Uninstall
Remove both the context menu and the hotkeys startup script.

.PARAMETER WhatIf
Print actions only; do not modify the registry or Startup folder.
#>
param(
[string] $PythonPath = "",
[string[]] $Languages = @(),
[switch] $SkipHotkeys,
[switch] $SkipContextMenu,
[switch] $Uninstall,
[switch] $WhatIf
)

$ErrorActionPreference = "Stop"

$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
$ContextPs1 = Join-Path $PSScriptRoot "context_menu\Install-TTS_ka-ContextMenu.ps1"
$HotkeysPs1 = Join-Path $RepoRoot "extras\autohotkey\Install-TTS_ka-Hotkeys.ps1"

foreach ($p in @($ContextPs1, $HotkeysPs1)) {
if (-not (Test-Path -LiteralPath $p)) {
Write-Error "Missing installer: $p (run this from the TTS_ka repo)."
}
}

function Test-TTSka {
# True if `tts-ka` console script or `python -m TTS_ka` resolves.
if (Get-Command "TTS_ka" -ErrorAction SilentlyContinue) { return $true }
foreach ($py in @("python", "py")) {
$c = Get-Command $py -ErrorAction SilentlyContinue
if (-not $c) { continue }
try {
& $c.Source -c "import TTS_ka" 2>$null
if ($LASTEXITCODE -eq 0) { return $true }
} catch { }
}
return $false
}

function Test-AutoHotkeyV2 {
$names = @("AutoHotkey64.exe", "AutoHotkey32.exe")
$roots = @(
"${env:ProgramFiles}\AutoHotkey\v2",
"${env:ProgramFiles(x86)}\AutoHotkey\v2",
"${env:LocalAppData}\Programs\AutoHotkey\v2"
)
foreach ($r in $roots) {
foreach ($n in $names) {
if (Test-Path -LiteralPath (Join-Path $r $n)) { return $true }
}
}
return [bool](Get-Command "AutoHotkey64.exe" -ErrorAction SilentlyContinue)
}

Write-Host "TTS_ka Windows setup"
Write-Host ("=" * 40)

# --- Uninstall path -------------------------------------------------------
if ($Uninstall) {
if (-not $SkipContextMenu) {
& $ContextPs1 -Uninstall -WhatIf:$WhatIf
}
if (-not $SkipHotkeys) {
& $HotkeysPs1 -Uninstall -WhatIf:$WhatIf
}
Write-Host ""
Write-Host "Uninstall complete. (AutoHotkey itself is left installed.)"
exit 0
}

# --- Prerequisite: TTS_ka must be runnable --------------------------------
if (-not (Test-TTSka)) {
Write-Warning "TTS_ka does not appear to be installed / on PATH."
Write-Host " Install it first, then re-run this script:"
Write-Host " pip install TTS_ka (add [hotkeys] for global hotkeys)"
Write-Host " Verify with: TTS_ka --version (or: python -m TTS_ka --version)"
exit 1
}
Write-Host "[ok] TTS_ka is runnable."

# --- Prerequisite: AutoHotkey v2 (hotkeys only) ---------------------------
$haveAhk = Test-AutoHotkeyV2
if (-not $SkipHotkeys) {
if ($haveAhk) {
Write-Host "[ok] AutoHotkey v2 found."
} else {
Write-Warning "AutoHotkey v2 not found - hotkeys need it. Install with:"
Write-Host " winget install AutoHotkey.AutoHotkey"
Write-Host " (The right-click context menu works without AutoHotkey.)"
}
}

# --- Install context menu -------------------------------------------------
if (-not $SkipContextMenu) {
Write-Host ""
Write-Host "-> Registering context menu..."
$ctxArgs = @{ WhatIf = [bool]$WhatIf }
if ($PythonPath) { $ctxArgs["PythonPath"] = $PythonPath }
if ($Languages -and $Languages.Count -gt 0) { $ctxArgs["Languages"] = $Languages }
& $ContextPs1 @ctxArgs
}

# --- Install hotkeys (only if AHK present) --------------------------------
if (-not $SkipHotkeys -and $haveAhk) {
Write-Host ""
Write-Host "-> Installing global hotkeys..."
& $HotkeysPs1 -WhatIf:$WhatIf
}

# --- Summary --------------------------------------------------------------
Write-Host ""
Write-Host "What you can do now"
Write-Host ("-" * 40)
if (-not $SkipContextMenu) {
Write-Host " * Copy text (Ctrl+C), right-click empty space in Explorer/Desktop"
Write-Host " -> 'Read with TTS_ka' -> pick a language."
}
if (-not $SkipHotkeys -and $haveAhk) {
Write-Host " * Global hotkeys: Alt+E (English), Alt+R (Russian), Alt+X (Georgian)."
Write-Host " Apps key (or Ctrl+Alt+Right-click) opens a language menu anywhere."
}
Write-Host ""
Write-Host " Note: Windows cannot add items to the text-selection menu inside Chrome,"
Write-Host " Edge, or Word. Use the AutoHotkey Apps-key menu there instead."
Write-Host ""
Write-Host " Verify your audio setup: TTS_ka --doctor"
Write-Host " Uninstall everything: ...\Install-TTS_ka-Windows.ps1 -Uninstall"
18 changes: 15 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,16 @@ Tools exposed:

| Tool | Purpose |
|------|--------|
| `speak(text, lang?, voice?)` | One-shot: synthesize and play immediately |
| `speak(text, lang?, voice?, blocking?)` | One-shot: synthesize and play. `blocking=True` waits for the audio's full duration before returning so the agent can sequence speech; the return string echoes the resolved settings |
| `stream_open(lang?, voice?)` | Start a streaming session, returns `session_id` |
| `stream_append(session_id, text)` | Push text; speaks each complete sentence |
| `stream_close(session_id)` | Drain remaining buffer, end the session |
| `session_status(session_id)` | Inspect progress: total, pending synths, buffer preview |
| `session_status(session_id)` | Inspect progress: total, pending synths, `synths_failed` + `last_error`, buffer preview |
| `list_sessions()` | All active session IDs |
| `stop()` | Abort all playback and tear down sessions |
| `list_voices(lang?)` | Voice catalog as JSON |

Why streaming over single `speak` calls: the LLM can push tokens as it generates them. Each completed sentence is synthesized immediately, so the user hears audio with sub-second latency from the LLM's first word. `session_status` reports `synths_pending` so the agent knows when the queue is backed up.
Why streaming over single `speak` calls: the LLM can push tokens as it generates them. Each completed sentence is synthesized immediately, so the user hears audio with sub-second latency from the LLM's first word. `session_status` reports `synths_pending` (queue backed up) and `synths_failed` / `last_error` (a synthesis failed) so the agent can react.

### `--json`: machine-readable progress

Expand Down Expand Up @@ -285,6 +285,18 @@ Debian/Ubuntu may need Tk: `sudo apt install python3-tk`.

## Windows extras

### One-step setup (recommended)

```powershell
powershell -ExecutionPolicy Bypass -File .\extras\windows\Install-TTS_ka-Windows.ps1
```

Checks that TTS_ka is runnable and that AutoHotkey v2 is present (printing
`winget install AutoHotkey.AutoHotkey` if not), then installs the Explorer/Desktop
context menu and the global hotkeys, and prints a short "what you can do now" summary.
Flags: `-SkipHotkeys`, `-SkipContextMenu`, `-PythonPath`, `-Uninstall`, `-WhatIf`. The
individual installers below still work if you want finer control.

### Native global hotkeys (no AutoHotkey)

```bash
Expand Down
39 changes: 36 additions & 3 deletions src/TTS_ka/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ class DepRow:
name: str
ok: bool
detail: str
fix: str = ""


def _platform_fix(win: str, mac: str, linux: str) -> str:
"""Return the install command for the current platform."""
if sys.platform.startswith("win"):
return win
if sys.platform == "darwin":
return mac
return linux


_FFMPEG_FIX = _platform_fix(
win="winget install Gyan.FFmpeg (or: choco install ffmpeg / scoop install ffmpeg)",
mac="brew install ffmpeg",
linux="sudo apt install ffmpeg (or your distro's package manager)",
)

_PLAYER_FIX = _platform_fix(
win="winget install mpv (or install VLC)",
mac="brew install mpv",
linux="sudo apt install mpv (or vlc)",
)


def _check_module(spec: str, import_name: str) -> DepRow:
Expand All @@ -34,7 +57,8 @@ def check_ffmpeg() -> DepRow:
return DepRow(
"ffmpeg",
False,
"not on PATH - install ffmpeg (required for merging chunks / pydub MP3)",
"not on PATH - required for merging chunks / pydub MP3",
fix=_FFMPEG_FIX,
)
try:
r = subprocess.run(
Expand All @@ -61,6 +85,7 @@ def check_streaming_player() -> DepRow:
"streaming player",
False,
"none of vlc, mpv, ffplay, mplayer found - optional unless you use --stream",
fix=_PLAYER_FIX,
)


Expand All @@ -69,7 +94,8 @@ def check_soundfile() -> DepRow:
return DepRow(
"soundfile",
False,
"optional pip install soundfile - faster merges when available",
"optional - faster merges when available",
fix="pip install soundfile",
)
return _check_module("soundfile", "soundfile")

Expand All @@ -81,7 +107,8 @@ def check_uvloop() -> DepRow:
return DepRow(
"uvloop",
False,
"optional - pip install uvloop for faster asyncio on Linux/macOS",
"optional - faster asyncio on Linux/macOS",
fix="pip install uvloop",
)
return _check_module("uvloop", "uvloop")

Expand Down Expand Up @@ -113,9 +140,15 @@ def format_dep_report(rows: Optional[List[DepRow]] = None) -> str:
else:
flag = "!!"
lines.append(f" [{flag}] {r.name.ljust(w)} {r.detail}")
if not r.ok and r.fix:
lines.append(f" {' ' * (len(flag) + 4)}{' ' * w} fix: {r.fix}")
lines.append("")
lines.append("ffmpeg: required for long/chunked output and reliable MP3 handling.")
lines.append("streaming player: needed only for --stream (live playback while generating).")
critical = ("edge-tts", "pydub", "ffmpeg")
if all(r.ok for r in rows if r.name in critical):
lines.append("")
lines.append('All set — try: tts-ka "Hello world" -l en')
return "\n".join(lines)


Expand Down
17 changes: 11 additions & 6 deletions src/TTS_ka/fast_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,21 +540,26 @@ def _spawn_detached(argv: List[str]) -> bool:
return False


def play_audio(file_path: str) -> None:
"""Play an audio file using a platform-appropriate command (no shell)."""
def play_audio(file_path: str) -> bool:
"""Play an audio file using a platform-appropriate command (no shell).

Returns ``True`` when a player was launched, ``False`` when no player
could open the file so callers can tell the user instead of leaving them
in silence.
"""
try:
abs_path = os.path.abspath(file_path)
if sys.platform.startswith("win"):
os.startfile(abs_path)
return
return True
if sys.platform == "darwin":
_spawn_detached(["open", abs_path])
return
return _spawn_detached(["open", abs_path])
for player in ("mpv", "vlc", "xdg-open"):
if shutil.which(player) and _spawn_detached([player, abs_path]):
return
return True
except OSError:
pass
return False


async def cleanup_http() -> None:
Expand Down
Loading
Loading