Compare commits

...

2 Commits

Author SHA1 Message Date
Joshua Yuen 72d733ba7d
Format Nix files with nixfmt 2026-09-12 12:46:47 -04:00
Joshua Yuen 0efda59204
Add hold-to-talk voice dictation 2026-09-12 12:46:30 -04:00
7 changed files with 189 additions and 18 deletions

View File

@ -1,31 +1,46 @@
# Do not modify this file! It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" "usb_storage" "sd_mod" ];
boot.initrd.availableKernelModules = [
"xhci_pci"
"thunderbolt"
"nvme"
"usb_storage"
"sd_mod"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/041a9bc5-aae2-44ec-bacf-7f6a08a696f9";
fileSystems."/" = {
device = "/dev/disk/by-uuid/041a9bc5-aae2-44ec-bacf-7f6a08a696f9";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/79D1-97BE";
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/79D1-97BE";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
options = [
"fmask=0077"
"dmask=0077"
];
};
swapDevices =
[ { device = "/dev/disk/by-uuid/82b669f5-d624-4a29-bf18-dd8bc453f537"; }
swapDevices = [
{ device = "/dev/disk/by-uuid/82b669f5-d624-4a29-bf18-dd8bc453f537"; }
];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";

View File

@ -5,6 +5,7 @@
imports = [
./direnv.nix
./discord.nix
./dictation.nix
./firefox.nix
./gpg.nix
./fzf.nix

128
modules/home/dictation.nix Normal file
View File

@ -0,0 +1,128 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.modules.dictation;
hyprlandCfg = config.modules.hyprland;
models = {
"tiny.en" = pkgs.fetchurl {
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin";
hash = "sha256-kh5M+Ghv3Zk9zQgaXaW2w2W/3hFi5ysI11rHUomSCx8=";
};
"base.en" = pkgs.fetchurl {
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin";
hash = "sha256-oDd5yG3zMjB19eeWyyzlAp8A7Ihp7uP9+4l6/jbG0AI=";
};
"small.en" = pkgs.fetchurl {
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.en.bin";
hash = "sha256-xhONbVjsyDIgl+D5h8MvG+i7ChhTKj+I9zTRu/nEHl0=";
};
};
runtimeDeps = with pkgs; [
coreutils
gnused
libnotify
pipewire
whisper-cpp
wtype
];
dictation = pkgs.writeShellScriptBin "dictation" ''
PATH=${lib.makeBinPath runtimeDeps}:''${PATH:-}
set -euo pipefail
model="${models.${cfg.model}}"
dir="''${XDG_RUNTIME_DIR:-/tmp}/dictation"
mkdir -p "$dir"
pidfile="$dir/pid"
wav="$dir/clip.wav"
notify() {
notify-send -t 2500 "Dictation" "$1" 2>/dev/null || true
}
case "''${1:-}" in
start)
if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
exit 0
fi
rm -f "$pidfile" "$wav"
notify "Listening..."
pw-record --rate 16000 --channels 1 --format s16 "$wav" >/dev/null 2>&1 &
pid=$!
echo "$pid" > "$pidfile"
disown
sleep 0.3
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pidfile"
notify "Microphone unavailable"
fi
;;
stop)
[ -f "$pidfile" ] || exit 0
pid="$(cat "$pidfile")"
rm -f "$pidfile"
if kill -0 "$pid" 2>/dev/null; then
kill -INT "$pid" 2>/dev/null || true
for _ in $(seq 1 40); do
kill -0 "$pid" 2>/dev/null || break
sleep 0.05
done
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true
fi
sleep 0.1
if [ ! -f "$wav" ]; then
notify "Recording missing"
exit 0
fi
# Ignore clips shorter than 0.5s (16 kHz, s16, mono: 32000 B/s + 44 B header)
size="$(stat -c %s "$wav" 2>/dev/null || echo 0)"
if [ "$size" -lt 16044 ]; then
rm -f "$wav"
exit 0
fi
if ! text="$(whisper-cli -np -nt -t "$(nproc)" -m "$model" -f "$wav" 2>/dev/null \
| sed -e 's/([^)]*)//g' -e 's/\[[^]]*\]//g' \
| tr '\n' ' ' \
| sed -e 's/ */ /g' -e 's/^ *//' -e 's/ *$//')"; then
rm -f "$wav"
notify "Transcription failed"
exit 1
fi
rm -f "$wav"
if [ -z "$text" ]; then
notify "No speech detected"
exit 0
fi
wtype -- "$text" || notify "Could not type transcription"
;;
*)
echo "usage: dictation {start|stop}" >&2
exit 1
;;
esac
'';
in
{
config = mkIf cfg.enable {
home.packages = [ dictation ];
wayland.windowManager.hyprland = mkIf hyprlandCfg.enable {
settings = {
bind = [ "SUPER, T, exec, dictation start" ];
bindr = [ "SUPER, T, exec, dictation stop" ];
};
};
};
}

View File

@ -4,6 +4,7 @@
{
imports = [
./discord.nix
./dictation.nix
./display.nix
./gnomeBoxes.nix
./gpg.nix

View File

@ -0,0 +1,24 @@
{
lib,
...
}:
with lib;
{
options.modules.dictation = {
enable = mkEnableOption {
default = false;
description = "Enable hold-to-talk voice dictation (hold SUPER+T)";
};
model = mkOption {
type = types.enum [
"tiny.en"
"base.en"
"small.en"
];
default = "base.en";
description = ''
The local Whisper model to use for transcription.
'';
};
};
}

View File

@ -22,5 +22,7 @@
modules.discord.enable = true;
modules.dictation.enable = true;
modules.tailscale.enable = true;
}