Merge branch 'master' of ssh://git.joshuabell.xyz:3032/ringofstorms/dotfiles

This commit is contained in:
RingOfStorms (Joshua Bell) 2026-01-16 12:04:03 -06:00
commit 28656f137d
20 changed files with 4499 additions and 102 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@
modules/ modules/
**/result **/result
*.qcow *.qcow
**/target

View file

@ -149,6 +149,9 @@ in
kdePackages.plasma-browser-integration kdePackages.plasma-browser-integration
# kdePackages.plasma-workspace-wallpapers # kdePackages.plasma-workspace-wallpapers
# On-screen keyboard (Plasma Wayland)
kdePackages.plasma-keyboard
# Panel applets required for widgets # Panel applets required for widgets
kdePackages.plasma-nm # org.kde.plasma.networkmanagement kdePackages.plasma-nm # org.kde.plasma.networkmanagement
kdePackages.bluedevil # org.kde.plasma.bluetooth kdePackages.bluedevil # org.kde.plasma.bluetooth
@ -206,16 +209,6 @@ in
''; '';
}) })
(mkIf ((length cfg.wallpapers) > 0) {
environment.etc."xdg/plasma-org.kde.plasma.desktop-appletsrc".text =
let
wallpaperPath = builtins.head cfg.wallpapers;
in
''
[Containments][1][Wallpaper][org.kde.image][General]
Image=file://${wallpaperPath}
'';
})
# GPU blocks # GPU blocks
(mkIf cfg.gpu.amd.enable { (mkIf cfg.gpu.amd.enable {
@ -290,6 +283,21 @@ in
"Groups/0/Items/0".Name = "keyboard-us"; "Groups/0/Items/0".Name = "keyboard-us";
"Groups/0/Items/1".Name = "mozc"; "Groups/0/Items/1".Name = "mozc";
}; };
# Disable emoji picker to prevent Super+. conflict with desktop shortcuts
addons = {
keyboard = {
globalSection = {
EnableEmoji = "False";
EnableQuickPhraseEmoji = "False";
};
};
unicode = {
globalSection = {
# Disable unicode picker trigger key
TriggerKey = "";
};
};
};
}; };
}; };
}; };

View file

@ -5,7 +5,9 @@
}: }:
let let
cfg = osConfig.ringofstorms.dePlasma; cfg = osConfig.ringofstorms.dePlasma;
inherit (lib) mkIf; inherit (lib) mkIf optionalAttrs;
# Get the first wallpaper from the list if available
wallpaper = if (builtins.length cfg.wallpapers) > 0 then builtins.head cfg.wallpapers else null;
in in
{ {
imports = [ imports = [
@ -294,6 +296,8 @@ in
lookAndFeel = "org.kde.breezedark.desktop"; lookAndFeel = "org.kde.breezedark.desktop";
theme = "breeze-dark"; theme = "breeze-dark";
cursor.theme = "breeze_cursors"; cursor.theme = "breeze_cursors";
} // optionalAttrs (wallpaper != null) {
wallpaper = wallpaper;
}; };
configFile = { configFile = {

108
flakes/stt_ime/README.md Normal file
View file

@ -0,0 +1,108 @@
# stt_ime - Speech-to-Text Input Method for Fcitx5
Local, privacy-preserving speech-to-text that integrates as a native Fcitx5 input method.
## Components
- **stt-stream**: Rust CLI that captures audio, runs VAD, and transcribes with Whisper
- **fcitx5-stt**: C++ Fcitx5 addon that spawns stt-stream and commits text to apps
## Modes
- **Manual**: Press `Ctrl+Space` or `Ctrl+R` to start/stop recording
- **Oneshot**: Automatically starts on speech, commits on silence, then resets
- **Continuous**: Always listening, commits each utterance automatically
Press `Ctrl+M` while STT is active to cycle between modes.
## Keys (when STT input method is active)
| Key | Action |
|-----|--------|
| `Ctrl+Space` / `Ctrl+R` | Toggle recording (manual mode) |
| `Ctrl+M` | Cycle mode (manual → oneshot → continuous) |
| `Enter` | Accept current preedit text |
| `Escape` | Cancel recording / clear preedit |
## Usage
### NixOS Module
```nix
# In your host's flake.nix inputs:
stt_ime.url = "git+https://git.ros.one/josh/nixos-config?dir=flakes/stt_ime";
# In your NixOS config:
{
imports = [ inputs.stt_ime.nixosModules.default ];
ringofstorms.sttIme = {
enable = true;
model = "base.en"; # tiny, base, small, medium, large-v3 (add .en for English-only)
useGpu = false; # set true for CUDA acceleration
};
}
```
### Standalone CLI
```bash
# Run with default settings (manual mode)
stt-stream
# Run in continuous mode
stt-stream --mode continuous
# Use a specific model
stt-stream --model small-en
# Commands via stdin (manual mode):
echo "start" | stt-stream # begin recording
echo "stop" | stt-stream # stop and transcribe
echo "cancel" | stt-stream # cancel without transcribing
echo "shutdown" | stt-stream # exit
```
### Output Format (NDJSON)
```json
{"type":"ready"}
{"type":"recording_started"}
{"type":"partial","text":"hello worl"}
{"type":"partial","text":"hello world"}
{"type":"final","text":"Hello world."}
{"type":"recording_stopped"}
{"type":"shutdown"}
```
## Models
Models are automatically downloaded from Hugging Face on first run and cached in `~/.cache/stt-stream/models/`.
| Model | Size | Speed | Quality |
|-------|------|-------|---------|
| tiny.en | ~75MB | Fastest | Basic |
| base.en | ~150MB | Fast | Good (default) |
| small.en | ~500MB | Medium | Better |
| medium.en | ~1.5GB | Slow | Great |
| large-v3 | ~3GB | Slowest | Best (multilingual) |
## Environment Variables
- `STT_STREAM_MODEL_PATH`: Path to a specific model file
- `STT_STREAM_MODEL`: Model name (overridden by CLI)
- `STT_STREAM_USE_GPU`: Set to "1" for GPU acceleration
## Building
```bash
cd flakes/stt_ime
nix build .#stt-stream # Rust CLI only
nix build .#fcitx5-stt # Fcitx5 addon (includes stt-stream)
nix build # Default: fcitx5-stt
```
## Integration with de_plasma
The addon is automatically added to Fcitx5 when `ringofstorms.sttIme.enable = true`.
It appears as "Speech to Text" (STT) in the input method switcher alongside US and Mozc.

View file

@ -0,0 +1,42 @@
cmake_minimum_required(VERSION 3.16)
project(fcitx5-stt VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find Fcitx5
find_package(Fcitx5Core REQUIRED)
find_package(Fcitx5Utils REQUIRED)
# Path to stt-stream binary (set by Nix)
if(NOT DEFINED STT_STREAM_PATH)
set(STT_STREAM_PATH "stt-stream")
endif()
# Configure header with path
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
)
# Build the addon shared library
add_library(stt MODULE
src/stt.cpp
)
target_include_directories(stt PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(stt PRIVATE
Fcitx5::Core
Fcitx5::Utils
)
# Set output name without "lib" prefix
set_target_properties(stt PROPERTIES PREFIX "")
# Install targets - use standard paths, Nix postInstall will handle fcitx5 paths
install(TARGETS stt DESTINATION lib/fcitx5)
install(FILES data/stt.conf DESTINATION share/fcitx5/addon)
install(FILES data/stt-inputmethod.conf DESTINATION share/fcitx5/inputmethod RENAME stt.conf)

View file

@ -0,0 +1,7 @@
[InputMethod]
Name=Speech to Text
Icon=audio-input-microphone
Label=STT
LangCode=
Addon=stt
Configurable=False

View file

@ -0,0 +1,7 @@
[Addon]
Name=stt
Category=InputMethod
Library=stt
Type=SharedLibrary
OnDemand=True
Configurable=False

View file

@ -0,0 +1,4 @@
#pragma once
// Path to stt-stream binary
#define STT_STREAM_PATH "@STT_STREAM_PATH@"

View file

@ -0,0 +1,533 @@
/*
* fcitx5-stt: Speech-to-Text Input Method Engine for Fcitx5
*
* This is a thin shim that spawns the stt-stream Rust binary and
* bridges its JSON events to Fcitx5's input method API.
*
* Modes:
* - Oneshot: Record until silence, commit, reset
* - Continuous: Always listen, commit on silence
* - Manual: Start/stop via hotkey
*
* UX:
* - Partial text shown as preedit (underlined)
* - Final text committed on stop/silence
* - Escape cancels without committing
* - Enter accepts current preedit
*/
#include <fcitx/addonfactory.h>
#include <fcitx/addonmanager.h>
#include <fcitx/inputcontext.h>
#include <fcitx/inputcontextmanager.h>
#include <fcitx/inputmethodengine.h>
#include <fcitx/inputpanel.h>
#include <fcitx/instance.h>
#include <fcitx-utils/event.h>
#include <fcitx-utils/i18n.h>
#include <fcitx-utils/log.h>
#include <fcitx-utils/utf8.h>
#include <memory>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <cstring>
#include <sstream>
#include "config.h"
namespace {
FCITX_DEFINE_LOG_CATEGORY(stt_log, "stt");
#define STT_DEBUG() FCITX_LOGC(stt_log, Debug)
#define STT_INFO() FCITX_LOGC(stt_log, Info)
#define STT_WARN() FCITX_LOGC(stt_log, Warn)
#define STT_ERROR() FCITX_LOGC(stt_log, Error)
// Operating modes
enum class SttMode {
Oneshot,
Continuous,
Manual
};
// Simple JSON parsing (we only need a few fields)
struct JsonEvent {
std::string type;
std::string text;
std::string message;
static JsonEvent parse(const std::string& line) {
JsonEvent ev;
// Very basic JSON parsing - find "type" and "text" fields
auto findValue = [&line](const std::string& key) -> std::string {
std::string search = "\"" + key + "\":\"";
auto pos = line.find(search);
if (pos == std::string::npos) return "";
pos += search.length();
auto end = line.find("\"", pos);
if (end == std::string::npos) return "";
return line.substr(pos, end - pos);
};
ev.type = findValue("type");
ev.text = findValue("text");
ev.message = findValue("message");
return ev;
}
};
} // anonymous namespace
class SttEngine;
class SttState : public fcitx::InputContextProperty {
public:
SttState(SttEngine* engine, fcitx::InputContext* ic)
: engine_(engine), ic_(ic) {}
void setPreedit(const std::string& text);
void commit(const std::string& text);
void clear();
bool isRecording() const { return recording_; }
void setRecording(bool r) { recording_ = r; }
const std::string& preeditText() const { return preedit_; }
private:
SttEngine* engine_;
fcitx::InputContext* ic_;
std::string preedit_;
bool recording_ = false;
};
class SttEngine : public fcitx::InputMethodEngineV2 {
public:
SttEngine(fcitx::Instance* instance);
~SttEngine() override;
// InputMethodEngine interface
void activate(const fcitx::InputMethodEntry& entry,
fcitx::InputContextEvent& event) override;
void deactivate(const fcitx::InputMethodEntry& entry,
fcitx::InputContextEvent& event) override;
void keyEvent(const fcitx::InputMethodEntry& entry,
fcitx::KeyEvent& keyEvent) override;
void reset(const fcitx::InputMethodEntry& entry,
fcitx::InputContextEvent& event) override;
// List input methods this engine provides
std::vector<fcitx::InputMethodEntry> listInputMethods() override {
std::vector<fcitx::InputMethodEntry> result;
result.emplace_back(
"stt", // unique name
_("Speech to Text"), // display name
"*", // language (any)
"stt" // addon name
);
return result;
}
fcitx::Instance* instance() { return instance_; }
// Process management
void startProcess();
void stopProcess();
void sendCommand(const std::string& cmd);
// Mode
SttMode mode() const { return mode_; }
void setMode(SttMode m);
void cycleMode();
private:
void onProcessOutput();
void handleEvent(const JsonEvent& ev);
fcitx::Instance* instance_;
fcitx::FactoryFor<SttState> factory_;
// Process state
pid_t childPid_ = -1;
int stdinFd_ = -1;
int stdoutFd_ = -1;
std::unique_ptr<fcitx::EventSourceIO> ioEvent_;
std::string readBuffer_;
// Mode
SttMode mode_ = SttMode::Oneshot;
// Current state
bool ready_ = false;
fcitx::InputContext* activeIc_ = nullptr;
};
// SttState implementation
void SttState::setPreedit(const std::string& text) {
preedit_ = text;
if (ic_->hasFocus()) {
fcitx::Text preeditText;
preeditText.append(text, fcitx::TextFormatFlag::Underline);
preeditText.setCursor(text.length());
ic_->inputPanel().setClientPreedit(preeditText);
ic_->updatePreedit();
}
}
void SttState::commit(const std::string& text) {
if (!text.empty() && ic_->hasFocus()) {
ic_->commitString(text);
}
clear();
}
void SttState::clear() {
preedit_.clear();
if (ic_->hasFocus()) {
ic_->inputPanel().reset();
ic_->updatePreedit();
ic_->updateUserInterface(fcitx::UserInterfaceComponent::InputPanel);
}
}
// SttEngine implementation
SttEngine::SttEngine(fcitx::Instance* instance)
: instance_(instance),
factory_([this](fcitx::InputContext& ic) {
return new SttState(this, &ic);
}) {
instance_->inputContextManager().registerProperty("sttState", &factory_);
STT_INFO() << "SttEngine initialized";
}
SttEngine::~SttEngine() {
stopProcess();
}
void SttEngine::activate(const fcitx::InputMethodEntry& entry,
fcitx::InputContextEvent& event) {
FCITX_UNUSED(entry);
auto* ic = event.inputContext();
activeIc_ = ic;
STT_INFO() << "STT activated";
// Start the backend process if not running
if (childPid_ < 0) {
startProcess();
}
// In continuous mode, start recording automatically
if (mode_ == SttMode::Continuous && ready_) {
sendCommand("start");
auto* state = ic->propertyFor(&factory_);
state->setRecording(true);
}
}
void SttEngine::deactivate(const fcitx::InputMethodEntry& entry,
fcitx::InputContextEvent& event) {
FCITX_UNUSED(entry);
auto* ic = event.inputContext();
auto* state = ic->propertyFor(&factory_);
// Stop recording if active
if (state->isRecording()) {
sendCommand("cancel");
state->setRecording(false);
}
state->clear();
activeIc_ = nullptr;
STT_INFO() << "STT deactivated";
}
void SttEngine::keyEvent(const fcitx::InputMethodEntry& entry,
fcitx::KeyEvent& keyEvent) {
FCITX_UNUSED(entry);
auto* ic = keyEvent.inputContext();
auto* state = ic->propertyFor(&factory_);
// Handle special keys
if (keyEvent.isRelease()) {
return;
}
auto key = keyEvent.key();
// Escape: cancel recording/preedit
if (key.check(FcitxKey_Escape)) {
if (state->isRecording() || !state->preeditText().empty()) {
sendCommand("cancel");
state->setRecording(false);
state->clear();
keyEvent.filterAndAccept();
return;
}
}
// Enter/Return: accept preedit
if (key.check(FcitxKey_Return) || key.check(FcitxKey_KP_Enter)) {
if (!state->preeditText().empty()) {
state->commit(state->preeditText());
sendCommand("cancel");
state->setRecording(false);
keyEvent.filterAndAccept();
return;
}
}
// Space or Ctrl+R: toggle recording (in manual mode)
if (mode_ == SttMode::Manual) {
if (key.check(FcitxKey_space, fcitx::KeyState::Ctrl) ||
key.check(FcitxKey_r, fcitx::KeyState::Ctrl)) {
if (state->isRecording()) {
sendCommand("stop");
state->setRecording(false);
} else {
state->clear();
sendCommand("start");
state->setRecording(true);
}
keyEvent.filterAndAccept();
return;
}
}
// Ctrl+M: cycle mode
if (key.check(FcitxKey_m, fcitx::KeyState::Ctrl)) {
cycleMode();
keyEvent.filterAndAccept();
return;
}
// In recording state, absorb most keys
if (state->isRecording()) {
keyEvent.filterAndAccept();
return;
}
}
void SttEngine::reset(const fcitx::InputMethodEntry& entry,
fcitx::InputContextEvent& event) {
FCITX_UNUSED(entry);
auto* ic = event.inputContext();
auto* state = ic->propertyFor(&factory_);
state->clear();
}
void SttEngine::startProcess() {
if (childPid_ > 0) {
return; // Already running
}
int stdinPipe[2];
int stdoutPipe[2];
if (pipe(stdinPipe) < 0 || pipe(stdoutPipe) < 0) {
STT_ERROR() << "Failed to create pipes";
return;
}
pid_t pid = fork();
if (pid < 0) {
STT_ERROR() << "Failed to fork";
close(stdinPipe[0]);
close(stdinPipe[1]);
close(stdoutPipe[0]);
close(stdoutPipe[1]);
return;
}
if (pid == 0) {
// Child process
close(stdinPipe[1]);
close(stdoutPipe[0]);
dup2(stdinPipe[0], STDIN_FILENO);
dup2(stdoutPipe[1], STDOUT_FILENO);
close(stdinPipe[0]);
close(stdoutPipe[1]);
// Determine mode string
const char* modeStr = "manual";
switch (mode_) {
case SttMode::Oneshot: modeStr = "oneshot"; break;
case SttMode::Continuous: modeStr = "continuous"; break;
case SttMode::Manual: modeStr = "manual"; break;
}
execlp(STT_STREAM_PATH, "stt-stream", "--mode", modeStr, nullptr);
_exit(127);
}
// Parent process
close(stdinPipe[0]);
close(stdoutPipe[1]);
childPid_ = pid;
stdinFd_ = stdinPipe[1];
stdoutFd_ = stdoutPipe[0];
// Set stdout non-blocking
int flags = fcntl(stdoutFd_, F_GETFL, 0);
fcntl(stdoutFd_, F_SETFL, flags | O_NONBLOCK);
// Watch stdout for events
ioEvent_ = instance_->eventLoop().addIOEvent(
stdoutFd_,
fcitx::IOEventFlag::In,
[this](fcitx::EventSourceIO*, int, fcitx::IOEventFlags) {
onProcessOutput();
return true;
}
);
STT_INFO() << "Started stt-stream process (pid=" << childPid_ << ")";
}
void SttEngine::stopProcess() {
if (childPid_ < 0) {
return;
}
ioEvent_.reset();
sendCommand("shutdown");
close(stdinFd_);
close(stdoutFd_);
// Wait for child to exit
int status;
waitpid(childPid_, &status, 0);
stdinFd_ = -1;
stdoutFd_ = -1;
childPid_ = -1;
ready_ = false;
STT_INFO() << "Stopped stt-stream process";
}
void SttEngine::sendCommand(const std::string& cmd) {
if (stdinFd_ < 0) {
return;
}
std::string line = cmd + "\n";
write(stdinFd_, line.c_str(), line.length());
}
void SttEngine::onProcessOutput() {
char buf[4096];
ssize_t n;
while ((n = read(stdoutFd_, buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
readBuffer_ += buf;
// Process complete lines
size_t pos;
while ((pos = readBuffer_.find('\n')) != std::string::npos) {
std::string line = readBuffer_.substr(0, pos);
readBuffer_ = readBuffer_.substr(pos + 1);
if (!line.empty()) {
auto ev = JsonEvent::parse(line);
handleEvent(ev);
}
}
}
}
void SttEngine::handleEvent(const JsonEvent& ev) {
STT_DEBUG() << "Event: type=" << ev.type << " text=" << ev.text;
if (ev.type == "ready") {
ready_ = true;
STT_INFO() << "stt-stream ready";
} else if (ev.type == "recording_started") {
// Update UI to show recording state
if (activeIc_) {
auto* state = activeIc_->propertyFor(&factory_);
state->setRecording(true);
}
} else if (ev.type == "recording_stopped") {
if (activeIc_) {
auto* state = activeIc_->propertyFor(&factory_);
state->setRecording(false);
}
} else if (ev.type == "partial") {
if (activeIc_) {
auto* state = activeIc_->propertyFor(&factory_);
state->setPreedit(ev.text);
}
} else if (ev.type == "final") {
if (activeIc_) {
auto* state = activeIc_->propertyFor(&factory_);
state->commit(ev.text);
state->setRecording(false);
// In oneshot mode, we're done
// In continuous mode, keep listening
if (mode_ == SttMode::Continuous && ready_) {
sendCommand("start");
state->setRecording(true);
}
}
} else if (ev.type == "error") {
STT_ERROR() << "stt-stream error: " << ev.message;
} else if (ev.type == "shutdown") {
ready_ = false;
}
}
void SttEngine::setMode(SttMode m) {
if (mode_ == m) return;
mode_ = m;
// Notify the backend
const char* modeStr = "manual";
switch (m) {
case SttMode::Oneshot: modeStr = "oneshot"; break;
case SttMode::Continuous: modeStr = "continuous"; break;
case SttMode::Manual: modeStr = "manual"; break;
}
std::string cmd = "{\"cmd\":\"set_mode\",\"mode\":\"";
cmd += modeStr;
cmd += "\"}";
sendCommand(cmd);
STT_INFO() << "Mode changed to: " << modeStr;
}
void SttEngine::cycleMode() {
switch (mode_) {
case SttMode::Manual:
setMode(SttMode::Oneshot);
break;
case SttMode::Oneshot:
setMode(SttMode::Continuous);
break;
case SttMode::Continuous:
setMode(SttMode::Manual);
break;
}
}
// Addon factory
class SttEngineFactory : public fcitx::AddonFactory {
public:
fcitx::AddonInstance* create(fcitx::AddonManager* manager) override {
return new SttEngine(manager->instance());
}
};
FCITX_ADDON_FACTORY(SttEngineFactory);

77
flakes/stt_ime/flake.lock generated Normal file
View file

@ -0,0 +1,77 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1768319649,
"narHash": "sha256-VFkNyxHxkqGp8gf8kfFMW1j6XeBy609kv6TE9uF/0Js=",
"owner": "ipetkov",
"repo": "crane",
"rev": "4b6527687cfd20da3c2ef8287e01b74c2d6c705b",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1768127708,
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

266
flakes/stt_ime/flake.nix Normal file
View file

@ -0,0 +1,266 @@
{
description = "Local speech-to-text input method for Fcitx5";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
crane.url = "github:ipetkov/crane";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
crane,
flake-utils,
...
}:
let
# Systems we support
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
];
in
flake-utils.lib.eachSystem supportedSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
craneLib = crane.mkLib pkgs;
# Common build inputs for stt-stream
commonNativeBuildInputs = with pkgs; [
pkg-config
cmake
git # required by whisper-rs-sys build
];
commonBuildInputs = with pkgs; [
alsa-lib
openssl
];
# CPU-only build (default)
stt-stream = craneLib.buildPackage {
pname = "stt-stream";
version = "0.1.0";
src = craneLib.cleanCargoSource ./stt-stream;
nativeBuildInputs = commonNativeBuildInputs ++ (with pkgs; [
clang
llvmPackages.libclang
]);
buildInputs = commonBuildInputs ++ (with pkgs; [
openblas
]);
# For bindgen to find libclang
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
};
# GPU build with ROCm/HIP support (AMD GPUs)
stt-stream-hip = craneLib.buildPackage {
pname = "stt-stream-hip";
version = "0.1.0";
src = craneLib.cleanCargoSource ./stt-stream;
nativeBuildInputs = commonNativeBuildInputs ++ (with pkgs; [
# ROCm toolchain - clr contains the properly wrapped hipcc
rocmPackages.clr
# rocminfo provides rocm_agent_enumerator which hipcc needs
rocmPackages.rocminfo
]);
buildInputs = commonBuildInputs ++ (with pkgs; [
# ROCm/HIP libraries needed at link time
rocmPackages.clr # HIP runtime
rocmPackages.hipblas
rocmPackages.rocblas
rocmPackages.rocm-runtime
rocmPackages.rocm-device-libs
rocmPackages.rocm-comgr
]);
# Enable hipblas feature
cargoExtraArgs = "--features hipblas";
# The clr package's hipcc is already wrapped with all the right paths,
# but we need LIBCLANG_PATH for bindgen
LIBCLANG_PATH = "${pkgs.rocmPackages.llvm.clang}/lib";
# Target common AMD GPU architectures (user can override via AMDGPU_TARGETS)
# gfx1030 = RX 6000 series, gfx1100 = RX 7000 series, gfx906/gfx908 = MI50/MI100
AMDGPU_TARGETS = "gfx1030;gfx1100";
};
# Fcitx5 C++ shim addon
fcitx5-stt = pkgs.stdenv.mkDerivation {
pname = "fcitx5-stt";
version = "0.1.0";
src = ./fcitx5-stt;
nativeBuildInputs = with pkgs; [
cmake
extra-cmake-modules
pkg-config
];
buildInputs = with pkgs; [
fcitx5
];
cmakeFlags = [
"-DSTT_STREAM_PATH=${stt-stream}/bin/stt-stream"
];
# Install to fcitx5 addon paths
postInstall = ''
mkdir -p $out/share/fcitx5/addon
mkdir -p $out/share/fcitx5/inputmethod
mkdir -p $out/lib/fcitx5
'';
};
# Fcitx5 addon variant using HIP-accelerated stt-stream
fcitx5-stt-hip = pkgs.stdenv.mkDerivation {
pname = "fcitx5-stt-hip";
version = "0.1.0";
src = ./fcitx5-stt;
nativeBuildInputs = with pkgs; [
cmake
extra-cmake-modules
pkg-config
];
buildInputs = with pkgs; [
fcitx5
];
cmakeFlags = [
"-DSTT_STREAM_PATH=${stt-stream-hip}/bin/stt-stream"
];
postInstall = ''
mkdir -p $out/share/fcitx5/addon
mkdir -p $out/share/fcitx5/inputmethod
mkdir -p $out/lib/fcitx5
'';
};
in
{
packages = {
inherit stt-stream stt-stream-hip fcitx5-stt fcitx5-stt-hip;
default = fcitx5-stt;
};
# Expose as runnable apps
apps = {
stt-stream = {
type = "app";
program = "${stt-stream}/bin/stt-stream";
};
stt-stream-hip = {
type = "app";
program = "${stt-stream-hip}/bin/stt-stream";
};
default = {
type = "app";
program = "${stt-stream}/bin/stt-stream";
};
};
devShells.default = pkgs.mkShell {
inputsFrom = [ stt-stream ];
packages = with pkgs; [
rust-analyzer
rustfmt
clippy
fcitx5
];
};
# Dev shell with ROCm/HIP for GPU development
devShells.hip = pkgs.mkShell {
inputsFrom = [ stt-stream-hip ];
packages = with pkgs; [
rust-analyzer
rustfmt
clippy
fcitx5
rocmPackages.rocminfo # For debugging GPU detection
];
};
}
)
// {
# NixOS module for integration
nixosModules.default =
{
config,
lib,
pkgs,
...
}:
let
cfg = config.ringofstorms.sttIme;
sttPkgs = self.packages.${pkgs.stdenv.hostPlatform.system};
# Select the appropriate package variant based on GPU backend
sttStreamPkg =
if cfg.gpuBackend == "hip" then sttPkgs.stt-stream-hip
else sttPkgs.stt-stream;
fcitx5SttPkg =
if cfg.gpuBackend == "hip" then sttPkgs.fcitx5-stt-hip
else sttPkgs.fcitx5-stt;
in
{
options.ringofstorms.sttIme = {
enable = lib.mkEnableOption "Speech-to-text input method for Fcitx5";
model = lib.mkOption {
type = lib.types.str;
default = "base.en";
description = "Whisper model to use (tiny, base, small, medium, large)";
};
gpuBackend = lib.mkOption {
type = lib.types.enum [ "cpu" "hip" ];
default = "cpu";
description = ''
GPU backend to use for acceleration:
- cpu: CPU-only (default, works everywhere)
- hip: AMD ROCm/HIP (requires AMD GPU with ROCm support)
'';
};
useGpu = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to request GPU acceleration at runtime (--gpu flag)";
};
};
config = lib.mkIf cfg.enable {
# Ensure fcitx5 addon is available
i18n.inputMethod.fcitx5.addons = [ fcitx5SttPkg ];
# Add STT to the Fcitx5 input method group
# This assumes de_plasma sets up Groups/0 with keyboard-us (0) and mozc (1)
i18n.inputMethod.fcitx5.settings.inputMethod = {
"Groups/0/Items/2".Name = "stt";
};
# Make stt-stream available system-wide
environment.systemPackages = [ sttStreamPkg ];
# Set default model via environment
environment.sessionVariables = {
STT_STREAM_MODEL = cfg.model;
STT_STREAM_GPU = if cfg.useGpu then "1" else "0";
};
};
};
};
}

2423
flakes/stt_ime/stt-stream/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
[package]
name = "stt-stream"
version = "0.1.0"
edition = "2021"
description = "Local speech-to-text streaming CLI for Fcitx5 integration"
license = "MIT"
[dependencies]
# Audio capture
cpal = "0.15"
# Resampling (48k -> 16k)
rubato = "0.15"
# Whisper inference
whisper-rs = "0.15"
# Voice activity detection
# Using silero via ONNX (reserved for future use)
# ort = { version = "2.0.0-rc.9", default-features = false, features = ["load-dynamic"] }
# ndarray = "0.16"
# Async runtime
tokio = { version = "1", features = ["full"] }
# CLI
clap = { version = "4", features = ["derive"] }
# Serialization for IPC protocol
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Error handling
anyhow = "1"
thiserror = "1"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Ring buffer for audio (reserved for future use)
# ringbuf = "0.4"
# For downloading models
hf-hub = "0.3"
[features]
default = []
cuda = ["whisper-rs/cuda"]
hipblas = ["whisper-rs/hipblas"]
metal = ["whisper-rs/metal"]
[profile.release]
lto = true
codegen-units = 1

View file

@ -0,0 +1,704 @@
//! stt-stream: Local speech-to-text streaming CLI
//!
//! Captures audio from microphone, performs VAD, transcribes with Whisper,
//! and outputs JSON events to stdout for Fcitx5 integration.
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use rubato::{FftFixedInOut, Resampler};
use serde::{Deserialize, Serialize};
use std::io::{BufRead, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tracing::{error, info, warn};
use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters};
/// Operating mode for the STT engine
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum Mode {
/// Record until silence, transcribe, then reset (one-shot)
Oneshot,
/// Always listen, emit text when speech detected (continuous)
Continuous,
/// Manual start/stop via stdin commands
Manual,
}
/// Whisper model size
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ModelSize {
Tiny,
TinyEn,
Base,
BaseEn,
Small,
SmallEn,
Medium,
MediumEn,
LargeV3,
}
impl ModelSize {
fn model_name(&self) -> &'static str {
match self {
ModelSize::Tiny => "tiny",
ModelSize::TinyEn => "tiny.en",
ModelSize::Base => "base",
ModelSize::BaseEn => "base.en",
ModelSize::Small => "small",
ModelSize::SmallEn => "small.en",
ModelSize::Medium => "medium",
ModelSize::MediumEn => "medium.en",
ModelSize::LargeV3 => "large-v3",
}
}
fn parse(input: &str) -> Option<Self> {
let normalized = input
.trim()
.to_lowercase()
.replace('.', "-")
.replace('_', "-");
match normalized.as_str() {
"tiny" => Some(ModelSize::Tiny),
"tiny-en" => Some(ModelSize::TinyEn),
"base" => Some(ModelSize::Base),
"base-en" => Some(ModelSize::BaseEn),
"small" => Some(ModelSize::Small),
"small-en" => Some(ModelSize::SmallEn),
"medium" => Some(ModelSize::Medium),
"medium-en" => Some(ModelSize::MediumEn),
"large-v3" => Some(ModelSize::LargeV3),
_ => None,
}
}
fn hf_repo(&self) -> &'static str {
"ggerganov/whisper.cpp"
}
fn hf_filename(&self) -> String {
format!("ggml-{}.bin", self.model_name())
}
}
#[derive(Parser, Debug)]
#[command(name = "stt-stream")]
#[command(about = "Local speech-to-text streaming for Fcitx5")]
struct Args {
/// Operating mode
#[arg(short, long, value_enum, default_value = "manual")]
mode: Mode,
/// Whisper model size
#[arg(short = 'M', long, value_enum, default_value = "base-en")]
model: ModelSize,
/// Path to whisper model file (overrides --model)
#[arg(long)]
model_path: Option<String>,
/// VAD threshold (0.0-1.0)
#[arg(long, default_value = "0.5")]
vad_threshold: f32,
/// Silence duration (ms) to end utterance
#[arg(long, default_value = "800")]
silence_ms: u64,
/// Emit partial transcripts while speaking
#[arg(long, default_value = "true")]
partials: bool,
/// Partial transcript interval (ms)
#[arg(long, default_value = "500")]
partial_interval_ms: u64,
/// Language code (e.g., "en", "ja", "auto")
#[arg(short, long, default_value = "en")]
language: String,
/// Use GPU acceleration
#[arg(long)]
gpu: bool,
/// Number of threads for transcription (default: auto-detect)
#[arg(long)]
threads: Option<i32>,
}
/// Events emitted to stdout as NDJSON
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SttEvent {
/// STT engine is ready
Ready,
/// Recording started
RecordingStarted,
/// Recording stopped
RecordingStopped,
/// Partial (unstable) transcript
Partial { text: String },
/// Final transcript
Final { text: String },
/// Error occurred
Error { message: String },
/// Engine shutting down
Shutdown,
}
/// Commands received from stdin as NDJSON
#[derive(Debug, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum SttCommand {
/// Start recording
Start,
/// Stop recording and transcribe
Stop,
/// Cancel current recording without transcribing
Cancel,
/// Shutdown the engine
Shutdown,
/// Switch mode
SetMode { mode: String },
}
fn emit_event(event: &SttEvent) {
if let Ok(json) = serde_json::to_string(event) {
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", json);
let _ = stdout.flush();
}
}
/// Simple energy-based VAD (placeholder for Silero VAD)
/// Returns true if the audio chunk likely contains speech
fn simple_vad(samples: &[f32], threshold: f32) -> bool {
if samples.is_empty() {
return false;
}
let energy: f32 = samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32;
let db = 10.0 * energy.max(1e-10).log10();
// Typical speech is around -20 to -10 dB, silence is < -40 dB
// Map threshold 0-1 to dB range -50 to -20
let threshold_db = -50.0 + (threshold * 30.0);
db > threshold_db
}
/// Download or locate the Whisper model
fn get_model_path(args: &Args) -> Result<String> {
if let Some(ref path) = args.model_path {
return Ok(path.clone());
}
// Check environment variable
if let Ok(path) = std::env::var("STT_STREAM_MODEL_PATH") {
if std::path::Path::new(&path).exists() {
return Ok(path);
}
}
// Check XDG cache
let cache_dir = dirs::cache_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("stt-stream")
.join("models");
let model_file = cache_dir.join(args.model.hf_filename());
if model_file.exists() {
return Ok(model_file.to_string_lossy().to_string());
}
// Download from Hugging Face
info!("Downloading model {} from Hugging Face...", args.model.model_name());
std::fs::create_dir_all(&cache_dir)?;
let api = hf_hub::api::sync::Api::new()?;
let repo = api.model(args.model.hf_repo().to_string());
let path = repo.get(&args.model.hf_filename())?;
Ok(path.to_string_lossy().to_string())
}
/// Audio processing state
struct AudioState {
/// Audio samples buffer (16kHz mono)
buffer: Vec<f32>,
/// Whether we're currently recording
is_recording: bool,
/// Whether speech was detected in current segment
speech_detected: bool,
/// Samples since last speech
silence_samples: usize,
/// Last partial emission time
last_partial: std::time::Instant,
/// Manual mode: stop requested, finalize next tick
pending_finalize: bool,
}
impl AudioState {
fn new() -> Self {
Self {
buffer: Vec::with_capacity(16000 * 30), // 30 seconds max
is_recording: false,
speech_detected: false,
silence_samples: 0,
last_partial: std::time::Instant::now(),
pending_finalize: false,
}
}
fn clear(&mut self) {
self.buffer.clear();
self.speech_detected = false;
self.silence_samples = 0;
self.pending_finalize = false;
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("stt_stream=info".parse().unwrap()),
)
.with_writer(std::io::stderr)
.init();
let mut args = Args::parse();
// Allow Nix/session configuration via env vars.
// Precedence: explicit CLI args > env vars > defaults.
//
// `ringofstorms.sttIme.model` uses dot notation (e.g. "tiny.en"),
// while clap's value enum expects kebab-case (e.g. "tiny-en").
let cli_has_model_flag = std::env::args().any(|a| a == "--model" || a == "-M");
if !cli_has_model_flag && args.model_path.is_none() {
if let Ok(model) = std::env::var("STT_STREAM_MODEL") {
if let Some(parsed) = ModelSize::parse(&model) {
args.model = parsed;
}
}
}
info!("Starting stt-stream with mode: {:?}", args.mode);
// Load Whisper model
let model_path = get_model_path(&args).context("Failed to get model path")?;
info!("Loading Whisper model from: {}", model_path);
// Configure GPU and context parameters
let mut ctx_params = WhisperContextParameters::default();
// Check for GPU env var override
let gpu_enabled = args.gpu || std::env::var("STT_STREAM_GPU").map(|v| v == "1" || v.to_lowercase() == "true").unwrap_or(false);
ctx_params.use_gpu(gpu_enabled);
if gpu_enabled {
ctx_params.flash_attn(true); // Enable flash attention for GPU acceleration
}
// Determine thread counts
let available_threads = std::thread::available_parallelism()
.map(|p| p.get() as i32)
.unwrap_or(4);
let final_threads = args.threads.unwrap_or(available_threads.min(8));
let partial_threads = (final_threads / 2).max(1);
// Log backend configuration
let gpu_feature_compiled = cfg!(feature = "hipblas") || cfg!(feature = "cuda") || cfg!(feature = "metal");
info!("Backend configuration:");
info!(" GPU requested: {}", gpu_enabled);
info!(" GPU feature compiled: {} (hipblas={}, cuda={}, metal={})",
gpu_feature_compiled,
cfg!(feature = "hipblas"),
cfg!(feature = "cuda"),
cfg!(feature = "metal")
);
info!(" Flash attention: {}", gpu_enabled);
info!(" Model: {:?}", args.model);
info!(" Threads (final/partial): {}/{}", final_threads, partial_threads);
if gpu_enabled && !gpu_feature_compiled {
warn!("GPU requested but no GPU feature compiled! Build with --features hipblas or --features cuda");
}
let whisper_ctx = WhisperContext::new_with_params(&model_path, ctx_params)
.context("Failed to load Whisper model")?;
let whisper_ctx = Arc::new(Mutex::new(whisper_ctx));
// Audio capture setup
let host = cpal::default_host();
let device = host
.default_input_device()
.context("No input device available")?;
info!("Using input device: {}", device.name().unwrap_or_default());
let config = device.default_input_config()?;
let sample_rate = config.sample_rate().0;
let channels = config.channels() as usize;
info!("Input config: {}Hz, {} channels", sample_rate, channels);
// Resampler: input rate -> 16kHz
let resampler = if sample_rate != 16000 {
Some(Arc::new(Mutex::new(
FftFixedInOut::<f32>::new(sample_rate as usize, 16000, 1024, 1)
.context("Failed to create resampler")?,
)))
} else {
None
};
// Shared state
let audio_state = Arc::new(Mutex::new(AudioState::new()));
let running = Arc::new(AtomicBool::new(true));
let mode = Arc::new(Mutex::new(args.mode));
// Channel for audio data
let (audio_tx, mut audio_rx) = mpsc::channel::<Vec<f32>>(100);
// Audio callback
let resampler_clone = resampler.clone();
let running_clone = running.clone();
let stream = device.build_input_stream(
&config.into(),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
if !running_clone.load(Ordering::Relaxed) {
return;
}
// Convert to mono if needed
let mono: Vec<f32> = if channels > 1 {
data.chunks(channels)
.map(|frame| frame.iter().sum::<f32>() / channels as f32)
.collect()
} else {
data.to_vec()
};
// Resample if needed
let resampled = if let Some(ref resampler) = resampler_clone {
if let Ok(mut r) = resampler.lock() {
// Pad input to required length
let input_frames = r.input_frames_next();
if mono.len() >= input_frames {
let input = vec![mono[..input_frames].to_vec()];
match r.process(&input, None) {
Ok(output) => output.into_iter().flatten().collect(),
Err(_) => return,
}
} else {
return;
}
} else {
return;
}
} else {
mono
};
let _ = audio_tx.blocking_send(resampled);
},
|err| {
error!("Audio stream error: {}", err);
},
None,
)?;
stream.play()?;
emit_event(&SttEvent::Ready);
// Stdin command reader
let running_stdin = running.clone();
let mode_stdin = mode.clone();
let audio_state_stdin = audio_state.clone();
let stdin_handle = std::thread::spawn(move || {
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
if !running_stdin.load(Ordering::Relaxed) {
break;
}
let line = match line {
Ok(l) => l,
Err(_) => continue,
};
let cmd: SttCommand = match serde_json::from_str(&line) {
Ok(c) => c,
Err(_) => {
// Try simple text commands
match line.trim().to_lowercase().as_str() {
"start" => SttCommand::Start,
"stop" => SttCommand::Stop,
"cancel" => SttCommand::Cancel,
"shutdown" | "quit" | "exit" => SttCommand::Shutdown,
_ => continue,
}
}
};
match cmd {
SttCommand::Start => {
if let Ok(mut state) = audio_state_stdin.lock() {
state.is_recording = true;
state.clear();
emit_event(&SttEvent::RecordingStarted);
}
}
SttCommand::Stop => {
if let Ok(mut state) = audio_state_stdin.lock() {
state.is_recording = false;
state.pending_finalize = true;
emit_event(&SttEvent::RecordingStopped);
}
}
SttCommand::Cancel => {
if let Ok(mut state) = audio_state_stdin.lock() {
state.is_recording = false;
state.clear();
emit_event(&SttEvent::RecordingStopped);
}
}
SttCommand::Shutdown => {
running_stdin.store(false, Ordering::Relaxed);
break;
}
SttCommand::SetMode { mode: m } => {
if let Ok(mut current_mode) = mode_stdin.lock() {
*current_mode = match m.as_str() {
"oneshot" => Mode::Oneshot,
"continuous" => Mode::Continuous,
"manual" => Mode::Manual,
_ => continue,
};
}
}
}
}
});
// Main processing loop
let vad_threshold = args.vad_threshold;
let silence_samples_threshold = (args.silence_ms as f32 * 16.0) as usize; // 16 samples per ms at 16kHz
let partial_interval = std::time::Duration::from_millis(args.partial_interval_ms);
let emit_partials = args.partials;
let language = args.language.clone();
while running.load(Ordering::Relaxed) {
// Receive audio data
let samples = match tokio::time::timeout(
std::time::Duration::from_millis(100),
audio_rx.recv(),
)
.await
{
Ok(Some(s)) => s,
Ok(None) => break,
Err(_) => continue, // Timeout, check running flag
};
let current_mode = *mode.lock().unwrap();
let mut state = audio_state.lock().unwrap();
// Mode-specific behavior
match current_mode {
Mode::Manual => {
// In manual mode we normally ignore audio unless explicitly recording.
// Exception: after receiving a "stop" command, we need one more tick
// to finalize and emit the transcript.
if !state.is_recording && !state.pending_finalize {
continue;
}
}
Mode::Oneshot | Mode::Continuous => {
// Auto-start on speech detection
let has_speech = simple_vad(&samples, vad_threshold);
if !state.is_recording && has_speech {
state.is_recording = true;
state.clear();
emit_event(&SttEvent::RecordingStarted);
}
if !state.is_recording {
continue;
}
}
}
// Accumulate audio
state.buffer.extend_from_slice(&samples);
// VAD check
let has_speech = simple_vad(&samples, vad_threshold);
if has_speech {
state.speech_detected = true;
state.silence_samples = 0;
} else {
state.silence_samples += samples.len();
}
// Emit partial transcript if enabled
if emit_partials
&& state.speech_detected
&& state.last_partial.elapsed() > partial_interval
&& state.buffer.len() > 16000 // At least 1 second
{
state.last_partial = std::time::Instant::now();
let buffer_copy = state.buffer.clone();
let ctx = whisper_ctx.clone();
let lang = language.clone();
let threads = partial_threads;
// Transcribe in background
tokio::task::spawn_blocking(move || {
if let Ok(text) = transcribe(&ctx, &buffer_copy, &lang, false, threads) {
if !text.is_empty() {
emit_event(&SttEvent::Partial { text });
}
}
});
}
// Check for end of utterance
let should_finalize = match current_mode {
Mode::Manual => state.pending_finalize && state.speech_detected,
Mode::Oneshot | Mode::Continuous => {
state.speech_detected && state.silence_samples > silence_samples_threshold
}
};
if should_finalize && !state.buffer.is_empty() {
let buffer_copy = state.buffer.clone();
let ctx = whisper_ctx.clone();
let lang = language.clone();
// Final transcription
match transcribe(&ctx, &buffer_copy, &lang, true, final_threads) {
Ok(text) => {
if !text.is_empty() {
emit_event(&SttEvent::Final { text });
}
}
Err(e) => {
emit_event(&SttEvent::Error {
message: e.to_string(),
});
}
}
state.clear();
state.is_recording = current_mode == Mode::Continuous;
if current_mode == Mode::Oneshot {
emit_event(&SttEvent::RecordingStopped);
}
}
// Prevent buffer from growing too large
if state.buffer.len() > 16000 * 30 {
warn!("Buffer too large, truncating");
let start = state.buffer.len() - 16000 * 20;
state.buffer = state.buffer[start..].to_vec();
}
}
// Cleanup
drop(stream);
emit_event(&SttEvent::Shutdown);
let _ = stdin_handle.join();
Ok(())
}
/// Transcribe audio buffer using Whisper
fn transcribe(
ctx: &Arc<Mutex<WhisperContext>>,
samples: &[f32],
language: &str,
is_final: bool,
threads: i32,
) -> Result<String> {
let start_time = std::time::Instant::now();
let ctx = ctx.lock().map_err(|_| anyhow::anyhow!("Lock poisoned"))?;
let mut state = ctx.create_state()?;
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
// Configure threads
params.set_n_threads(threads);
// Configure for speed vs accuracy
if is_final {
// Final transcription: balanced speed and accuracy
params.set_single_segment(false);
} else {
// Partial transcription: optimize for speed
params.set_no_context(true);
params.set_single_segment(true); // Faster for streaming
params.set_no_timestamps(true); // We don't use timestamps for partials
params.set_temperature_inc(0.0); // Disable fallback retries for speed
}
params.set_language(Some(language));
params.set_print_special(false);
params.set_print_progress(false);
params.set_print_realtime(false);
params.set_print_timestamps(false);
params.set_suppress_blank(true);
params.set_suppress_nst(true);
// Run inference
state.full(params, samples)?;
let inference_time = start_time.elapsed();
let audio_duration_secs = samples.len() as f32 / 16000.0;
tracing::debug!(
"Transcription took {:?} for {:.1}s audio (RTF: {:.2}x)",
inference_time,
audio_duration_secs,
inference_time.as_secs_f32() / audio_duration_secs
);
// Collect segments
let num_segments = state.full_n_segments();
let mut text = String::new();
for i in 0..num_segments {
if let Some(segment) = state.get_segment(i) {
if let Ok(segment_text) = segment.to_str_lossy() {
text.push_str(&segment_text);
}
}
}
Ok(text.trim().to_string())
}
/// Stub for dirs crate functionality
mod dirs {
use std::path::PathBuf;
pub fn cache_dir() -> Option<PathBuf> {
std::env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.ok()
.or_else(|| {
std::env::var("HOME")
.map(|h| PathBuf::from(h).join(".cache"))
.ok()
})
}
}

165
hosts/juni/flake.lock generated
View file

@ -6,11 +6,11 @@
}, },
"locked": { "locked": {
"dir": "flakes/beszel", "dir": "flakes/beszel",
"lastModified": 1768000280, "lastModified": 1768431400,
"narHash": "sha256-JegPSldfsBcANqnV53mEAQOx/Fv22hUd0G2VTZGUR8Y=", "narHash": "sha256-g4YBBC4SqGIkApJIN5w+JczHEQiDGJDkIH3nYKUGqgc=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "a4e2cc00d86d2f3401918cfdf9f0643939871a42", "rev": "e8c7befd8804c4d836d6ac4bcbbc37d337b77f53",
"revCount": 1115, "revCount": 1141,
"type": "git", "type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
@ -38,19 +38,28 @@
}, },
"common": { "common": {
"locked": { "locked": {
"dir": "flakes/common", "path": "../../flakes/common",
"lastModified": 1768255305, "type": "path"
"narHash": "sha256-XcXl5M0WNYhCCqE9qc9Aj2/2Jb/T0NHZnu2ZuVBvlHw=",
"ref": "refs/heads/master",
"rev": "15769eda748f6fcc6fdab04f79f14ed9b1ffc548",
"revCount": 1125,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
"original": { "original": {
"dir": "flakes/common", "path": "../../flakes/common",
"type": "git", "type": "path"
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" },
"parent": []
},
"crane": {
"locked": {
"lastModified": 1768319649,
"narHash": "sha256-VFkNyxHxkqGp8gf8kfFMW1j6XeBy609kv6TE9uF/0Js=",
"owner": "ipetkov",
"repo": "crane",
"rev": "4b6527687cfd20da3c2ef8287e01b74c2d6c705b",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
} }
}, },
"de_plasma": { "de_plasma": {
@ -58,19 +67,31 @@
"plasma-manager": "plasma-manager" "plasma-manager": "plasma-manager"
}, },
"locked": { "locked": {
"dir": "flakes/de_plasma", "path": "../../flakes/de_plasma",
"lastModified": 1768255305, "type": "path"
"narHash": "sha256-XcXl5M0WNYhCCqE9qc9Aj2/2Jb/T0NHZnu2ZuVBvlHw=",
"ref": "refs/heads/master",
"rev": "15769eda748f6fcc6fdab04f79f14ed9b1ffc548",
"revCount": 1125,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
"original": { "original": {
"dir": "flakes/de_plasma", "path": "../../flakes/de_plasma",
"type": "git", "type": "path"
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" },
"parent": []
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
} }
}, },
"flatpaks": { "flatpaks": {
@ -79,11 +100,11 @@
}, },
"locked": { "locked": {
"dir": "flakes/flatpaks", "dir": "flakes/flatpaks",
"lastModified": 1768000280, "lastModified": 1768431400,
"narHash": "sha256-JegPSldfsBcANqnV53mEAQOx/Fv22hUd0G2VTZGUR8Y=", "narHash": "sha256-g4YBBC4SqGIkApJIN5w+JczHEQiDGJDkIH3nYKUGqgc=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "a4e2cc00d86d2f3401918cfdf9f0643939871a42", "rev": "e8c7befd8804c4d836d6ac4bcbbc37d337b77f53",
"revCount": 1115, "revCount": 1141,
"type": "git", "type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
@ -192,11 +213,11 @@
}, },
"nixos-hardware": { "nixos-hardware": {
"locked": { "locked": {
"lastModified": 1767185284, "lastModified": 1768397375,
"narHash": "sha256-ljDBUDpD1Cg5n3mJI81Hz5qeZAwCGxon4kQW3Ho3+6Q=", "narHash": "sha256-7QqbFi3ERvKjEdAzEYPv7iSGwpUKSrQW5wPLMFq45AQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "40b1a28dce561bea34858287fbb23052c3ee63fe", "rev": "efe2094529d69a3f54892771b6be8ee4a0ebef0f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -224,11 +245,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1767892417, "lastModified": 1768127708,
"narHash": "sha256-dhhvQY67aboBk8b0/u0XB6vwHdgbROZT3fJAjyNh5Ww=", "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba", "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -272,11 +293,11 @@
}, },
"nixpkgs_4": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1767799921, "lastModified": 1768323494,
"narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", "narHash": "sha256-yBXJLE6WCtrGo7LKiB6NOt6nisBEEkguC/lq/rP3zRQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d351d0653aeb7877273920cd3e823994e7579b0b", "rev": "2c3e5ec5df46d3aeee2a1da0bfedd74e21f4bf3a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -288,11 +309,11 @@
}, },
"nixpkgs_5": { "nixpkgs_5": {
"locked": { "locked": {
"lastModified": 1767364772, "lastModified": 1768302833,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=", "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa", "rev": "61db79b0c6b838d9894923920b612048e1201926",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -318,6 +339,22 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_7": {
"locked": {
"lastModified": 1768127708,
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nvim_plugin-Almo7aya/openingh.nvim": { "nvim_plugin-Almo7aya/openingh.nvim": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -1187,15 +1224,15 @@
"nixpkgs": "nixpkgs_5" "nixpkgs": "nixpkgs_5"
}, },
"locked": { "locked": {
"lastModified": 1767994684, "lastModified": 1768427084,
"narHash": "sha256-UIijTI9ndnvhRC4tJDiSc19iMxeZZbDjkYTnfCbJpV8=", "narHash": "sha256-v+42hqkPtOo0jAPZoqSc3eifsjPsdOPK5YXAyOkQc2s=",
"owner": "sst", "owner": "anomalyco",
"repo": "opencode", "repo": "opencode",
"rev": "563b4c33f2bace782403de88e60de4f9167a3c93", "rev": "096e14d787adf10a9a8e0815d92ad3b19e274bfc",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "sst", "owner": "anomalyco",
"repo": "opencode", "repo": "opencode",
"type": "github" "type": "github"
} }
@ -1232,7 +1269,8 @@
"nixpkgs-unstable": "nixpkgs-unstable", "nixpkgs-unstable": "nixpkgs-unstable",
"opencode": "opencode", "opencode": "opencode",
"ros_neovim": "ros_neovim", "ros_neovim": "ros_neovim",
"secrets-bao": "secrets-bao" "secrets-bao": "secrets-bao",
"stt_ime": "stt_ime"
} }
}, },
"ros_neovim": { "ros_neovim": {
@ -1339,6 +1377,37 @@
"type": "path" "type": "path"
}, },
"parent": [] "parent": []
},
"stt_ime": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_7"
},
"locked": {
"path": "../../flakes/stt_ime",
"type": "path"
},
"original": {
"path": "../../flakes/stt_ime",
"type": "path"
},
"parent": []
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -9,18 +9,20 @@
impermanence.url = "github:nix-community/impermanence"; impermanence.url = "github:nix-community/impermanence";
# Use relative to get current version for testin # Use relative to get current version for testin
# common.url = "path:../../flakes/common"; common.url = "path:../../flakes/common";
common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common"; # common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common";
# secrets-bao.url = "path:../../flakes/secrets-bao"; # secrets-bao.url = "path:../../flakes/secrets-bao";
secrets-bao.url = "path:../../flakes/secrets-bao"; secrets-bao.url = "path:../../flakes/secrets-bao";
# flatpaks.url = "path:../../flakes/flatpaks"; # flatpaks.url = "path:../../flakes/flatpaks";
flatpaks.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/flatpaks"; flatpaks.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/flatpaks";
# beszel.url = "path:../../flakes/beszel"; # beszel.url = "path:../../flakes/beszel";
beszel.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/beszel"; beszel.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/beszel";
# de_plasma.url = "path:../../flakes/de_plasma"; de_plasma.url = "path:../../flakes/de_plasma";
de_plasma.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/de_plasma"; # de_plasma.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/de_plasma";
stt_ime.url = "path:../../flakes/stt_ime";
# stt_ime.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/stt_ime";
opencode.url = "github:sst/opencode"; opencode.url = "github:anomalyco/opencode";
ros_neovim.url = "git+https://git.joshuabell.xyz/ringofstorms/nvim"; ros_neovim.url = "git+https://git.joshuabell.xyz/ringofstorms/nvim";
}; };
@ -63,12 +65,19 @@
gpu.intel.enable = true; gpu.intel.enable = true;
sddm.autologinUser = "josh"; sddm.autologinUser = "josh";
wallpapers = [ wallpapers = [
../../_shared_assets/wallpapers/pixel_neon.png ../../hosts/_shared_assets/wallpapers/pixel_neon.png
../../_shared_assets/wallpapers/pixel_neon_v.png ../../hosts/_shared_assets/wallpapers/pixel_neon_v.png
]; ];
}; };
}) })
inputs.common.nixosModules.jetbrains_font inputs.common.nixosModules.jetbrains_font
inputs.stt_ime.nixosModules.default
({
ringofstorms.sttIme = {
enable = true;
model = "tiny.en";
};
})
inputs.ros_neovim.nixosModules.default inputs.ros_neovim.nixosModules.default
({ ({

View file

@ -108,6 +108,7 @@ lib.mkMerge [
serviceConfig.KeyringMode = "shared"; serviceConfig.KeyringMode = "shared";
}; };
# Resets my root to a fresh snapshot. I do this my simply moving root to an old snapshots directory
boot.initrd.systemd.services.bcachefs-reset-root = { boot.initrd.systemd.services.bcachefs-reset-root = {
description = "Reset bcachefs root subvolume before pivot"; description = "Reset bcachefs root subvolume before pivot";
@ -171,14 +172,8 @@ lib.mkMerge [
''; '';
}; };
}) })
# If you mess up decruption password this reboots for retry instead of getting stuck # Bcachefs auto decryption / unlock (will use usb key if provided above, else just prompts password)
(lib.mkIf ENCRYPTED { # We use this for password instead of the default one because default doesn't let you retry if you misstype the password
boot.kernelParams = [
"rd.shell=0"
"rd.emergency=reboot"
];
})
# Bcachefs auto decryption / unlock
(lib.mkIf ENCRYPTED { (lib.mkIf ENCRYPTED {
boot.supportedFilesystems = [ boot.supportedFilesystems = [
"bcachefs" "bcachefs"

View file

@ -32,8 +32,6 @@
files = [ files = [
"/machine-key.json" "/machine-key.json"
"/etc/machine-id" "/etc/machine-id"
"/etc/localtime"
"/etc/timezone"
"/etc/adjtime" "/etc/adjtime"
# NOTE: if you want mutable passwords across reboots, persist these, # NOTE: if you want mutable passwords across reboots, persist these,
# but you must do a one-time migration (see notes in chat). # but you must do a one-time migration (see notes in chat).
@ -55,6 +53,9 @@
".local/share/zoxide" ".local/share/zoxide"
# Hugging Face cache (e.g. whisper.cpp models via hf-hub)
".cache/huggingface"
".config/opencode" ".config/opencode"
# KDE # KDE
@ -65,12 +66,14 @@
# neovim ros_neovim # neovim ros_neovim
".local/state/nvim_ringofstorms_helium" ".local/state/nvim_ringofstorms_helium"
".local/state/opencode"
".local/share/flatpak" ".local/share/flatpak"
".var/app" ".var/app"
]; ];
files = [ files = [
# ".config/kglobalshortcutsrc"
# ".config/plasma-org.kde.plasma.desktop-appletsrc"
]; ];
}; };
}; };

118
hosts/lio/flake.lock generated
View file

@ -87,6 +87,21 @@
"type": "github" "type": "github"
} }
}, },
"crane_2": {
"locked": {
"lastModified": 1768319649,
"narHash": "sha256-VFkNyxHxkqGp8gf8kfFMW1j6XeBy609kv6TE9uF/0Js=",
"owner": "ipetkov",
"repo": "crane",
"rev": "4b6527687cfd20da3c2ef8287e01b74c2d6c705b",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"darwin": { "darwin": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -116,20 +131,14 @@
"plasma-manager": "plasma-manager" "plasma-manager": "plasma-manager"
}, },
"locked": { "locked": {
"dir": "flakes/de_plasma", "path": "../../flakes/de_plasma",
"lastModified": 1768233301, "type": "path"
"narHash": "sha256-m7Og7WuCT8VdQdLhsR6J7ZCR+aFM5ddJ7A1Kt2LBXQs=",
"ref": "refs/heads/master",
"rev": "128209e4aa8927b7514bcfd2acaf097ac0d59310",
"revCount": 1122,
"type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
"original": { "original": {
"dir": "flakes/de_plasma", "path": "../../flakes/de_plasma",
"type": "git", "type": "path"
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" },
} "parent": []
}, },
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
@ -149,6 +158,24 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils_2": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flatpaks": { "flatpaks": {
"inputs": { "inputs": {
"nix-flatpak": "nix-flatpak" "nix-flatpak": "nix-flatpak"
@ -315,11 +342,11 @@
}, },
"nixpkgs_4": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1767273430, "lastModified": 1768302833,
"narHash": "sha256-kDpoFwQ8GLrPiS3KL+sAwreXrph2KhdXuJzo5+vSLoo=", "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "76eec3925eb9bbe193934987d3285473dbcfad50", "rev": "61db79b0c6b838d9894923920b612048e1201926",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -361,6 +388,22 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_7": {
"locked": {
"lastModified": 1768127708,
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nvim_plugin-Almo7aya/openingh.nvim": { "nvim_plugin-Almo7aya/openingh.nvim": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -1230,16 +1273,15 @@
"nixpkgs": "nixpkgs_4" "nixpkgs": "nixpkgs_4"
}, },
"locked": { "locked": {
"lastModified": 1767388146, "lastModified": 1768396176,
"narHash": "sha256-E4Zce3466wABErQl0wMm+09BbH06FFShUCdJGcSqmQk=", "narHash": "sha256-JqLZY6/s3O5IVNjZs4vi4BGQhA730aLLMA7DgENCTTU=",
"owner": "sst", "owner": "anomalyco",
"repo": "opencode", "repo": "opencode",
"rev": "0cf0294787322664c6d668fa5ab0a9ce26796f78", "rev": "ee6ca104e5eb1693b63901128ea315754f88f595",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "sst", "owner": "anomalyco",
"ref": "latest",
"repo": "opencode", "repo": "opencode",
"type": "github" "type": "github"
} }
@ -1297,7 +1339,8 @@
"opencode": "opencode", "opencode": "opencode",
"ros_neovim": "ros_neovim", "ros_neovim": "ros_neovim",
"secrets": "secrets", "secrets": "secrets",
"secrets-bao": "secrets-bao" "secrets-bao": "secrets-bao",
"stt_ime": "stt_ime"
} }
}, },
"ros_neovim": { "ros_neovim": {
@ -1447,6 +1490,22 @@
}, },
"parent": [] "parent": []
}, },
"stt_ime": {
"inputs": {
"crane": "crane_2",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_7"
},
"locked": {
"path": "../../flakes/stt_ime",
"type": "path"
},
"original": {
"path": "../../flakes/stt_ime",
"type": "path"
},
"parent": []
},
"systems": { "systems": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
@ -1476,6 +1535,21 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -16,10 +16,12 @@
flatpaks.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/flatpaks"; flatpaks.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/flatpaks";
# beszel.url = "path:../../flakes/beszel"; # beszel.url = "path:../../flakes/beszel";
beszel.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/beszel"; beszel.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/beszel";
# de_plasma.url = "path:../../flakes/de_plasma"; de_plasma.url = "path:../../flakes/de_plasma";
de_plasma.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/de_plasma"; # de_plasma.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/de_plasma";
stt_ime.url = "path:../../flakes/stt_ime";
# stt_ime.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/stt_ime";
opencode.url = "github:sst/opencode?ref=latest"; opencode.url = "github:anomalyco/opencode";
ros_neovim.url = "git+https://git.joshuabell.xyz/ringofstorms/nvim"; ros_neovim.url = "git+https://git.joshuabell.xyz/ringofstorms/nvim";
}; };
@ -69,6 +71,15 @@
# sddm.autologinUser = "josh"; # sddm.autologinUser = "josh";
}; };
}) })
inputs.stt_ime.nixosModules.default
({
ringofstorms.sttIme = {
enable = true;
gpuBackend = "hip"; # Use AMD ROCm/HIP acceleration
useGpu = true;
};
})
secrets.nixosModules.default secrets.nixosModules.default
ros_neovim.nixosModules.default ros_neovim.nixosModules.default
({ ({