mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Windows joins the three systems as its own entry in each table: DirectShow through ffmpeg for capture, the Win32 clipboard and SendInput for the paste, RegisterHotKey for the global shortcut, and the whisper.cpp and llama.cpp Windows zips (the OpenBLAS whisper build, which transcribes about twice as fast on a plain CPU). Settings go to APPDATA, data to LOCALAPPDATA, and install.ps1 adds the Start Menu entry, the dikte command and an optional autostart. Meetings are not supported yet: Windows offers nothing to record the far side from. Porting surfaced three fixes that were not Windows specific: - A stopped or overlong download tried to delete its .part file while still holding it open, which Windows refuses. The unlinks now wait for the handle. - The CLI transcribed files without handing the local servers their settings first, so a local provider failed with "no model downloaded" wherever the GUI had not run in the same process. - The audio content types are pinned instead of asked of the registry, which answers differently machine to machine. One fix is Windows specific but sits in shared code: shutdown() does not end a blocked recv there, so stopping a request also closes the socket handle. Co-Authored-By: Claude Fable 5 <[email protected]>
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
"""The socket the running instance listens on, and one request over it.
|
|
|
|
A command typed at a terminal is answered rather than only obeyed: the reply
|
|
carries the transcript, the agent's answer, or the reason nothing happened,
|
|
which is what lets a script wait for a dictation instead of guessing when it is
|
|
done. One JSON object goes each way per connection. A bare verb is still
|
|
understood, because that is what earlier versions sent and what a stale KDE
|
|
shortcut may still send.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
from PyQt6.QtNetwork import QLocalSocket
|
|
|
|
SERVER_NAME = "dikte-" + (
|
|
str(os.getuid()) if hasattr(os, "getuid")
|
|
else os.environ.get("USERNAME", "user"))
|
|
|
|
# Long enough for a process that is already running to answer, short enough that
|
|
# "nothing is running" is not a noticeable pause in front of a key press.
|
|
CONNECT_MS = 800
|
|
|
|
|
|
def script_path():
|
|
return os.path.realpath(
|
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "dikte.py")
|
|
)
|
|
|
|
|
|
def command_for(verb):
|
|
"""The command line a KDE shortcut runs for one of the verbs."""
|
|
return f"{sys.executable} {script_path()} {verb}"
|
|
|
|
|
|
def send(cmd, wait=False, timeout=0, **args):
|
|
"""Send one request; the reply, or None when no instance is running.
|
|
|
|
`wait` asks the instance to hold its reply back until the job the request
|
|
started is over, which is how a terminal gets the transcript rather than
|
|
only the fact that recording began. `timeout` bounds that wait in seconds;
|
|
0 waits for as long as the job takes.
|
|
"""
|
|
sock = QLocalSocket()
|
|
sock.connectToServer(SERVER_NAME)
|
|
if not sock.waitForConnected(CONNECT_MS):
|
|
return None
|
|
|
|
request = {"cmd": cmd}
|
|
request.update({key: value for key, value in args.items() if value is not None})
|
|
if wait:
|
|
request["wait"] = True
|
|
# A verb carrying nothing goes as the bare word it used to be, so that an
|
|
# instance still running the older code obeys it: that is the one request
|
|
# that has to work across an update, since it is how you install the update.
|
|
line = cmd if list(request) == ["cmd"] else json.dumps(request)
|
|
sock.write((line + "\n").encode("utf-8"))
|
|
sock.flush()
|
|
sock.waitForBytesWritten(CONNECT_MS)
|
|
|
|
limit = (int(timeout * 1000) if timeout else -1) if wait else CONNECT_MS
|
|
buffer = b""
|
|
while b"\n" not in buffer:
|
|
if not sock.waitForReadyRead(limit):
|
|
break
|
|
buffer += bytes(sock.readAll())
|
|
sock.disconnectFromServer()
|
|
|
|
line = buffer.decode("utf-8", "replace").strip()
|
|
if not line:
|
|
# An instance from before replies existed answers by staying silent, and
|
|
# for a fire-and-forget verb that silence means it went through. A wait
|
|
# that ends this way did not: the run never reported back.
|
|
return ({"ok": False, "legacy": True,
|
|
"error": "the running instance is too old to answer; "
|
|
"reload it with: dikte restart"}
|
|
if wait else {"ok": True, "legacy": True})
|
|
try:
|
|
reply = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
return {"ok": True, "legacy": True}
|
|
return reply if isinstance(reply, dict) else {"ok": True, "legacy": True}
|