Switch to sounddevice recording
This commit is contained in:
parent
afdf088d17
commit
b6c0fc0793
9 changed files with 250 additions and 468 deletions
16
AGENTS.md
16
AGENTS.md
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- `lel.sh` is the primary entrypoint; it records audio, runs `whisper`, and prints the transcript.
|
||||
- `env/` is a local Python virtual environment (optional) used to install runtime dependencies.
|
||||
- There are no separate source, test, or asset directories at this time.
|
||||
- `src/leld.py` is the primary entrypoint (X11 transcription daemon).
|
||||
- `src/recorder.py` handles audio capture using PortAudio via `sounddevice`.
|
||||
- `src/stt.py` wraps faster-whisper for transcription.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `./lel.sh` streams transcription from the microphone until you press Enter.
|
||||
- Example with overrides: `WHISPER_MODEL=small WHISPER_LANG=pt WHISPER_DEVICE=cuda ./lel.sh`.
|
||||
- Dependencies expected on PATH: `ffmpeg` and `whisper` (the OpenAI Whisper CLI).
|
||||
- Install deps: `uv sync`.
|
||||
- Run daemon: `uv run python3 src/leld.py --config ~/.config/lel/config.json`.
|
||||
- Open settings: `uv run python3 src/leld.py --settings --config ~/.config/lel/config.json`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
|
|
@ -30,7 +30,5 @@
|
|||
|
||||
## Configuration Tips
|
||||
|
||||
- Audio input is controlled via `WHISPER_FFMPEG_IN` (default `pulse:default`), e.g., `alsa:default`.
|
||||
- Streaming is on by default; set `WHISPER_STREAM=0` to transcribe after recording.
|
||||
- Segment duration for streaming is `WHISPER_SEGMENT_SEC` (default `5`).
|
||||
- Audio input is controlled via `WHISPER_FFMPEG_IN` (device index or name).
|
||||
- Model, language, device, and extra args can be set with `WHISPER_MODEL`, `WHISPER_LANG`, `WHISPER_DEVICE`, and `WHISPER_EXTRA_ARGS`.
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -5,12 +5,13 @@ Python X11 transcription daemon that records audio, runs Whisper, logs the trans
|
|||
## Requirements
|
||||
|
||||
- X11 (not Wayland)
|
||||
- `ffmpeg`
|
||||
- `sounddevice` (PortAudio)
|
||||
- `soundfile` (libsndfile)
|
||||
- `faster-whisper`
|
||||
- `pactl` (PulseAudio utilities for mic selection)
|
||||
- Tray icon deps: `gtk3`
|
||||
- i3 window manager (focus metadata via i3 IPC)
|
||||
- Python deps: `pillow`, `python-xlib`, `faster-whisper`, `PyGObject`, `i3ipc`
|
||||
- Python deps: `pillow`, `python-xlib`, `faster-whisper`, `PyGObject`, `i3ipc`, `sounddevice`, `soundfile`
|
||||
|
||||
System packages (example names): `portaudio`/`libportaudio2` and `libsndfile`.
|
||||
|
||||
## Python Daemon
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ Create `~/.config/lel/config.json`:
|
|||
```json
|
||||
{
|
||||
"daemon": { "hotkey": "Cmd+m" },
|
||||
"recording": { "input": "pulse:default" },
|
||||
"recording": { "input": "0" },
|
||||
"transcribing": { "model": "base", "device": "cpu" },
|
||||
"injection": { "backend": "clipboard" },
|
||||
|
||||
|
|
@ -56,11 +57,14 @@ Create `~/.config/lel/config.json`:
|
|||
Env overrides:
|
||||
|
||||
- `WHISPER_MODEL`, `WHISPER_DEVICE`
|
||||
- `WHISPER_FFMPEG_IN`
|
||||
- `WHISPER_FFMPEG_IN` (device index or name)
|
||||
- `LEL_HOTKEY`, `LEL_INJECTION_BACKEND`
|
||||
- `LEL_AI_CLEANUP_ENABLED`, `LEL_AI_CLEANUP_MODEL`, `LEL_AI_CLEANUP_TEMPERATURE`
|
||||
- `LEL_AI_CLEANUP_BASE_URL`, `LEL_AI_CLEANUP_API_KEY`
|
||||
|
||||
Recording input can be a device index (preferred) or a substring of the device
|
||||
name. Use the settings window to list available input devices.
|
||||
|
||||
## systemd user service
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ dependencies = [
|
|||
"python-xlib",
|
||||
"PyGObject",
|
||||
"i3ipc",
|
||||
"sounddevice",
|
||||
"soundfile",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def _parse_bool(val: str) -> bool:
|
|||
@dataclass
|
||||
class Config:
|
||||
daemon: dict = field(default_factory=lambda: {"hotkey": "Cmd+m"})
|
||||
recording: dict = field(default_factory=lambda: {"input": "pulse:default"})
|
||||
recording: dict = field(default_factory=lambda: {"input": ""})
|
||||
transcribing: dict = field(default_factory=lambda: {"model": "base", "device": "cpu"})
|
||||
injection: dict = field(default_factory=lambda: {"backend": "clipboard"})
|
||||
ai_cleanup: dict = field(
|
||||
|
|
@ -55,7 +55,7 @@ def load(path: str | None) -> Config:
|
|||
if not isinstance(cfg.daemon, dict):
|
||||
cfg.daemon = {"hotkey": "Cmd+m"}
|
||||
if not isinstance(cfg.recording, dict):
|
||||
cfg.recording = {"input": "pulse:default"}
|
||||
cfg.recording = {"input": ""}
|
||||
if not isinstance(cfg.transcribing, dict):
|
||||
cfg.transcribing = {"model": "base", "device": "cpu"}
|
||||
if not isinstance(cfg.injection, dict):
|
||||
|
|
|
|||
141
src/history.py
141
src/history.py
|
|
@ -1,141 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
from config import redacted_dict
|
||||
|
||||
|
||||
def _default_db_path() -> Path:
|
||||
return Path.home() / ".local" / "share" / "lel" / "history.db"
|
||||
|
||||
|
||||
class HistoryStore:
|
||||
def __init__(self, path: Path | None = None):
|
||||
self.path = path or _default_db_path()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.conn = sqlite3.connect(str(self.path), check_same_thread=False)
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at REAL NOT NULL,
|
||||
phase TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
config_json TEXT,
|
||||
context_json TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS artifacts (
|
||||
run_id INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
data_json TEXT,
|
||||
file_path TEXT,
|
||||
created_at REAL NOT NULL,
|
||||
FOREIGN KEY(run_id) REFERENCES runs(id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def add_run(self, phase: str, status: str, config, context: dict | None = None) -> int:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO runs (created_at, phase, status, config_json, context_json) VALUES (?, ?, ?, ?, ?)",
|
||||
(
|
||||
time.time(),
|
||||
phase,
|
||||
status,
|
||||
json.dumps(redacted_dict(config)) if config else None,
|
||||
json.dumps(context) if context else None,
|
||||
),
|
||||
)
|
||||
self.conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
|
||||
def add_artifact(self, run_id: int, kind: str, data: dict | None = None, file_path: str | None = None):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO artifacts (run_id, kind, data_json, file_path, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(
|
||||
run_id,
|
||||
kind,
|
||||
json.dumps(data) if data is not None else None,
|
||||
file_path,
|
||||
time.time(),
|
||||
),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def list_runs(self, phase: str | None = None, limit: int = 200) -> list[dict]:
|
||||
cur = self.conn.cursor()
|
||||
if phase:
|
||||
cur.execute(
|
||||
"SELECT id, created_at, phase, status, config_json, context_json FROM runs WHERE phase = ? ORDER BY id DESC LIMIT ?",
|
||||
(phase, limit),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"SELECT id, created_at, phase, status, config_json, context_json FROM runs ORDER BY id DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
rows = []
|
||||
for row in cur.fetchall():
|
||||
rows.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"created_at": row[1],
|
||||
"phase": row[2],
|
||||
"status": row[3],
|
||||
"config": json.loads(row[4]) if row[4] else None,
|
||||
"context": json.loads(row[5]) if row[5] else None,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
def list_artifacts(self, run_id: int) -> list[dict]:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT kind, data_json, file_path, created_at FROM artifacts WHERE run_id = ? ORDER BY created_at ASC",
|
||||
(run_id,),
|
||||
)
|
||||
out = []
|
||||
for row in cur.fetchall():
|
||||
out.append(
|
||||
{
|
||||
"kind": row[0],
|
||||
"data": json.loads(row[1]) if row[1] else None,
|
||||
"file_path": row[2],
|
||||
"created_at": row[3],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
def prune(self, limit_per_phase: int = 1000):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT DISTINCT phase FROM runs")
|
||||
phases = [r[0] for r in cur.fetchall()]
|
||||
for phase in phases:
|
||||
cur.execute("SELECT id FROM runs WHERE phase = ? ORDER BY id DESC LIMIT ?", (phase, limit_per_phase))
|
||||
keep_ids = [r[0] for r in cur.fetchall()]
|
||||
if not keep_ids:
|
||||
continue
|
||||
cur.execute(
|
||||
"DELETE FROM runs WHERE phase = ? AND id NOT IN (%s)" % ",".join("?" * len(keep_ids)),
|
||||
(phase, *keep_ids),
|
||||
)
|
||||
cur.execute(
|
||||
"DELETE FROM artifacts WHERE run_id NOT IN (%s)" % ",".join("?" * len(keep_ids)),
|
||||
(*keep_ids,),
|
||||
)
|
||||
self.conn.commit()
|
||||
27
src/leld.py
27
src/leld.py
|
|
@ -15,7 +15,6 @@ from stt import FasterWhisperSTT, STTConfig
|
|||
from aiprocess import AIConfig, build_processor
|
||||
from context import I3Provider
|
||||
from inject import inject
|
||||
from history import HistoryStore
|
||||
from x11_hotkey import listen
|
||||
from tray import run_tray
|
||||
from settings_window import open_settings_window
|
||||
|
|
@ -32,8 +31,6 @@ class State:
|
|||
class Daemon:
|
||||
def __init__(self, cfg: Config):
|
||||
self.cfg = cfg
|
||||
self.history = HistoryStore()
|
||||
self.history.prune(1000)
|
||||
self.lock = threading.Lock()
|
||||
self.state = State.IDLE
|
||||
self.proc = None
|
||||
|
|
@ -75,7 +72,7 @@ class Daemon:
|
|||
|
||||
def _start_recording_locked(self):
|
||||
try:
|
||||
proc, record = start_recording(self.cfg.recording.get("input", "pulse:default"))
|
||||
proc, record = start_recording(self.cfg.recording.get("input", ""))
|
||||
except Exception as exc:
|
||||
logging.error("record start failed: %s", exc)
|
||||
return
|
||||
|
|
@ -100,8 +97,6 @@ class Daemon:
|
|||
self.record = record
|
||||
self.state = State.RECORDING
|
||||
logging.info("recording started (%s)", record.wav_path)
|
||||
run_id = self.history.add_run("record", "started", self.cfg, self._context_json(self.context))
|
||||
self.history.add_artifact(run_id, "audio", {"path": record.wav_path}, record.wav_path)
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = threading.Timer(300, self._timeout_stop)
|
||||
|
|
@ -130,7 +125,7 @@ class Daemon:
|
|||
|
||||
logging.info("stopping recording (user)")
|
||||
try:
|
||||
stop_recording(proc)
|
||||
stop_recording(proc, record)
|
||||
except Exception as exc:
|
||||
logging.error("record stop failed: %s", exc)
|
||||
self.set_state(State.IDLE)
|
||||
|
|
@ -156,9 +151,6 @@ class Daemon:
|
|||
return
|
||||
|
||||
logging.info("stt: %s", text)
|
||||
run_id = self.history.add_run("stt", "ok", self.cfg, self._context_json(self.context))
|
||||
self.history.add_artifact(run_id, "input", {"wav_path": record.wav_path, "language": "en"})
|
||||
self.history.add_artifact(run_id, "output", {"text": text})
|
||||
|
||||
ai_enabled = self.cfg.ai_cleanup.get("enabled", False)
|
||||
ai_prompt_file = ""
|
||||
|
|
@ -180,17 +172,6 @@ class Daemon:
|
|||
)
|
||||
ai_input = text
|
||||
text = processor.process(ai_input) or text
|
||||
run_id = self.history.add_run("ai", "ok", self.cfg, self._context_json(self.context))
|
||||
self.history.add_artifact(
|
||||
run_id,
|
||||
"input",
|
||||
{
|
||||
"text": ai_input,
|
||||
"model": self.cfg.ai_cleanup.get("model", ""),
|
||||
"temperature": self.cfg.ai_cleanup.get("temperature", 0.0),
|
||||
},
|
||||
)
|
||||
self.history.add_artifact(run_id, "output", {"text": text})
|
||||
except Exception as exc:
|
||||
logging.error("ai process failed: %s", exc)
|
||||
|
||||
|
|
@ -206,8 +187,6 @@ class Daemon:
|
|||
return
|
||||
backend = self.cfg.injection.get("backend", "clipboard")
|
||||
inject(text, backend)
|
||||
run_id = self.history.add_run("inject", "ok", self.cfg, self._context_json(self.context))
|
||||
self.history.add_artifact(run_id, "input", {"text": text, "backend": backend})
|
||||
except Exception as exc:
|
||||
logging.error("output failed: %s", exc)
|
||||
finally:
|
||||
|
|
@ -263,7 +242,7 @@ def main():
|
|||
open_settings_window(cfg, config_path)
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gtk # type: ignore[import-not-found]
|
||||
Gtk.main()
|
||||
return
|
||||
|
||||
|
|
|
|||
116
src/recorder.py
116
src/recorder.py
|
|
@ -1,68 +1,92 @@
|
|||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
import numpy as np
|
||||
import sounddevice as sd # type: ignore[import-not-found]
|
||||
import soundfile as sf # type: ignore[import-not-found]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecordResult:
|
||||
wav_path: str
|
||||
temp_dir: str
|
||||
frames: list[np.ndarray] = field(default_factory=list)
|
||||
samplerate: int = 16000
|
||||
channels: int = 1
|
||||
dtype: str = "int16"
|
||||
|
||||
|
||||
def _resolve_ffmpeg_path() -> str:
|
||||
appdir = os.getenv("APPDIR")
|
||||
if appdir:
|
||||
candidate = Path(appdir) / "usr" / "bin" / "ffmpeg"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
return "ffmpeg"
|
||||
def list_input_devices() -> list[dict]:
|
||||
devices = []
|
||||
for idx, info in enumerate(sd.query_devices()):
|
||||
if info.get("max_input_channels", 0) > 0:
|
||||
devices.append({"index": idx, "name": info.get("name", "")})
|
||||
return devices
|
||||
|
||||
|
||||
def _ffmpeg_input_args(spec: str) -> list[str]:
|
||||
if not spec:
|
||||
spec = "pulse:default"
|
||||
kind = spec
|
||||
name = "default"
|
||||
if ":" in spec:
|
||||
kind, name = spec.split(":", 1)
|
||||
return ["-f", kind, "-i", name]
|
||||
def default_input_device() -> int | None:
|
||||
default = sd.default.device
|
||||
if isinstance(default, (tuple, list)) and default:
|
||||
return default[0]
|
||||
if isinstance(default, int):
|
||||
return default
|
||||
return None
|
||||
|
||||
|
||||
def start_recording(ffmpeg_input: str) -> tuple[subprocess.Popen, RecordResult]:
|
||||
def resolve_input_device(spec: str | int | None) -> int | None:
|
||||
if spec is None:
|
||||
return None
|
||||
if isinstance(spec, int):
|
||||
return spec
|
||||
text = str(spec).strip()
|
||||
if not text:
|
||||
return None
|
||||
if text.isdigit():
|
||||
return int(text)
|
||||
lowered = text.lower()
|
||||
for device in list_input_devices():
|
||||
name = (device.get("name") or "").lower()
|
||||
if lowered in name:
|
||||
return int(device["index"])
|
||||
return None
|
||||
|
||||
|
||||
def start_recording(input_spec: str | int | None) -> tuple[sd.InputStream, RecordResult]:
|
||||
tmpdir = tempfile.mkdtemp(prefix="lel-")
|
||||
wav = str(Path(tmpdir) / "mic.wav")
|
||||
record = RecordResult(wav_path=wav, temp_dir=tmpdir)
|
||||
device = resolve_input_device(input_spec)
|
||||
|
||||
args = ["-hide_banner", "-loglevel", "error"]
|
||||
args += _ffmpeg_input_args(ffmpeg_input)
|
||||
args += ["-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", wav]
|
||||
def callback(indata, _frames, _time, _status):
|
||||
record.frames.append(indata.copy())
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[_resolve_ffmpeg_path(), *args],
|
||||
preexec_fn=os.setsid,
|
||||
stream = sd.InputStream(
|
||||
samplerate=record.samplerate,
|
||||
channels=record.channels,
|
||||
dtype=record.dtype,
|
||||
device=device,
|
||||
callback=callback,
|
||||
)
|
||||
return proc, RecordResult(wav_path=wav, temp_dir=tmpdir)
|
||||
stream.start()
|
||||
return stream, record
|
||||
|
||||
|
||||
def stop_recording(proc: subprocess.Popen, timeout_sec: float = 5.0) -> None:
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGINT)
|
||||
except ProcessLookupError:
|
||||
return
|
||||
start = time.time()
|
||||
while proc.poll() is None:
|
||||
if time.time() - start > timeout_sec:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
break
|
||||
time.sleep(0.05)
|
||||
def stop_recording(stream: sd.InputStream, record: RecordResult) -> None:
|
||||
if stream:
|
||||
stream.stop()
|
||||
stream.close()
|
||||
_write_wav(record)
|
||||
|
||||
# ffmpeg returns 255 on SIGINT; treat as success
|
||||
if proc.returncode not in (0, 255, None):
|
||||
raise RuntimeError(f"ffmpeg exited with status {proc.returncode}")
|
||||
|
||||
def _write_wav(record: RecordResult) -> None:
|
||||
data = _flatten_frames(record.frames)
|
||||
sf.write(record.wav_path, data, record.samplerate, subtype="PCM_16")
|
||||
|
||||
|
||||
def _flatten_frames(frames: Iterable[np.ndarray]) -> np.ndarray:
|
||||
frames = list(frames)
|
||||
if not frames:
|
||||
return np.zeros((0, 1), dtype=np.int16)
|
||||
return np.concatenate(frames, axis=0)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
|
@ -11,10 +10,10 @@ import gi
|
|||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
|
||||
from gi.repository import Gdk, Gtk
|
||||
from gi.repository import Gdk, Gtk # type: ignore[import-not-found]
|
||||
|
||||
from config import Config, validate
|
||||
from history import HistoryStore
|
||||
from recorder import default_input_device, list_input_devices, resolve_input_device
|
||||
from aiprocess import list_models
|
||||
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ class SettingsWindow:
|
|||
def __init__(self, cfg: Config, config_path: Path):
|
||||
self.cfg = cfg
|
||||
self.config_path = config_path
|
||||
self.history = HistoryStore()
|
||||
self._model_cache: dict[str, list[str]] = {}
|
||||
self.window = Gtk.Window(title="lel settings")
|
||||
self.window.set_default_size(920, 700)
|
||||
|
|
@ -56,67 +54,6 @@ class SettingsWindow:
|
|||
self.window.add(vbox)
|
||||
self.window.show_all()
|
||||
|
||||
def _refresh_history(self, *_args):
|
||||
if not hasattr(self, "history_list"):
|
||||
return
|
||||
for row in self.history_list.get_children():
|
||||
self.history_list.remove(row)
|
||||
phase = self.widgets["history_phase"].get_active_text()
|
||||
if phase == "all":
|
||||
phase = None
|
||||
runs = self.history.list_runs(phase=phase, limit=200)
|
||||
for run in runs:
|
||||
row = Gtk.ListBoxRow()
|
||||
label = Gtk.Label(
|
||||
label=f"#{run['id']} {run['phase']} {run['status']} {time.strftime('%H:%M:%S', time.localtime(run['created_at']))}"
|
||||
)
|
||||
label.set_xalign(0.0)
|
||||
row.add(label)
|
||||
row._run = run
|
||||
self.history_list.add(row)
|
||||
self.history_list.show_all()
|
||||
|
||||
def _on_history_select(self, _listbox, row):
|
||||
if not row:
|
||||
return
|
||||
run = row._run
|
||||
artifacts = self.history.list_artifacts(run["id"])
|
||||
buf = self.history_detail.get_buffer()
|
||||
buf.set_text(self._format_run(run, artifacts))
|
||||
|
||||
def _format_run(self, run: dict, artifacts: list[dict]) -> str:
|
||||
lines = [f"Run #{run['id']} ({run['phase']})", f"Status: {run['status']}"]
|
||||
if run.get("context"):
|
||||
lines.append(f"Context: {run['context']}")
|
||||
for art in artifacts:
|
||||
lines.append(f"- {art['kind']}: {art.get('data') or art.get('file_path')}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _on_history_copy(self, *_args):
|
||||
row = self.history_list.get_selected_row()
|
||||
if not row:
|
||||
return
|
||||
run = row._run
|
||||
artifacts = self.history.list_artifacts(run["id"])
|
||||
text = ""
|
||||
for art in artifacts:
|
||||
if art["kind"] == "output" and art.get("data") and art["data"].get("text"):
|
||||
text = art["data"]["text"]
|
||||
if text:
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(text, -1)
|
||||
clipboard.store()
|
||||
|
||||
def _on_history_rerun(self, *_args):
|
||||
row = self.history_list.get_selected_row()
|
||||
if not row:
|
||||
return
|
||||
run = row._run
|
||||
artifacts = self.history.list_artifacts(run["id"])
|
||||
phase = run["phase"]
|
||||
if phase == "ai":
|
||||
self._open_ai_rerun(run, artifacts)
|
||||
|
||||
def _on_quick_run(self, *_args):
|
||||
buf = self.quick_text.get_buffer()
|
||||
start, end = buf.get_bounds()
|
||||
|
|
@ -162,23 +99,7 @@ class SettingsWindow:
|
|||
)
|
||||
)
|
||||
output = processor.process(output)
|
||||
run_id = self.history.add_run("ai", "ok", self.cfg, None)
|
||||
self.history.add_artifact(
|
||||
run_id,
|
||||
"input",
|
||||
{
|
||||
"step": idx,
|
||||
"text": output,
|
||||
"language": language,
|
||||
"model": step["model"],
|
||||
"temperature": step["temperature"],
|
||||
"prompt_text": step.get("prompt_text") or "",
|
||||
"base_url": step["base_url"],
|
||||
},
|
||||
)
|
||||
self.history.add_artifact(run_id, "output", {"text": output})
|
||||
self.widgets["quick_status"].set_text("Done")
|
||||
self._refresh_history()
|
||||
|
||||
def _collect_quick_steps(self) -> list[dict]:
|
||||
steps: list[dict] = []
|
||||
|
|
@ -205,75 +126,10 @@ class SettingsWindow:
|
|||
)
|
||||
return steps
|
||||
|
||||
def _open_ai_rerun(self, _run: dict, artifacts: list[dict]):
|
||||
input_text = ""
|
||||
for art in artifacts:
|
||||
if art["kind"] == "input" and art.get("data"):
|
||||
input_text = art["data"].get("text", "")
|
||||
dialog = Gtk.Dialog(title="Re-run AI", transient_for=self.window, flags=0)
|
||||
dialog.add_button("Run", Gtk.ResponseType.OK)
|
||||
dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||
box = dialog.get_content_area()
|
||||
textview = Gtk.TextView()
|
||||
textview.get_buffer().set_text(input_text)
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
scroll.add(textview)
|
||||
scroll.set_size_request(600, 300)
|
||||
box.add(scroll)
|
||||
dialog.show_all()
|
||||
resp = dialog.run()
|
||||
if resp == Gtk.ResponseType.OK:
|
||||
buf = textview.get_buffer()
|
||||
start, end = buf.get_bounds()
|
||||
text = buf.get_text(start, end, True)
|
||||
from aiprocess import AIConfig, build_processor
|
||||
|
||||
processor = build_processor(
|
||||
AIConfig(
|
||||
model=self.cfg.ai_cleanup.get("model", ""),
|
||||
temperature=self.cfg.ai_cleanup.get("temperature", 0.0),
|
||||
system_prompt_file="",
|
||||
base_url=self.cfg.ai_cleanup.get("base_url", ""),
|
||||
api_key=self.cfg.ai_cleanup.get("api_key", ""),
|
||||
timeout_sec=25,
|
||||
)
|
||||
)
|
||||
output = processor.process(text)
|
||||
run_id = self.history.add_run("ai", "ok", self.cfg, None)
|
||||
self.history.add_artifact(run_id, "input", {"text": text})
|
||||
self.history.add_artifact(run_id, "output", {"text": output})
|
||||
self._refresh_history()
|
||||
dialog.destroy()
|
||||
|
||||
def _build_tabs(self):
|
||||
self._add_tab("Settings", self._build_settings_tab())
|
||||
self._add_tab("History", self._build_history_tab())
|
||||
self._add_tab("Quick Run", self._build_quick_run_tab())
|
||||
|
||||
def _build_settings_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
box.set_border_width(8)
|
||||
|
||||
box.pack_start(self._section_label("Daemon"), False, False, 0)
|
||||
box.pack_start(self._build_hotkeys_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("Recording"), False, False, 0)
|
||||
box.pack_start(self._build_recording_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("Transcribing"), False, False, 0)
|
||||
box.pack_start(self._build_stt_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("Injection"), False, False, 0)
|
||||
box.pack_start(self._build_injection_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("AI Cleanup"), False, False, 0)
|
||||
box.pack_start(self._build_ai_tab(), False, False, 0)
|
||||
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
scroll.add(box)
|
||||
return scroll
|
||||
|
||||
def _add_tab(self, title: str, widget: Gtk.Widget):
|
||||
label = Gtk.Label(label=title)
|
||||
self.notebook.append_page(widget, label)
|
||||
|
|
@ -334,78 +190,48 @@ class SettingsWindow:
|
|||
def _build_recording_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
grid = self._grid()
|
||||
self.widgets["ffmpeg_input"] = Gtk.ComboBoxText()
|
||||
self.widgets["input_device"] = Gtk.ComboBoxText()
|
||||
self._populate_mic_sources()
|
||||
refresh_btn = Gtk.Button(label="Refresh")
|
||||
refresh_btn.connect("clicked", lambda *_: self._populate_mic_sources())
|
||||
mic_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
mic_row.pack_start(self.widgets["ffmpeg_input"], True, True, 0)
|
||||
mic_row.pack_start(self.widgets["input_device"], True, True, 0)
|
||||
mic_row.pack_start(refresh_btn, False, False, 0)
|
||||
self._row(grid, 0, "Microphone", mic_row)
|
||||
self._row(grid, 0, "Input Device", mic_row)
|
||||
# Record timeout is fixed (300s); no UI control.
|
||||
box.pack_start(grid, False, False, 0)
|
||||
return box
|
||||
|
||||
def _selected_mic_source(self) -> str:
|
||||
combo = self.widgets["ffmpeg_input"]
|
||||
text = combo.get_active_text() or ""
|
||||
if text.startswith("pulse:"):
|
||||
return text.split(" ", 1)[0]
|
||||
return self.cfg.recording.get("input", "pulse:default")
|
||||
combo = self.widgets["input_device"]
|
||||
text = (combo.get_active_text() or "").strip()
|
||||
if ":" in text:
|
||||
prefix = text.split(":", 1)[0]
|
||||
if prefix.isdigit():
|
||||
return prefix
|
||||
return self.cfg.recording.get("input", "")
|
||||
|
||||
def _populate_mic_sources(self):
|
||||
combo: Gtk.ComboBoxText = self.widgets["ffmpeg_input"]
|
||||
combo: Gtk.ComboBoxText = self.widgets["input_device"]
|
||||
combo.remove_all()
|
||||
sources, default_name = self._list_pulse_sources()
|
||||
self._mic_sources = sources
|
||||
selected = self.cfg.recording.get("input") or "pulse:default"
|
||||
devices = list_input_devices()
|
||||
selected_spec = self.cfg.recording.get("input") or ""
|
||||
selected_device = resolve_input_device(selected_spec)
|
||||
default_device = default_input_device()
|
||||
selected_index = 0
|
||||
for idx, (name, desc) in enumerate(sources):
|
||||
text = f"pulse:{name} ({desc})"
|
||||
combo.append_text(text)
|
||||
if selected.startswith(f"pulse:{name}"):
|
||||
for idx, device in enumerate(devices):
|
||||
label = f"{device['index']}: {device['name']}"
|
||||
combo.append_text(label)
|
||||
if selected_device is not None and device["index"] == selected_device:
|
||||
selected_index = idx
|
||||
if selected == "pulse:default" and default_name:
|
||||
for idx, (name, _desc) in enumerate(sources):
|
||||
if name == default_name:
|
||||
selected_index = idx
|
||||
break
|
||||
if sources:
|
||||
elif selected_device is None and default_device is not None and device["index"] == default_device:
|
||||
selected_index = idx
|
||||
if devices:
|
||||
combo.set_active(selected_index)
|
||||
else:
|
||||
combo.append_text("pulse:default (default)")
|
||||
combo.append_text("default (system)")
|
||||
combo.set_active(0)
|
||||
|
||||
def _list_pulse_sources(self) -> tuple[list[tuple[str, str]], str | None]:
|
||||
default_name = None
|
||||
try:
|
||||
proc = subprocess.run(["pactl", "list", "sources", "short"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
return ([], None)
|
||||
out = []
|
||||
for line in proc.stdout.splitlines():
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2:
|
||||
name = parts[1]
|
||||
desc = parts[-1] if parts[-1] else name
|
||||
out.append((name, desc))
|
||||
default_name = self._get_pulse_default_source()
|
||||
return (out, default_name)
|
||||
except Exception:
|
||||
return ([], None)
|
||||
|
||||
def _get_pulse_default_source(self) -> str | None:
|
||||
try:
|
||||
proc = subprocess.run(["pactl", "info"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
for line in proc.stdout.splitlines():
|
||||
if line.lower().startswith("default source:"):
|
||||
return line.split(":", 1)[1].strip()
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def _build_stt_tab(self) -> Gtk.Widget:
|
||||
grid = self._grid()
|
||||
models = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3"]
|
||||
|
|
@ -441,47 +267,6 @@ class SettingsWindow:
|
|||
# AI timeout is fixed (25s); no UI control.
|
||||
return grid
|
||||
|
||||
def _build_history_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
header = Gtk.Label(label="History")
|
||||
header.set_xalign(0.0)
|
||||
box.pack_start(header, False, False, 0)
|
||||
|
||||
filter_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
self.widgets["history_phase"] = self._combo(["all", "record", "stt", "ai", "inject"], "all")
|
||||
refresh_btn = Gtk.Button(label="Refresh")
|
||||
refresh_btn.connect("clicked", self._refresh_history)
|
||||
filter_row.pack_start(Gtk.Label(label="Phase"), False, False, 0)
|
||||
filter_row.pack_start(self.widgets["history_phase"], False, False, 0)
|
||||
filter_row.pack_start(refresh_btn, False, False, 0)
|
||||
|
||||
box.pack_start(filter_row, False, False, 0)
|
||||
|
||||
self.history_list = Gtk.ListBox()
|
||||
self.history_list.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
self.history_list.connect("row-selected", self._on_history_select)
|
||||
box.pack_start(self.history_list, True, True, 0)
|
||||
|
||||
self.history_detail = Gtk.TextView()
|
||||
self.history_detail.set_editable(False)
|
||||
self.history_detail.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||
detail_scroll = Gtk.ScrolledWindow()
|
||||
detail_scroll.add(self.history_detail)
|
||||
detail_scroll.set_vexpand(True)
|
||||
box.pack_start(detail_scroll, True, True, 0)
|
||||
|
||||
action_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
self.widgets["history_rerun"] = Gtk.Button(label="Re-run")
|
||||
self.widgets["history_rerun"].connect("clicked", self._on_history_rerun)
|
||||
self.widgets["history_copy"] = Gtk.Button(label="Copy Output")
|
||||
self.widgets["history_copy"].connect("clicked", self._on_history_copy)
|
||||
action_row.pack_start(self.widgets["history_rerun"], False, False, 0)
|
||||
action_row.pack_start(self.widgets["history_copy"], False, False, 0)
|
||||
box.pack_start(action_row, False, False, 0)
|
||||
|
||||
self._refresh_history()
|
||||
return box
|
||||
|
||||
def _build_quick_run_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
header = Gtk.Label(label="Bypass recording and run from text")
|
||||
|
|
|
|||
131
uv.lock
generated
131
uv.lock
generated
|
|
@ -86,6 +86,88 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
|
|
@ -322,6 +404,8 @@ dependencies = [
|
|||
{ name = "pillow" },
|
||||
{ name = "pygobject" },
|
||||
{ name = "python-xlib" },
|
||||
{ name = "sounddevice" },
|
||||
{ name = "soundfile" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
|
|
@ -331,6 +415,8 @@ requires-dist = [
|
|||
{ name = "pillow" },
|
||||
{ name = "pygobject" },
|
||||
{ name = "python-xlib" },
|
||||
{ name = "sounddevice" },
|
||||
{ name = "soundfile" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -671,6 +757,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d7/54/123f6239685f5f3f2edc123f1e38d2eefacebee18cf3c532d2f4bd51d0ef/pycairo-1.29.0-cp314-cp314t-win_arm64.whl", hash = "sha256:caba0837a4b40d47c8dfb0f24cccc12c7831e3dd450837f2a356c75f21ce5a15", size = 721404, upload-time = "2025-11-11T19:12:36.919Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygobject"
|
||||
version = "3.54.5"
|
||||
|
|
@ -783,6 +878,42 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sounddevice"
|
||||
version = "0.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soundfile"
|
||||
version = "0.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue