speaksy 0.1.0__py3-none-any.whl
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.
- speaksy/__init__.py +3 -0
- speaksy/__main__.py +6 -0
- speaksy/cli.py +307 -0
- speaksy/config.py +157 -0
- speaksy/core.py +540 -0
- speaksy/runner.py +31 -0
- speaksy/service.py +205 -0
- speaksy/setup_wizard.py +216 -0
- speaksy-0.1.0.dist-info/METADATA +246 -0
- speaksy-0.1.0.dist-info/RECORD +13 -0
- speaksy-0.1.0.dist-info/WHEEL +4 -0
- speaksy-0.1.0.dist-info/entry_points.txt +2 -0
- speaksy-0.1.0.dist-info/licenses/LICENSE +21 -0
speaksy/service.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Systemd service management for Speaksy."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from speaksy.config import CONFIG_DIR, ENV_FILE
|
|
11
|
+
|
|
12
|
+
SYSTEMD_USER_DIR = Path.home() / ".config" / "systemd" / "user"
|
|
13
|
+
SERVICE_FILE = SYSTEMD_USER_DIR / "speaksy.service"
|
|
14
|
+
|
|
15
|
+
SERVICE_TEMPLATE = """[Unit]
|
|
16
|
+
Description=Speaksy - Voice Typing for Linux
|
|
17
|
+
After=graphical-session.target
|
|
18
|
+
PartOf=graphical-session.target
|
|
19
|
+
|
|
20
|
+
[Service]
|
|
21
|
+
Type=simple
|
|
22
|
+
ExecStart={python_path} -m speaksy.runner
|
|
23
|
+
Restart=on-failure
|
|
24
|
+
RestartSec=5
|
|
25
|
+
EnvironmentFile={env_file}
|
|
26
|
+
|
|
27
|
+
[Install]
|
|
28
|
+
WantedBy=graphical-session.target
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_python_path() -> str:
|
|
33
|
+
"""Get the path to the current Python interpreter."""
|
|
34
|
+
return sys.executable
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def install_service() -> bool:
|
|
38
|
+
"""Install the systemd user service."""
|
|
39
|
+
try:
|
|
40
|
+
SYSTEMD_USER_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
service_content = SERVICE_TEMPLATE.format(
|
|
43
|
+
python_path=get_python_path(),
|
|
44
|
+
env_file=ENV_FILE,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
with open(SERVICE_FILE, "w") as f:
|
|
48
|
+
f.write(service_content)
|
|
49
|
+
|
|
50
|
+
# Reload systemd
|
|
51
|
+
subprocess.run(
|
|
52
|
+
["systemctl", "--user", "daemon-reload"],
|
|
53
|
+
check=True,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Enable the service
|
|
58
|
+
subprocess.run(
|
|
59
|
+
["systemctl", "--user", "enable", "speaksy.service"],
|
|
60
|
+
check=True,
|
|
61
|
+
capture_output=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return True
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def uninstall_service() -> bool:
|
|
70
|
+
"""Uninstall the systemd user service."""
|
|
71
|
+
try:
|
|
72
|
+
stop_service()
|
|
73
|
+
|
|
74
|
+
subprocess.run(
|
|
75
|
+
["systemctl", "--user", "disable", "speaksy.service"],
|
|
76
|
+
capture_output=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if SERVICE_FILE.exists():
|
|
80
|
+
SERVICE_FILE.unlink()
|
|
81
|
+
|
|
82
|
+
subprocess.run(
|
|
83
|
+
["systemctl", "--user", "daemon-reload"],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
except Exception:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def start_service() -> bool:
|
|
93
|
+
"""Start the speaksy service."""
|
|
94
|
+
try:
|
|
95
|
+
result = subprocess.run(
|
|
96
|
+
["systemctl", "--user", "start", "speaksy.service"],
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
)
|
|
100
|
+
return result.returncode == 0
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def stop_service() -> bool:
|
|
106
|
+
"""Stop the speaksy service."""
|
|
107
|
+
try:
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
["systemctl", "--user", "stop", "speaksy.service"],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
)
|
|
113
|
+
return result.returncode == 0
|
|
114
|
+
except Exception:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def restart_service() -> bool:
|
|
119
|
+
"""Restart the speaksy service."""
|
|
120
|
+
try:
|
|
121
|
+
result = subprocess.run(
|
|
122
|
+
["systemctl", "--user", "restart", "speaksy.service"],
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
)
|
|
126
|
+
return result.returncode == 0
|
|
127
|
+
except Exception:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def is_running() -> bool:
|
|
132
|
+
"""Check if the speaksy service is running."""
|
|
133
|
+
try:
|
|
134
|
+
result = subprocess.run(
|
|
135
|
+
["systemctl", "--user", "is-active", "speaksy.service"],
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
)
|
|
139
|
+
return result.stdout.strip() == "active"
|
|
140
|
+
except Exception:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_installed() -> bool:
|
|
145
|
+
"""Check if the service is installed."""
|
|
146
|
+
return SERVICE_FILE.exists()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_uptime() -> str:
|
|
150
|
+
"""Get how long the service has been running."""
|
|
151
|
+
try:
|
|
152
|
+
result = subprocess.run(
|
|
153
|
+
[
|
|
154
|
+
"systemctl", "--user", "show", "speaksy.service",
|
|
155
|
+
"--property=ActiveEnterTimestamp",
|
|
156
|
+
],
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
)
|
|
160
|
+
if result.returncode == 0:
|
|
161
|
+
line = result.stdout.strip()
|
|
162
|
+
if "=" in line:
|
|
163
|
+
timestamp_str = line.split("=", 1)[1].strip()
|
|
164
|
+
if timestamp_str:
|
|
165
|
+
# Parse timestamp and calculate uptime
|
|
166
|
+
try:
|
|
167
|
+
# Format: "Thu 2026-02-05 16:23:51 CST"
|
|
168
|
+
from dateutil import parser
|
|
169
|
+
start_time = parser.parse(timestamp_str)
|
|
170
|
+
delta = datetime.now(start_time.tzinfo) - start_time
|
|
171
|
+
hours, remainder = divmod(int(delta.total_seconds()), 3600)
|
|
172
|
+
minutes, _ = divmod(remainder, 60)
|
|
173
|
+
if hours > 0:
|
|
174
|
+
return f"{hours}h {minutes}m"
|
|
175
|
+
return f"{minutes}m"
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
return "unknown"
|
|
179
|
+
except Exception:
|
|
180
|
+
return "unknown"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_logs(lines: int = 20) -> str:
|
|
184
|
+
"""Get recent service logs."""
|
|
185
|
+
try:
|
|
186
|
+
result = subprocess.run(
|
|
187
|
+
[
|
|
188
|
+
"journalctl", "--user", "-u", "speaksy.service",
|
|
189
|
+
"-n", str(lines), "--no-pager",
|
|
190
|
+
],
|
|
191
|
+
capture_output=True,
|
|
192
|
+
text=True,
|
|
193
|
+
)
|
|
194
|
+
return result.stdout if result.returncode == 0 else "No logs available"
|
|
195
|
+
except Exception:
|
|
196
|
+
return "Unable to fetch logs"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_status() -> dict:
|
|
200
|
+
"""Get comprehensive service status."""
|
|
201
|
+
return {
|
|
202
|
+
"installed": is_installed(),
|
|
203
|
+
"running": is_running(),
|
|
204
|
+
"uptime": get_uptime() if is_running() else None,
|
|
205
|
+
}
|
speaksy/setup_wizard.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Interactive setup wizard for Speaksy."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
10
|
+
|
|
11
|
+
from speaksy import config
|
|
12
|
+
from speaksy import service
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_system_deps() -> dict:
|
|
18
|
+
"""Check if required system dependencies are installed."""
|
|
19
|
+
deps = {
|
|
20
|
+
"xclip": shutil.which("xclip") is not None,
|
|
21
|
+
"xdotool": shutil.which("xdotool") is not None,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Check audio
|
|
25
|
+
try:
|
|
26
|
+
import sounddevice as sd
|
|
27
|
+
devices = sd.query_devices()
|
|
28
|
+
deps["audio"] = any(d.get("max_input_channels", 0) > 0 for d in devices)
|
|
29
|
+
except Exception:
|
|
30
|
+
deps["audio"] = False
|
|
31
|
+
|
|
32
|
+
return deps
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def install_missing_deps(missing: list) -> bool:
|
|
36
|
+
"""Attempt to install missing dependencies."""
|
|
37
|
+
console.print("\n[yellow]trying to install missing deps...[/yellow]")
|
|
38
|
+
|
|
39
|
+
apt_packages = []
|
|
40
|
+
if "xclip" in missing:
|
|
41
|
+
apt_packages.append("xclip")
|
|
42
|
+
if "xdotool" in missing:
|
|
43
|
+
apt_packages.append("xdotool")
|
|
44
|
+
|
|
45
|
+
if apt_packages:
|
|
46
|
+
try:
|
|
47
|
+
cmd = ["sudo", "apt", "install", "-y"] + apt_packages
|
|
48
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
49
|
+
if result.returncode != 0:
|
|
50
|
+
console.print("[red]failed to install. try manually:[/red]")
|
|
51
|
+
console.print(f"[dim]sudo apt install {' '.join(apt_packages)}[/dim]")
|
|
52
|
+
return False
|
|
53
|
+
except Exception:
|
|
54
|
+
console.print("[red]couldn't run apt. install manually:[/red]")
|
|
55
|
+
console.print(f"[dim]sudo apt install {' '.join(apt_packages)}[/dim]")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_api_key(api_key: str) -> tuple:
|
|
62
|
+
"""Validate the Groq API key by making a test request."""
|
|
63
|
+
try:
|
|
64
|
+
resp = httpx.get(
|
|
65
|
+
"https://api.groq.com/openai/v1/models",
|
|
66
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
67
|
+
timeout=10.0,
|
|
68
|
+
)
|
|
69
|
+
if resp.status_code == 200:
|
|
70
|
+
return True, None
|
|
71
|
+
elif resp.status_code == 401:
|
|
72
|
+
return False, "invalid_api_key"
|
|
73
|
+
else:
|
|
74
|
+
return False, f"api_error_{resp.status_code}"
|
|
75
|
+
except httpx.TimeoutException:
|
|
76
|
+
return False, "timeout"
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return False, str(e)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run_setup():
|
|
82
|
+
"""Run the interactive setup wizard."""
|
|
83
|
+
console.print()
|
|
84
|
+
console.print("[bold cyan]aight let's get you set up real quick[/bold cyan]")
|
|
85
|
+
console.print()
|
|
86
|
+
console.print("[dim]" + "━" * 40 + "[/dim]")
|
|
87
|
+
console.print()
|
|
88
|
+
|
|
89
|
+
# Check system deps
|
|
90
|
+
console.print("[bold]checking system deps...[/bold]")
|
|
91
|
+
deps = check_system_deps()
|
|
92
|
+
|
|
93
|
+
all_good = True
|
|
94
|
+
for dep, found in deps.items():
|
|
95
|
+
if found:
|
|
96
|
+
console.print(f" [green]├─ {dep}: found ✓[/green]")
|
|
97
|
+
else:
|
|
98
|
+
console.print(f" [red]├─ {dep}: missing ✗[/red]")
|
|
99
|
+
all_good = False
|
|
100
|
+
|
|
101
|
+
if not all_good:
|
|
102
|
+
missing = [d for d, found in deps.items() if not found]
|
|
103
|
+
if "audio" in missing:
|
|
104
|
+
console.print("\n[red]no audio input detected. check your mic![/red]")
|
|
105
|
+
missing.remove("audio")
|
|
106
|
+
|
|
107
|
+
if missing:
|
|
108
|
+
if Confirm.ask("\n[yellow]want me to try installing missing deps?[/yellow]"):
|
|
109
|
+
if not install_missing_deps(missing):
|
|
110
|
+
return False
|
|
111
|
+
# Recheck
|
|
112
|
+
deps = check_system_deps()
|
|
113
|
+
if not all(deps.values()):
|
|
114
|
+
console.print("\n[red]still missing deps. fix and try again[/red]")
|
|
115
|
+
return False
|
|
116
|
+
else:
|
|
117
|
+
console.print("\n[yellow]install them manually and run /setup again[/yellow]")
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
console.print()
|
|
121
|
+
|
|
122
|
+
# Get API key
|
|
123
|
+
console.print("[bold]drop your Groq API key[/bold]")
|
|
124
|
+
console.print("[dim](get one free at console.groq.com/keys)[/dim]")
|
|
125
|
+
console.print()
|
|
126
|
+
|
|
127
|
+
while True:
|
|
128
|
+
api_key = Prompt.ask(" [cyan]key[/cyan]", password=True)
|
|
129
|
+
|
|
130
|
+
if not api_key:
|
|
131
|
+
console.print(" [red]need a key to continue[/red]")
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
if not api_key.startswith("gsk_"):
|
|
135
|
+
console.print(" [yellow]hmm that doesn't look like a groq key[/yellow]")
|
|
136
|
+
console.print(" [dim]should start with gsk_[/dim]")
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
console.print(" [dim]validating...[/dim]", end=" ")
|
|
140
|
+
valid, error = validate_api_key(api_key)
|
|
141
|
+
|
|
142
|
+
if valid:
|
|
143
|
+
console.print("[green]we're in ✓[/green]")
|
|
144
|
+
break
|
|
145
|
+
else:
|
|
146
|
+
console.print(f"[red]nah that ain't it[/red]")
|
|
147
|
+
if error == "invalid_api_key":
|
|
148
|
+
console.print(" [dim]double check your key and try again[/dim]")
|
|
149
|
+
else:
|
|
150
|
+
console.print(f" [dim]error: {error}[/dim]")
|
|
151
|
+
|
|
152
|
+
# Save API key
|
|
153
|
+
config.save_api_key(api_key)
|
|
154
|
+
console.print()
|
|
155
|
+
|
|
156
|
+
# Hotkey customization
|
|
157
|
+
if Confirm.ask("[bold]wanna customize hotkeys?[/bold]", default=False):
|
|
158
|
+
console.print()
|
|
159
|
+
console.print("[dim]examples: Key.ctrl_r, Key.f8, Key.alt_l[/dim]")
|
|
160
|
+
|
|
161
|
+
current_ptt, current_toggle = config.get_hotkeys()
|
|
162
|
+
|
|
163
|
+
ptt = Prompt.ask(
|
|
164
|
+
f" [cyan]push-to-talk[/cyan] [dim](default: {current_ptt})[/dim]",
|
|
165
|
+
default=current_ptt,
|
|
166
|
+
)
|
|
167
|
+
toggle = Prompt.ask(
|
|
168
|
+
f" [cyan]toggle mode[/cyan] [dim](default: {current_toggle})[/dim]",
|
|
169
|
+
default=current_toggle,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
config.set_hotkeys(ptt, toggle)
|
|
173
|
+
console.print(" [green]locked in ✓[/green]")
|
|
174
|
+
else:
|
|
175
|
+
# Save default config
|
|
176
|
+
cfg = config.load_config()
|
|
177
|
+
config.save_config(cfg)
|
|
178
|
+
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
# Install service
|
|
182
|
+
console.print("[bold]installing service...[/bold]")
|
|
183
|
+
if service.install_service():
|
|
184
|
+
console.print(" [green]└─ auto-start on login: enabled ✓[/green]")
|
|
185
|
+
else:
|
|
186
|
+
console.print(" [red]└─ failed to install service[/red]")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
# Start service
|
|
190
|
+
console.print()
|
|
191
|
+
console.print("[dim]starting speaksy...[/dim]")
|
|
192
|
+
if service.start_service():
|
|
193
|
+
console.print("[green]service started ✓[/green]")
|
|
194
|
+
else:
|
|
195
|
+
console.print("[red]failed to start service[/red]")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
console.print()
|
|
199
|
+
console.print("[dim]" + "━" * 40 + "[/dim]")
|
|
200
|
+
console.print()
|
|
201
|
+
|
|
202
|
+
# Success message
|
|
203
|
+
ptt, toggle = config.get_hotkeys()
|
|
204
|
+
ptt_display = ptt.replace("Key.", "").replace("_", " ").title()
|
|
205
|
+
toggle_display = toggle.replace("Key.", "").upper()
|
|
206
|
+
|
|
207
|
+
console.print("[bold green]you're all set fam![/bold green]")
|
|
208
|
+
console.print()
|
|
209
|
+
console.print(f" [cyan]hold {ptt_display}[/cyan] = push-to-talk")
|
|
210
|
+
console.print(f" [cyan]tap {toggle_display}[/cyan] = toggle on/off")
|
|
211
|
+
console.print()
|
|
212
|
+
console.print("[dim]speaksy is now running in the background[/dim]")
|
|
213
|
+
console.print("[dim]just start talking wherever you type ✨[/dim]")
|
|
214
|
+
console.print()
|
|
215
|
+
|
|
216
|
+
return True
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: speaksy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Voice typing for Linux. Talk it. Type it. Ship it.
|
|
5
|
+
Project-URL: Homepage, https://github.com/oneKn8/speaksy
|
|
6
|
+
Project-URL: Repository, https://github.com/oneKn8/speaksy
|
|
7
|
+
Project-URL: Issues, https://github.com/oneKn8/speaksy/issues
|
|
8
|
+
Author: oneknight
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: dictation,linux,speech-to-text,typing,voice,whisper
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
23
|
+
Classifier: Topic :: Text Processing
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: faster-whisper>=1.1.0
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Requires-Dist: numpy>=1.24.0
|
|
28
|
+
Requires-Dist: pillow>=10.0.0
|
|
29
|
+
Requires-Dist: pynput>=1.7.0
|
|
30
|
+
Requires-Dist: pystray>=0.19.0
|
|
31
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
32
|
+
Requires-Dist: pyyaml>=6.0
|
|
33
|
+
Requires-Dist: rich>=13.0.0
|
|
34
|
+
Requires-Dist: sounddevice>=0.5.0
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="https://img.shields.io/badge/speaksy-voice%20typing-blueviolet?style=for-the-badge&logo=microphone" alt="speaksy">
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<h1 align="center">speaksy</h1>
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<strong>talk it. type it. ship it.</strong>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<a href="https://github.com/oneKn8/speaksy/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
|
|
49
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python"></a>
|
|
50
|
+
<a href="https://github.com/oneKn8/speaksy"><img src="https://img.shields.io/badge/platform-Linux-orange.svg" alt="Platform"></a>
|
|
51
|
+
<a href="https://console.groq.com"><img src="https://img.shields.io/badge/powered%20by-Groq-ff6600.svg" alt="Groq"></a>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<p align="center">
|
|
55
|
+
<em>Voice typing for Linux that actually works.<br>Hold a key, speak, release — your words appear wherever you're typing.</em>
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Demo
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
$ speaksy
|
|
64
|
+
|
|
65
|
+
╭────────────────────────────────────────╮
|
|
66
|
+
│ SPEAKSY │
|
|
67
|
+
│ talk it. type it. ship it. │
|
|
68
|
+
╰────────────────────────────────────────╯
|
|
69
|
+
|
|
70
|
+
Status: vibing
|
|
71
|
+
Hotkeys: Right Ctrl (hold) | F8 (toggle)
|
|
72
|
+
|
|
73
|
+
speaksy> _
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
<!-- TODO: Add demo GIF here -->
|
|
77
|
+
<!--  -->
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Install
|
|
85
|
+
pipx install speaksy
|
|
86
|
+
|
|
87
|
+
# Run (interactive setup on first launch)
|
|
88
|
+
speaksy
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That's it. 30 seconds to voice typing.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Features
|
|
96
|
+
|
|
97
|
+
| | Feature | Description |
|
|
98
|
+
|---|---------|-------------|
|
|
99
|
+
| **Speed** | < 1 second latency | Groq's Whisper API is blazing fast |
|
|
100
|
+
| **Smart** | AI text cleanup | Fixes grammar, removes "um", "uh", "like" |
|
|
101
|
+
| **Free** | No credit card | Groq's free tier is generous |
|
|
102
|
+
| **Offline** | Local fallback | Works without internet via faster-whisper |
|
|
103
|
+
| **Private** | Privacy mode | Keep voice 100% on your machine |
|
|
104
|
+
| **Auto** | Runs on login | Always ready when you are |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## How It Works
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
112
|
+
│ Hold Key │ -> │ Speak │ -> │ Release │ -> │ Text Appears│
|
|
113
|
+
│ (Right Ctrl) │ naturally │ │ key │ │ at cursor │
|
|
114
|
+
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
|
115
|
+
|
|
|
116
|
+
v
|
|
117
|
+
┌─────────────────┐
|
|
118
|
+
│ Groq Whisper │
|
|
119
|
+
│ + LLM cleanup │
|
|
120
|
+
└─────────────────┘
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
1. Press hotkey (Right Ctrl = hold, F8 = toggle)
|
|
124
|
+
2. Speak naturally
|
|
125
|
+
3. Release — text appears in < 1 second
|
|
126
|
+
|
|
127
|
+
Works everywhere: browser, terminal, IDE, Slack, Discord, anywhere you type.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
Run `speaksy` to open the interactive CLI:
|
|
134
|
+
|
|
135
|
+
| Command | Description |
|
|
136
|
+
|---------|-------------|
|
|
137
|
+
| `/setup` | Configure API key & hotkeys |
|
|
138
|
+
| `/start` | Start voice typing |
|
|
139
|
+
| `/stop` | Take a break |
|
|
140
|
+
| `/status` | Check the vibe |
|
|
141
|
+
| `/logs` | View receipts |
|
|
142
|
+
| `/config` | Tweak settings |
|
|
143
|
+
| `/help` | Get backup |
|
|
144
|
+
| `/quit` | Peace out |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Requirements
|
|
149
|
+
|
|
150
|
+
- **OS:** Linux (X11 or XWayland)
|
|
151
|
+
- **Python:** 3.10+
|
|
152
|
+
- **API Key:** Free from [console.groq.com](https://console.groq.com)
|
|
153
|
+
|
|
154
|
+
System dependencies (auto-installed during setup):
|
|
155
|
+
```bash
|
|
156
|
+
sudo apt install xclip xdotool
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Privacy Mode
|
|
162
|
+
|
|
163
|
+
By default, audio goes to Groq for fast transcription. Want to keep it local?
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
speaksy> /config
|
|
167
|
+
# Select "Privacy mode" -> "local"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Local mode uses [faster-whisper](https://github.com/SYSTRAN/faster-whisper) on your CPU. Slower (~3-5s) but your voice never leaves your machine.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Troubleshooting
|
|
175
|
+
|
|
176
|
+
<details>
|
|
177
|
+
<summary><strong>No audio input detected</strong></summary>
|
|
178
|
+
|
|
179
|
+
- Check your mic is connected
|
|
180
|
+
- Run `arecord -l` to list audio devices
|
|
181
|
+
</details>
|
|
182
|
+
|
|
183
|
+
<details>
|
|
184
|
+
<summary><strong>Text not appearing</strong></summary>
|
|
185
|
+
|
|
186
|
+
- Install dependencies: `sudo apt install xclip xdotool`
|
|
187
|
+
- Some pure Wayland apps may not work with xdotool
|
|
188
|
+
</details>
|
|
189
|
+
|
|
190
|
+
<details>
|
|
191
|
+
<summary><strong>Service won't start</strong></summary>
|
|
192
|
+
|
|
193
|
+
- Check logs: run `speaksy` then `/logs`
|
|
194
|
+
- Verify API key at console.groq.com
|
|
195
|
+
</details>
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Uninstall
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Stop service
|
|
203
|
+
speaksy
|
|
204
|
+
# > /stop
|
|
205
|
+
# > /quit
|
|
206
|
+
|
|
207
|
+
# Remove package
|
|
208
|
+
pipx uninstall speaksy
|
|
209
|
+
|
|
210
|
+
# Remove config (optional)
|
|
211
|
+
rm -rf ~/.config/speaksy
|
|
212
|
+
rm ~/.config/systemd/user/speaksy.service
|
|
213
|
+
systemctl --user daemon-reload
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Tech Stack
|
|
219
|
+
|
|
220
|
+
- **STT:** [Groq Whisper API](https://groq.com) / [faster-whisper](https://github.com/SYSTRAN/faster-whisper)
|
|
221
|
+
- **LLM:** Llama 3.1 8B (via Groq) for text cleanup
|
|
222
|
+
- **Audio:** [sounddevice](https://python-sounddevice.readthedocs.io/)
|
|
223
|
+
- **Hotkeys:** [pynput](https://pynput.readthedocs.io/)
|
|
224
|
+
- **CLI:** [Rich](https://rich.readthedocs.io/)
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Contributing
|
|
229
|
+
|
|
230
|
+
PRs and issues welcome!
|
|
231
|
+
|
|
232
|
+
<a href="https://github.com/oneKn8/speaksy/issues">Report Bug</a>
|
|
233
|
+
·
|
|
234
|
+
<a href="https://github.com/oneKn8/speaksy/issues">Request Feature</a>
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT - do whatever you want with it.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
<p align="center">
|
|
245
|
+
<sub>Built with caffeine and voice commands</sub>
|
|
246
|
+
</p>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
speaksy/__init__.py,sha256=eG5kdi4U1tayyWwhBUByiHrdFcZpCJ6bninHJclA4VU,63
|
|
2
|
+
speaksy/__main__.py,sha256=Y5c4dtERDNv0x1C-HcP1nauSAbssfyuP7MlyN8l1M28,110
|
|
3
|
+
speaksy/cli.py,sha256=Fd-g5g11RMFyKgpdYGtvQAuYIVY6BcL7nyOwFa5GMHM,10225
|
|
4
|
+
speaksy/config.py,sha256=bGiATdV79E27pu7TQTm2VxDDdAKYdzjQrrIbu5GCz2o,4163
|
|
5
|
+
speaksy/core.py,sha256=xkE0KtS3paGzZSnc481R47bECnns3_wSXkysSW66Uzs,16987
|
|
6
|
+
speaksy/runner.py,sha256=l5aiczErHVLBc3Ir3XIfqxZUAiq4tN6T8OCVqTWwnEU,643
|
|
7
|
+
speaksy/service.py,sha256=EwQs3yB3Ys-WwvP0UI2K_U5_zJqJCJLkK29_MTTcVTE,5449
|
|
8
|
+
speaksy/setup_wizard.py,sha256=-Glu74R-ZKL097Ji8vOmEDC7salaSk9kueONxWs0t4c,7033
|
|
9
|
+
speaksy-0.1.0.dist-info/METADATA,sha256=JwjfdFK_CSimqUGGZC0VwJxW1lvWmrCENw9B56cFCBw,6926
|
|
10
|
+
speaksy-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
speaksy-0.1.0.dist-info/entry_points.txt,sha256=LFch1KPmgD4hHNVXY_DSJ24fPnB8GaVKF4aKUrqDjzs,45
|
|
12
|
+
speaksy-0.1.0.dist-info/licenses/LICENSE,sha256=KgmDIQPh17s8aGNha9ebeUXZHi533ew6VyCLcY7IJE4,1066
|
|
13
|
+
speaksy-0.1.0.dist-info/RECORD,,
|