fow-cli 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {fow_cli-0.3.0 → fow_cli-0.4.0}/CHANGELOG.md +17 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/PKG-INFO +39 -1
- {fow_cli-0.3.0 → fow_cli-0.4.0}/README.md +38 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/pyproject.toml +1 -1
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/__init__.py +1 -1
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cli.py +2 -0
- fow_cli-0.4.0/src/fly_on_the_wall/cli_service.py +235 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/glossary.py +8 -2
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/providers/elevenlabs.py +15 -14
- {fow_cli-0.3.0 → fow_cli-0.4.0}/.gitignore +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/LICENSE +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/api_keys.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/audio.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/audio_metadata.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cache.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cleanup.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cli_costs.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cli_glossary.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cli_menu.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cli_publish.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cli_speaker_review.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/cli_watch.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/config.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/costs.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/db.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/doctor.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/embeddings.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/exporting.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/meetings.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/normalization.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/people.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/people_embeddings.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/pipeline.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/processing.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/providers/__init__.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/providers/openai_analysis.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/providers/openai_cleanup.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/publishing.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/py.typed +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/reanalysis.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/recording_quality.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/rendering.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/secrets.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/service_pricing.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/setup.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/speaker_identity.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/speaker_matching.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/speakers.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/storage.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/voice_samples.py +0 -0
- {fow_cli-0.3.0 → fow_cli-0.4.0}/src/fly_on_the_wall/watch.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Fly on the Wall are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-07-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `fow service` commands for installing and managing the Linux systemd user watch service.
|
|
10
|
+
- Added a generated `service.env` placeholder for background service environment variables.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Made the Linux service install paths respect `XDG_CONFIG_HOME`.
|
|
15
|
+
|
|
16
|
+
## [0.3.1] - 2026-06-15
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed ElevenLabs keyterms encoding for glossary and known-person transcription hints.
|
|
21
|
+
|
|
5
22
|
## [0.3.0] - 2026-06-13
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fow-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Personal CLI note-taker for turning meeting audio into cleaned meeting manuscripts.
|
|
5
5
|
Project-URL: Repository, https://github.com/henriksvensson/fly-on-the-wall
|
|
6
6
|
License-Expression: MIT
|
|
@@ -329,6 +329,36 @@ fow watch run
|
|
|
329
329
|
|
|
330
330
|
The watcher tolerates missing/remounted folders and uses periodic scans because cloud/removable mounts may not emit reliable filesystem events.
|
|
331
331
|
|
|
332
|
+
### Background Service
|
|
333
|
+
|
|
334
|
+
On Linux with systemd user services, install the watcher as a background service:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
fow service install
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
This writes `fow-watch.service` under the user systemd config directory, creates a non-overwriting placeholder `service.env` next to the app config, and reloads the user systemd manager. By default those paths are `~/.config/systemd/user/fow-watch.service` and `~/.config/fly-on-the-wall/service.env`; if `XDG_CONFIG_HOME` is set, the service installer uses that config root instead. It does not enable or start the service by default.
|
|
341
|
+
|
|
342
|
+
Enable it for future user sessions and start it now:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
fow service install --enable --start
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Starting from install refuses to run if no enabled watch folders are configured, because `fow watch run` would exit immediately. Add a folder first with `fow watch folders add <path>`, or pass `--force-start` if you intentionally want to start anyway.
|
|
349
|
+
|
|
350
|
+
Useful service commands:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
fow service status
|
|
354
|
+
fow service logs
|
|
355
|
+
fow service restart
|
|
356
|
+
fow service stop
|
|
357
|
+
fow service uninstall
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Optional service environment variables can be placed in the generated `service.env` file. The file is created as a commented placeholder during service install and is not overwritten by later installs.
|
|
361
|
+
|
|
332
362
|
## Publishing To Obsidian
|
|
333
363
|
|
|
334
364
|
Publishing is separate from internal exports.
|
|
@@ -446,6 +476,14 @@ Run tests:
|
|
|
446
476
|
uv run pytest
|
|
447
477
|
```
|
|
448
478
|
|
|
479
|
+
When developing a checkout while the background service is installed, stop the service first so it does not process files with the installed command:
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
fow service stop
|
|
483
|
+
uv run fow
|
|
484
|
+
fow service start
|
|
485
|
+
```
|
|
486
|
+
|
|
449
487
|
Run lint and formatting checks:
|
|
450
488
|
|
|
451
489
|
```bash
|
|
@@ -299,6 +299,36 @@ fow watch run
|
|
|
299
299
|
|
|
300
300
|
The watcher tolerates missing/remounted folders and uses periodic scans because cloud/removable mounts may not emit reliable filesystem events.
|
|
301
301
|
|
|
302
|
+
### Background Service
|
|
303
|
+
|
|
304
|
+
On Linux with systemd user services, install the watcher as a background service:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
fow service install
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
This writes `fow-watch.service` under the user systemd config directory, creates a non-overwriting placeholder `service.env` next to the app config, and reloads the user systemd manager. By default those paths are `~/.config/systemd/user/fow-watch.service` and `~/.config/fly-on-the-wall/service.env`; if `XDG_CONFIG_HOME` is set, the service installer uses that config root instead. It does not enable or start the service by default.
|
|
311
|
+
|
|
312
|
+
Enable it for future user sessions and start it now:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
fow service install --enable --start
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Starting from install refuses to run if no enabled watch folders are configured, because `fow watch run` would exit immediately. Add a folder first with `fow watch folders add <path>`, or pass `--force-start` if you intentionally want to start anyway.
|
|
319
|
+
|
|
320
|
+
Useful service commands:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
fow service status
|
|
324
|
+
fow service logs
|
|
325
|
+
fow service restart
|
|
326
|
+
fow service stop
|
|
327
|
+
fow service uninstall
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Optional service environment variables can be placed in the generated `service.env` file. The file is created as a commented placeholder during service install and is not overwritten by later installs.
|
|
331
|
+
|
|
302
332
|
## Publishing To Obsidian
|
|
303
333
|
|
|
304
334
|
Publishing is separate from internal exports.
|
|
@@ -416,6 +446,14 @@ Run tests:
|
|
|
416
446
|
uv run pytest
|
|
417
447
|
```
|
|
418
448
|
|
|
449
|
+
When developing a checkout while the background service is installed, stop the service first so it does not process files with the installed command:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
fow service stop
|
|
453
|
+
uv run fow
|
|
454
|
+
fow service start
|
|
455
|
+
```
|
|
456
|
+
|
|
419
457
|
Run lint and formatting checks:
|
|
420
458
|
|
|
421
459
|
```bash
|
|
@@ -11,6 +11,7 @@ from fly_on_the_wall import __version__
|
|
|
11
11
|
from fly_on_the_wall.cli_costs import costs_app
|
|
12
12
|
from fly_on_the_wall.cli_glossary import glossary_app
|
|
13
13
|
from fly_on_the_wall.cli_publish import publish_app
|
|
14
|
+
from fly_on_the_wall.cli_service import service_app
|
|
14
15
|
from fly_on_the_wall.cli_speaker_review import speakers_review
|
|
15
16
|
from fly_on_the_wall.cli_watch import watch_app
|
|
16
17
|
from fly_on_the_wall.config import load_config
|
|
@@ -81,6 +82,7 @@ app.add_typer(refresh_app, name="refresh")
|
|
|
81
82
|
app.add_typer(secrets_app, name="secrets")
|
|
82
83
|
app.add_typer(glossary_app, name="glossary")
|
|
83
84
|
app.add_typer(watch_app, name="watch")
|
|
85
|
+
app.add_typer(service_app, name="service")
|
|
84
86
|
app.add_typer(publish_app, name="publish")
|
|
85
87
|
app.add_typer(costs_app, name="costs")
|
|
86
88
|
console = Console()
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from fly_on_the_wall.config import config_dir
|
|
14
|
+
from fly_on_the_wall.db import database
|
|
15
|
+
from fly_on_the_wall.watch import list_watch_folders
|
|
16
|
+
|
|
17
|
+
SERVICE_NAME = "fow-watch.service"
|
|
18
|
+
ENV_FILE_NAME = "service.env"
|
|
19
|
+
ENV_FILE_TEMPLATE = """# Optional environment variables for fow-watch.service.
|
|
20
|
+
# Use this if your background service cannot access API keys from the OS keyring.
|
|
21
|
+
# ELEVENLABS_API_KEY=
|
|
22
|
+
# OPENAI_API_KEY=
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
service_app = typer.Typer(help="Manage the background watch service.", no_args_is_help=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@service_app.command("install")
|
|
30
|
+
def service_install(
|
|
31
|
+
enable: Annotated[
|
|
32
|
+
bool,
|
|
33
|
+
typer.Option("--enable", help="Enable the service to start when your user session starts. Default: disabled."),
|
|
34
|
+
] = False,
|
|
35
|
+
start: Annotated[
|
|
36
|
+
bool,
|
|
37
|
+
typer.Option("--start", help="Start the service after installing it. Default: do not start."),
|
|
38
|
+
] = False,
|
|
39
|
+
force_start: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
typer.Option("--force-start", help="Start even when no enabled watch folders are configured."),
|
|
42
|
+
] = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Install the watch service for the current user."""
|
|
45
|
+
_ensure_supported_platform()
|
|
46
|
+
if start and not force_start:
|
|
47
|
+
_ensure_enabled_watch_folders()
|
|
48
|
+
|
|
49
|
+
unit_path = _unit_file_path()
|
|
50
|
+
unit_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
unit_path.write_text(render_unit(_resolve_fow_executable(), _env_file_path()), encoding="utf-8")
|
|
52
|
+
console.print(f"Installed service unit: {unit_path}")
|
|
53
|
+
_ensure_env_file()
|
|
54
|
+
_run_or_exit(_systemctl("daemon-reload"))
|
|
55
|
+
|
|
56
|
+
if enable:
|
|
57
|
+
_run_or_exit(_systemctl("enable", SERVICE_NAME))
|
|
58
|
+
console.print("Service enabled for future user sessions.")
|
|
59
|
+
if start:
|
|
60
|
+
_run_or_exit(_systemctl("start", SERVICE_NAME))
|
|
61
|
+
console.print("Service started.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@service_app.command("uninstall")
|
|
65
|
+
def service_uninstall() -> None:
|
|
66
|
+
"""Remove the watch service for the current user."""
|
|
67
|
+
_ensure_supported_platform()
|
|
68
|
+
unit_path = _unit_file_path()
|
|
69
|
+
if unit_path.exists():
|
|
70
|
+
_run_or_exit(_systemctl("disable", "--now", SERVICE_NAME))
|
|
71
|
+
unit_path.unlink()
|
|
72
|
+
console.print(f"Removed service unit: {unit_path}")
|
|
73
|
+
else:
|
|
74
|
+
console.print(f"Service unit was not installed: {unit_path}")
|
|
75
|
+
_run_or_exit(_systemctl("daemon-reload"))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@service_app.command("start")
|
|
79
|
+
def service_start(
|
|
80
|
+
force: Annotated[
|
|
81
|
+
bool,
|
|
82
|
+
typer.Option("--force", help="Start even when no enabled watch folders are configured."),
|
|
83
|
+
] = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Start the watch service."""
|
|
86
|
+
_run_watch_service_action("start", force)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@service_app.command("stop")
|
|
90
|
+
def service_stop() -> None:
|
|
91
|
+
"""Stop the watch service."""
|
|
92
|
+
_ensure_supported_platform()
|
|
93
|
+
_run_or_exit(_systemctl("stop", SERVICE_NAME))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@service_app.command("restart")
|
|
97
|
+
def service_restart(
|
|
98
|
+
force: Annotated[
|
|
99
|
+
bool,
|
|
100
|
+
typer.Option("--force", help="Restart even when no enabled watch folders are configured."),
|
|
101
|
+
] = False,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Restart the watch service."""
|
|
104
|
+
_run_watch_service_action("restart", force)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run_watch_service_action(action: str, force: bool) -> None:
|
|
108
|
+
_ensure_supported_platform()
|
|
109
|
+
if not force:
|
|
110
|
+
_ensure_enabled_watch_folders()
|
|
111
|
+
_run_or_exit(_systemctl(action, SERVICE_NAME))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@service_app.command("status")
|
|
115
|
+
def service_status() -> None:
|
|
116
|
+
"""Show watch service status."""
|
|
117
|
+
_ensure_supported_platform()
|
|
118
|
+
_run_or_exit(["systemctl", "--user", "--no-pager", "status", SERVICE_NAME])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@service_app.command("logs")
|
|
122
|
+
def service_logs(
|
|
123
|
+
lines: Annotated[int, typer.Option("--lines", "-n", min=1, help="Number of recent log lines to show.")] = 100,
|
|
124
|
+
follow: Annotated[bool, typer.Option("--follow", "-f", help="Follow logs until interrupted.")] = False,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Show watch service logs."""
|
|
127
|
+
_ensure_supported_platform()
|
|
128
|
+
command = ["journalctl", "--user", "-u", SERVICE_NAME, "-n", str(lines)]
|
|
129
|
+
if follow:
|
|
130
|
+
command.append("--follow")
|
|
131
|
+
else:
|
|
132
|
+
command.append("--no-pager")
|
|
133
|
+
_run_or_exit(command)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@service_app.command("print-unit")
|
|
137
|
+
def service_print_unit() -> None:
|
|
138
|
+
"""Print the generated service unit without installing it."""
|
|
139
|
+
_ensure_supported_platform()
|
|
140
|
+
console.print(render_unit(_resolve_fow_executable(), _env_file_path()), end="", markup=False)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def render_unit(fow_executable: Path, env_file_path: Path) -> str:
|
|
144
|
+
if not fow_executable.is_absolute():
|
|
145
|
+
raise ValueError("fow executable path must be absolute")
|
|
146
|
+
if not env_file_path.is_absolute():
|
|
147
|
+
raise ValueError("service environment file path must be absolute")
|
|
148
|
+
return f"""[Unit]
|
|
149
|
+
Description=Fly on the Wall watch service
|
|
150
|
+
After=default.target
|
|
151
|
+
|
|
152
|
+
[Service]
|
|
153
|
+
Type=simple
|
|
154
|
+
Environment=PYTHONUNBUFFERED=1
|
|
155
|
+
EnvironmentFile=-{_systemd_quote(env_file_path)}
|
|
156
|
+
ExecStart={_systemd_quote(fow_executable)} watch run
|
|
157
|
+
Restart=on-failure
|
|
158
|
+
RestartSec=10
|
|
159
|
+
|
|
160
|
+
[Install]
|
|
161
|
+
WantedBy=default.target
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _ensure_supported_platform() -> None:
|
|
166
|
+
if sys.platform != "linux":
|
|
167
|
+
console.print("Unsupported platform: fow service currently supports Linux systemd user services only.")
|
|
168
|
+
raise typer.Exit(code=1)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _ensure_enabled_watch_folders() -> None:
|
|
172
|
+
if _has_enabled_watch_folder():
|
|
173
|
+
return
|
|
174
|
+
console.print("No enabled watch folders configured; the watch service would exit immediately.")
|
|
175
|
+
console.print("Add one with: fow watch folders add <path>")
|
|
176
|
+
console.print("To start anyway, pass --force or --force-start.")
|
|
177
|
+
raise typer.Exit(code=1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _has_enabled_watch_folder() -> bool:
|
|
181
|
+
with database() as connection:
|
|
182
|
+
return any(folder.enabled for folder in list_watch_folders(connection))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_fow_executable() -> Path:
|
|
186
|
+
executable = shutil.which("fow")
|
|
187
|
+
if executable is None:
|
|
188
|
+
console.print("Could not find the fow executable on PATH.")
|
|
189
|
+
raise typer.Exit(code=1)
|
|
190
|
+
return Path(executable).resolve()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _unit_file_path() -> Path:
|
|
194
|
+
return config_dir().parent / "systemd" / "user" / SERVICE_NAME
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _env_file_path() -> Path:
|
|
198
|
+
return config_dir() / ENV_FILE_NAME
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _ensure_env_file() -> None:
|
|
202
|
+
env_path = _env_file_path()
|
|
203
|
+
if env_path.exists():
|
|
204
|
+
console.print(f"Service environment file already exists: {env_path}")
|
|
205
|
+
return
|
|
206
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
env_path.write_text(ENV_FILE_TEMPLATE, encoding="utf-8")
|
|
208
|
+
env_path.chmod(0o600)
|
|
209
|
+
console.print(f"Created service environment placeholder: {env_path}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _systemctl(*arguments: str) -> list[str]:
|
|
213
|
+
return ["systemctl", "--user", *arguments]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _run_or_exit(command: Sequence[str]) -> None:
|
|
217
|
+
try:
|
|
218
|
+
_run_command(command)
|
|
219
|
+
except FileNotFoundError as exc:
|
|
220
|
+
console.print(f"Required command not found: {command[0]}")
|
|
221
|
+
raise typer.Exit(code=1) from exc
|
|
222
|
+
except subprocess.CalledProcessError as exc:
|
|
223
|
+
console.print(f"Command failed with exit code {exc.returncode}: {' '.join(command)}")
|
|
224
|
+
raise typer.Exit(code=exc.returncode) from exc
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _run_command(command: Sequence[str]) -> None:
|
|
228
|
+
subprocess.run(command, check=True)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _systemd_quote(path: Path) -> str:
|
|
232
|
+
text = str(path)
|
|
233
|
+
if not any(character.isspace() for character in text) and '"' not in text:
|
|
234
|
+
return text
|
|
235
|
+
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
@@ -31,7 +31,10 @@ def create_glossary_term(connection: Connection, term: str, description: str | N
|
|
|
31
31
|
""",
|
|
32
32
|
(term_id, normalized, normalized_description),
|
|
33
33
|
)
|
|
34
|
-
|
|
34
|
+
created = get_glossary_term(connection, normalized)
|
|
35
|
+
if created is None:
|
|
36
|
+
raise RuntimeError(f"Created glossary term was not found: {normalized}")
|
|
37
|
+
return created
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
def get_glossary_term(connection: Connection, term_or_id: str) -> GlossaryTerm | None:
|
|
@@ -80,7 +83,10 @@ def update_glossary_term(
|
|
|
80
83
|
""",
|
|
81
84
|
(updated_term, updated_description, int(updated_enabled), existing.id),
|
|
82
85
|
)
|
|
83
|
-
|
|
86
|
+
updated = get_glossary_term(connection, existing.id)
|
|
87
|
+
if updated is None:
|
|
88
|
+
raise RuntimeError(f"Updated glossary term was not found: {existing.id}")
|
|
89
|
+
return updated
|
|
84
90
|
|
|
85
91
|
|
|
86
92
|
def remove_glossary_term(connection: Connection, term_or_id: str) -> bool:
|
|
@@ -34,31 +34,32 @@ def transcribe_audio(
|
|
|
34
34
|
if not resolved_api_key:
|
|
35
35
|
raise ElevenLabsError("Missing ELEVENLABS_API_KEY.")
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
"model_id"
|
|
39
|
-
"tag_audio_events"
|
|
40
|
-
"timestamps_granularity"
|
|
41
|
-
"diarize"
|
|
42
|
-
"temperature"
|
|
43
|
-
"seed"
|
|
44
|
-
"no_verbatim"
|
|
45
|
-
|
|
37
|
+
form_fields = [
|
|
38
|
+
("model_id", MODEL),
|
|
39
|
+
("tag_audio_events", "true"),
|
|
40
|
+
("timestamps_granularity", "word"),
|
|
41
|
+
("diarize", "true"),
|
|
42
|
+
("temperature", "0"),
|
|
43
|
+
("seed", "1"),
|
|
44
|
+
("no_verbatim", str(no_verbatim).lower()),
|
|
45
|
+
]
|
|
46
46
|
if num_speakers is not None:
|
|
47
|
-
|
|
47
|
+
form_fields.append(("num_speakers", str(num_speakers)))
|
|
48
48
|
if diarization_threshold is not None:
|
|
49
|
-
|
|
49
|
+
form_fields.append(("diarization_threshold", str(diarization_threshold)))
|
|
50
50
|
if keyterms:
|
|
51
|
-
|
|
51
|
+
form_fields.extend(("keyterms", keyterm) for keyterm in keyterms)
|
|
52
52
|
|
|
53
53
|
close_client = client is None
|
|
54
54
|
http_client = client or httpx.Client(timeout=600)
|
|
55
55
|
try:
|
|
56
56
|
with audio_path.open("rb") as audio_file:
|
|
57
|
+
files: list[tuple[str, Any]] = [(name, (None, value)) for name, value in form_fields]
|
|
58
|
+
files.append(("file", (audio_path.name, audio_file)))
|
|
57
59
|
response = http_client.post(
|
|
58
60
|
API_URL,
|
|
59
61
|
headers={"xi-api-key": resolved_api_key},
|
|
60
|
-
|
|
61
|
-
files={"file": (audio_path.name, audio_file)},
|
|
62
|
+
files=files,
|
|
62
63
|
)
|
|
63
64
|
response.raise_for_status()
|
|
64
65
|
return response.json()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|