@staff0rd/assist 0.751.0 → 0.753.0
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/README.md +5 -30
- package/allowed.cli-reads +0 -1
- package/allowed.cli-writes +0 -1
- package/claude/settings.json +1 -22
- package/claude/skills/review-ci/scripts/review.mjs +111 -111
- package/dist/allowed.cli-reads +0 -1
- package/dist/allowed.cli-writes +0 -1
- package/dist/commands/sessions/web/bundle.js +1 -1
- package/dist/index.js +1863 -2328
- package/package.json +1 -1
- package/claude/commands/handover.md +0 -65
- package/claude/commands/journal.md +0 -74
- package/claude/commands/recall.md +0 -55
- package/claude/commands/standup.md +0 -35
- package/claude/commands/voice-logs.md +0 -5
- package/claude/commands/voice-setup.md +0 -5
- package/claude/commands/voice-start.md +0 -5
- package/claude/commands/voice-status.md +0 -5
- package/claude/commands/voice-stop.md +0 -5
- package/dist/commands/voice/python/CLAUDE.md +0 -11
- package/dist/commands/voice/python/audio_capture.py +0 -52
- package/dist/commands/voice/python/check_mi.py +0 -21
- package/dist/commands/voice/python/dispatch.py +0 -14
- package/dist/commands/voice/python/keyboard.py +0 -73
- package/dist/commands/voice/python/list_devices.py +0 -20
- package/dist/commands/voice/python/logger.py +0 -38
- package/dist/commands/voice/python/pyproject.toml +0 -26
- package/dist/commands/voice/python/setup_models.py +0 -91
- package/dist/commands/voice/python/smart_turn.py +0 -63
- package/dist/commands/voice/python/stt.py +0 -54
- package/dist/commands/voice/python/uv.lock +0 -5946
- package/dist/commands/voice/python/vad.py +0 -50
- package/dist/commands/voice/python/voice_daemon.py +0 -528
- package/dist/commands/voice/python/wake_word.py +0 -26
package/package.json
CHANGED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Write a session handover note for the next conversation
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Write a concise handover note for the next session working on this repo, then save it to the backlog database. The note captures everything the next session needs to pick up where this one left off.
|
|
6
|
-
|
|
7
|
-
Handover notes are stored in the backlog DB (scoped by git origin), never on disk. `/handover` can be run any number of times — each run appends a new note.
|
|
8
|
-
|
|
9
|
-
## Step 1: Compose the note
|
|
10
|
-
|
|
11
|
-
Always operate on the current working directory. Compose the note body using the following sections, in this order, and omit none even if empty (use a single "—" placeholder line):
|
|
12
|
-
|
|
13
|
-
```markdown
|
|
14
|
-
# Handover
|
|
15
|
-
|
|
16
|
-
## Current Task
|
|
17
|
-
|
|
18
|
-
What is actively being worked on right now — one or two sentences. Include the backlog item ID or PR number if applicable.
|
|
19
|
-
|
|
20
|
-
## Just Done
|
|
21
|
-
|
|
22
|
-
The most recent things completed in this session. Bullet list. Include file paths where relevant.
|
|
23
|
-
|
|
24
|
-
## Next Steps
|
|
25
|
-
|
|
26
|
-
What the next session should do next, in order. Bullet list. Be concrete — name files, commands, or decisions.
|
|
27
|
-
|
|
28
|
-
## Key Files
|
|
29
|
-
|
|
30
|
-
Files that the next session will most likely need to read or edit. Bullet list of `path:line` references where useful.
|
|
31
|
-
|
|
32
|
-
## Open Decisions
|
|
33
|
-
|
|
34
|
-
Choices that have not yet been made, or were deferred. State the options briefly so the next session can resume the deliberation.
|
|
35
|
-
|
|
36
|
-
## Watch Out For
|
|
37
|
-
|
|
38
|
-
Non-obvious gotchas, in-flight refactors, broken tests, half-applied changes, or anything that could trip up the next session.
|
|
39
|
-
|
|
40
|
-
## Don't Do
|
|
41
|
-
|
|
42
|
-
Things that have been ruled out, attempted unsuccessfully, or that the user has explicitly asked not to do. Saves the next session from repeating dead ends.
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Step 2: Save the note
|
|
46
|
-
|
|
47
|
-
Author a one-line summary (under 100 chars) describing the note — this is what `/recall` shows when listing notes. Then save it by piping the composed note into `assist handover save` via stdin:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
assist handover save --summary "<one-line summary>" <<'HANDOVER_EOF'
|
|
51
|
-
# Handover
|
|
52
|
-
|
|
53
|
-
## Current Task
|
|
54
|
-
...
|
|
55
|
-
HANDOVER_EOF
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Use a quoted heredoc delimiter (`<<'HANDOVER_EOF'`) so nothing in the note body is expanded by the shell. The command resolves the repo's git origin and inserts a new row in the backlog DB.
|
|
59
|
-
|
|
60
|
-
## Guidelines
|
|
61
|
-
|
|
62
|
-
- Keep each section terse — bullets, not paragraphs. The next session will read this cold.
|
|
63
|
-
- Reference concrete file paths and line numbers wherever it helps orientation.
|
|
64
|
-
- Do not summarise context that is already obvious from the code or git history.
|
|
65
|
-
- Do not write `.assist/HANDOVER.md` or any other file — handovers live only in the backlog DB.
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Append a journal entry summarising recent work, decisions, and notable observations
|
|
3
|
-
allowed_args: "[optional focus or note]"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Append a structured journal entry to today's daily file. This should be fast — do not read existing journal files or attempt to deduplicate.
|
|
7
|
-
|
|
8
|
-
## Step 1: Determine the date and file
|
|
9
|
-
|
|
10
|
-
**IMPORTANT:** Do not rely on your own sense of the current date — it may be stale in long-running sessions. Always run `date +%Y-%m-%d` and `date +%H:%M` via Bash to get the actual current date and time.
|
|
11
|
-
|
|
12
|
-
The journal lives in `~/.claude/journal/`, one file per day, named `YYYY-MM-DD.md`.
|
|
13
|
-
|
|
14
|
-
If today's file doesn't exist, create it with a heading:
|
|
15
|
-
|
|
16
|
-
```markdown
|
|
17
|
-
# YYYY-MM-DD
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Step 2: Resolve project context
|
|
21
|
-
|
|
22
|
-
Determine the current project by matching the working directory against `~/.claude/skills/projects.json`. If not inside a registered project, use "general" as the project name.
|
|
23
|
-
|
|
24
|
-
## Step 3: Write the entry
|
|
25
|
-
|
|
26
|
-
Append an entry to the daily file. Each entry should follow this structure:
|
|
27
|
-
|
|
28
|
-
```markdown
|
|
29
|
-
## HH:MM — {project name} #{project name}
|
|
30
|
-
|
|
31
|
-
{Summary of what was done — keep it concise but capture the key points}
|
|
32
|
-
|
|
33
|
-
**Decisions:** {any notable decisions made and why, or "None"}
|
|
34
|
-
|
|
35
|
-
**Topics:** {flag anything blog-worthy, reusable, or worth revisiting — or "None"}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Tag the project name in the heading (e.g., `#project1`, `#general`). When flagging topics, prefix with `#blog-worthy`, `#reusable`, or `#demo-worthy`:
|
|
39
|
-
|
|
40
|
-
Guidelines:
|
|
41
|
-
|
|
42
|
-
- Summarise what was accomplished, not every step taken
|
|
43
|
-
- Capture the "why" behind decisions — this is what you'll forget
|
|
44
|
-
- Flag blog-worthy topics with a brief note on why it's interesting
|
|
45
|
-
- Flag reusable patterns, utilities, or approaches worth extracting
|
|
46
|
-
- Flag demo-worthy work — things that would make a good presentation, show-and-tell, or live walkthrough
|
|
47
|
-
- If the user provided `$ARGUMENTS`, use it to focus or annotate the entry
|
|
48
|
-
- Keep entries concise — a few lines per section, not paragraphs
|
|
49
|
-
|
|
50
|
-
## Step 4: Write detail files (when topics are flagged)
|
|
51
|
-
|
|
52
|
-
When a topic is flagged as blog-worthy or reusable, create a supporting detail file that captures the context needed to act on it later. Without this, the journal flags opportunities but loses the detail needed to follow through.
|
|
53
|
-
|
|
54
|
-
Detail files live in `~/.claude/journal/details/` and are named `YYYY-MM-DD-{slug}.md`.
|
|
55
|
-
|
|
56
|
-
Link them from the journal entry:
|
|
57
|
-
|
|
58
|
-
```markdown
|
|
59
|
-
**Topics:**
|
|
60
|
-
|
|
61
|
-
- #blog-worthy Teaching Claude to reflect — [detail](details/2026-02-13-reflect-workflow.md)
|
|
62
|
-
- #reusable The skill/hook pattern — [detail](details/2026-02-13-starter-kit.md)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Each detail file should include whichever of the following are relevant:
|
|
66
|
-
|
|
67
|
-
- **Context** — what problem was being solved and why
|
|
68
|
-
- **Approach** — what was tried, including dead ends and alternatives considered
|
|
69
|
-
- **Key code** — relevant snippets, patterns, or configurations that were created
|
|
70
|
-
- **Outcome** — what worked, what didn't, and why
|
|
71
|
-
- **Blog angle** — if blog-worthy, what makes it interesting to write about
|
|
72
|
-
- **Extraction notes** — if reusable, what could be extracted and how it might be generalised
|
|
73
|
-
|
|
74
|
-
These files are meant to preserve enough context that someone (including a future Claude session) could flesh out a blog post or extract reusable code without the original conversation.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Recall context from the most recent prior session in this repo
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Load the most recent handover note for this repo so the current session can pick up where the last one left off. Always operate on the current working directory; do not ask which repo to recall.
|
|
6
|
-
|
|
7
|
-
Handover notes are stored in the backlog database (scoped by git origin), authored by `/handover`. `/recall` reads only those notes — it does not read session transcripts or any disk files.
|
|
8
|
-
|
|
9
|
-
## Step 1: List unrecalled notes
|
|
10
|
-
|
|
11
|
-
Run:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
assist handover list
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
This prints one unrecalled handover per line, most recent first, as tab-separated `id`, ISO-8601 created timestamp, and one-line summary.
|
|
18
|
-
|
|
19
|
-
- **No output** — there are no unrecalled handovers. Skip to Step 3 and emit the "no handover" block.
|
|
20
|
-
- **Exactly one line** — recall it directly in Step 2 with no id.
|
|
21
|
-
- **More than one line** — present the summaries (with their created timestamps) to the user and ask which to recall, then pass the chosen `id` in Step 2.
|
|
22
|
-
|
|
23
|
-
## Step 2: Recall the chosen note
|
|
24
|
-
|
|
25
|
-
Run:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
assist handover recall [id]
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
With no argument this recalls the most recent unrecalled handover; with an `id` it recalls that specific note. Either way it prints the note's content to stdout and marks it recalled (so it drops out of the SessionStart advisory and future recalls).
|
|
32
|
-
|
|
33
|
-
## Step 3: Surface it
|
|
34
|
-
|
|
35
|
-
If the command printed content, present it to the user as a `# Recall` block so they can resume:
|
|
36
|
-
|
|
37
|
-
```markdown
|
|
38
|
-
# Recall
|
|
39
|
-
|
|
40
|
-
<the recalled handover content>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
If the command printed nothing, there are no unrecalled handovers — emit:
|
|
44
|
-
|
|
45
|
-
```markdown
|
|
46
|
-
# Recall
|
|
47
|
-
|
|
48
|
-
No handover found for this repo.
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Guidelines
|
|
52
|
-
|
|
53
|
-
- Always operate on the current working directory — never guess or substitute another repo.
|
|
54
|
-
- `/recall` is the counterpart to `/handover`: it reads notes the previous session authored, nothing else.
|
|
55
|
-
- Do not write or modify any files.
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Summarise recent journal entries as a standup update
|
|
3
|
-
allowed_args: "[days back, default 1] [project name]"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Summarise recent journal entries.
|
|
7
|
-
|
|
8
|
-
## Step 1: Determine the range
|
|
9
|
-
|
|
10
|
-
Parse `$ARGUMENTS` for:
|
|
11
|
-
|
|
12
|
-
- A number (days to look back, default 1)
|
|
13
|
-
- A project name (filter to that project only, default all)
|
|
14
|
-
|
|
15
|
-
Examples:
|
|
16
|
-
|
|
17
|
-
- `/standup` — yesterday's entries, all projects
|
|
18
|
-
- `/standup 3` — last 3 days, all projects
|
|
19
|
-
- `/standup project1` — yesterday's entries for project1
|
|
20
|
-
- `/standup 7 project2` — last 7 days for project2
|
|
21
|
-
|
|
22
|
-
## Step 2: Read journal files
|
|
23
|
-
|
|
24
|
-
Read the daily files from `~/.claude/journal/` for the date range. Files are named `YYYY-MM-DD.md`.
|
|
25
|
-
|
|
26
|
-
## Step 3: Present the summary
|
|
27
|
-
|
|
28
|
-
Summarise the entries concisely, grouped by day and project. Highlight:
|
|
29
|
-
|
|
30
|
-
- What was accomplished
|
|
31
|
-
- Key decisions made
|
|
32
|
-
- Any flagged blog topics or reusable IP
|
|
33
|
-
- Anything left in progress
|
|
34
|
-
|
|
35
|
-
Keep it brief — this is a standup, not a report.
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
Never use `pip install` directly. All Python dependency management uses `uv`.
|
|
2
|
-
|
|
3
|
-
**The venv MUST live in `~/.assist/voice/.venv`, NEVER in this directory or anywhere in the repo.**
|
|
4
|
-
|
|
5
|
-
Every `uv` command that touches this project MUST set `UV_PROJECT_ENVIRONMENT` to point to the home dir venv. This includes `uv sync`, `uv run`, and any other uv command. There must be no `.venv/`, `__pycache__/`, `*.egg-info/`, or any other generated artifacts in this directory.
|
|
6
|
-
|
|
7
|
-
- Dependencies are declared in `pyproject.toml`
|
|
8
|
-
- Runtime deps (torch, nemo, etc.) are in the `runtime` optional group; dev tools (ruff, xenon, radon) are in `dev`
|
|
9
|
-
- Bootstrap uses `uv sync --project <dir> --extra runtime --no-install-project` with `UV_PROJECT_ENVIRONMENT=~/.assist/voice/.venv`
|
|
10
|
-
- To add a dependency: edit `pyproject.toml`, then run `UV_PROJECT_ENVIRONMENT=~/.assist/voice/.venv uv lock --project src/commands/voice/python`
|
|
11
|
-
- No build system — this is a virtual project, not an installable package
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"""Microphone capture via sounddevice (16kHz PCM)."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import queue
|
|
5
|
-
import numpy as np
|
|
6
|
-
import sounddevice as sd
|
|
7
|
-
|
|
8
|
-
from logger import log
|
|
9
|
-
|
|
10
|
-
SAMPLE_RATE = 16000
|
|
11
|
-
BLOCK_SIZE = 512 # Silero VAD requires exactly 512 samples at 16kHz
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class AudioCapture:
|
|
15
|
-
def __init__(self):
|
|
16
|
-
self._queue: queue.Queue[np.ndarray] = queue.Queue()
|
|
17
|
-
self._stream: sd.InputStream | None = None
|
|
18
|
-
device_name = os.environ.get("VOICE_MIC")
|
|
19
|
-
self._device = device_name if device_name else None
|
|
20
|
-
|
|
21
|
-
def _callback(self, indata: np.ndarray, frames: int, time_info, status) -> None:
|
|
22
|
-
if status:
|
|
23
|
-
log("audio_status", str(status), level="warn")
|
|
24
|
-
self._queue.put(indata[:, 0].copy())
|
|
25
|
-
|
|
26
|
-
def start(self) -> None:
|
|
27
|
-
log(
|
|
28
|
-
"audio_start",
|
|
29
|
-
f"device={self._device}, rate={SAMPLE_RATE}, block={BLOCK_SIZE}",
|
|
30
|
-
)
|
|
31
|
-
self._stream = sd.InputStream(
|
|
32
|
-
samplerate=SAMPLE_RATE,
|
|
33
|
-
channels=1,
|
|
34
|
-
dtype="float32",
|
|
35
|
-
blocksize=BLOCK_SIZE,
|
|
36
|
-
device=self._device,
|
|
37
|
-
callback=self._callback,
|
|
38
|
-
)
|
|
39
|
-
self._stream.start()
|
|
40
|
-
|
|
41
|
-
def read(self, timeout: float = 1.0) -> np.ndarray | None:
|
|
42
|
-
try:
|
|
43
|
-
return self._queue.get(timeout=timeout)
|
|
44
|
-
except queue.Empty:
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
def stop(self) -> None:
|
|
48
|
-
if self._stream:
|
|
49
|
-
self._stream.stop()
|
|
50
|
-
self._stream.close()
|
|
51
|
-
self._stream = None
|
|
52
|
-
log("audio_stop")
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"""Check file-level maintainability index using radon."""
|
|
2
|
-
|
|
3
|
-
import glob
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from radon.metrics import mi_visit
|
|
7
|
-
|
|
8
|
-
MIN_MI = 20 # Grade B threshold
|
|
9
|
-
|
|
10
|
-
failed = []
|
|
11
|
-
for path in sorted(glob.glob("src/commands/voice/python/**/*.py", recursive=True)):
|
|
12
|
-
with open(path) as f:
|
|
13
|
-
mi = mi_visit(f.read(), True)
|
|
14
|
-
if mi < MIN_MI:
|
|
15
|
-
failed.append((path, mi))
|
|
16
|
-
|
|
17
|
-
if failed:
|
|
18
|
-
print("Files with low maintainability index:")
|
|
19
|
-
for path, mi in failed:
|
|
20
|
-
print(f" {path} - MI: {mi:.1f}")
|
|
21
|
-
sys.exit(1)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""Dispatch — keyboard-based input into the active terminal."""
|
|
2
|
-
|
|
3
|
-
from logger import log
|
|
4
|
-
|
|
5
|
-
import keyboard
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def dispatch(command: str) -> str:
|
|
9
|
-
"""Type the command and press Enter."""
|
|
10
|
-
log("dispatch_start", command)
|
|
11
|
-
keyboard.type_text(command)
|
|
12
|
-
keyboard.press_enter()
|
|
13
|
-
log("dispatch_done", command)
|
|
14
|
-
return command
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"""Simulate keyboard input on Windows via SendInput."""
|
|
2
|
-
|
|
3
|
-
import ctypes
|
|
4
|
-
import ctypes.wintypes as w
|
|
5
|
-
|
|
6
|
-
user32 = ctypes.windll.user32
|
|
7
|
-
|
|
8
|
-
INPUT_KEYBOARD = 1
|
|
9
|
-
KEYEVENTF_UNICODE = 0x0004
|
|
10
|
-
KEYEVENTF_KEYUP = 0x0002
|
|
11
|
-
KEYEVENTF_SCANCODE = 0x0008
|
|
12
|
-
VK_RETURN = 0x0D
|
|
13
|
-
VK_BACK = 0x08
|
|
14
|
-
SCAN_RETURN = 0x1C
|
|
15
|
-
SCAN_BACK = 0x0E
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class KEYBDINPUT(ctypes.Structure):
|
|
19
|
-
_fields_ = [
|
|
20
|
-
("wVk", w.WORD),
|
|
21
|
-
("wScan", w.WORD),
|
|
22
|
-
("dwFlags", w.DWORD),
|
|
23
|
-
("time", w.DWORD),
|
|
24
|
-
("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class MOUSEINPUT(ctypes.Structure):
|
|
29
|
-
_fields_ = [
|
|
30
|
-
("dx", ctypes.c_long),
|
|
31
|
-
("dy", ctypes.c_long),
|
|
32
|
-
("mouseData", w.DWORD),
|
|
33
|
-
("dwFlags", w.DWORD),
|
|
34
|
-
("time", w.DWORD),
|
|
35
|
-
("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class INPUT(ctypes.Structure):
|
|
40
|
-
class _INPUT(ctypes.Union):
|
|
41
|
-
_fields_ = [("mi", MOUSEINPUT), ("ki", KEYBDINPUT)]
|
|
42
|
-
|
|
43
|
-
_anonymous_ = ("_input",)
|
|
44
|
-
_fields_ = [("type", w.DWORD), ("_input", _INPUT)]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _send_key(vk: int = 0, scan: int = 0, flags: int = 0) -> None:
|
|
48
|
-
inp = INPUT(type=INPUT_KEYBOARD)
|
|
49
|
-
inp.ki.wVk = vk
|
|
50
|
-
inp.ki.wScan = scan
|
|
51
|
-
inp.ki.dwFlags = flags
|
|
52
|
-
user32.SendInput(1, ctypes.byref(inp), ctypes.sizeof(inp))
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def type_text(text: str) -> None:
|
|
56
|
-
"""Type a string by sending Unicode keystrokes."""
|
|
57
|
-
for ch in text:
|
|
58
|
-
code = ord(ch)
|
|
59
|
-
_send_key(scan=code, flags=KEYEVENTF_UNICODE)
|
|
60
|
-
_send_key(scan=code, flags=KEYEVENTF_UNICODE | KEYEVENTF_KEYUP)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def backspace(n: int = 1) -> None:
|
|
64
|
-
"""Press backspace n times."""
|
|
65
|
-
for _ in range(n):
|
|
66
|
-
_send_key(vk=VK_BACK, scan=SCAN_BACK)
|
|
67
|
-
_send_key(vk=VK_BACK, scan=SCAN_BACK, flags=KEYEVENTF_KEYUP)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def press_enter() -> None:
|
|
71
|
-
"""Press the Enter key."""
|
|
72
|
-
_send_key(vk=VK_RETURN, scan=SCAN_RETURN)
|
|
73
|
-
_send_key(vk=VK_RETURN, scan=SCAN_RETURN, flags=KEYEVENTF_KEYUP)
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"""List available audio input devices."""
|
|
2
|
-
|
|
3
|
-
import sounddevice as sd
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def main() -> None:
|
|
7
|
-
devices = sd.query_devices()
|
|
8
|
-
print("Audio input devices:\n")
|
|
9
|
-
for i, dev in enumerate(devices):
|
|
10
|
-
if dev["max_input_channels"] > 0:
|
|
11
|
-
default = " (default)" if i == sd.default.device[0] else ""
|
|
12
|
-
print(f" [{i}] {dev['name']}{default}")
|
|
13
|
-
ch = dev["max_input_channels"]
|
|
14
|
-
rate = dev["default_samplerate"]
|
|
15
|
-
print(f" channels={ch}, rate={rate}")
|
|
16
|
-
print()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if __name__ == "__main__":
|
|
20
|
-
main()
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""JSON Lines structured logging to voice.log."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
LOG_FILE = os.environ.get(
|
|
10
|
-
"VOICE_LOG_FILE", os.path.expanduser("~/.assist/voice/voice.log")
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
DEBUG = os.environ.get("VOICE_DEBUG", "") == "1"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _write(entry: dict) -> None:
|
|
17
|
-
entry["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
18
|
-
line = json.dumps(entry)
|
|
19
|
-
try:
|
|
20
|
-
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
|
21
|
-
f.write(line + "\n")
|
|
22
|
-
except OSError:
|
|
23
|
-
pass
|
|
24
|
-
if DEBUG:
|
|
25
|
-
ts = entry["timestamp"][11:19]
|
|
26
|
-
level = entry.get("level", "info").upper()
|
|
27
|
-
event = entry.get("event", "")
|
|
28
|
-
msg = entry.get("message", "")
|
|
29
|
-
print(f"{ts} {level:5s} [{event}] {msg}", file=sys.stderr, flush=True)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def log(event: str, message: str = "", *, level: str = "info", **data) -> None:
|
|
33
|
-
entry: dict = {"event": event, "level": level}
|
|
34
|
-
if message:
|
|
35
|
-
entry["message"] = message
|
|
36
|
-
if data:
|
|
37
|
-
entry["data"] = data
|
|
38
|
-
_write(entry)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "assist-voice"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
requires-python = ">=3.10"
|
|
5
|
-
dependencies = []
|
|
6
|
-
|
|
7
|
-
[project.optional-dependencies]
|
|
8
|
-
runtime = [
|
|
9
|
-
"onnxruntime>=1.17",
|
|
10
|
-
"sounddevice>=0.4",
|
|
11
|
-
"numpy>=1.24",
|
|
12
|
-
"nemo_toolkit[asr]>=1.22",
|
|
13
|
-
"silero-vad>=5.1",
|
|
14
|
-
"torch>=2.0",
|
|
15
|
-
]
|
|
16
|
-
dev = ["radon>=6.0", "ruff>=0.8", "xenon>=0.9"]
|
|
17
|
-
|
|
18
|
-
[[tool.uv.index]]
|
|
19
|
-
name = "pytorch-cu124"
|
|
20
|
-
url = "https://download.pytorch.org/whl/cu124"
|
|
21
|
-
explicit = true
|
|
22
|
-
|
|
23
|
-
[tool.uv.sources]
|
|
24
|
-
torch = { index = "pytorch-cu124" }
|
|
25
|
-
torchaudio = { index = "pytorch-cu124" }
|
|
26
|
-
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
"""Download and prepare all required voice models."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from logger import log
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_models_dir() -> str:
|
|
10
|
-
return os.environ.get(
|
|
11
|
-
"VOICE_MODELS_DIR",
|
|
12
|
-
os.path.expanduser("~/.assist/voice/models"),
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def setup_silero_vad(models_dir: str) -> None:
|
|
17
|
-
target = os.path.join(models_dir, "silero_vad.onnx")
|
|
18
|
-
if os.path.exists(target):
|
|
19
|
-
print(" silero_vad.onnx already exists")
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
print(" Downloading Silero VAD ONNX model...")
|
|
23
|
-
import urllib.request
|
|
24
|
-
|
|
25
|
-
url = "https://github.com/snakers4/silero-vad/raw/master/src/silero_vad/data/silero_vad.onnx"
|
|
26
|
-
urllib.request.urlretrieve(url, target)
|
|
27
|
-
log("setup_vad", f"Downloaded to {target}")
|
|
28
|
-
print(" silero_vad.onnx downloaded")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def setup_smart_turn(models_dir: str) -> None:
|
|
32
|
-
target = os.path.join(models_dir, "smart-turn-v3.2-cpu.onnx")
|
|
33
|
-
if os.path.exists(target):
|
|
34
|
-
print(" smart-turn-v3.2-cpu.onnx already exists")
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
print(" Downloading Smart Turn ONNX model from HuggingFace...")
|
|
38
|
-
from huggingface_hub import hf_hub_download
|
|
39
|
-
|
|
40
|
-
path = hf_hub_download(
|
|
41
|
-
repo_id="pipecat-ai/smart-turn-v3",
|
|
42
|
-
filename="smart-turn-v3.2-cpu.onnx",
|
|
43
|
-
local_dir=models_dir,
|
|
44
|
-
)
|
|
45
|
-
log("setup_smart_turn", f"Downloaded to {path}")
|
|
46
|
-
print(" smart-turn-v3.2-cpu.onnx downloaded")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def setup_stt(models_dir: str) -> None:
|
|
50
|
-
model_name = os.environ.get("VOICE_MODEL_STT", "nvidia/parakeet-ctc-1.1b")
|
|
51
|
-
print(f" Downloading STT model: {model_name}...")
|
|
52
|
-
print(" (this may take a while on first run)")
|
|
53
|
-
|
|
54
|
-
import nemo.collections.asr as nemo_asr
|
|
55
|
-
|
|
56
|
-
nemo_asr.models.EncDecCTCModelBPE.from_pretrained(model_name)
|
|
57
|
-
log("setup_stt", f"Model ready: {model_name}")
|
|
58
|
-
print(f" STT model ready: {model_name}")
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def main() -> None:
|
|
62
|
-
models_dir = get_models_dir()
|
|
63
|
-
os.makedirs(models_dir, exist_ok=True)
|
|
64
|
-
print(f"Models directory: {models_dir}\n")
|
|
65
|
-
|
|
66
|
-
print("[1/3] Silero VAD")
|
|
67
|
-
try:
|
|
68
|
-
setup_silero_vad(models_dir)
|
|
69
|
-
except Exception as e:
|
|
70
|
-
log("setup_vad_error", str(e), level="error")
|
|
71
|
-
print(f" ERROR: {e}", file=sys.stderr)
|
|
72
|
-
|
|
73
|
-
print("\n[2/3] Smart Turn (pipecat-ai)")
|
|
74
|
-
try:
|
|
75
|
-
setup_smart_turn(models_dir)
|
|
76
|
-
except Exception as e:
|
|
77
|
-
log("setup_smart_turn_error", str(e), level="error")
|
|
78
|
-
print(f" ERROR: {e}", file=sys.stderr)
|
|
79
|
-
|
|
80
|
-
print("\n[3/3] Parakeet STT (NeMo)")
|
|
81
|
-
try:
|
|
82
|
-
setup_stt(models_dir)
|
|
83
|
-
except Exception as e:
|
|
84
|
-
log("setup_stt_error", str(e), level="error")
|
|
85
|
-
print(f" ERROR: {e}", file=sys.stderr)
|
|
86
|
-
|
|
87
|
-
print("\nSetup complete.")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if __name__ == "__main__":
|
|
91
|
-
main()
|