Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "whispers"
version = "0.1.1"
version = "0.2.1"
edition = "2024"
rust-version = "1.85"
description = "Speech-to-text dictation tool for Wayland"
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ whispers setup

# one-shot dictation
whispers

# live mode
whispers voice
```

Default config path:
Expand Down Expand Up @@ -86,9 +83,8 @@ bindsym $mod+Alt+d exec whispers
# setup
whispers setup

# dictation
# one-shot dictation
whispers
whispers voice
whispers transcribe audio.wav

# ASR models
Expand Down
10 changes: 4 additions & 6 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ flash_attn = true
idle_timeout_ms = 120000

[postprocess]
# "raw" (default), "advanced_local", "agentic_rewrite", or "legacy_basic" for deprecated cleanup configs
# "raw" (default), "rewrite", or "legacy_basic" for deprecated cleanup configs
mode = "raw"

[session]
Expand All @@ -59,7 +59,7 @@ snippet_trigger = "insert"
backend = "local"
# Cloud fallback behavior ("local" or "none")
fallback = "local"
# Managed rewrite model name for advanced_local mode
# Managed rewrite model name for rewrite mode
selected_model = "qwen-3.5-4b-q4_k_m"
# Manual GGUF path override (empty = use selected managed model)
# Custom rewrite models should be chat-capable GGUFs with an embedded
Expand All @@ -77,11 +77,9 @@ idle_timeout_ms = 120000
max_output_chars = 1200
# Maximum tokens to generate for rewritten output
max_tokens = 256

[agentic_rewrite]
# App-aware rewrite policy rules used by postprocess.mode = "agentic_rewrite"
# App-aware rewrite policy rules used by postprocess.mode = "rewrite"
policy_path = "~/.local/share/whispers/app-rewrite-policy.toml"
# Technical glossary used by postprocess.mode = "agentic_rewrite"
# Technical glossary used by postprocess.mode = "rewrite"
glossary_path = "~/.local/share/whispers/technical-glossary.toml"
# Default correction policy ("conservative", "balanced", or "aggressive")
default_correction_policy = "balanced"
Expand Down
16 changes: 8 additions & 8 deletions src/agentic_rewrite/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ use super::{AppRule, ContextMatcher, GlossaryEntry, store};

pub(super) fn print_app_rule_path(config_override: Option<&Path>) -> Result<()> {
let config = Config::load(config_override)?;
println!("{}", config.resolved_agentic_policy_path().display());
println!("{}", config.resolved_rewrite_policy_path().display());
Ok(())
}

pub(super) fn print_glossary_path(config_override: Option<&Path>) -> Result<()> {
let config = Config::load(config_override)?;
println!("{}", config.resolved_agentic_glossary_path().display());
println!("{}", config.resolved_rewrite_glossary_path().display());
Ok(())
}

pub(super) fn list_app_rules(config_override: Option<&Path>) -> Result<()> {
let config = Config::load(config_override)?;
let rules = store::read_policy_file(&config.resolved_agentic_policy_path())?;
let rules = store::read_policy_file(&config.resolved_rewrite_policy_path())?;
if rules.is_empty() {
println!("No app rules configured.");
return Ok(());
Expand Down Expand Up @@ -49,7 +49,7 @@ pub(super) fn add_app_rule(
correction_policy: Option<RewriteCorrectionPolicy>,
) -> Result<()> {
let config = Config::load(config_override)?;
let path = config.resolved_agentic_policy_path();
let path = config.resolved_rewrite_policy_path();
let mut rules = store::read_policy_file(&path)?;
store::upsert_app_rule(
&mut rules,
Expand All @@ -68,7 +68,7 @@ pub(super) fn add_app_rule(

pub(super) fn remove_app_rule(config_override: Option<&Path>, name: &str) -> Result<()> {
let config = Config::load(config_override)?;
let path = config.resolved_agentic_policy_path();
let path = config.resolved_rewrite_policy_path();
let mut rules = store::read_policy_file(&path)?;
let removed = store::remove_app_rule_entry(&mut rules, name);
store::write_policy_file(&path, &rules)?;
Expand All @@ -83,7 +83,7 @@ pub(super) fn remove_app_rule(config_override: Option<&Path>, name: &str) -> Res

pub(super) fn list_glossary(config_override: Option<&Path>) -> Result<()> {
let config = Config::load(config_override)?;
let entries = store::read_glossary_file(&config.resolved_agentic_glossary_path())?;
let entries = store::read_glossary_file(&config.resolved_rewrite_glossary_path())?;
if entries.is_empty() {
println!("No glossary entries configured.");
return Ok(());
Expand Down Expand Up @@ -113,7 +113,7 @@ pub(super) fn add_glossary_entry(
matcher: ContextMatcher,
) -> Result<()> {
let config = Config::load(config_override)?;
let path = config.resolved_agentic_glossary_path();
let path = config.resolved_rewrite_glossary_path();
let mut entries = store::read_glossary_file(&path)?;
store::upsert_glossary_entry(
&mut entries,
Expand All @@ -131,7 +131,7 @@ pub(super) fn add_glossary_entry(

pub(super) fn remove_glossary_entry(config_override: Option<&Path>, term: &str) -> Result<()> {
let config = Config::load(config_override)?;
let path = config.resolved_agentic_glossary_path();
let path = config.resolved_rewrite_glossary_path();
let mut entries = store::read_glossary_file(&path)?;
let removed = store::remove_glossary_entry_by_term(&mut entries, term);
store::write_glossary_file(&path, &entries)?;
Expand Down
17 changes: 9 additions & 8 deletions src/agentic_rewrite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ pub fn default_glossary_path() -> &'static str {
}

pub fn apply_runtime_policy(config: &Config, transcript: &mut RewriteTranscript) {
let policy_rules = store::load_policy_file_for_runtime(&config.resolved_agentic_policy_path());
let policy_rules = store::load_policy_file_for_runtime(&config.resolved_rewrite_policy_path());
let glossary_entries =
store::load_glossary_file_for_runtime(&config.resolved_agentic_glossary_path());
store::load_glossary_file_for_runtime(&config.resolved_rewrite_glossary_path());

let policy_context = runtime::resolve_policy_context(
config.agentic_rewrite.default_correction_policy,
config.rewrite.default_correction_policy,
transcript.typing_context.as_ref(),
&transcript.rewrite_candidates,
&policy_rules,
Expand Down Expand Up @@ -175,6 +175,7 @@ mod tests {
text: "type script and sir dee json".into(),
}],
recommended_candidate: None,
edit_context: Default::default(),
policy_context: RewritePolicyContext::default(),
}
}
Expand All @@ -193,7 +194,7 @@ mod tests {
crate::test_support::remove_env("XDG_DATA_HOME");

let config = Config::default();
let glossary_path = config.resolved_agentic_glossary_path();
let glossary_path = config.resolved_rewrite_glossary_path();
store::write_glossary_file(
&glossary_path,
&[GlossaryEntry {
Expand Down Expand Up @@ -242,7 +243,7 @@ mod tests {
)
.expect("add app rule");
let config = Config::load(None).expect("config");
let rules = store::read_policy_file(&config.resolved_agentic_policy_path()).expect("rules");
let rules = store::read_policy_file(&config.resolved_rewrite_policy_path()).expect("rules");
assert_eq!(rules.len(), 1);

add_glossary_entry(
Expand All @@ -253,15 +254,15 @@ mod tests {
)
.expect("add glossary entry");
let entries =
store::read_glossary_file(&config.resolved_agentic_glossary_path()).expect("entries");
store::read_glossary_file(&config.resolved_rewrite_glossary_path()).expect("entries");
assert_eq!(entries.len(), 1);

remove_app_rule(None, "zed").expect("remove app rule");
remove_glossary_entry(None, "serde_json").expect("remove glossary entry");

let rules = store::read_policy_file(&config.resolved_agentic_policy_path()).expect("rules");
let rules = store::read_policy_file(&config.resolved_rewrite_policy_path()).expect("rules");
let entries =
store::read_glossary_file(&config.resolved_agentic_glossary_path()).expect("entries");
store::read_glossary_file(&config.resolved_rewrite_glossary_path()).expect("entries");
assert!(rules.is_empty());
assert!(entries.is_empty());
}
Expand Down
4 changes: 4 additions & 0 deletions src/agentic_rewrite/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ mod tests {
text: "type script and sir dee json".into(),
}],
recommended_candidate: None,
edit_context: Default::default(),
policy_context: RewritePolicyContext::default(),
}
}
Expand Down Expand Up @@ -862,6 +863,7 @@ mod tests {
text: "I'm currently using the window manager hyperland.".into(),
}],
recommended_candidate: None,
edit_context: Default::default(),
policy_context: RewritePolicyContext::default(),
};
hyperland_transcript.policy_context.correction_policy =
Expand Down Expand Up @@ -890,6 +892,7 @@ mod tests {
text: "I'm switching from Sui to Hyperland.".into(),
}],
recommended_candidate: None,
edit_context: Default::default(),
policy_context: RewritePolicyContext::default(),
};
switch_transcript.policy_context.correction_policy = RewriteCorrectionPolicy::Conservative;
Expand Down Expand Up @@ -920,6 +923,7 @@ mod tests {
text: "cargo clipy".into(),
}],
recommended_candidate: None,
edit_context: Default::default(),
policy_context: RewritePolicyContext::default(),
};
transcript.policy_context.correction_policy = RewriteCorrectionPolicy::Conservative;
Expand Down
12 changes: 6 additions & 6 deletions src/agentic_rewrite/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::config::{Config, PostprocessMode};
use crate::config::Config;
use crate::error::{Result, WhsprError};

use super::{AppRule, GlossaryEntry};

const DEFAULT_POLICY_PATH: &str = "~/.local/share/whispers/app-rewrite-policy.toml";
const DEFAULT_GLOSSARY_PATH: &str = "~/.local/share/whispers/technical-glossary.toml";

const POLICY_STARTER: &str = r#"# App-aware rewrite policy for whispers agentic_rewrite mode.
const POLICY_STARTER: &str = r#"# App-aware rewrite policy for whispers rewrite mode.
# Rules are layered, not first-match. Matching rules apply in this order:
# global defaults, surface_kind, app_id, window_title_contains, browser_domain_contains.
# Later, more specific rules override earlier fields.
Expand All @@ -35,7 +35,7 @@ const POLICY_STARTER: &str = r#"# App-aware rewrite policy for whispers agentic_
# instructions = "Preserve identifiers, filenames, snake_case, camelCase, and Rust terminology."
"#;

const GLOSSARY_STARTER: &str = r#"# Technical glossary for whispers agentic_rewrite mode.
const GLOSSARY_STARTER: &str = r#"# Technical glossary for whispers rewrite mode.
# Each entry defines a canonical term plus likely spoken or mis-transcribed aliases.
#
# Uncomment and edit the examples below.
Expand Down Expand Up @@ -77,17 +77,17 @@ pub(super) fn default_glossary_path() -> &'static str {
}

pub(super) fn ensure_starter_files(config: &Config) -> Result<Vec<String>> {
if config.postprocess.mode != PostprocessMode::AgenticRewrite {
if !config.postprocess.mode.uses_rewrite() {
return Ok(Vec::new());
}

let mut created = Vec::new();
let policy_path = config.resolved_agentic_policy_path();
let policy_path = config.resolved_rewrite_policy_path();
if ensure_text_file(&policy_path, POLICY_STARTER)? {
created.push(policy_path.display().to_string());
}

let glossary_path = config.resolved_agentic_glossary_path();
let glossary_path = config.resolved_rewrite_glossary_path();
if ensure_text_file(&glossary_path, GLOSSARY_STARTER)? {
created.push(glossary_path.display().to_string());
}
Expand Down
25 changes: 4 additions & 21 deletions src/bin/whispers-osd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,27 +469,10 @@ fn render_frame(
}

fn render_meter_overlay(pixels: &mut [u8], w: u32, h: u32, bars: &BarState) {
let shell_x = 8;
let shell_y = 10;
let shell_w = w.saturating_sub(shell_x * 2);
let shell_h = h.saturating_sub(shell_y * 2 + 2);
let shell_radius = shell_h / 2;

draw_surface_shell(
pixels,
w,
h,
shell_x,
shell_y,
shell_w,
shell_h,
shell_radius,
);

let track_x = shell_x + 14;
let track_y = shell_y + 10;
let track_w = shell_w.saturating_sub(28);
let track_h = shell_h.saturating_sub(20);
let track_x = 22;
let track_y = 20;
let track_w = w.saturating_sub(track_x * 2);
let track_h = h.saturating_sub(track_y * 2 + 2);
draw_track_shell(
pixels,
w,
Expand Down
Loading