add voxtype

This commit is contained in:
Joakim Repomaa
2026-03-07 12:51:03 +02:00
parent a05fd03aa8
commit 9fbe748aa1
6 changed files with 264 additions and 4 deletions

View File

@@ -0,0 +1,92 @@
{
config,
osConfig,
lib,
pkgs,
pkgs-unstable,
...
}:
let
cfg = config.programs.voxtype;
postProcessUnwrapped =
pkgs.runCommand "voxtype-post-process-unwrapped"
{
code = ./post-process.cr;
nativeBuildInputs = [
pkgs-unstable.crystal
];
}
''
mkdir -p $out/bin
crystal build $code -o $out/bin/voxtype-post-process
'';
postProcess = pkgs.symlinkJoin {
name = "voxtype-post-process";
paths = [ postProcessUnwrapped ];
nativeBuildInputs = [ pkgs.makeBinaryWrapper ];
postBuild = ''
wrapProgram $out/bin/voxtype-post-process \
--set OLLAMA_PORT ${toString osConfig.services.ollama.port} \
--set WALKER_BIN ${lib.getExe config.services.walker.package}
'';
meta.mainProgram = "voxtype-post-process";
};
in
{
options.programs.voxtype = {
postProcessing = lib.mkOption {
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Enable post-processing of transcriptions";
settings = lib.mkOption {
type = lib.types.submodule {
options = {
model = lib.mkOption {
type = lib.types.str;
description = "The ollama model to use for post-processing";
};
commonInstructions = lib.mkOption {
type = lib.types.str;
default = "no quotes, no emojis, no explanations";
description = "Instructions to include in every post-processing prompt";
};
prompts = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
title = lib.mkOption {
type = lib.types.str;
description = "A title for this prompt, used in the selector";
};
instructions = lib.mkOption {
type = lib.types.str;
description = "Instructions to include in the post-processing prompt, in addition to the common instructions";
};
};
}
);
default = [
{
title = "Clean up";
instructions = "Clean up this dictation. Remove filler words, fix grammar and punctuation. Output ONLY the cleaned text";
}
];
};
};
};
default = { };
};
};
};
};
};
config = lib.mkIf cfg.postProcessing.enable {
xdg.configFile."voxtype/post-processing.json".text = builtins.toJSON cfg.postProcessing.settings;
programs.voxtype.settings.output.post_process = {
command = lib.getExe postProcess;
timeout_ms = 5 * 60 * 1000; # 5 minutes
};
};
}

View File

@@ -0,0 +1,54 @@
require "json"
require "http/client"
struct OllamaResponse
include JSON::Serializable
getter response : String
end
struct Prompt
include JSON::Serializable
getter title : String
getter instructions : String
end
struct Config
include JSON::Serializable
getter model : String
getter prompts : Array(Prompt)
@[JSON::Field(key: "commonInstructions")]
getter common_instructions : String
end
config_path = "#{ENV.fetch("XDG_CONFIG_HOME", "~/.config")}/voxtype/post-processing.json"
config = File.open(config_path) { |file| Config.from_json(file) }
client = HTTP::Client.new("localhost", ENV.fetch("OLLAMA_PORT", "11434").to_i)
prompt_selection = Process.run(ENV["WALKER_BIN"], ["--dmenu"]) do |process|
config.prompts.each do |prompt|
process.input.puts prompt.title
end
process.input.close
process.output.gets_to_end.chomp
end
instructions = config.prompts.find { |prompt| prompt.title == prompt_selection }.try(&.instructions) || prompt_selection
payload = {
model: config.model,
prompt: "#{instructions} - #{config.common_instructions}:\n\n#{STDIN.gets_to_end.chomp}",
think: false,
stream: false,
}
client.post("/api/generate", body: payload.to_json) do |response|
if response.status_code == 200
puts OllamaResponse.from_json(response.body_io).response.strip
else
abort "Ollama API error: #{response.status_code} #{response.body}"
end
end