mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Merge pull request #37 from yusufipk/package-layout
Put the modules in a package and the scripts in a folder
This commit is contained in:
@@ -71,6 +71,6 @@ jobs:
|
|||||||
- name: Check the installer parses
|
- name: Check the installer parses
|
||||||
run: |
|
run: |
|
||||||
bash -n install.sh
|
bash -n install.sh
|
||||||
bash -n install-mac.sh
|
bash -n scripts/install-mac.sh
|
||||||
bash -n update.sh
|
bash -n scripts/update.sh
|
||||||
bash -n uninstall.sh
|
bash -n scripts/uninstall.sh
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ tools instead:
|
|||||||
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
On macOS the same `./install.sh` runs and hands over to `install-mac.sh`, which
|
On macOS the same `./install.sh` runs and hands over to `scripts/install-mac.sh`, which
|
||||||
puts down a `Dikte.app` in `~/Applications`, the `dikte` command and a
|
puts down a `Dikte.app` in `~/Applications`, the `dikte` command and a
|
||||||
LaunchAgent:
|
LaunchAgent:
|
||||||
|
|
||||||
@@ -84,9 +84,10 @@ transcribe in the cloud. A meeting needs BlackHole or Loopback
|
|||||||
(`brew install blackhole-2ch`); dictation does not.
|
(`brew install blackhole-2ch`); dictation does not.
|
||||||
|
|
||||||
`install.sh` adds the `dikte` command, a menu entry, an autostart entry and the
|
`install.sh` adds the `dikte` command, a menu entry, an autostart entry and the
|
||||||
two global shortcuts, whose keys are its two arguments. `./update.sh` pulls and
|
two global shortcuts, whose keys are its two arguments, or the ones already in
|
||||||
puts all of that back, keeping the keys you chose; `./uninstall.sh` takes it away
|
your settings when it is given none. `./scripts/update.sh` pulls and puts all of
|
||||||
again and leaves your settings and dictations alone unless you pass `--purge`.
|
that back; `./scripts/uninstall.sh` takes it away again and leaves your settings
|
||||||
|
and dictations alone unless you pass `--purge`.
|
||||||
|
|
||||||
Speech to text and cleanup each pick a provider in the settings window, and both
|
Speech to text and cleanup each pick a provider in the settings window, and both
|
||||||
run here by default, on models of your own. The cloud is the other option:
|
run here by default, on models of your own. The cloud is the other option:
|
||||||
@@ -201,8 +202,13 @@ keys.
|
|||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
Everything below is in the `dikte` package, which is what `python3 -m dikte`
|
||||||
|
runs and what the `__main__.py` in it hands to every launcher and shortcut.
|
||||||
|
`scripts/` holds install-mac.sh, update.sh and uninstall.sh; install.sh stays at
|
||||||
|
the top, and `tests/` has a file per module.
|
||||||
|
|
||||||
```
|
```
|
||||||
dikte.py entry point, tray icon, state machine
|
app.py entry point, tray icon, state machine
|
||||||
cli.py the command line: every verb, and what it answers with
|
cli.py the command line: every verb, and what it answers with
|
||||||
ipc.py one request and one reply over the local socket
|
ipc.py one request and one reply over the local socket
|
||||||
audio.py PCM capture: pw-record for dictation, ffmpeg for a meeting
|
audio.py PCM capture: pw-record for dictation, ffmpeg for a meeting
|
||||||
@@ -224,7 +230,7 @@ i18n.py the string table
|
|||||||
```
|
```
|
||||||
|
|
||||||
The indicator is drawn through XWayland, because a Wayland client cannot place a
|
The indicator is drawn through XWayland, because a Wayland client cannot place a
|
||||||
window in a screen corner; `dikte.py` sets `QT_QPA_PLATFORM=xcb` for that.
|
window in a screen corner; `app.py` sets `QT_QPA_PLATFORM=xcb` for that.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -55,7 +55,7 @@ araçlarıyla çalışır:
|
|||||||
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
macOS'ta da aynı `./install.sh` çalışır, işi `install-mac.sh`'a devreder;
|
macOS'ta da aynı `./install.sh` çalışır, işi `scripts/install-mac.sh`'a devreder;
|
||||||
`~/Applications` içine bir `Dikte.app`, `dikte` komutunu ve bir LaunchAgent
|
`~/Applications` içine bir `Dikte.app`, `dikte` komutunu ve bir LaunchAgent
|
||||||
kurar:
|
kurar:
|
||||||
|
|
||||||
@@ -83,10 +83,10 @@ BlackHole veya Loopback gerekiyor (`brew install blackhole-2ch`); dikte için
|
|||||||
gerekmiyor.
|
gerekmiyor.
|
||||||
|
|
||||||
`install.sh` `dikte` komutunu, menü girdisini, oturum açılışında otomatik
|
`install.sh` `dikte` komutunu, menü girdisini, oturum açılışında otomatik
|
||||||
başlatmayı ve iki global kısayolu kurar; tuşları da iki argümanı. `./update.sh`
|
başlatmayı ve iki global kısayolu kurar; tuşları iki argümanı, argüman
|
||||||
son sürümü çeker ve bunları senin seçtiğin tuşlarla yerine koyar;
|
verilmezse ayarlarında duranlar. `./scripts/update.sh` son sürümü çeker ve
|
||||||
`./uninstall.sh` hepsini geri alır, `--purge` demedikçe ayarlarına ve
|
bunları yerine koyar; `./scripts/uninstall.sh` hepsini geri alır, `--purge`
|
||||||
diktelerine dokunmaz.
|
demedikçe ayarlarına ve diktelerine dokunmaz.
|
||||||
|
|
||||||
Sesi yazıya çevirme ve temizleme, ayarlar penceresinde ayrı ayrı sağlayıcı
|
Sesi yazıya çevirme ve temizleme, ayarlar penceresinde ayrı ayrı sağlayıcı
|
||||||
seçer; ikisi de varsayılan olarak burada, kendi modellerinle çalışır. Bulutu
|
seçer; ikisi de varsayılan olarak burada, kendi modellerinle çalışır. Bulutu
|
||||||
@@ -198,8 +198,13 @@ Kısayollar sekmesi bağlanacak komutu gösterir.
|
|||||||
|
|
||||||
## Dosyalar
|
## Dosyalar
|
||||||
|
|
||||||
|
Aşağıdakilerin hepsi `dikte` paketinin içinde: `python3 -m dikte` bunu çalıştırır,
|
||||||
|
içindeki `__main__.py` de her başlatıcının ve kısayolun adlandırdığı dosyadır.
|
||||||
|
`scripts/` altında install-mac.sh, update.sh ve uninstall.sh var; install.sh en
|
||||||
|
üstte kalır, `tests/` içinde de her modülün bir dosyası.
|
||||||
|
|
||||||
```
|
```
|
||||||
dikte.py giriş noktası, tepsi simgesi, durum makinesi
|
app.py giriş noktası, tepsi simgesi, durum makinesi
|
||||||
cli.py komut satırı: bütün fiiller ve verdikleri cevap
|
cli.py komut satırı: bütün fiiller ve verdikleri cevap
|
||||||
ipc.py yerel sokette bir istek, bir cevap
|
ipc.py yerel sokette bir istek, bir cevap
|
||||||
audio.py PCM kaydı: diktede pw-record, toplantıda ffmpeg
|
audio.py PCM kaydı: diktede pw-record, toplantıda ffmpeg
|
||||||
@@ -221,7 +226,7 @@ i18n.py metin tablosu
|
|||||||
```
|
```
|
||||||
|
|
||||||
Gösterge XWayland üzerinden çizilir; Wayland'da bir pencereyi belirli bir köşeye
|
Gösterge XWayland üzerinden çizilir; Wayland'da bir pencereyi belirli bir köşeye
|
||||||
yerleştirmenin yolu yok, `dikte.py` bu yüzden `QT_QPA_PLATFORM=xcb` ayarlar.
|
yerleştirmenin yolu yok, `app.py` bu yüzden `QT_QPA_PLATFORM=xcb` ayarlar.
|
||||||
|
|
||||||
## Lisans
|
## Lisans
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Dikte: press a key, talk, press again to transcribe, clean up and paste.
|
||||||
|
|
||||||
|
The package is the application. Nothing is imported here on purpose: `dikte
|
||||||
|
config get` runs through the same package as the tray icon does, and it has no
|
||||||
|
business loading Qt to answer one question.
|
||||||
|
"""
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""What `python3 -m dikte` and the installed `dikte` command both run.
|
||||||
|
|
||||||
|
The file is also executed by path, because that is what a desktop shortcut and
|
||||||
|
the launcher in ~/.local/bin do: neither knows a working directory to be in.
|
||||||
|
Run that way there is no package around it, so the checkout has to be put on
|
||||||
|
the import path here, before the first line of the application is imported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if not __package__:
|
||||||
|
# realpath, because the launcher is a symlink into the checkout: what has to
|
||||||
|
# end up on the path is the checkout, not ~/.local/bin. The directory this
|
||||||
|
# file is in comes off the path in exchange, so that a module beside it is
|
||||||
|
# only ever reachable as part of the package.
|
||||||
|
here = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
sys.path[:] = [p for p in sys.path if os.path.realpath(p or ".") != here]
|
||||||
|
sys.path.insert(0, os.path.dirname(here))
|
||||||
|
|
||||||
|
from dikte.app import main # noqa: E402
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -22,8 +22,8 @@ import threading
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
import ggml
|
from . import ggml
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
APP_URL = "https://github.com/yusufipk/dikte"
|
APP_URL = "https://github.com/yusufipk/dikte"
|
||||||
USER_AGENT = f"dikte/1.0 (+{APP_URL})"
|
USER_AGENT = f"dikte/1.0 (+{APP_URL})"
|
||||||
Executable → Regular
+18
-21
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Dikte: press Ctrl+Space, talk, press again to transcribe, clean up and paste.
|
"""Dikte: press Ctrl+Space, talk, press again to transcribe, clean up and paste.
|
||||||
|
|
||||||
This is the application: the tray icon, the state machine, and the socket the
|
This is the application: the tray icon, the state machine, and the socket the
|
||||||
terminal talks to. Every verb it answers is in cli.py, which is also what runs
|
terminal talks to. Every verb it answers is in cli.py, which is also what runs
|
||||||
`dikte.py --help`; the only argument handled here is --gui, which is how the
|
`dikte --help`; the only argument handled here is --gui, which is how the
|
||||||
command line says "there is no instance to talk to, so be one".
|
command line says "there is no instance to talk to, so be one".
|
||||||
|
|
||||||
|
Nothing runs this file directly; __main__.py is what the launchers start.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
@@ -36,21 +37,21 @@ from PyQt6.QtGui import QAction, QIcon # noqa: E402
|
|||||||
from PyQt6.QtNetwork import QLocalServer, QLocalSocket # noqa: E402
|
from PyQt6.QtNetwork import QLocalServer, QLocalSocket # noqa: E402
|
||||||
from PyQt6.QtWidgets import QApplication, QMenu, QSystemTrayIcon # noqa: E402
|
from PyQt6.QtWidgets import QApplication, QMenu, QSystemTrayIcon # noqa: E402
|
||||||
|
|
||||||
import assistant # noqa: E402
|
from . import assistant # noqa: E402
|
||||||
import audio # noqa: E402
|
from . import audio # noqa: E402
|
||||||
import cli # noqa: E402
|
from . import cli # noqa: E402
|
||||||
import config as cfg # noqa: E402
|
from . import config as cfg # noqa: E402
|
||||||
import ggml # noqa: E402
|
from . import ggml # noqa: E402
|
||||||
import hotkey # noqa: E402
|
from . import hotkey # noqa: E402
|
||||||
import i18n # noqa: E402
|
from . import i18n # noqa: E402
|
||||||
import ipc # noqa: E402
|
from . import ipc # noqa: E402
|
||||||
import meeting # noqa: E402
|
from . import meeting # noqa: E402
|
||||||
import trayicon # noqa: E402
|
from . import trayicon # noqa: E402
|
||||||
from i18n import t # noqa: E402
|
from .i18n import t # noqa: E402
|
||||||
from meeting import MeetingPipeline # noqa: E402
|
from .meeting import MeetingPipeline # noqa: E402
|
||||||
from overlay import Overlay # noqa: E402
|
from .overlay import Overlay # noqa: E402
|
||||||
from settings_ui import SettingsWindow # noqa: E402
|
from .settings_ui import SettingsWindow # noqa: E402
|
||||||
from worker import Pipeline # noqa: E402
|
from .worker import Pipeline # noqa: E402
|
||||||
|
|
||||||
SERVER_NAME = ipc.SERVER_NAME
|
SERVER_NAME = ipc.SERVER_NAME
|
||||||
IDLE, RECORDING, BUSY = "idle", "recording", "busy"
|
IDLE, RECORDING, BUSY = "idle", "recording", "busy"
|
||||||
@@ -1168,7 +1169,3 @@ def run_app(args):
|
|||||||
QTimer.singleShot(0, dikte.toggle_meeting)
|
QTimer.singleShot(0, dikte.toggle_meeting)
|
||||||
|
|
||||||
return app.exec()
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -29,9 +29,9 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
SESSION_FILE = cfg.DATA_DIR / "assistant.json"
|
SESSION_FILE = cfg.DATA_DIR / "assistant.json"
|
||||||
PROVIDERS = ("claude", "codex", "openrouter")
|
PROVIDERS = ("claude", "codex", "openrouter")
|
||||||
@@ -31,7 +31,7 @@ import wave
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
RATE = 16000
|
RATE = 16000
|
||||||
CHANNELS = 1
|
CHANNELS = 1
|
||||||
@@ -18,10 +18,10 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import ggml
|
from . import ggml
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
PROVIDERS = ("openrouter", "local", "claude", "codex")
|
PROVIDERS = ("openrouter", "local", "claude", "codex")
|
||||||
|
|
||||||
+10
-10
@@ -22,16 +22,16 @@ import time
|
|||||||
|
|
||||||
from PyQt6.QtCore import QCoreApplication, QTimer
|
from PyQt6.QtCore import QCoreApplication, QTimer
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import audio
|
from . import audio
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import filetranscribe
|
from . import filetranscribe
|
||||||
import hotkey
|
from . import hotkey
|
||||||
import ipc
|
from . import ipc
|
||||||
import meeting
|
from . import meeting
|
||||||
import paste
|
from . import paste
|
||||||
|
|
||||||
NOT_RUNNING = 3
|
NOT_RUNNING = 3
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import ggml
|
from . import ggml
|
||||||
import i18n
|
from . import i18n
|
||||||
import paste
|
from . import paste
|
||||||
import paths
|
from . import paths
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
|
|
||||||
_MACOS = sys.platform == "darwin"
|
_MACOS = sys.platform == "darwin"
|
||||||
@@ -23,10 +23,10 @@ import wave
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import ggml
|
from . import ggml
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
UPLOAD_LIMIT = 24 * 1024 * 1024 # the APIs take 25 MB; leave the form its room
|
UPLOAD_LIMIT = 24 * 1024 * 1024 # the APIs take 25 MB; leave the form its room
|
||||||
MP3_BITRATE = "48k" # mono speech at 16 kHz: whisper hears nothing less
|
MP3_BITRATE = "48k" # mono speech at 16 kHz: whisper hears nothing less
|
||||||
@@ -44,9 +44,9 @@ import time
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
import hub
|
from . import hub
|
||||||
import paths
|
from . import paths
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
HOST = "127.0.0.1"
|
HOST = "127.0.0.1"
|
||||||
# The path api.py asks for, so its URL and the server's line up.
|
# The path api.py asks for, so its URL and the server's line up.
|
||||||
@@ -30,7 +30,7 @@ import threading
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
DESKTOP_ID = "dikte-toggle.desktop"
|
DESKTOP_ID = "dikte-toggle.desktop"
|
||||||
CANCEL_DESKTOP_ID = "dikte-cancel.desktop"
|
CANCEL_DESKTOP_ID = "dikte-cancel.desktop"
|
||||||
@@ -25,7 +25,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
GITHUB_API = "https://api.github.com"
|
GITHUB_API = "https://api.github.com"
|
||||||
HF_API = "https://huggingface.co/api"
|
HF_API = "https://huggingface.co/api"
|
||||||
@@ -22,8 +22,13 @@ CONNECT_MS = 800
|
|||||||
|
|
||||||
|
|
||||||
def script_path():
|
def script_path():
|
||||||
|
"""The package entry point, as a path.
|
||||||
|
|
||||||
|
A shortcut and a relaunch both start a second process, and neither has a
|
||||||
|
working directory to run `-m dikte` from, so the file is named outright.
|
||||||
|
"""
|
||||||
return os.path.realpath(
|
return os.path.realpath(
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "dikte.py")
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "__main__.py")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,13 +24,13 @@ import wave
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import filetranscribe
|
from . import filetranscribe
|
||||||
import vad
|
from . import vad
|
||||||
from filetranscribe import Cancelled, format_timestamp
|
from .filetranscribe import Cancelled, format_timestamp
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
# Where the document stops being prose and starts being the transcript. It is a
|
# Where the document stops being prose and starts being the transcript. It is a
|
||||||
# comment, so it never shows up in a rendered document, and it is what a retry
|
# comment, so it never shows up in a rendered document, and it is what a retry
|
||||||
@@ -19,7 +19,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
# Linux input event codes (linux/input-event-codes.h), which is what ydotool
|
# Linux input event codes (linux/input-event-codes.h), which is what ydotool
|
||||||
# takes. They are also the list of keys a paste shortcut may be built from, so
|
# takes. They are also the list of keys a paste shortcut may be built from, so
|
||||||
@@ -13,19 +13,19 @@ from PyQt6.QtWidgets import (
|
|||||||
QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
|
QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import audio
|
from . import audio
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import filetranscribe
|
from . import filetranscribe
|
||||||
import ggml
|
from . import ggml
|
||||||
import hotkey
|
from . import hotkey
|
||||||
import ipc
|
from . import ipc
|
||||||
import meeting
|
from . import meeting
|
||||||
import paste
|
from . import paste
|
||||||
from filetranscribe import FileTranscriber
|
from .filetranscribe import FileTranscriber
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
UI_LANGUAGES = [("Automatic (system)", "auto"), ("Turkish", "tr"), ("English", "en")]
|
UI_LANGUAGES = [("Automatic (system)", "auto"), ("Turkish", "tr"), ("English", "en")]
|
||||||
LANGUAGES = [
|
LANGUAGES = [
|
||||||
@@ -118,7 +118,7 @@ def _working(painter, size, ink):
|
|||||||
painter.drawPath(head)
|
painter.drawPath(head)
|
||||||
|
|
||||||
|
|
||||||
# The names Linux themes use, which are what dikte.py asks for either way.
|
# The names Linux themes use, which are what app.py asks for either way.
|
||||||
SHAPES = {
|
SHAPES = {
|
||||||
"audio-input-microphone": _microphone,
|
"audio-input-microphone": _microphone,
|
||||||
"media-record": _record,
|
"media-record": _record,
|
||||||
@@ -15,15 +15,15 @@ import traceback
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import audio
|
from . import audio
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import i18n
|
from . import i18n
|
||||||
import paste
|
from . import paste
|
||||||
import vad
|
from . import vad
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
CHUNK_SECONDS = audio.CHUNK_FRAMES / audio.RATE
|
CHUNK_SECONDS = audio.CHUNK_FRAMES / audio.RATE
|
||||||
|
|
||||||
+43
-14
@@ -9,18 +9,35 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
# path. That is a second script rather than a branch through this one, and
|
# path. That is a second script rather than a branch through this one, and
|
||||||
# update.sh reaches it through here without having to know which it is on.
|
# update.sh reaches it through here without having to know which it is on.
|
||||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
exec "$DIR/install-mac.sh" "$@"
|
exec "$DIR/scripts/install-mac.sh" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PY="$(command -v python3)"
|
PY="$(command -v python3)"
|
||||||
|
# The one file that starts the application, whoever is asking: the launcher
|
||||||
|
# below, both .desktop files, and every shortcut Dikte registers.
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
BIN_DIR="$HOME/.local/bin"
|
BIN_DIR="$HOME/.local/bin"
|
||||||
APP_DIR="$HOME/.local/share/applications"
|
APP_DIR="$HOME/.local/share/applications"
|
||||||
AUTOSTART_DIR="$HOME/.config/autostart"
|
AUTOSTART_DIR="$HOME/.config/autostart"
|
||||||
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons"
|
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons"
|
||||||
SHORTCUT="${1:-Ctrl+Space}"
|
# Only the one: the discard key's default is the settings' own, read back below.
|
||||||
# Without the colon, so that a second argument given as "" stays empty. That is
|
DEFAULT_SHORTCUT="Ctrl+Space"
|
||||||
# how update.sh says "this one was turned off", as against not saying anything.
|
# Given as arguments, or asked of the settings further down. An installer run
|
||||||
CANCEL_SHORTCUT="${2-Ctrl+Alt+Space}"
|
# again, which is what every update does, must not undo a key you chose in
|
||||||
|
# Settings, so silence here means "keep whatever is there".
|
||||||
|
SHORTCUT="${1:-}"
|
||||||
|
CANCEL_SHORTCUT="${2-}"
|
||||||
|
# Passed as "" means the discard key is off, which is not the same answer as
|
||||||
|
# not being passed at all.
|
||||||
|
CANCEL_GIVEN=$(( $# >= 2 ))
|
||||||
|
# One caller says both without meaning either: an updater from before Dikte
|
||||||
|
# became a package looks for the settings at a path that no longer exists, and
|
||||||
|
# so passes the default key and an empty discard key rather than yours. This
|
||||||
|
# can go once nobody is updating across that commit any more.
|
||||||
|
if [[ "$SHORTCUT" == "$DEFAULT_SHORTCUT" && $CANCEL_GIVEN == 1 && -z "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
SHORTCUT=""
|
||||||
|
CANCEL_GIVEN=0
|
||||||
|
fi
|
||||||
|
|
||||||
say() { printf ' %s\n' "$1"; }
|
say() { printf ' %s\n' "$1"; }
|
||||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
||||||
@@ -83,8 +100,8 @@ fi
|
|||||||
|
|
||||||
# 3. Launchers -------------------------------------------------------------
|
# 3. Launchers -------------------------------------------------------------
|
||||||
mkdir -p "$BIN_DIR" "$APP_DIR" "$AUTOSTART_DIR" "$ICON_DIR"
|
mkdir -p "$BIN_DIR" "$APP_DIR" "$AUTOSTART_DIR" "$ICON_DIR"
|
||||||
ln -sf "$DIR/dikte.py" "$BIN_DIR/dikte"
|
ln -sf "$ENTRY" "$BIN_DIR/dikte"
|
||||||
chmod +x "$DIR/dikte.py"
|
chmod +x "$ENTRY"
|
||||||
ok "Command installed: $BIN_DIR/dikte"
|
ok "Command installed: $BIN_DIR/dikte"
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$BIN_DIR:"*) ;;
|
*":$BIN_DIR:"*) ;;
|
||||||
@@ -97,7 +114,7 @@ esac
|
|||||||
# bare X11 login Qt is left with hicolor, which has no such name, and the entry
|
# bare X11 login Qt is left with hicolor, which has no such name, and the entry
|
||||||
# comes out blank. hicolor is also where this goes, since it is the theme every
|
# comes out blank. hicolor is also where this goes, since it is the theme every
|
||||||
# desktop must fall back to.
|
# desktop must fall back to.
|
||||||
if "$PY" "$DIR/trayicon.py" --hicolor "$ICON_DIR" >/dev/null 2>&1; then
|
if PYTHONPATH="$DIR" "$PY" -m dikte.trayicon --hicolor "$ICON_DIR" >/dev/null 2>&1; then
|
||||||
ICON=dikte
|
ICON=dikte
|
||||||
# Only GTK reads a cache, and only if one is already there; a stale cache
|
# Only GTK reads a cache, and only if one is already there; a stale cache
|
||||||
# would otherwise hide the file we just wrote.
|
# would otherwise hide the file we just wrote.
|
||||||
@@ -116,7 +133,7 @@ cat > "$APP_DIR/dikte.desktop" <<EOF
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=Dikte
|
Name=Dikte
|
||||||
Comment=Voice dictation: record, transcribe, clean up, paste
|
Comment=Voice dictation: record, transcribe, clean up, paste
|
||||||
Exec=$PY $DIR/dikte.py
|
Exec=$PY $ENTRY
|
||||||
Icon=$ICON
|
Icon=$ICON
|
||||||
Categories=Utility;AudioVideo;
|
Categories=Utility;AudioVideo;
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
@@ -127,7 +144,7 @@ cat > "$AUTOSTART_DIR/dikte.desktop" <<EOF
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Dikte
|
Name=Dikte
|
||||||
Exec=$PY $DIR/dikte.py
|
Exec=$PY $ENTRY
|
||||||
Icon=$ICON
|
Icon=$ICON
|
||||||
X-GNOME-Autostart-enabled=true
|
X-GNOME-Autostart-enabled=true
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
@@ -143,14 +160,26 @@ ok "Will start automatically on login"
|
|||||||
# knows which desktop it is on, and it stores the combination in the settings
|
# knows which desktop it is on, and it stores the combination in the settings
|
||||||
# as well, which is where the built-in listener reads it from. A key written
|
# as well, which is where the built-in listener reads it from. A key written
|
||||||
# to only one of the two places is a key that half works.
|
# to only one of the two places is a key that half works.
|
||||||
if [[ "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
# What was not asked for is read back out of the settings, which is where Dikte
|
||||||
warn "Both arguments are $SHORTCUT, so the discard key was left out."
|
# keeps the keys and so what a second run of this script has to leave alone.
|
||||||
|
stored() { PYTHONPATH="$DIR" "$PY" -m dikte config get "$1" 2>/dev/null || true; }
|
||||||
|
if [[ -z "$SHORTCUT" ]]; then
|
||||||
|
SHORTCUT="$(stored shortcut)"
|
||||||
|
SHORTCUT="${SHORTCUT:-$DEFAULT_SHORTCUT}"
|
||||||
|
fi
|
||||||
|
if [[ $CANCEL_GIVEN == 0 ]]; then
|
||||||
|
# An empty answer here is a discard key that was turned off, and it stays off.
|
||||||
|
CANCEL_SHORTCUT="$(stored cancel_shortcut)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CANCEL_SHORTCUT" && "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
warn "Both keys are $SHORTCUT, so the discard key was left out."
|
||||||
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
||||||
CANCEL_SHORTCUT=""
|
CANCEL_SHORTCUT=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
register() { # which combination label
|
register() { # which combination label
|
||||||
if out="$("$PY" "$DIR/dikte.py" shortcut install "$1" --combo "$2" 2>&1)"; then
|
if out="$("$PY" "$ENTRY" shortcut install "$1" --combo "$2" 2>&1)"; then
|
||||||
ok "$3: $2"
|
ok "$3: $2"
|
||||||
else
|
else
|
||||||
# One line: the rest of what it has to say about KWin is printed below.
|
# One line: the rest of what it has to say about KWin is printed below.
|
||||||
@@ -167,7 +196,7 @@ if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
|||||||
# not this script's. Guessing from XDG_CURRENT_DESKTOP here is how every
|
# not this script's. Guessing from XDG_CURRENT_DESKTOP here is how every
|
||||||
# session that was neither GNOME nor KDE used to be promised a KWin that was
|
# session that was neither GNOME nor KDE used to be promised a KWin that was
|
||||||
# never running.
|
# never running.
|
||||||
case "$("$PY" -c 'import sys; sys.path.insert(0, sys.argv[1]); import hotkey; print(hotkey.backend())' "$DIR" 2>/dev/null)" in
|
case "$(PYTHONPATH="$DIR" "$PY" -c 'from dikte import hotkey; print(hotkey.backend())' 2>/dev/null)" in
|
||||||
kde)
|
kde)
|
||||||
warn "KWin only reads these at startup, so they go live after your next"
|
warn "KWin only reads these at startup, so they go live after your next"
|
||||||
say "login. Until then open Settings → Shortcuts and turn on the"
|
say "login. Until then open Settings → Shortcuts and turn on the"
|
||||||
|
|||||||
@@ -11,19 +11,38 @@
|
|||||||
# Homebrew moves that copy.
|
# Homebrew moves that copy.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
# The checkout, one level up: this script lives in scripts/, everything it
|
||||||
|
# touches is at the top of the tree.
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# The one file that starts the application, whoever is asking: the wrapper in
|
||||||
|
# ~/.local/bin, the bundle, and every shortcut Dikte registers.
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
APP_DIR="$HOME/Applications"
|
APP_DIR="$HOME/Applications"
|
||||||
APP="$APP_DIR/Dikte.app"
|
APP="$APP_DIR/Dikte.app"
|
||||||
BIN_DIR="$HOME/.local/bin"
|
BIN_DIR="$HOME/.local/bin"
|
||||||
AGENT_DIR="$HOME/Library/LaunchAgents"
|
AGENT_DIR="$HOME/Library/LaunchAgents"
|
||||||
AGENT_ID="io.github.yusufipk.dikte"
|
AGENT_ID="io.github.yusufipk.dikte"
|
||||||
AGENT="$AGENT_DIR/$AGENT_ID.plist"
|
AGENT="$AGENT_DIR/$AGENT_ID.plist"
|
||||||
SHORTCUT="${1:-Ctrl+Option+Space}"
|
# Only the one: the discard key's default is the settings' own, read back below.
|
||||||
# Without the colon, so that a second argument given as "" stays empty. That is
|
DEFAULT_SHORTCUT="Ctrl+Option+Space"
|
||||||
# how update.sh says "this one was turned off", as against not saying anything.
|
# Given as arguments, or asked of the settings further down. An installer run
|
||||||
CANCEL_SHORTCUT="${2-Ctrl+Option+D}"
|
# again, which is what every update does, must not undo a key you chose in
|
||||||
|
# Settings, so silence here means "keep whatever is there".
|
||||||
|
SHORTCUT="${1:-}"
|
||||||
|
CANCEL_SHORTCUT="${2-}"
|
||||||
|
# Passed as "" means the discard key is off, which is not the same answer as
|
||||||
|
# not being passed at all.
|
||||||
|
CANCEL_GIVEN=$(( $# >= 2 ))
|
||||||
|
# One caller says both without meaning either: an updater from before Dikte
|
||||||
|
# became a package looks for the settings at a path that no longer exists, and
|
||||||
|
# so passes the default key and an empty discard key rather than yours. This
|
||||||
|
# can go once nobody is updating across that commit any more.
|
||||||
|
if [[ "$SHORTCUT" == "$DEFAULT_SHORTCUT" && $CANCEL_GIVEN == 1 && -z "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
SHORTCUT=""
|
||||||
|
CANCEL_GIVEN=0
|
||||||
|
fi
|
||||||
|
|
||||||
# The two places Homebrew installs to, in front, for the same reason dikte.py
|
# The two places Homebrew installs to, in front, for the same reason the app
|
||||||
# puts them there: a shell that has not been logged into since Homebrew was
|
# puts them there: a shell that has not been logged into since Homebrew was
|
||||||
# installed does not have them, and this script would then report ffmpeg as
|
# installed does not have them, and this script would then report ffmpeg as
|
||||||
# missing while the application finds it perfectly well.
|
# missing while the application finds it perfectly well.
|
||||||
@@ -147,7 +166,7 @@ if [ ! -d "\$PYTHONHOME" ]; then
|
|||||||
osascript -e 'display alert "Dikte" message "The Python this was installed against is gone, most likely after a brew upgrade. Run ./install.sh again."' >/dev/null 2>&1
|
osascript -e 'display alert "Dikte" message "The Python this was installed against is gone, most likely after a brew upgrade. Run ./install.sh again."' >/dev/null 2>&1
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exec "\$HERE/python3" "$DIR/dikte.py" --gui "\$@"
|
exec "\$HERE/python3" "$ENTRY" --gui "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$APP/Contents/MacOS/Dikte"
|
chmod +x "$APP/Contents/MacOS/Dikte"
|
||||||
|
|
||||||
@@ -186,7 +205,7 @@ printf 'APPL????' > "$APP/Contents/PkgInfo"
|
|||||||
# and no second place to change what Dikte looks like. Failing to draw it is
|
# and no second place to change what Dikte looks like. Failing to draw it is
|
||||||
# not worth stopping for: a bundle with no icon gets the generic one.
|
# not worth stopping for: a bundle with no icon gets the generic one.
|
||||||
iconset="$(mktemp -d)/Dikte.iconset"
|
iconset="$(mktemp -d)/Dikte.iconset"
|
||||||
if "$PY" "$DIR/trayicon.py" "$iconset" >/dev/null 2>&1 \
|
if PYTHONPATH="$DIR" "$PY" -m dikte.trayicon "$iconset" >/dev/null 2>&1 \
|
||||||
&& iconutil -c icns "$iconset" -o "$APP/Contents/Resources/Dikte.icns" 2>/dev/null; then
|
&& iconutil -c icns "$iconset" -o "$APP/Contents/Resources/Dikte.icns" 2>/dev/null; then
|
||||||
ok "Icon drawn"
|
ok "Icon drawn"
|
||||||
else
|
else
|
||||||
@@ -212,7 +231,7 @@ fi
|
|||||||
-f "$APP" >/dev/null 2>&1 || true
|
-f "$APP" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# 4. The command ------------------------------------------------------------
|
# 4. The command ------------------------------------------------------------
|
||||||
# A wrapper, where Linux gets a symlink to dikte.py. The shebang there is
|
# A wrapper, where Linux gets a symlink to the entry point. The shebang there is
|
||||||
# `env python3`, and on a Mac that is Apple's 3.9: the symlink would resolve to
|
# `env python3`, and on a Mac that is Apple's 3.9: the symlink would resolve to
|
||||||
# the one interpreter that cannot run this. Naming the interpreter here also
|
# the one interpreter that cannot run this. Naming the interpreter here also
|
||||||
# gives update.sh and uninstall.sh somewhere to read it from, so that the three
|
# gives update.sh and uninstall.sh somewhere to read it from, so that the three
|
||||||
@@ -221,9 +240,9 @@ mkdir -p "$BIN_DIR"
|
|||||||
cat > "$BIN_DIR/dikte" <<EOF
|
cat > "$BIN_DIR/dikte" <<EOF
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Written by install-mac.sh. Edit that, not this.
|
# Written by install-mac.sh. Edit that, not this.
|
||||||
exec "$PY" "$DIR/dikte.py" "\$@"
|
exec "$PY" "$ENTRY" "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$BIN_DIR/dikte" "$DIR/dikte.py"
|
chmod +x "$BIN_DIR/dikte" "$ENTRY"
|
||||||
ok "Command installed: $BIN_DIR/dikte"
|
ok "Command installed: $BIN_DIR/dikte"
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$BIN_DIR:"*) ;;
|
*":$BIN_DIR:"*) ;;
|
||||||
@@ -271,14 +290,27 @@ fi
|
|||||||
# shortcut registry at all, so "installed" means the running application is
|
# shortcut registry at all, so "installed" means the running application is
|
||||||
# holding the combination, and the settings file is where it reads it from
|
# holding the combination, and the settings file is where it reads it from
|
||||||
# after a restart. Nothing here needs a logout, unlike KDE.
|
# after a restart. Nothing here needs a logout, unlike KDE.
|
||||||
if [[ "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
#
|
||||||
warn "Both arguments are $SHORTCUT, so the discard key was left out."
|
# What was not asked for is read back out of the settings, which is what makes
|
||||||
|
# a second run of this script leave the keys you chose alone.
|
||||||
|
stored() { PYTHONPATH="$DIR" "$PY" -m dikte config get "$1" 2>/dev/null || true; }
|
||||||
|
if [[ -z "$SHORTCUT" ]]; then
|
||||||
|
SHORTCUT="$(stored shortcut)"
|
||||||
|
SHORTCUT="${SHORTCUT:-$DEFAULT_SHORTCUT}"
|
||||||
|
fi
|
||||||
|
if [[ $CANCEL_GIVEN == 0 ]]; then
|
||||||
|
# An empty answer here is a discard key that was turned off, and it stays off.
|
||||||
|
CANCEL_SHORTCUT="$(stored cancel_shortcut)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CANCEL_SHORTCUT" && "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
warn "Both keys are $SHORTCUT, so the discard key was left out."
|
||||||
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
||||||
CANCEL_SHORTCUT=""
|
CANCEL_SHORTCUT=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
register() { # which combination label
|
register() { # which combination label
|
||||||
if out="$("$PY" "$DIR/dikte.py" shortcut install "$1" --combo "$2" 2>&1)"; then
|
if out="$("$PY" "$ENTRY" shortcut install "$1" --combo "$2" 2>&1)"; then
|
||||||
ok "$3: $2"
|
ok "$3: $2"
|
||||||
else
|
else
|
||||||
warn "${out%%$'\n'*}"
|
warn "${out%%$'\n'*}"
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
# is the word that deletes them.
|
# is the word that deletes them.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
# The checkout, one level up: this script lives in scripts/, everything it
|
||||||
|
# touches is at the top of the tree.
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
USER_NAME="$(id -un)"
|
USER_NAME="$(id -un)"
|
||||||
BIN_DIR="$HOME/.local/bin"
|
BIN_DIR="$HOME/.local/bin"
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ count() {
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: ./uninstall.sh [--purge] [--yes]
|
Usage: ./scripts/uninstall.sh [--purge] [--yes]
|
||||||
|
|
||||||
--purge also delete the settings ($CONFIG_DIR)
|
--purge also delete the settings ($CONFIG_DIR)
|
||||||
and the dictations, meetings and recordings ($DATA_DIR)
|
and the dictations, meetings and recordings ($DATA_DIR)
|
||||||
@@ -93,9 +96,9 @@ if ((MACOS)); then
|
|||||||
say "Nothing to unregister: macOS shortcuts live only while Dikte runs."
|
say "Nothing to unregister: macOS shortcuts live only while Dikte runs."
|
||||||
elif [[ -n "$PY" ]] && "$PY" -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
elif [[ -n "$PY" ]] && "$PY" -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
||||||
for which in toggle pause cancel ask meeting; do
|
for which in toggle pause cancel ask meeting; do
|
||||||
"$PY" "$DIR/dikte.py" shortcut remove "$which" >/dev/null 2>&1 || true
|
"$PY" "$ENTRY" shortcut remove "$which" >/dev/null 2>&1 || true
|
||||||
done
|
done
|
||||||
case "$("$PY" -c 'import sys; sys.path.insert(0, sys.argv[1]); import hotkey; print(hotkey.backend())' "$DIR" 2>/dev/null)" in
|
case "$(PYTHONPATH="$DIR" "$PY" -c 'from dikte import hotkey; print(hotkey.backend())' 2>/dev/null)" in
|
||||||
kde)
|
kde)
|
||||||
ok "Global shortcuts unregistered"
|
ok "Global shortcuts unregistered"
|
||||||
say "KWin reads that file at startup, so the keys are free after your next login."
|
say "KWin reads that file at startup, so the keys are free after your next login."
|
||||||
@@ -111,10 +114,12 @@ fi
|
|||||||
# 2. The running instance --------------------------------------------------
|
# 2. The running instance --------------------------------------------------
|
||||||
# It holds a tray icon and a socket; asking it to quit is tidier than pulling
|
# It holds a tray icon and a socket; asking it to quit is tidier than pulling
|
||||||
# its launchers out from under it.
|
# its launchers out from under it.
|
||||||
if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then
|
# The pattern matches an instance started before Dikte became a package as well,
|
||||||
[[ -n "$PY" ]] && "$PY" "$DIR/dikte.py" quit >/dev/null 2>&1 || true
|
# which is what an uninstall run straight after an update finds running.
|
||||||
|
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
|
||||||
|
[[ -n "$PY" ]] && "$PY" "$ENTRY" quit >/dev/null 2>&1 || true
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then
|
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
|
||||||
warn "Dikte is still running; close it from the tray icon"
|
warn "Dikte is still running; close it from the tray icon"
|
||||||
else
|
else
|
||||||
ok "Stopped the running instance"
|
ok "Stopped the running instance"
|
||||||
@@ -216,11 +221,11 @@ elif [[ "$CONFIG_DIR" == "$DATA_DIR" ]]; then
|
|||||||
# macOS keeps both in the one directory a Mac user's backup already knows
|
# macOS keeps both in the one directory a Mac user's backup already knows
|
||||||
# about, so naming it twice would only look like two things were kept.
|
# about, so naming it twice would only look like two things were kept.
|
||||||
say "Settings and dictations kept: $CONFIG_DIR"
|
say "Settings and dictations kept: $CONFIG_DIR"
|
||||||
say "Delete them too with: ./uninstall.sh --purge"
|
say "Delete them too with: ./scripts/uninstall.sh --purge"
|
||||||
else
|
else
|
||||||
say "Settings kept: $CONFIG_DIR"
|
say "Settings kept: $CONFIG_DIR"
|
||||||
say "Dictations kept: $DATA_DIR"
|
say "Dictations kept: $DATA_DIR"
|
||||||
say "Delete them too with: ./uninstall.sh --purge"
|
say "Delete them too with: ./scripts/uninstall.sh --purge"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
# Dikte updater: pull, put the launchers back, restart what was running.
|
# Dikte updater: pull, put the launchers back, restart what was running.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
# The checkout, one level up: this script lives in scripts/, everything it
|
||||||
|
# touches is at the top of the tree.
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
USER_NAME="$(id -un)"
|
USER_NAME="$(id -un)"
|
||||||
|
|
||||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
@@ -13,10 +16,8 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
|
|||||||
# make a missing file the end of the script rather than a question answered no.
|
# make a missing file the end of the script rather than a question answered no.
|
||||||
PY="$(sed -n 's/^exec "\([^"]*\)".*/\1/p' "$HOME/.local/bin/dikte" 2>/dev/null | head -1 || true)"
|
PY="$(sed -n 's/^exec "\([^"]*\)".*/\1/p' "$HOME/.local/bin/dikte" 2>/dev/null | head -1 || true)"
|
||||||
[[ -x "$PY" ]] || PY="$(command -v python3 || true)"
|
[[ -x "$PY" ]] || PY="$(command -v python3 || true)"
|
||||||
DEFAULT_SHORTCUT="Ctrl+Option+Space"
|
|
||||||
else
|
else
|
||||||
PY="$(command -v python3 || true)"
|
PY="$(command -v python3 || true)"
|
||||||
DEFAULT_SHORTCUT="Ctrl+Space"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
say() { printf ' %s\n' "$1"; }
|
say() { printf ' %s\n' "$1"; }
|
||||||
@@ -24,13 +25,6 @@ ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
|||||||
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
|
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
|
||||||
die() { printf ' \033[31m✗\033[0m %s\n' "$1"; echo; exit 1; }
|
die() { printf ' \033[31m✗\033[0m %s\n' "$1"; echo; exit 1; }
|
||||||
|
|
||||||
# The combination stored in the settings, which is where Dikte itself reads it
|
|
||||||
# from and the one place that is the same whichever mechanism the session has.
|
|
||||||
setting() {
|
|
||||||
[[ -n "$PY" ]] || return 0
|
|
||||||
"$PY" "$DIR/dikte.py" config get "$1" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Updating Dikte"
|
echo "Updating Dikte"
|
||||||
echo "──────────────"
|
echo "──────────────"
|
||||||
@@ -90,18 +84,15 @@ echo
|
|||||||
|
|
||||||
# 4. Launchers --------------------------------------------------------------
|
# 4. Launchers --------------------------------------------------------------
|
||||||
# An update can add a dependency or move a file, so the installer runs again.
|
# An update can add a dependency or move a file, so the installer runs again.
|
||||||
# It would otherwise register its own defaults over the keys you chose, so it
|
# With no keys named it keeps the ones stored in the settings, which are the
|
||||||
# is told what those are. Read before the installer runs, since it is the one
|
# ones you chose.
|
||||||
# writing them.
|
"$DIR/install.sh"
|
||||||
shortcut="$(setting shortcut)"
|
|
||||||
cancel_shortcut="$(setting cancel_shortcut)"
|
|
||||||
# Positional, so a chosen discard key cannot be passed without the other one.
|
|
||||||
"$DIR/install.sh" "${shortcut:-$DEFAULT_SHORTCUT}" "${cancel_shortcut:-}"
|
|
||||||
|
|
||||||
# 5. The running instance ---------------------------------------------------
|
# 5. The running instance ---------------------------------------------------
|
||||||
# It is still running the code from before the pull.
|
# It is still running the code from before the pull, which on an update across
|
||||||
if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then
|
# the move into a package is a process still named after the old entry point.
|
||||||
if [[ -n "$PY" ]] && "$PY" "$DIR/dikte.py" restart >/dev/null 2>&1; then
|
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
|
||||||
|
if [[ -n "$PY" ]] && "$PY" "$ENTRY" restart >/dev/null 2>&1; then
|
||||||
ok "Restarted, so the new version is the one running"
|
ok "Restarted, so the new version is the one running"
|
||||||
else
|
else
|
||||||
warn "Could not restart it; use the tray menu → Restart"
|
warn "Could not restart it; use the tray menu → Restart"
|
||||||
+3
-3
@@ -22,9 +22,9 @@ import urllib.request
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import assistant
|
from dikte import assistant
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import i18n
|
from dikte import i18n
|
||||||
|
|
||||||
# What the application is, rather than what it does: PipeWire, wl-clipboard,
|
# What the application is, rather than what it does: PipeWire, wl-clipboard,
|
||||||
# ydotool, KDE's shortcut file, /dev/input. A port to another desktop replaces
|
# ydotool, KDE's shortcut file, /dev/input. A port to another desktop replaces
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import ggml
|
from dikte import ggml
|
||||||
from tests.support import (
|
from tests.support import (
|
||||||
DikteTest,
|
DikteTest,
|
||||||
fake_urlopen,
|
fake_urlopen,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import time
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import assistant
|
from dikte import assistant
|
||||||
from tests.support import DikteTest, fake_urlopen, only_these_tools
|
from tests.support import DikteTest, fake_urlopen, only_these_tools
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import unittest
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import audio
|
from dikte import audio
|
||||||
from tests.support import (
|
from tests.support import (
|
||||||
DikteTest,
|
DikteTest,
|
||||||
FakeCompleted,
|
FakeCompleted,
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import subprocess
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import cleanup
|
from dikte import cleanup
|
||||||
import ggml
|
from dikte import ggml
|
||||||
from tests.support import DikteTest, fake_urlopen, sent_json, url_error
|
from tests.support import DikteTest, fake_urlopen, sent_json, url_error
|
||||||
from tests.test_api import FakeServer, chat_reply
|
from tests.test_api import FakeServer, chat_reply
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -12,10 +12,10 @@ import json
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import cli
|
from dikte import cli
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import hotkey
|
from dikte import hotkey
|
||||||
import ipc
|
from dikte import ipc
|
||||||
from tests.support import DikteTest, fake_urlopen
|
from tests.support import DikteTest, fake_urlopen
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import cleanup
|
from dikte import cleanup
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import ggml
|
from dikte import ggml
|
||||||
import i18n
|
from dikte import i18n
|
||||||
import paste
|
from dikte import paste
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import unittest
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import filetranscribe as ft
|
from dikte import filetranscribe as ft
|
||||||
from tests.support import DikteTest, make_wav, silence, tone
|
from tests.support import DikteTest, make_wav, silence, tone
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import ggml
|
from dikte import ggml
|
||||||
import hub
|
from dikte import hub
|
||||||
from tests.support import (DikteTest, fake_urlopen, http_error, json_body,
|
from tests.support import (DikteTest, fake_urlopen, http_error, json_body,
|
||||||
linux_only, url_error)
|
linux_only, url_error)
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import subprocess
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import hotkey
|
from dikte import hotkey
|
||||||
from tests.support import DikteTest, FakeCompleted, linux_only
|
from tests.support import DikteTest, FakeCompleted, linux_only
|
||||||
|
|
||||||
SHORTCUTS_RC = """[services][dikte-toggle.desktop]
|
SHORTCUTS_RC = """[services][dikte-toggle.desktop]
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import hub
|
from dikte import hub
|
||||||
from tests.support import DikteTest, fake_urlopen, http_error, url_error
|
from tests.support import DikteTest, fake_urlopen, http_error, url_error
|
||||||
|
|
||||||
RELEASE = {
|
RELEASE = {
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import string
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import i18n
|
from dikte import i18n
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ import sys
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import ipc
|
from dikte import ipc
|
||||||
|
|
||||||
|
|
||||||
class FakeSocket:
|
class FakeSocket:
|
||||||
@@ -57,7 +57,7 @@ class FakeSocket:
|
|||||||
|
|
||||||
class Paths(unittest.TestCase):
|
class Paths(unittest.TestCase):
|
||||||
def test_script_path_points_at_dikte(self):
|
def test_script_path_points_at_dikte(self):
|
||||||
self.assertTrue(ipc.script_path().endswith("dikte.py"))
|
self.assertTrue(ipc.script_path().endswith("dikte/__main__.py"))
|
||||||
self.assertTrue(os.path.exists(ipc.script_path()))
|
self.assertTrue(os.path.exists(ipc.script_path()))
|
||||||
|
|
||||||
def test_the_shortcut_command_runs_it_with_this_interpreter(self):
|
def test_the_shortcut_command_runs_it_with_this_interpreter(self):
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import unittest
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import meeting
|
from dikte import meeting
|
||||||
from tests.support import DikteTest, make_wav, silence, speech, stereo, tone
|
from tests.support import DikteTest, make_wav, silence, speech, stereo, tone
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ import unittest
|
|||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import paste
|
from dikte import paste
|
||||||
from tests.support import DikteTest, FakeCompleted, only_these_tools
|
from tests.support import DikteTest, FakeCompleted, only_these_tools
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -9,9 +9,9 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import ggml
|
from dikte import ggml
|
||||||
import paths
|
from dikte import paths
|
||||||
|
|
||||||
|
|
||||||
class Directories(unittest.TestCase):
|
class Directories(unittest.TestCase):
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from unittest import mock
|
|||||||
from PyQt6.QtGui import QImage
|
from PyQt6.QtGui import QImage
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
import trayicon
|
from dikte import trayicon
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
# One application for the whole run; Qt allows no second one.
|
# One application for the whole run; Qt allows no second one.
|
||||||
@@ -66,7 +66,7 @@ class Tray(DikteTest):
|
|||||||
self.patch_attr(trayicon, "_cache", {})
|
self.patch_attr(trayicon, "_cache", {})
|
||||||
|
|
||||||
def test_a_name_we_do_not_draw_is_a_null_icon(self):
|
def test_a_name_we_do_not_draw_is_a_null_icon(self):
|
||||||
# dikte.py asks the theme first and falls through to here, so anything
|
# app.py asks the theme first and falls through to here, so anything
|
||||||
# answered with a picture would be one the theme should have given.
|
# answered with a picture would be one the theme should have given.
|
||||||
self.assertTrue(trayicon.icon("emblem-important").isNull())
|
self.assertTrue(trayicon.icon("emblem-important").isNull())
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -16,12 +16,12 @@ from PyQt6.QtCore import QPoint, QPointF, Qt
|
|||||||
from PyQt6.QtGui import QWheelEvent
|
from PyQt6.QtGui import QWheelEvent
|
||||||
from PyQt6.QtWidgets import QApplication, QMessageBox
|
from PyQt6.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
import cleanup
|
from dikte import cleanup
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import hotkey
|
from dikte import hotkey
|
||||||
import overlay as overlay_module
|
from dikte import overlay as overlay_module
|
||||||
import paste
|
from dikte import paste
|
||||||
import settings_ui
|
from dikte import settings_ui
|
||||||
from tests.support import DikteTest, only_these_tools
|
from tests.support import DikteTest, only_these_tools
|
||||||
|
|
||||||
# One application for the whole run; Qt allows no second one.
|
# One application for the whole run; Qt allows no second one.
|
||||||
@@ -281,7 +281,7 @@ class Settings(DikteTest):
|
|||||||
text = self.shortcut_tab_text(window)
|
text = self.shortcut_tab_text(window)
|
||||||
self.assertIn("i3 keeps no shortcut registry", text)
|
self.assertIn("i3 keeps no shortcut registry", text)
|
||||||
self.assertNotIn("KWin", text)
|
self.assertNotIn("KWin", text)
|
||||||
self.assertIn("dikte.py toggle", text)
|
self.assertIn("__main__.py toggle", text)
|
||||||
# Not a choice to offer where it is the only mechanism there is.
|
# Not a choice to offer where it is the only mechanism there is.
|
||||||
self.assertTrue(window.evdev_enabled.isHidden())
|
self.assertTrue(window.evdev_enabled.isHidden())
|
||||||
self.assertFalse([button for button in
|
self.assertFalse([button for button in
|
||||||
@@ -458,7 +458,7 @@ class MacSettings(Settings):
|
|||||||
text = self.shortcut_tab_text(window)
|
text = self.shortcut_tab_text(window)
|
||||||
self.assertIn("Dikte asks macOS for these combinations", text)
|
self.assertIn("Dikte asks macOS for these combinations", text)
|
||||||
self.assertNotIn("KWin", text)
|
self.assertNotIn("KWin", text)
|
||||||
self.assertNotIn("dikte.py toggle", text)
|
self.assertNotIn("__main__.py toggle", text)
|
||||||
|
|
||||||
def test_the_paste_keys_on_offer_are_the_ones_a_mac_uses(self):
|
def test_the_paste_keys_on_offer_are_the_ones_a_mac_uses(self):
|
||||||
window = self.window(cfg.Config())
|
window = self.window(cfg.Config())
|
||||||
@@ -482,7 +482,7 @@ class KdeSettings(Settings):
|
|||||||
self.assertIn("KWin only reads shortcut settings at startup", text)
|
self.assertIn("KWin only reads shortcut settings at startup", text)
|
||||||
self.assertIn("Install as a KDE shortcut", text)
|
self.assertIn("Install as a KDE shortcut", text)
|
||||||
self.assertNotIn("keeps no shortcut registry", text)
|
self.assertNotIn("keeps no shortcut registry", text)
|
||||||
self.assertNotIn("dikte.py toggle", text)
|
self.assertNotIn("__main__.py toggle", text)
|
||||||
# Here it is a choice: the wait for the next login, or the key press
|
# Here it is a choice: the wait for the next login, or the key press
|
||||||
# reaching the focused application as well.
|
# reaching the focused application as well.
|
||||||
self.assertFalse(window.evdev_enabled.isHidden())
|
self.assertFalse(window.evdev_enabled.isHidden())
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import vad
|
from dikte import vad
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
CHUNK = 1024 / 16000 # what worker.py feeds it: one chunk of the level meter
|
CHUNK = 1024 / 16000 # what worker.py feeds it: one chunk of the level meter
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import assistant
|
from dikte import assistant
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import paste
|
from dikte import paste
|
||||||
import worker
|
from dikte import worker
|
||||||
from tests.support import DikteTest, make_wav, speech
|
from tests.support import DikteTest, make_wav, speech
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user