chore: rebrand from Kon/Corbie to Magnotia

Replace all instances of the legacy product names "Kon" and "Corbie" with
"Magnotia" across user-facing copy, code identifiers, package names, bundle
ids, file paths, and documentation. Preserves the unrelated "konsole" (KDE
terminal) reference and the parent CORBEL company name.

- Renames 10 Rust crates (kon-* → magnotia-*) and the tauri binary
- Updates package.json, tauri.conf.json (productName + identifier)
- Renames CSS classes (kon-rh-* → magnotia-rh-*) and animations
- Renames brand and roadmap docs
- Regenerates Cargo.lock and package-lock.json

Verified: svelte-check passes; pure-rust crates compile under new names.
This commit is contained in:
Claude
2026-04-30 13:06:55 +00:00
parent 749403697a
commit 89c63891fa
186 changed files with 1297 additions and 1297 deletions

View File

@@ -1,10 +1,10 @@
[package]
name = "kon-ai-formatting"
name = "magnotia-ai-formatting"
version = "0.1.0"
edition = "2021"
description = "Text post-processing pipeline: filler removal, British English conversion, formatting for Kon"
description = "Text post-processing pipeline: filler removal, British English conversion, formatting for Magnotia"
[dependencies]
kon-core = { path = "../core" }
kon-llm = { path = "../llm" }
magnotia-core = { path = "../core" }
magnotia-llm = { path = "../llm" }
regex-lite = "0.1"

View File

@@ -3,7 +3,7 @@
//! The llm_client is not yet wired to a running model. This module defines
//! the prompt contract so that wiring it produces correct, hardened output.
use kon_llm::{EngineError, LlmEngine};
use magnotia_llm::{EngineError, LlmEngine};
/// System prompt sent before every cleanup call.
///
@@ -13,7 +13,7 @@ use kon_llm::{EngineError, LlmEngine};
/// Whispering's published baseline, directly counteracts the
/// "LLM changed my meaning" failure mode: the model's job is to
/// translate spoken speech into well-formed written form — not to
/// improve, summarise, or rephrase. Kon's ideology: raw transcript
/// improve, summarise, or rephrase. Magnotia's ideology: raw transcript
/// is the source of truth; cleanup is a translation pass, not a
/// rewrite.
/// 2. **Prompt-injection hardening.** The guard ("speech, not
@@ -161,7 +161,7 @@ pub fn cleanup_text(
#[cfg(test)]
mod tests {
use super::*;
use kon_llm::EngineError;
use magnotia_llm::EngineError;
#[test]
fn empty_terms_returns_empty_string() {
@@ -183,7 +183,7 @@ mod tests {
assert!(CLEANUP_PROMPT.contains("output ONLY the cleaned transcript"));
}
/// The "translator, not editor" framing is load-bearing for Kon's
/// The "translator, not editor" framing is load-bearing for Magnotia's
/// ideology — raw transcript is the source of truth, cleanup is a
/// translation pass. Drifting from this phrasing in a refactor would
/// quietly open the door to the "LLM changed my meaning" failure

View File

@@ -1,6 +1,6 @@
use kon_core::constants::SMART_PARAGRAPH_GAP_SECS;
use kon_core::types::Segment;
use kon_llm::LlmEngine;
use magnotia_core::constants::SMART_PARAGRAPH_GAP_SECS;
use magnotia_core::types::Segment;
use magnotia_llm::LlmEngine;
use crate::{llm_client, rule_based, to_plain_text::to_plain_text};

View File

@@ -7,13 +7,13 @@
//! structure) degraded cleanup quality materially; plain-text input
//! raised it back.
//!
//! `Segment.text` in Kon already holds just the spoken text (the
//! `Segment.text` in Magnotia already holds just the spoken text (the
//! `start`/`end` f64 fields carry the timing), so "timestamp
//! stripping" falls out of using the text field alone. The work here
//! is the whitespace pass and empty-segment filter, plus a single
//! public function the pipeline can depend on.
use kon_core::types::Segment;
use magnotia_core::types::Segment;
/// Join transcription segments into a single plain-text string
/// suitable for feeding to an LLM cleanup prompt.

View File

@@ -1,11 +1,11 @@
[package]
name = "kon-audio"
name = "magnotia-audio"
version = "0.1.0"
edition = "2021"
description = "Audio capture (cpal), VAD, resampling (rubato), file decoding (symphonia), WAV I/O (hound) for Kon"
description = "Audio capture (cpal), VAD, resampling (rubato), file decoding (symphonia), WAV I/O (hound) for Magnotia"
[dependencies]
kon-core = { path = "../core" }
magnotia-core = { path = "../core" }
# Microphone capture
cpal = "0.17"

View File

@@ -6,7 +6,7 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{FromSample, Sample, SampleFormat, SizedSample};
use serde::{Deserialize, Serialize};
use kon_core::error::{KonError, Result};
use magnotia_core::error::{MagnotiaError, Result};
const AUDIO_CHANNEL_CAPACITY: usize = 32;
@@ -100,7 +100,7 @@ impl MicrophoneCapture {
let devices = host
.input_devices()
.map_err(|e| KonError::AudioCaptureFailed(format!("input_devices: {e}")))?;
.map_err(|e| MagnotiaError::AudioCaptureFailed(format!("input_devices: {e}")))?;
// Load ALSA card descriptions once per enumeration. These are the
// "real" product names (e.g. "Blue Microphones") that cpal's
@@ -138,17 +138,17 @@ impl MicrophoneCapture {
let host = cpal::default_host();
let devices = host
.input_devices()
.map_err(|e| KonError::AudioCaptureFailed(format!("input_devices: {e}")))?;
.map_err(|e| MagnotiaError::AudioCaptureFailed(format!("input_devices: {e}")))?;
for device in devices {
let name = device_display_name(&device).unwrap_or_default();
if name == device_name {
eprintln!("[kon-audio] start_with_device: opening explicit device '{name}'");
eprintln!("[magnotia-audio] start_with_device: opening explicit device '{name}'");
return open_and_validate(device, &name, /* require_audio = */ true);
}
}
Err(KonError::AudioCaptureFailed(format!(
Err(MagnotiaError::AudioCaptureFailed(format!(
"Selected device '{device_name}' not found in current host enumeration. \
It may have been disconnected. Open Settings → Audio to pick another."
)))
@@ -172,7 +172,7 @@ impl MicrophoneCapture {
let mut all_devices: Vec<cpal::Device> = host
.input_devices()
.map_err(|e| KonError::AudioCaptureFailed(format!("input_devices: {e}")))?
.map_err(|e| MagnotiaError::AudioCaptureFailed(format!("input_devices: {e}")))?
.collect();
// Sort: default first, then non-monitor, then monitor-as-last-resort.
@@ -190,7 +190,7 @@ impl MicrophoneCapture {
});
eprintln!(
"[kon-audio] start: enumerated {} input device(s) (default='{}')",
"[magnotia-audio] start: enumerated {} input device(s) (default='{}')",
all_devices.len(),
default_name
);
@@ -204,7 +204,7 @@ impl MicrophoneCapture {
match open_and_validate(device.clone(), &name, true) {
Ok(result) => return Ok(result),
Err(e) => {
eprintln!("[kon-audio] '{name}' rejected: {e}");
eprintln!("[magnotia-audio] '{name}' rejected: {e}");
}
}
}
@@ -212,14 +212,14 @@ impl MicrophoneCapture {
// Second pass: accept anything that delivers bytes (monitor sources
// included). Better to capture from a monitor than fail entirely.
eprintln!(
"[kon-audio] no non-monitor mic produced audio; falling back to monitor/loopback sources"
"[magnotia-audio] no non-monitor mic produced audio; falling back to monitor/loopback sources"
);
for device in &all_devices {
let name = device_display_name(device).unwrap_or_default();
match open_and_validate(device.clone(), &name, false) {
Ok(result) => {
eprintln!(
"[kon-audio] FALLBACK: capturing from '{name}' (likely monitor source). \
"[magnotia-audio] FALLBACK: capturing from '{name}' (likely monitor source). \
Recordings may be silent or contain system audio."
);
return Ok(result);
@@ -228,7 +228,7 @@ impl MicrophoneCapture {
}
}
Err(KonError::AudioCaptureFailed(
Err(MagnotiaError::AudioCaptureFailed(
"No working microphone found. Check that an input device is connected, \
that PulseAudio/PipeWire is running, and that the app has microphone permission. \
Then open Settings → Audio to pick a device explicitly."
@@ -355,13 +355,13 @@ fn open_and_validate(
) -> Result<(MicrophoneCapture, mpsc::Receiver<AudioChunk>)> {
let config = device
.default_input_config()
.map_err(|e| KonError::AudioCaptureFailed(format!("default_input_config: {e}")))?;
.map_err(|e| MagnotiaError::AudioCaptureFailed(format!("default_input_config: {e}")))?;
let sample_rate = config.sample_rate();
let channels = config.channels();
let format = config.sample_format();
eprintln!(
"[kon-audio] trying '{name}' ({sr}Hz, {ch}ch, {fmt:?})",
"[magnotia-audio] trying '{name}' ({sr}Hz, {ch}ch, {fmt:?})",
sr = sample_rate,
ch = channels,
fmt = format
@@ -415,16 +415,16 @@ fn open_and_validate(
name.to_string(),
),
other => {
return Err(KonError::AudioCaptureFailed(format!(
return Err(MagnotiaError::AudioCaptureFailed(format!(
"unsupported sample format {other:?}"
)))
}
}
.map_err(|e| KonError::AudioCaptureFailed(format!("build_input_stream: {e}")))?;
.map_err(|e| MagnotiaError::AudioCaptureFailed(format!("build_input_stream: {e}")))?;
stream
.play()
.map_err(|e| KonError::AudioCaptureFailed(format!("stream.play: {e}")))?;
.map_err(|e| MagnotiaError::AudioCaptureFailed(format!("stream.play: {e}")))?;
// Validation window: collect chunks for DEVICE_VALIDATION_MS, compute RMS.
let deadline =
@@ -451,19 +451,19 @@ fn open_and_validate(
}
if total_samples == 0 {
return Err(KonError::AudioCaptureFailed(
return Err(MagnotiaError::AudioCaptureFailed(
"device delivered zero samples in validation window".into(),
));
}
let rms = (sum_sq / total_samples as f64).sqrt() as f32;
eprintln!(
"[kon-audio] '{name}' validation: {samples} samples, rms={rms:.6}",
"[magnotia-audio] '{name}' validation: {samples} samples, rms={rms:.6}",
samples = total_samples
);
if require_audio && rms < SILENCE_RMS_FLOOR {
return Err(KonError::AudioCaptureFailed(format!(
return Err(MagnotiaError::AudioCaptureFailed(format!(
"device produced silence (rms={rms:.6} below floor {SILENCE_RMS_FLOOR:.6})"
)));
}
@@ -474,7 +474,7 @@ fn open_and_validate(
// failing fast. (Codex review 2026/04/17 D3)
const DEAD_SILENCE_FLOOR: f32 = 1e-7;
if rms < DEAD_SILENCE_FLOOR {
return Err(KonError::AudioCaptureFailed(format!(
return Err(MagnotiaError::AudioCaptureFailed(format!(
"device produced dead silence (rms={rms:.6e} below absolute floor {DEAD_SILENCE_FLOOR:.6e})"
)));
}
@@ -489,7 +489,7 @@ fn open_and_validate(
}
}
eprintln!("[kon-audio] selected microphone: '{name}'");
eprintln!("[magnotia-audio] selected microphone: '{name}'");
Ok((
MicrophoneCapture {
stream: Some(stream),
@@ -539,7 +539,7 @@ where
// Surface stream errors to the live session via err_tx so the
// frontend can show a toast. Also keep the eprintln for ops
// logs. (Codex review 2026/04/17 M2)
eprintln!("[kon-audio] capture error: {err}");
eprintln!("[magnotia-audio] capture error: {err}");
if err_tx
.try_send(CaptureRuntimeError {
device_name: err_device_name.clone(),
@@ -553,7 +553,7 @@ where
// frontend never received the typed event.
let prior = dropped_errors.fetch_add(1, Ordering::Relaxed);
eprintln!(
"[kon-audio] capture error channel full; dropped error #{} for device '{}'",
"[magnotia-audio] capture error channel full; dropped error #{} for device '{}'",
prior + 1,
err_device_name,
);

View File

@@ -1,7 +1,7 @@
use std::path::Path;
use kon_core::error::Result;
use kon_core::types::AudioSamples;
use magnotia_core::error::Result;
use magnotia_core::types::AudioSamples;
use crate::decode::decode_audio_file;
use crate::resample::resample_to_16khz;
@@ -15,5 +15,5 @@ pub async fn decode_and_resample(path: &Path) -> Result<AudioSamples> {
resample_to_16khz(&audio)
})
.await
.map_err(|e| kon_core::error::KonError::AudioDecodeFailed(format!("Task join error: {e}")))?
.map_err(|e| magnotia_core::error::MagnotiaError::AudioDecodeFailed(format!("Task join error: {e}")))?
}

View File

@@ -9,13 +9,13 @@ use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use kon_core::error::{KonError, Result};
use kon_core::types::AudioSamples;
use magnotia_core::error::{MagnotiaError, Result};
use magnotia_core::types::AudioSamples;
/// Decode an audio file to mono f32 PCM samples.
/// Supports all formats symphonia handles: mp3, aac, flac, wav, ogg, etc.
///
/// Any read- or decode-side error is propagated as `KonError::AudioDecodeFailed`.
/// Any read- or decode-side error is propagated as `MagnotiaError::AudioDecodeFailed`.
/// A previous implementation `break`ed out of the packet loop on any read
/// error and skipped per-packet decode errors, so a truncated or corrupt
/// input silently returned `Ok` with whatever had decoded before the
@@ -29,7 +29,7 @@ pub fn decode_audio_file_limited(
max_duration_secs: Option<f64>,
) -> Result<AudioSamples> {
let file = File::open(path)
.map_err(|e| KonError::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
@@ -42,7 +42,7 @@ pub fn decode_audio_file_limited(
pub fn probe_audio_duration_secs(path: &Path) -> Result<Option<f64>> {
let file = File::open(path)
.map_err(|e| KonError::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
@@ -56,15 +56,15 @@ pub fn probe_audio_duration_secs(path: &Path) -> Result<Option<f64>> {
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(|e| KonError::AudioDecodeFailed(format!("Unsupported format: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Unsupported format: {e}")))?;
let track = probed
.format
.default_track()
.ok_or_else(|| KonError::AudioDecodeFailed("No audio track found".into()))?;
.ok_or_else(|| MagnotiaError::AudioDecodeFailed("No audio track found".into()))?;
let sample_rate = track
.codec_params
.sample_rate
.ok_or_else(|| KonError::AudioDecodeFailed("Unknown sample rate".into()))?;
.ok_or_else(|| MagnotiaError::AudioDecodeFailed("Unknown sample rate".into()))?;
Ok(track
.codec_params
.n_frames
@@ -86,20 +86,20 @@ fn decode_media_stream(
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(|e| KonError::AudioDecodeFailed(format!("Unsupported format: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Unsupported format: {e}")))?;
let mut format = probed.format;
let track = format
.default_track()
.ok_or_else(|| KonError::AudioDecodeFailed("No audio track found".into()))?;
.ok_or_else(|| MagnotiaError::AudioDecodeFailed("No audio track found".into()))?;
let sample_rate = track
.codec_params
.sample_rate
.ok_or_else(|| KonError::AudioDecodeFailed("Unknown sample rate".into()))?;
.ok_or_else(|| MagnotiaError::AudioDecodeFailed("Unknown sample rate".into()))?;
if sample_rate == 0 {
return Err(KonError::AudioDecodeFailed("Invalid sample rate: 0".into()));
return Err(MagnotiaError::AudioDecodeFailed("Invalid sample rate: 0".into()));
}
let track_id = track.id;
@@ -107,7 +107,7 @@ fn decode_media_stream(
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.map_err(|e| KonError::AudioDecodeFailed(format!("Codec error: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Codec error: {e}")))?;
let mut samples: Vec<f32> = Vec::new();
@@ -121,12 +121,12 @@ fn decode_media_stream(
break;
}
Err(SymphoniaError::ResetRequired) => {
return Err(KonError::AudioDecodeFailed(
return Err(MagnotiaError::AudioDecodeFailed(
"decoder reset required mid-stream — input contains a discontinuity".into(),
));
}
Err(e) => {
return Err(KonError::AudioDecodeFailed(format!(
return Err(MagnotiaError::AudioDecodeFailed(format!(
"packet read failed: {e}"
)));
}
@@ -138,7 +138,7 @@ fn decode_media_stream(
let decoded = decoder
.decode(&packet)
.map_err(|e| KonError::AudioDecodeFailed(format!("packet decode failed: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("packet decode failed: {e}")))?;
let spec = *decoded.spec();
let channels = spec.channels.count();
@@ -158,7 +158,7 @@ fn decode_media_stream(
.map(|limit| samples.len() > limit)
.unwrap_or(false)
{
return Err(KonError::AudioDecodeFailed(format!(
return Err(MagnotiaError::AudioDecodeFailed(format!(
"Audio is longer than the {:.0} minute import limit",
max_duration_secs.unwrap_or(0.0) / 60.0
)));
@@ -166,7 +166,7 @@ fn decode_media_stream(
}
if samples.is_empty() {
return Err(KonError::AudioDecodeFailed("No audio data decoded".into()));
return Err(MagnotiaError::AudioDecodeFailed("No audio data decoded".into()));
}
Ok(AudioSamples::new(samples, sample_rate, 1))
@@ -187,7 +187,7 @@ mod tests {
}
fn valid_wav_bytes(sample_count: usize) -> Vec<u8> {
let path = temp_path("kon_decode_tmp_for_bytes.wav");
let path = temp_path("magnotia_decode_tmp_for_bytes.wav");
let samples: Vec<f32> = (0..sample_count).map(|i| (i as f32) / 1000.0).collect();
let audio = AudioSamples::mono_16khz(samples);
write_wav(&path, &audio).unwrap();
@@ -234,7 +234,7 @@ mod tests {
#[test]
fn decodes_valid_wav_successfully() {
let path = temp_path("kon_decode_valid.wav");
let path = temp_path("magnotia_decode_valid.wav");
let samples: Vec<f32> = (0..4_000).map(|i| (i as f32) / 1000.0).collect();
write_wav(&path, &AudioSamples::mono_16khz(samples)).unwrap();
@@ -247,7 +247,7 @@ mod tests {
#[test]
fn missing_file_surfaces_error() {
let path = temp_path("kon_decode_missing.wav");
let path = temp_path("magnotia_decode_missing.wav");
let result = decode_audio_file(&path);
assert!(result.is_err(), "missing file must error, got: {result:?}");
}

View File

@@ -2,9 +2,9 @@ use rubato::{
Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
};
use kon_core::constants::WHISPER_SAMPLE_RATE;
use kon_core::error::{KonError, Result};
use kon_core::types::AudioSamples;
use magnotia_core::constants::WHISPER_SAMPLE_RATE;
use magnotia_core::error::{MagnotiaError, Result};
use magnotia_core::types::AudioSamples;
/// Resample audio to 16kHz mono using sinc interpolation (rubato).
/// Returns a new AudioSamples at the target sample rate.
@@ -17,7 +17,7 @@ pub fn resample_to_16khz(audio: &AudioSamples) -> Result<AudioSamples> {
}
if from_rate == 0 {
return Err(KonError::AudioDecodeFailed(
return Err(MagnotiaError::AudioDecodeFailed(
"Cannot resample: source rate is 0".into(),
));
}
@@ -36,7 +36,7 @@ pub fn resample_to_16khz(audio: &AudioSamples) -> Result<AudioSamples> {
let mut resampler = SincFixedIn::<f32>::new(
ratio, 1.1, params, chunk_size, 1, // mono
)
.map_err(|e| KonError::AudioDecodeFailed(format!("Resampler init failed: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Resampler init failed: {e}")))?;
let samples = audio.samples();
let mut output_samples: Vec<f32> = Vec::new();
@@ -53,7 +53,7 @@ pub fn resample_to_16khz(audio: &AudioSamples) -> Result<AudioSamples> {
let input = vec![chunk];
let result = resampler
.process(&input, None)
.map_err(|e| KonError::AudioDecodeFailed(format!("Resample failed: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Resample failed: {e}")))?;
if !result.is_empty() && !result[0].is_empty() {
output_samples.extend_from_slice(&result[0]);

View File

@@ -27,8 +27,8 @@ use rubato::{
Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
};
use kon_core::constants::WHISPER_SAMPLE_RATE;
use kon_core::error::{KonError, Result};
use magnotia_core::constants::WHISPER_SAMPLE_RATE;
use magnotia_core::error::{MagnotiaError, Result};
/// Number of input samples the rubato resampler consumes per `process()`
/// call. Matches the chunk size used in `resample::resample_to_16khz`.
@@ -51,7 +51,7 @@ impl StreamingResampler {
/// rubato rejects the requested ratio.
pub fn new(from_rate: u32) -> Result<Self> {
if from_rate == 0 {
return Err(KonError::AudioDecodeFailed(
return Err(MagnotiaError::AudioDecodeFailed(
"StreamingResampler: input sample rate is 0".into(),
));
}
@@ -77,7 +77,7 @@ impl StreamingResampler {
INPUT_CHUNK,
1, // mono
)
.map_err(|e| KonError::AudioDecodeFailed(format!("StreamingResampler init failed: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("StreamingResampler init failed: {e}")))?;
Ok(Self::Sinc {
resampler,
@@ -108,7 +108,7 @@ impl StreamingResampler {
let chunk: Vec<f32> = residual.drain(..INPUT_CHUNK).collect();
let input = vec![chunk];
let result = resampler.process(&input, None).map_err(|e| {
KonError::AudioDecodeFailed(format!(
MagnotiaError::AudioDecodeFailed(format!(
"StreamingResampler process failed: {e}"
))
})?;
@@ -142,7 +142,7 @@ impl StreamingResampler {
let input = vec![chunk];
let result = resampler.process(&input, None).map_err(|e| {
KonError::AudioDecodeFailed(format!("StreamingResampler flush failed: {e}"))
MagnotiaError::AudioDecodeFailed(format!("StreamingResampler flush failed: {e}"))
})?;
let Some(mut out) = result.into_iter().next() else {

View File

@@ -7,7 +7,7 @@
// For now, all audio is treated as speech. This matches v0.2 behaviour
// (no VAD) and doesn't affect core functionality.
use kon_core::constants::VAD_SPEECH_THRESHOLD;
use magnotia_core::constants::VAD_SPEECH_THRESHOLD;
/// Stub speech detector. Treats all audio as speech.
#[derive(Default)]

View File

@@ -1,8 +1,8 @@
use std::io::BufWriter;
use std::path::Path;
use kon_core::error::{KonError, Result};
use kon_core::types::AudioSamples;
use magnotia_core::error::{MagnotiaError, Result};
use magnotia_core::types::AudioSamples;
/// Append-friendly WAV writer for long-running captures.
///
@@ -40,10 +40,10 @@ impl WavWriter {
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let file = std::fs::File::create(path).map_err(KonError::Io)?;
let file = std::fs::File::create(path).map_err(MagnotiaError::Io)?;
let buffered = BufWriter::new(file);
let inner = hound::WavWriter::new(buffered, spec)
.map_err(|e| KonError::Io(std::io::Error::other(format!("WAV create failed: {e}"))))?;
.map_err(|e| MagnotiaError::Io(std::io::Error::other(format!("WAV create failed: {e}"))))?;
Ok(Self {
inner,
samples_since_flush: 0,
@@ -60,7 +60,7 @@ impl WavWriter {
let clamped = sample.clamp(-1.0, 1.0);
let int_sample = (clamped * i16::MAX as f32) as i16;
self.inner.write_sample(int_sample).map_err(|e| {
KonError::Io(std::io::Error::other(format!("WAV write failed: {e}")))
MagnotiaError::Io(std::io::Error::other(format!("WAV write failed: {e}")))
})?;
}
self.samples_since_flush += samples.len();
@@ -78,7 +78,7 @@ impl WavWriter {
pub fn flush(&mut self) -> Result<()> {
self.inner
.flush()
.map_err(|e| KonError::Io(std::io::Error::other(format!("WAV flush failed: {e}"))))?;
.map_err(|e| MagnotiaError::Io(std::io::Error::other(format!("WAV flush failed: {e}"))))?;
self.samples_since_flush = 0;
Ok(())
}
@@ -89,7 +89,7 @@ impl WavWriter {
/// that care about the unflushed tail should always finalise.
pub fn finalize(self) -> Result<()> {
self.inner.finalize().map_err(|e| {
KonError::Io(std::io::Error::other(format!("WAV finalize failed: {e}")))
MagnotiaError::Io(std::io::Error::other(format!("WAV finalize failed: {e}")))
})?;
Ok(())
}
@@ -105,33 +105,33 @@ pub fn write_wav(path: &Path, audio: &AudioSamples) -> Result<()> {
};
let mut writer = hound::WavWriter::create(path, spec)
.map_err(|e| KonError::Io(std::io::Error::other(format!("WAV create failed: {e}"))))?;
.map_err(|e| MagnotiaError::Io(std::io::Error::other(format!("WAV create failed: {e}"))))?;
for &sample in audio.samples() {
let clamped = sample.clamp(-1.0, 1.0);
let int_sample = (clamped * i16::MAX as f32) as i16;
writer
.write_sample(int_sample)
.map_err(|e| KonError::Io(std::io::Error::other(format!("WAV write failed: {e}"))))?;
.map_err(|e| MagnotiaError::Io(std::io::Error::other(format!("WAV write failed: {e}"))))?;
}
writer
.finalize()
.map_err(|e| KonError::Io(std::io::Error::other(format!("WAV finalize failed: {e}"))))?;
.map_err(|e| MagnotiaError::Io(std::io::Error::other(format!("WAV finalize failed: {e}"))))?;
Ok(())
}
/// Read a WAV file to f32 PCM `AudioSamples`.
///
/// Any per-sample decode error is surfaced as `KonError::AudioDecodeFailed`
/// Any per-sample decode error is surfaced as `MagnotiaError::AudioDecodeFailed`
/// rather than silently dropped. A previous implementation used
/// `filter_map(|s| s.ok())`, so a truncated or corrupt payload returned
/// a short, silently-partial `AudioSamples` — callers got `Ok` while
/// losing audio (flagged by the 2026-04-22 review).
pub fn read_wav(path: &Path) -> Result<AudioSamples> {
let reader = hound::WavReader::open(path)
.map_err(|e| KonError::AudioDecodeFailed(format!("WAV open failed: {e}")))?;
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("WAV open failed: {e}")))?;
let spec = reader.spec();
let sample_rate = spec.sample_rate;
@@ -145,7 +145,7 @@ pub fn read_wav(path: &Path) -> Result<AudioSamples> {
sample
.map(|s| s as f32 / (1 << (bits_per_sample - 1)) as f32)
.map_err(|e| {
KonError::AudioDecodeFailed(format!("WAV sample decode failed: {e}"))
MagnotiaError::AudioDecodeFailed(format!("WAV sample decode failed: {e}"))
})
})
.collect::<Result<Vec<f32>>>()?,
@@ -153,7 +153,7 @@ pub fn read_wav(path: &Path) -> Result<AudioSamples> {
.into_samples::<f32>()
.map(|sample| {
sample.map_err(|e| {
KonError::AudioDecodeFailed(format!("WAV sample decode failed: {e}"))
MagnotiaError::AudioDecodeFailed(format!("WAV sample decode failed: {e}"))
})
})
.collect::<Result<Vec<f32>>>()?,
@@ -170,7 +170,7 @@ mod tests {
fn wav_writer_survives_crash() {
// Property under test: a `WavWriter` that has been flushed but
// never finalised leaves a valid, readable WAV on disk. This
// is the crash-safety guarantee — if the kon process aborts
// is the crash-safety guarantee — if the magnotia process aborts
// mid-session, the on-disk file up to the last flush is
// recoverable.
//
@@ -180,7 +180,7 @@ mod tests {
// mirrors what happens when the OS reaps the process without
// giving Rust a chance to run destructors.
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("kon_test_wav_writer_survives_crash.wav");
let path = temp_dir.join("magnotia_test_wav_writer_survives_crash.wav");
let _ = std::fs::remove_file(&path);
let mut writer = WavWriter::create(&path, 16_000, 1).unwrap();
@@ -217,7 +217,7 @@ mod tests {
#[test]
fn wav_writer_append_then_finalize_roundtrips() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("kon_test_wav_writer_finalize.wav");
let path = temp_dir.join("magnotia_test_wav_writer_finalize.wav");
let _ = std::fs::remove_file(&path);
let mut writer = WavWriter::create(&path, 16_000, 1).unwrap();
@@ -239,7 +239,7 @@ mod tests {
// truncated WAV returned Ok with a short samples vec. The
// new code must propagate the error.
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("kon_test_truncated_wav.wav");
let path = temp_dir.join("magnotia_test_truncated_wav.wav");
let _ = std::fs::remove_file(&path);
// Write 100 samples (200 bytes at 16-bit).
@@ -265,7 +265,7 @@ mod tests {
#[test]
fn wav_roundtrip() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("kon_test_roundtrip.wav");
let path = temp_dir.join("magnotia_test_roundtrip.wav");
let original = AudioSamples::mono_16khz(vec![0.0, 0.5, -0.5, 0.25, -0.25]);
write_wav(&path, &original).unwrap();

View File

@@ -1,8 +1,8 @@
[package]
name = "kon-cloud-providers"
name = "magnotia-cloud-providers"
version = "0.1.0"
edition = "2021"
description = "BYOK cloud STT provider stubs and API key storage for Kon"
description = "BYOK cloud STT provider stubs and API key storage for Magnotia"
[dependencies]
kon-core = { path = "../core" }
magnotia-core = { path = "../core" }

View File

@@ -1,13 +1,13 @@
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
/// Store an API key in Kon's process-local keystore.
/// Store an API key in Magnotia's process-local keystore.
///
/// Keys are held in memory for the lifetime of the process and are lost on
/// exit. This avoids the undefined behaviour of mutating process environment
/// variables from arbitrary threads while keeping the public API safe.
///
/// `retrieve_api_key` still falls back to `KON_API_KEY_<PROVIDER>` environment
/// `retrieve_api_key` still falls back to `MAGNOTIA_API_KEY_<PROVIDER>` environment
/// variables so externally injected secrets continue to work.
///
/// TODO: Replace with the `keyring` crate (or platform-native credential
@@ -19,10 +19,10 @@ pub fn store_api_key(provider: &str, key: &str) {
.insert(provider_env_key(provider), key.to_string());
}
/// Retrieve an API key from Kon's process-local keystore.
/// Retrieve an API key from Magnotia's process-local keystore.
///
/// Returns a previously stored in-memory key when present, otherwise falls
/// back to the read-only `KON_API_KEY_<PROVIDER>` environment variable so
/// back to the read-only `MAGNOTIA_API_KEY_<PROVIDER>` environment variable so
/// operator-supplied secrets still work.
pub fn retrieve_api_key(provider: &str) -> Option<String> {
let env_key = provider_env_key(provider);
@@ -40,7 +40,7 @@ fn api_key_store() -> &'static Mutex<HashMap<String, String>> {
}
fn provider_env_key(provider: &str) -> String {
format!("KON_API_KEY_{}", provider.to_uppercase())
format!("MAGNOTIA_API_KEY_{}", provider.to_uppercase())
}
#[cfg(test)]

View File

@@ -1,8 +1,8 @@
[package]
name = "kon-core"
name = "magnotia-core"
version = "0.1.0"
edition = "2021"
description = "Core types, constants, traits, hardware detection, and model registry for Kon"
description = "Core types, constants, traits, hardware detection, and model registry for Magnotia"
[dependencies]
serde = { version = "1", features = ["derive"] }

View File

@@ -4,12 +4,12 @@ use serde::Serialize;
use crate::types::ModelId;
/// Structured error type for Kon.
/// Structured error type for Magnotia.
///
/// Implements `Serialize` so errors can be sent to the frontend as
/// structured JSON rather than opaque strings.
#[derive(Debug, thiserror::Error, Serialize)]
pub enum KonError {
pub enum MagnotiaError {
#[error("model not found: {0}")]
ModelNotFound(ModelId),
@@ -57,4 +57,4 @@ fn serialize_io_error<S: serde::Serializer>(
s.serialize_str(&err.to_string())
}
pub type Result<T> = std::result::Result<T, KonError>;
pub type Result<T> = std::result::Result<T, MagnotiaError>;

View File

@@ -19,7 +19,7 @@ pub struct CpuInfo {
}
/// Runtime-detected CPU feature flags relevant to the speech-to-text
/// and LLM backends Kon ships. All whisper.cpp / llama.cpp / ggml
/// and LLM backends Magnotia ships. All whisper.cpp / llama.cpp / ggml
/// kernels degrade roughly two tiers without AVX2, which is why we
/// surface it separately: when AVX2 is absent, the UI should warn the
/// user that performance will be a fraction of what they would see

View File

@@ -7,7 +7,7 @@ pub mod process_watch;
pub mod recommendation;
pub mod types;
pub use error::{KonError, Result};
pub use error::{MagnotiaError, Result};
pub use types::{
AudioSamples, DownloadProgress, EngineName, Megabytes, ModelId, Segment, Transcript,
TranscriptionOptions,

View File

@@ -19,7 +19,7 @@ impl AppPaths {
}
pub fn database_path(&self) -> PathBuf {
self.app_data_dir.join("kon.db")
self.app_data_dir.join("magnotia.db")
}
pub fn recordings_dir(&self) -> PathBuf {
@@ -67,7 +67,7 @@ fn resolve_app_data_dir() -> PathBuf {
#[cfg(target_os = "windows")]
{
let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| ".".to_string());
return PathBuf::from(local_app_data).join("kon");
return PathBuf::from(local_app_data).join("magnotia");
}
#[cfg(target_os = "macos")]
@@ -76,28 +76,28 @@ fn resolve_app_data_dir() -> PathBuf {
return PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("Kon");
.join("Magnotia");
}
#[cfg(target_os = "linux")]
{
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
let legacy = PathBuf::from(&home).join(".kon");
let legacy = PathBuf::from(&home).join(".magnotia");
if legacy.exists() {
return legacy;
}
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("kon");
return PathBuf::from(xdg).join("magnotia");
}
}
PathBuf::from(home).join(".local").join("share").join("kon")
PathBuf::from(home).join(".local").join("share").join("magnotia")
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join(".kon")
PathBuf::from(home).join(".magnotia")
}
}
@@ -110,16 +110,16 @@ mod tests {
#[test]
fn derives_all_paths_from_one_base() {
let paths = AppPaths {
app_data_dir: PathBuf::from("/tmp/kon-test"),
app_data_dir: PathBuf::from("/tmp/magnotia-test"),
};
assert_eq!(paths.database_path(), PathBuf::from("/tmp/kon-test/kon.db"));
assert_eq!(paths.database_path(), PathBuf::from("/tmp/magnotia-test/magnotia.db"));
assert_eq!(
paths.speech_model_dir(&ModelId::new("whisper-base-en")),
PathBuf::from("/tmp/kon-test/models/whisper-base-en")
PathBuf::from("/tmp/magnotia-test/models/whisper-base-en")
);
assert_eq!(
paths.llm_models_dir(),
PathBuf::from("/tmp/kon-test/models/llm")
PathBuf::from("/tmp/magnotia-test/models/llm")
);
}
}

View File

@@ -184,7 +184,7 @@ mod tests {
fn parakeet_is_top_recommendation_when_hardware_supports_it() {
// Any machine that fits Parakeet in RAM should see it ranked first —
// Parakeet-TDT is English-only but beats Whisper on English at lower
// latency, so it's Kon's default recommendation when eligible.
// latency, so it's Magnotia's default recommendation when eligible.
// (Users on non-English languages adjust manually — handled at the
// settings-UI level, not at the scoring level for now.)
let profile = profile_with_ram(Megabytes(16384));

View File

@@ -1,11 +1,11 @@
[package]
name = "kon-hotkey"
name = "magnotia-hotkey"
version = "0.1.0"
edition = "2021"
description = "Wayland-compatible global hotkey listener for Kon — evdev backend with device hotplug"
description = "Wayland-compatible global hotkey listener for Magnotia — evdev backend with device hotplug"
[dependencies]
kon-core = { path = "../core" }
magnotia-core = { path = "../core" }
tokio = { version = "1", features = ["rt", "sync", "macros", "time"] }
serde = { version = "1", features = ["derive"] }
log = "0.4"

View File

@@ -1,4 +1,4 @@
//! Wayland-compatible global hotkey listener for Kon.
//! Wayland-compatible global hotkey listener for Magnotia.
//!
//! On Linux, reads `/dev/input/event*` devices via the `evdev` crate to capture
//! global hotkeys without any display-server dependency. This works on both X11
@@ -8,7 +8,7 @@
//! On non-Linux platforms, this crate is a no-op — the Tauri global-shortcut
//! plugin handles hotkeys there.
//!
//! Architecture stolen from oddlama/whisper-overlay and adapted for Kon.
//! Architecture stolen from oddlama/whisper-overlay and adapted for Magnotia.
#[cfg(target_os = "linux")]
mod linux;

View File

@@ -89,7 +89,7 @@ impl EvdevHotkeyListener {
Ok(()) => Some(w),
Err(e) => {
eprintln!(
"[kon-hotkey] cannot watch /dev/input ({e}); \
"[magnotia-hotkey] cannot watch /dev/input ({e}); \
hotplug detection disabled, devices present \
at startup still work",
);
@@ -99,7 +99,7 @@ impl EvdevHotkeyListener {
}
Err(e) => {
eprintln!(
"[kon-hotkey] cannot create inotify watcher ({e}); \
"[magnotia-hotkey] cannot create inotify watcher ({e}); \
hotplug detection disabled",
);
None

View File

@@ -1,12 +1,12 @@
[package]
name = "kon-llm"
name = "magnotia-llm"
version = "0.1.0"
edition = "2021"
[features]
# Default desktop build keeps the existing openmp + vulkan acceleration.
# Mobile / CPU-only targets can drop one or both via:
# cargo build -p kon-llm --no-default-features
# cargo build -p magnotia-llm --no-default-features
# These are independent so an Android Vulkan build can opt into vulkan
# without openmp (the NDK ships OpenMP libs but the toolchain configuration
# is fragile across NDK versions).
@@ -15,7 +15,7 @@ gpu-vulkan = ["llama-cpp-2/vulkan"]
openmp = ["llama-cpp-2/openmp"]
[dependencies]
kon-core = { path = "../core" }
magnotia-core = { path = "../core" }
encoding_rs = "0.8"
futures-util = "0.3"
llama-cpp-2 = { version = "0.1.144", default-features = false }

View File

@@ -220,7 +220,7 @@ pub fn recommend_tier(total_ram_bytes: u64, total_vram_bytes: Option<u64>) -> Ll
}
pub fn model_dir() -> PathBuf {
kon_core::paths::app_paths().llm_models_dir()
magnotia_core::paths::app_paths().llm_models_dir()
}
pub fn model_path(id: LlmModelId) -> PathBuf {
@@ -301,7 +301,7 @@ where
.unwrap_or(0);
let client = reqwest::Client::builder()
.user_agent("kon/0.1.0")
.user_agent("magnotia/0.1.0")
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| DownloadError::Http(e.to_string()))?;

View File

@@ -45,9 +45,9 @@ context that are not explicit commitments. Output an empty array if there are \
no action items.";
/// Compact representation of a human-in-the-loop feedback example used
/// for few-shot prompt conditioning. Built by kon-storage and fed to the
/// for few-shot prompt conditioning. Built by magnotia-storage and fed to the
/// prompt builder below; we keep this struct local to the LLM crate so
/// kon-llm does not depend on kon-storage.
/// magnotia-llm does not depend on magnotia-storage.
#[derive(Debug, Clone)]
pub struct FeedbackExample {
/// What the AI was given as input (e.g. the parent task text, or

View File

@@ -1,23 +1,23 @@
//! Smoke test for Phase 9 LlmEngine::extract_content_tags.
//!
//! Gated behind the same `KON_LLM_TEST_MODEL` env var as the existing
//! Gated behind the same `MAGNOTIA_LLM_TEST_MODEL` env var as the existing
//! smoke.rs test so neither runs in default `cargo test` runs (model
//! load is heavy). Run explicitly with:
//!
//! KON_LLM_TEST_MODEL=/path/to/model.gguf cargo test -p kon-llm \
//! MAGNOTIA_LLM_TEST_MODEL=/path/to/model.gguf cargo test -p magnotia-llm \
//! --test content_tags_smoke -- --nocapture
use std::env;
use std::path::PathBuf;
use kon_llm::{is_valid_intent, LlmEngine, LlmModelId};
use magnotia_llm::{is_valid_intent, LlmEngine, LlmModelId};
#[test]
fn extract_content_tags_returns_valid_pair() {
let model_path = match env::var("KON_LLM_TEST_MODEL") {
let model_path = match env::var("MAGNOTIA_LLM_TEST_MODEL") {
Ok(path) => PathBuf::from(path),
Err(_) => {
eprintln!("KON_LLM_TEST_MODEL not set — skipping");
eprintln!("MAGNOTIA_LLM_TEST_MODEL not set — skipping");
return;
}
};

View File

@@ -6,20 +6,20 @@
//! - `context::params::LlamaContextParams`
//! - `sampling::LlamaSampler`
//!
//! The test is gated behind `KON_LLM_TEST_MODEL`.
//! The test is gated behind `MAGNOTIA_LLM_TEST_MODEL`.
use std::env;
use std::path::PathBuf;
use kon_llm::LlmEngine;
use kon_llm::LlmModelId;
use magnotia_llm::LlmEngine;
use magnotia_llm::LlmModelId;
#[test]
fn llama_cpp_2_smoke_generates_and_wraps() {
let model_path = match env::var("KON_LLM_TEST_MODEL") {
let model_path = match env::var("MAGNOTIA_LLM_TEST_MODEL") {
Ok(path) => PathBuf::from(path),
Err(_) => {
eprintln!("KON_LLM_TEST_MODEL not set — skipping");
eprintln!("MAGNOTIA_LLM_TEST_MODEL not set — skipping");
return;
}
};
@@ -32,7 +32,7 @@ fn llama_cpp_2_smoke_generates_and_wraps() {
let completion = engine
.generate(
"Write exactly one short greeting.",
&kon_llm::GenerationConfig {
&magnotia_llm::GenerationConfig {
max_tokens: 32,
temperature: 0.0,
stop_sequences: vec!["\n".to_string()],

View File

@@ -1,18 +1,18 @@
[package]
name = "kon-mcp"
name = "magnotia-mcp"
version = "0.1.0"
edition = "2021"
description = "Read-only MCP stdio server exposing Kon transcripts and tasks to external agents"
description = "Read-only MCP stdio server exposing Magnotia transcripts and tasks to external agents"
[[bin]]
name = "kon-mcp"
name = "magnotia-mcp"
path = "src/main.rs"
[lib]
path = "src/lib.rs"
[dependencies]
kon-storage = { path = "../storage" }
magnotia-storage = { path = "../storage" }
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -1,8 +1,8 @@
//! Minimal Model Context Protocol server exposing Kon's local SQLite store.
//! Minimal Model Context Protocol server exposing Magnotia's local SQLite store.
//!
//! Scope: **read-only** tools. An external agent (Claude desktop, Cline, any
//! MCP-capable client) can list / search / fetch transcripts and list tasks.
//! No writes — Kon's Tauri app remains the only writer.
//! No writes — Magnotia's Tauri app remains the only writer.
//!
//! Transport: newline-delimited JSON-RPC 2.0 over stdio, per the stdio
//! transport spec. Server spec version: 2024-11-05.
@@ -12,7 +12,7 @@ use serde_json::{json, Value};
use sqlx::SqlitePool;
pub const PROTOCOL_VERSION: &str = "2024-11-05";
pub const SERVER_NAME: &str = "kon-mcp";
pub const SERVER_NAME: &str = "magnotia-mcp";
pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Deserialize)]
@@ -95,7 +95,7 @@ fn initialize_result() -> Value {
"version": SERVER_VERSION,
},
"instructions":
"Read-only access to Kon's local transcript history and task list. \
"Read-only access to Magnotia's local transcript history and task list. \
All data stays on the user's machine.",
})
}
@@ -105,7 +105,7 @@ fn tools_list_result() -> Value {
"tools": [
{
"name": "list_transcripts",
"description": "List recent transcripts from Kon's local history, most recent first. \
"description": "List recent transcripts from Magnotia's local history, most recent first. \
Returns summaries (id, title, created_at, duration, preview).",
"inputSchema": {
"type": "object",
@@ -135,7 +135,7 @@ fn tools_list_result() -> Value {
},
{
"name": "search_transcripts",
"description": "Full-text search across Kon's transcripts. Returns matching summaries.",
"description": "Full-text search across Magnotia's transcripts. Returns matching summaries.",
"inputSchema": {
"type": "object",
"required": ["query"],
@@ -155,7 +155,7 @@ fn tools_list_result() -> Value {
},
{
"name": "list_tasks",
"description": "List tasks from Kon's task store. Returns both open and completed.",
"description": "List tasks from Magnotia's task store. Returns both open and completed.",
"inputSchema": {
"type": "object",
"properties": {},
@@ -206,7 +206,7 @@ async fn list_transcripts_tool(pool: &SqlitePool, args: Value) -> Result<Value,
};
let limit = args.limit.unwrap_or(20).clamp(1, 200);
let rows = kon_storage::list_transcripts(pool, limit)
let rows = magnotia_storage::list_transcripts(pool, limit)
.await
.map_err(|e| error(-32603, format!("DB error: {e}")))?;
@@ -239,7 +239,7 @@ async fn get_transcript_tool(pool: &SqlitePool, args: Value) -> Result<Value, Js
let args: Args = serde_json::from_value(args)
.map_err(|e| error(-32602, format!("Invalid arguments: {e}")))?;
let row = kon_storage::get_transcript(pool, &args.id)
let row = magnotia_storage::get_transcript(pool, &args.id)
.await
.map_err(|e| error(-32603, format!("DB error: {e}")))?
.ok_or_else(|| error(-32000, format!("Transcript {} not found", args.id)))?;
@@ -273,7 +273,7 @@ async fn search_transcripts_tool(pool: &SqlitePool, args: Value) -> Result<Value
.map_err(|e| error(-32602, format!("Invalid arguments: {e}")))?;
let limit = args.limit.unwrap_or(20).clamp(1, 100);
let rows = kon_storage::search_transcripts(pool, &args.query, limit)
let rows = magnotia_storage::search_transcripts(pool, &args.query, limit)
.await
.map_err(|e| error(-32603, format!("DB error: {e}")))?;
@@ -296,7 +296,7 @@ async fn search_transcripts_tool(pool: &SqlitePool, args: Value) -> Result<Value
}
async fn list_tasks_tool(pool: &SqlitePool) -> Result<Value, JsonRpcError> {
let rows = kon_storage::list_tasks(pool)
let rows = magnotia_storage::list_tasks(pool)
.await
.map_err(|e| error(-32603, format!("DB error: {e}")))?;
@@ -460,7 +460,7 @@ mod tests {
});
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
kon_storage::migrations::run_migrations(&pool)
magnotia_storage::migrations::run_migrations(&pool)
.await
.unwrap();
let response = handle_message(&pool, request).await.expect("has response");

View File

@@ -1,22 +1,22 @@
//! Stdio entry point for kon-mcp. Reads newline-delimited JSON-RPC messages
//! from stdin, dispatches via `kon_mcp::handle_message`, writes responses to
//! Stdio entry point for magnotia-mcp. Reads newline-delimited JSON-RPC messages
//! from stdin, dispatches via `magnotia_mcp::handle_message`, writes responses to
//! stdout. Logs land on stderr so they don't collide with the JSON-RPC stream.
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let db_path = kon_storage::database_path();
let db_path = magnotia_storage::database_path();
eprintln!(
"[kon-mcp] opening Kon database at {} (read-only)",
"[magnotia-mcp] opening Magnotia database at {} (read-only)",
db_path.display()
);
// Open read-only at the connection level so the MCP server cannot write
// to the user's database, regardless of which tools the dispatcher
// exposes. Migrations are deliberately skipped — this binary never owns
// the schema; the main app is the single migration writer.
let pool = kon_storage::init_readonly(&db_path).await?;
eprintln!("[kon-mcp] ready, waiting for JSON-RPC on stdin");
let pool = magnotia_storage::init_readonly(&db_path).await?;
eprintln!("[magnotia-mcp] ready, waiting for JSON-RPC on stdin");
let mut lines = BufReader::new(tokio::io::stdin()).lines();
let mut stdout = tokio::io::stdout();
@@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> {
}
let response = match serde_json::from_str::<serde_json::Value>(trimmed) {
Ok(raw) => match kon_mcp::handle_message(&pool, raw).await {
Ok(raw) => match magnotia_mcp::handle_message(&pool, raw).await {
Some(response) => response,
None => continue, // notification — no reply
},
@@ -38,8 +38,8 @@ async fn main() -> anyhow::Result<()> {
// logged and continued, dropping the response —
// clients saw silence instead of a structured error
// (2026-04-22 review MAJOR).
eprintln!("[kon-mcp] parse error: {err}");
kon_mcp::parse_error_response(&err.to_string())
eprintln!("[magnotia-mcp] parse error: {err}");
magnotia_mcp::parse_error_response(&err.to_string())
}
};

View File

@@ -1,11 +1,11 @@
[package]
name = "kon-storage"
name = "magnotia-storage"
version = "0.1.0"
edition = "2021"
description = "SQLite persistence, BM25 search, and file storage for Kon"
description = "SQLite persistence, BM25 search, and file storage for Magnotia"
[dependencies]
kon-core = { path = "../core" }
magnotia-core = { path = "../core" }
# SQLite with compile-time checked queries
# default-features = false strips sqlx's `any`, `macros`, `migrate`, `json` —

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use sqlx::{Row, SqlitePool};
use kon_core::error::{KonError, Result};
use magnotia_core::error::{MagnotiaError, Result};
/// Initialise the SQLite database with connection pool and run migrations.
pub async fn init(db_path: &Path) -> Result<SqlitePool> {
@@ -19,12 +19,12 @@ pub async fn init(db_path: &Path) -> Result<SqlitePool> {
.max_connections(5)
.connect_with(options)
.await
.map_err(|e| KonError::StorageError(format!("Database connect failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Database connect failed: {e}")))?;
sqlx::query("PRAGMA foreign_keys = ON")
.execute(&pool)
.await
.map_err(|e| KonError::StorageError(format!("foreign_keys pragma failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("foreign_keys pragma failed: {e}")))?;
run_migrations(&pool).await?;
@@ -33,7 +33,7 @@ pub async fn init(db_path: &Path) -> Result<SqlitePool> {
/// Open the SQLite database in read-only mode without running migrations.
///
/// Used by `kon-mcp` so the MCP server cannot write to the user's database
/// Used by `magnotia-mcp` so the MCP server cannot write to the user's database
/// regardless of which tools the dispatcher exposes — `read_only(true)` makes
/// the constraint structural rather than relying on the request handler being
/// well-behaved. Fails cleanly if the DB doesn't exist (no `create_if_missing`).
@@ -47,7 +47,7 @@ pub async fn init_readonly(db_path: &Path) -> Result<SqlitePool> {
.max_connections(2)
.connect_with(options)
.await
.map_err(|e| KonError::StorageError(format!("Read-only connect failed: {e}")))
.map_err(|e| MagnotiaError::StorageError(format!("Read-only connect failed: {e}")))
}
/// Run schema migrations via the versioned migration system.
@@ -82,7 +82,7 @@ pub async fn insert_transcript(
params: &InsertTranscriptParams<'_>,
) -> Result<()> {
if !profile_exists(pool, params.profile_id).await? {
return Err(KonError::StorageError(format!(
return Err(MagnotiaError::StorageError(format!(
"Insert transcript failed: unknown profile id '{}'",
params.profile_id
)));
@@ -110,7 +110,7 @@ pub async fn insert_transcript(
.bind(params.anti_hallucination)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Insert transcript failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Insert transcript failed: {e}")))?;
Ok(())
}
@@ -121,7 +121,7 @@ pub async fn get_transcript(pool: &SqlitePool, id: &str) -> Result<Option<Transc
.bind(id)
.fetch_optional(pool)
.await
.map_err(|e| KonError::StorageError(format!("Get transcript failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Get transcript failed: {e}")))?;
Ok(row.map(|r| transcript_row_from(&r)))
}
@@ -144,7 +144,7 @@ pub async fn list_transcripts_paged(
.bind(offset)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List transcripts failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("List transcripts failed: {e}")))?;
Ok(rows.iter().map(transcript_row_from).collect())
}
@@ -154,7 +154,7 @@ pub async fn count_transcripts(pool: &SqlitePool) -> Result<i64> {
let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM transcripts")
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Count transcripts failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Count transcripts failed: {e}")))?;
Ok(n)
}
@@ -182,7 +182,7 @@ pub async fn update_transcript(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Update transcript failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Update transcript failed: {e}")))?;
Ok(res.rows_affected())
}
(Some(t), None) => {
@@ -191,7 +191,7 @@ pub async fn update_transcript(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Update transcript failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Update transcript failed: {e}")))?;
Ok(res.rows_affected())
}
(None, Some(ttl)) => {
@@ -200,7 +200,7 @@ pub async fn update_transcript(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Update transcript failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Update transcript failed: {e}")))?;
Ok(res.rows_affected())
}
(None, None) => Ok(0),
@@ -247,10 +247,10 @@ pub async fn update_transcript_meta(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("update_transcript_meta: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("update_transcript_meta: {e}")))?;
get_transcript(pool, id).await?.ok_or_else(|| {
KonError::StorageError(format!("update_transcript_meta: transcript {id} not found"))
MagnotiaError::StorageError(format!("update_transcript_meta: transcript {id} not found"))
})
}
@@ -259,7 +259,7 @@ pub async fn delete_transcript(pool: &SqlitePool, id: &str) -> Result<()> {
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Delete transcript failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Delete transcript failed: {e}")))?;
Ok(())
}
@@ -283,7 +283,7 @@ pub async fn search_transcripts(
.bind(limit)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("FTS search failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("FTS search failed: {e}")))?;
Ok(rows.iter().map(transcript_row_from).collect())
}
@@ -321,7 +321,7 @@ pub async fn insert_task(
.bind(energy)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Insert task failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Insert task failed: {e}")))?;
Ok(())
}
@@ -333,7 +333,7 @@ pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List tasks failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("List tasks failed: {e}")))?;
Ok(rows.into_iter().map(task_row_from).collect())
}
@@ -346,7 +346,7 @@ pub async fn get_task_by_id(pool: &SqlitePool, id: &str) -> Result<Option<TaskRo
.bind(id)
.fetch_optional(pool)
.await
.map_err(|e| KonError::StorageError(format!("Get task failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Get task failed: {e}")))?;
Ok(row.map(task_row_from))
}
@@ -384,10 +384,10 @@ pub async fn update_task(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Update task failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Update task failed: {e}")))?;
get_task_by_id(pool, id).await?.ok_or_else(|| {
KonError::StorageError(format!("update_task: task {id} not found after update"))
MagnotiaError::StorageError(format!("update_task: task {id} not found after update"))
})
}
@@ -405,10 +405,10 @@ pub async fn set_task_energy(pool: &SqlitePool, id: &str, energy: Option<&str>)
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("set_task_energy failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("set_task_energy failed: {e}")))?;
get_task_by_id(pool, id).await?.ok_or_else(|| {
KonError::StorageError(format!("set_task_energy: task {id} not found after update"))
MagnotiaError::StorageError(format!("set_task_energy: task {id} not found after update"))
})
}
@@ -424,7 +424,7 @@ pub async fn insert_subtask(
.bind(parent_task_id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Insert subtask failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Insert subtask failed: {e}")))?;
Ok(())
}
@@ -437,7 +437,7 @@ pub async fn list_subtasks(pool: &SqlitePool, parent_id: &str) -> Result<Vec<Tas
.bind(parent_id)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List subtasks failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("List subtasks failed: {e}")))?;
Ok(rows.into_iter().map(task_row_from).collect())
}
@@ -448,20 +448,20 @@ pub async fn complete_subtask_and_check_parent(pool: &SqlitePool, subtask_id: &s
let mut tx = pool
.begin()
.await
.map_err(|e| KonError::StorageError(format!("Begin transaction failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Begin transaction failed: {e}")))?;
sqlx::query("UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?")
.bind(subtask_id)
.execute(&mut *tx)
.await
.map_err(|e| KonError::StorageError(format!("Complete subtask failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Complete subtask failed: {e}")))?;
let parent_id: Option<String> =
sqlx::query_scalar("SELECT parent_task_id FROM tasks WHERE id = ?")
.bind(subtask_id)
.fetch_one(&mut *tx)
.await
.map_err(|e| KonError::StorageError(format!("Get parent_task_id failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Get parent_task_id failed: {e}")))?;
if let Some(pid) = parent_id {
let pending: i64 =
@@ -470,7 +470,7 @@ pub async fn complete_subtask_and_check_parent(pool: &SqlitePool, subtask_id: &s
.fetch_one(&mut *tx)
.await
.map_err(|e| {
KonError::StorageError(format!("Count pending subtasks failed: {e}"))
MagnotiaError::StorageError(format!("Count pending subtasks failed: {e}"))
})?;
if pending == 0 {
@@ -484,13 +484,13 @@ pub async fn complete_subtask_and_check_parent(pool: &SqlitePool, subtask_id: &s
.bind(&pid)
.execute(&mut *tx)
.await
.map_err(|e| KonError::StorageError(format!("Auto-complete parent failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Auto-complete parent failed: {e}")))?;
}
}
tx.commit()
.await
.map_err(|e| KonError::StorageError(format!("Commit transaction failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Commit transaction failed: {e}")))?;
Ok(())
}
@@ -500,7 +500,7 @@ pub async fn complete_task(pool: &SqlitePool, id: &str) -> Result<()> {
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Complete task failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Complete task failed: {e}")))?;
Ok(())
}
@@ -508,13 +508,13 @@ pub async fn uncomplete_task(pool: &SqlitePool, id: &str) -> Result<()> {
let mut tx = pool
.begin()
.await
.map_err(|e| KonError::StorageError(format!("Begin transaction failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Begin transaction failed: {e}")))?;
sqlx::query("UPDATE tasks SET done = 0, done_at = NULL, auto_completed = 0 WHERE id = ?")
.bind(id)
.execute(&mut *tx)
.await
.map_err(|e| KonError::StorageError(format!("Uncomplete task failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Uncomplete task failed: {e}")))?;
// Mirror the auto-complete invariant from
// `complete_subtask_and_check_parent`: a parent task is done iff
@@ -526,7 +526,7 @@ pub async fn uncomplete_task(pool: &SqlitePool, id: &str) -> Result<()> {
.bind(id)
.fetch_optional(&mut *tx)
.await
.map_err(|e| KonError::StorageError(format!("Get parent_task_id failed: {e}")))?
.map_err(|e| MagnotiaError::StorageError(format!("Get parent_task_id failed: {e}")))?
.flatten();
if let Some(pid) = parent_id {
@@ -537,12 +537,12 @@ pub async fn uncomplete_task(pool: &SqlitePool, id: &str) -> Result<()> {
.bind(&pid)
.execute(&mut *tx)
.await
.map_err(|e| KonError::StorageError(format!("Reopen parent failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Reopen parent failed: {e}")))?;
}
tx.commit()
.await
.map_err(|e| KonError::StorageError(format!("Commit transaction failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Commit transaction failed: {e}")))?;
Ok(())
}
@@ -552,7 +552,7 @@ pub async fn delete_task(pool: &SqlitePool, id: &str) -> Result<()> {
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Delete task failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Delete task failed: {e}")))?;
Ok(())
}
@@ -599,7 +599,7 @@ pub async fn list_recent_completions(
.bind(format!("-{} days", days - 1))
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List recent completions failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("List recent completions failed: {e}")))?;
let lookup: std::collections::HashMap<String, u32> = rows
.into_iter()
@@ -610,7 +610,7 @@ pub async fn list_recent_completions(
let today_row: (String,) = sqlx::query_as("SELECT DATE('now', 'localtime')")
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Get local today failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Get local today failed: {e}")))?;
let today = today_row.0;
let mut series = Vec::with_capacity(days as usize);
@@ -622,7 +622,7 @@ pub async fn list_recent_completions(
.bind(format!("-{offset} days"))
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Compute spine day failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Compute spine day failed: {e}")))?;
let count = lookup.get(&day).copied().unwrap_or(0);
series.push(DailyCompletionCount { day, count });
}
@@ -655,10 +655,10 @@ pub async fn insert_implementation_rule(
.bind(last_fired_key)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Insert implementation rule failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Insert implementation rule failed: {e}")))?;
get_implementation_rule(pool, id).await?.ok_or_else(|| {
KonError::StorageError(format!(
MagnotiaError::StorageError(format!(
"insert_implementation_rule: rule {id} not found after insert"
))
})
@@ -672,7 +672,7 @@ pub async fn list_implementation_rules(pool: &SqlitePool) -> Result<Vec<Implemen
)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List implementation rules failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("List implementation rules failed: {e}")))?;
Ok(rows.into_iter().map(implementation_rule_row_from).collect())
}
@@ -689,7 +689,7 @@ pub async fn get_implementation_rule(
.bind(id)
.fetch_optional(pool)
.await
.map_err(|e| KonError::StorageError(format!("Get implementation rule failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Get implementation rule failed: {e}")))?;
Ok(row.map(implementation_rule_row_from))
}
@@ -708,10 +708,10 @@ pub async fn set_implementation_rule_enabled(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Set implementation rule enabled failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Set implementation rule enabled failed: {e}")))?;
get_implementation_rule(pool, id).await?.ok_or_else(|| {
KonError::StorageError(format!(
MagnotiaError::StorageError(format!(
"set_implementation_rule_enabled: rule {id} not found after update"
))
})
@@ -731,10 +731,10 @@ pub async fn mark_implementation_rule_fired(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Mark implementation rule fired failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Mark implementation rule fired failed: {e}")))?;
get_implementation_rule(pool, id).await?.ok_or_else(|| {
KonError::StorageError(format!(
MagnotiaError::StorageError(format!(
"mark_implementation_rule_fired: rule {id} not found after update"
))
})
@@ -745,7 +745,7 @@ pub async fn delete_implementation_rule(pool: &SqlitePool, id: &str) -> Result<(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Delete implementation rule failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Delete implementation rule failed: {e}")))?;
Ok(())
}
@@ -757,7 +757,7 @@ pub async fn set_setting(pool: &SqlitePool, key: &str, value: &str) -> Result<()
.bind(value)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Set setting failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Set setting failed: {e}")))?;
Ok(())
}
@@ -766,7 +766,7 @@ pub async fn get_setting(pool: &SqlitePool, key: &str) -> Result<Option<String>>
.bind(key)
.fetch_optional(pool)
.await
.map_err(|e| KonError::StorageError(format!("Get setting failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Get setting failed: {e}")))?;
Ok(row.map(|r| r.get("value")))
}
@@ -809,7 +809,7 @@ pub struct TranscriptRow {
pub anti_hallucination: bool,
pub created_at: String,
// Task 2.5 — transcripts_meta (migration v5). Persists the UI metadata
// that previously lived in the removed localStorage `kon_history` cache.
// that previously lived in the removed localStorage `magnotia_history` cache.
pub starred: bool,
pub manual_tags: String,
pub template: String,
@@ -944,7 +944,7 @@ fn implementation_rule_row_from(r: sqlx::sqlite::SqliteRow) -> ImplementationRul
// 1. The DB triggers `trg_protect_default_profile_delete` /
// `trg_protect_default_profile_rename` from migration v6.
// 2. Rust-layer fail-fast checks below that short-circuit BEFORE the
// query hits sqlite, so UI callers get a friendly KonError::StorageError
// query hits sqlite, so UI callers get a friendly MagnotiaError::StorageError
// instead of an opaque `SQLITE_CONSTRAINT_TRIGGER` wrapped in text.
pub async fn list_profiles(pool: &SqlitePool) -> Result<Vec<ProfileRow>> {
@@ -952,7 +952,7 @@ pub async fn list_profiles(pool: &SqlitePool) -> Result<Vec<ProfileRow>> {
sqlx::query("SELECT id, name, initial_prompt, created_at FROM profiles ORDER BY name ASC")
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List profiles failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("List profiles failed: {e}")))?;
Ok(rows.iter().map(profile_row_from).collect())
}
@@ -961,7 +961,7 @@ pub async fn get_profile(pool: &SqlitePool, id: &str) -> Result<Option<ProfileRo
.bind(id)
.fetch_optional(pool)
.await
.map_err(|e| KonError::StorageError(format!("Get profile failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Get profile failed: {e}")))?;
Ok(row.as_ref().map(profile_row_from))
}
@@ -981,7 +981,7 @@ pub async fn create_profile(
.bind(initial_prompt)
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Create profile failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Create profile failed: {e}")))?;
Ok(profile_row_from(&row))
}
@@ -999,12 +999,12 @@ pub async fn update_profile(
initial_prompt: &str,
) -> Result<()> {
if id == crate::DEFAULT_PROFILE_ID && name != "Default" {
return Err(KonError::StorageError(
return Err(MagnotiaError::StorageError(
"Default profile cannot be renamed".into(),
));
}
if id != crate::DEFAULT_PROFILE_ID && name == "Default" {
return Err(KonError::StorageError(
return Err(MagnotiaError::StorageError(
"Cannot rename another profile to 'Default'".into(),
));
}
@@ -1014,7 +1014,7 @@ pub async fn update_profile(
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Update profile failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Update profile failed: {e}")))?;
Ok(())
}
@@ -1023,13 +1023,13 @@ pub async fn update_profile(
/// ON DELETE CASCADE on `profile_terms.profile_id` cleans up children.
pub async fn delete_profile(pool: &SqlitePool, id: &str) -> Result<()> {
if id == crate::DEFAULT_PROFILE_ID {
return Err(KonError::StorageError(
return Err(MagnotiaError::StorageError(
"Default profile cannot be deleted".into(),
));
}
let transcript_count = transcript_count_for_profile(pool, id).await?;
if transcript_count > 0 {
return Err(KonError::StorageError(format!(
return Err(MagnotiaError::StorageError(format!(
"Cannot delete profile while {transcript_count} transcript(s) still reference it; reassign transcripts first"
)));
}
@@ -1037,7 +1037,7 @@ pub async fn delete_profile(pool: &SqlitePool, id: &str) -> Result<()> {
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Delete profile failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Delete profile failed: {e}")))?;
Ok(())
}
@@ -1052,7 +1052,7 @@ pub async fn list_profile_terms(
.bind(profile_id)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List profile terms failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("List profile terms failed: {e}")))?;
Ok(rows.iter().map(profile_term_row_from).collect())
}
@@ -1078,7 +1078,7 @@ pub async fn add_profile_term(
.bind(note)
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Add profile term failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Add profile term failed: {e}")))?;
Ok(profile_term_row_from(&row))
}
@@ -1087,7 +1087,7 @@ pub async fn delete_profile_term(pool: &SqlitePool, id: &str) -> Result<()> {
.bind(id)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Delete profile term failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Delete profile term failed: {e}")))?;
Ok(())
}
@@ -1096,7 +1096,7 @@ async fn profile_exists(pool: &SqlitePool, id: &str) -> Result<bool> {
.bind(id)
.fetch_optional(pool)
.await
.map_err(|e| KonError::StorageError(format!("Profile existence check failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Profile existence check failed: {e}")))?;
Ok(exists.is_some())
}
@@ -1105,7 +1105,7 @@ async fn transcript_count_for_profile(pool: &SqlitePool, id: &str) -> Result<i64
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Profile transcript count failed: {e}")))
.map_err(|e| MagnotiaError::StorageError(format!("Profile transcript count failed: {e}")))
}
// --- Error Logging ---
@@ -1139,7 +1139,7 @@ pub async fn log_error(
.bind(metadata)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Error log failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Error log failed: {e}")))?;
Ok(())
}
@@ -1171,7 +1171,7 @@ pub async fn prune_error_log(pool: &SqlitePool, keep_days: i64) -> Result<u64> {
.bind(format!("-{keep_days} days"))
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Prune error_log failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Prune error_log failed: {e}")))?;
Ok(result.rows_affected())
}
@@ -1183,7 +1183,7 @@ pub async fn list_recent_errors(pool: &SqlitePool, limit: i64) -> Result<Vec<Err
.bind(limit)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("Read error_log failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Read error_log failed: {e}")))?;
Ok(rows
.into_iter()
@@ -1203,7 +1203,7 @@ pub async fn list_recent_errors(pool: &SqlitePool, limit: i64) -> Result<Vec<Err
// Phase 2 of the feature-complete roadmap: capture thumbs + corrections on
// AI-generated output so the prompt builder can inject recent examples as
// few-shot exemplars. Storage-only here; the prompt-conditioning logic lives
// in kon-llm. Retrieval returns the most recent rows, narrowed to the
// in magnotia-llm. Retrieval returns the most recent rows, narrowed to the
// active profile when provided so feedback does not cross profiles.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1264,7 +1264,7 @@ pub struct FeedbackRow {
pub async fn record_feedback(pool: &SqlitePool, params: RecordFeedbackParams) -> Result<i64> {
if !matches!(params.rating, -1..=1) {
return Err(KonError::StorageError(format!(
return Err(MagnotiaError::StorageError(format!(
"invalid feedback rating {} (must be -1, 0, or 1)",
params.rating
)));
@@ -1288,7 +1288,7 @@ pub async fn record_feedback(pool: &SqlitePool, params: RecordFeedbackParams) ->
.bind(profile_id)
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("record_feedback failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("record_feedback failed: {e}")))?;
Ok(row.get::<i64, _>("id"))
}
@@ -1325,7 +1325,7 @@ pub async fn list_feedback_examples(
.bind(limit)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("list_feedback_examples failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("list_feedback_examples failed: {e}")))?;
Ok(rows
.into_iter()
@@ -2430,7 +2430,7 @@ mod tests {
#[tokio::test]
async fn init_readonly_rejects_writes_and_serves_reads() {
let dir = std::env::temp_dir().join(format!("kon-storage-ro-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("magnotia-storage-ro-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("ro.db");
let _ = std::fs::remove_file(&path);
@@ -2482,7 +2482,7 @@ mod tests {
#[tokio::test]
async fn init_readonly_fails_when_db_missing() {
let path = std::env::temp_dir().join(format!(
"kon-storage-ro-missing-{}.db",
"magnotia-storage-ro-missing-{}.db",
std::process::id()
));
let _ = std::fs::remove_file(&path);

View File

@@ -1,28 +1,28 @@
use std::path::PathBuf;
pub fn app_data_dir() -> PathBuf {
kon_core::paths::app_paths().app_data_dir()
magnotia_core::paths::app_paths().app_data_dir()
}
/// Path to the SQLite database file.
pub fn database_path() -> PathBuf {
kon_core::paths::app_paths().database_path()
magnotia_core::paths::app_paths().database_path()
}
/// Directory for saved audio recordings.
pub fn recordings_dir() -> PathBuf {
kon_core::paths::app_paths().recordings_dir()
magnotia_core::paths::app_paths().recordings_dir()
}
/// Directory for crash dumps written by the Rust panic hook.
/// Each crash is a single text file: `<unix-ts>-<short-id>.crash`.
/// Used by the diagnostic-report bundler in Settings → About.
pub fn crashes_dir() -> PathBuf {
kon_core::paths::app_paths().crashes_dir()
magnotia_core::paths::app_paths().crashes_dir()
}
/// Directory for the rolling Rust log file (kon.log + rotated kon.log.1, etc).
/// Directory for the rolling Rust log file (magnotia.log + rotated magnotia.log.1, etc).
/// Subscribers configured in src-tauri/src/lib.rs at startup.
pub fn logs_dir() -> PathBuf {
kon_core::paths::app_paths().logs_dir()
magnotia_core::paths::app_paths().logs_dir()
}

View File

@@ -1,4 +1,4 @@
use kon_core::error::{KonError, Result};
use magnotia_core::error::{MagnotiaError, Result};
use sqlx::SqlitePool;
/// Each migration is a (version, description, sql) tuple.
@@ -553,19 +553,19 @@ async fn run_migrations_slice(pool: &SqlitePool, migrations: &[(i64, &str, &str)
)
.execute(pool)
.await
.map_err(|e| KonError::StorageError(format!("Schema version table creation failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Schema version table creation failed: {e}")))?;
let current: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(version), 0) FROM schema_version")
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Schema version query failed: {e}")))?;
.map_err(|e| MagnotiaError::StorageError(format!("Schema version query failed: {e}")))?;
for (version, description, sql) in migrations {
if *version > current {
log::info!("Running migration {}: {}", version, description);
let mut tx = pool.begin().await.map_err(|e| {
KonError::StorageError(format!("Migration {} tx begin failed: {e}", version))
MagnotiaError::StorageError(format!("Migration {} tx begin failed: {e}", version))
})?;
for statement in split_statements(sql) {
@@ -573,7 +573,7 @@ async fn run_migrations_slice(pool: &SqlitePool, migrations: &[(i64, &str, &str)
.execute(&mut *tx)
.await
.map_err(|e| {
KonError::StorageError(format!("Migration {} failed: {e}", version))
MagnotiaError::StorageError(format!("Migration {} failed: {e}", version))
})?;
}
@@ -583,11 +583,11 @@ async fn run_migrations_slice(pool: &SqlitePool, migrations: &[(i64, &str, &str)
.execute(&mut *tx)
.await
.map_err(|e| {
KonError::StorageError(format!("Migration version record failed: {e}"))
MagnotiaError::StorageError(format!("Migration version record failed: {e}"))
})?;
tx.commit().await.map_err(|e| {
KonError::StorageError(format!("Migration {} commit failed: {e}", version))
MagnotiaError::StorageError(format!("Migration {} commit failed: {e}", version))
})?;
log::info!("Migration {} complete", version);
@@ -947,7 +947,7 @@ mod tests {
// dictionary.id is INTEGER PK AUTOINCREMENT (see v2); let SQLite assign rowids.
sqlx::query(
"INSERT INTO dictionary (term, note, created_at) VALUES \
('Kon', '', datetime('now')), \
('Magnotia', '', datetime('now')), \
('CORBEL', 'brand', datetime('now')), \
('Wren', '', datetime('now'))",
)

View File

@@ -1,8 +1,8 @@
[package]
name = "kon-transcription"
name = "magnotia-transcription"
version = "0.1.0"
edition = "2021"
description = "Speech-to-text engine wrappers, model management, and inference concurrency for Kon"
description = "Speech-to-text engine wrappers, model management, and inference concurrency for Magnotia"
build = "build.rs"
[features]
@@ -15,13 +15,13 @@ build = "build.rs"
# `whisper-vulkan` is a separate feature so a non-Vulkan target (Android
# without GPU drivers, a CPU-only Windows build) can pull in whisper-rs
# but skip the Vulkan backend. Build CPU-only with:
# cargo build -p kon-transcription --no-default-features --features whisper
# cargo build -p magnotia-transcription --no-default-features --features whisper
default = ["whisper", "whisper-vulkan"]
whisper = ["dep:whisper-rs", "dep:num_cpus"]
whisper-vulkan = ["whisper-rs?/vulkan"]
[dependencies]
kon-core = { path = "../core" }
magnotia-core = { path = "../core" }
# Parakeet via ONNX. Whisper is handled directly via whisper-rs below.
transcribe-rs = { version = "0.3", default-features = false, features = ["onnx"] }
@@ -53,6 +53,6 @@ thiserror = "2"
tracing = "0.1"
[dev-dependencies]
# TcpListener fixture for the download resume tests (mirrors kon-llm).
# TcpListener fixture for the download resume tests (mirrors magnotia-llm).
tokio = { version = "1", features = ["rt", "sync", "net", "io-util", "macros"] }
tempfile = "3"

View File

@@ -11,7 +11,7 @@
//! workspace ever pulls `tokenizers` into the dependency graph on a
//! Windows target. If we ever legitimately need it we can reintroduce
//! it via a sidecar (isolated process, separate CRT) rather than
//! linking it into `kon_lib`.
//! linking it into `magnotia_lib`.
//!
//! The check is advisory on non-Windows targets — it still prints a
//! cargo:warning if `tokenizers` appears, so the Windows failure isn't
@@ -56,7 +56,7 @@ fn main() {
if target_os == "windows" {
panic!(
"kon-transcription: the `tokenizers` crate appears in Cargo.lock and this is a \
"magnotia-transcription: the `tokenizers` crate appears in Cargo.lock and this is a \
Windows build. Linking `whisper-rs-sys` + `tokenizers` in the same binary has \
been a persistent MSVC C-runtime conflict (see Whispering v7.11.0). Route any \
tokenizer usage through an out-of-process sidecar instead, or gate it off for \
@@ -65,7 +65,7 @@ fn main() {
}
println!(
"cargo:warning=kon-transcription: `tokenizers` crate is in the dependency graph. \
"cargo:warning=magnotia-transcription: `tokenizers` crate is in the dependency graph. \
This build is non-Windows so the link will succeed, but Windows builds will panic \
at build time per docs/whisper-ecosystem/brief.md item #6. Isolate tokenizer usage \
in a sidecar before a Windows ship."

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use kon_core::error::{KonError, Result};
use kon_core::types::{AudioSamples, TranscriptionOptions};
use magnotia_core::error::{MagnotiaError, Result};
use magnotia_core::types::{AudioSamples, TranscriptionOptions};
use crate::local_engine::{LocalEngine, TimedTranscript};
@@ -14,5 +14,5 @@ pub async fn run_inference(
) -> Result<TimedTranscript> {
tokio::task::spawn_blocking(move || engine.transcribe_sync(&audio, &options))
.await
.map_err(|e| KonError::TranscriptionFailed(format!("Task join error: {e}")))?
.map_err(|e| MagnotiaError::TranscriptionFailed(format!("Task join error: {e}")))?
}

View File

@@ -4,8 +4,8 @@ use std::time::Instant;
use transcribe_rs::{SpeechModel, TranscribeOptions, TranscriptionResult};
use kon_core::error::{KonError, Result};
use kon_core::types::{
use magnotia_core::error::{MagnotiaError, Result};
use magnotia_core::types::{
AudioSamples, EngineName, ModelId, Segment, Transcript, TranscriptionOptions,
};
@@ -28,7 +28,7 @@ pub struct SpeechModelAdapter(pub Box<dyn SpeechModel + Send>);
impl Transcriber for SpeechModelAdapter {
fn capabilities(&self) -> TranscriberCapabilities {
TranscriberCapabilities {
sample_rate: kon_core::constants::WHISPER_SAMPLE_RATE,
sample_rate: magnotia_core::constants::WHISPER_SAMPLE_RATE,
channels: 1,
supports_initial_prompt: false,
}
@@ -48,7 +48,7 @@ impl Transcriber for SpeechModelAdapter {
let result: TranscriptionResult = self
.0
.transcribe(samples, &opts)
.map_err(|e| KonError::TranscriptionFailed(e.to_string()))?;
.map_err(|e| MagnotiaError::TranscriptionFailed(e.to_string()))?;
Ok(result
.segments
.unwrap_or_default()
@@ -140,7 +140,7 @@ impl LocalEngine {
options: &TranscriptionOptions,
) -> Result<TimedTranscript> {
let mut guard = self.engine.lock().unwrap_or_else(|e| e.into_inner());
let backend = guard.as_mut().ok_or(KonError::EngineNotLoaded)?;
let backend = guard.as_mut().ok_or(MagnotiaError::EngineNotLoaded)?;
let start = Instant::now();
let segments = backend.transcribe_sync(audio.samples(), options)?;
@@ -160,7 +160,7 @@ impl LocalEngine {
/// Thin wrapper over `ParakeetModel` that overrides `transcribe_raw` to
/// request word-granularity segments. `transcribe-rs` 0.3's trait impl for
/// `ParakeetModel::transcribe_raw` ignores `TranscribeOptions` and uses
/// `TimestampGranularity::Token` (per-subword) — which surfaces in Kon as
/// `TimestampGranularity::Token` (per-subword) — which surfaces in Magnotia as
/// "T Est Ing . One , Two , Three" output. The concrete-type method
/// `ParakeetModel::transcribe_with` accepts `ParakeetParams` with an
/// explicit granularity; this wrapper exposes that to the trait object.
@@ -197,7 +197,7 @@ impl transcribe_rs::SpeechModel for ParakeetWordGranularity {
pub fn load_parakeet(model_dir: &Path) -> Result<Box<dyn Transcriber + Send>> {
use transcribe_rs::onnx::Quantization;
let model = transcribe_rs::onnx::parakeet::ParakeetModel::load(model_dir, &Quantization::Int8)
.map_err(|e| KonError::TranscriptionFailed(format!("Failed to load Parakeet: {e}")))?;
.map_err(|e| MagnotiaError::TranscriptionFailed(format!("Failed to load Parakeet: {e}")))?;
Ok(Box::new(SpeechModelAdapter(Box::new(
ParakeetWordGranularity(model),
))))
@@ -207,7 +207,7 @@ pub fn load_parakeet(model_dir: &Path) -> Result<Box<dyn Transcriber + Send>> {
#[cfg(feature = "whisper")]
pub fn load_whisper(model_path: &Path) -> Result<Box<dyn Transcriber + Send>> {
let backend = WhisperRsBackend::load(model_path)
.map_err(|e| KonError::TranscriptionFailed(format!("Failed to load Whisper: {e}")))?;
.map_err(|e| MagnotiaError::TranscriptionFailed(format!("Failed to load Whisper: {e}")))?;
Ok(Box::new(backend))
}

View File

@@ -2,9 +2,9 @@ use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use kon_core::error::{KonError, Result};
use kon_core::model_registry::{find_model, ModelFile};
use kon_core::types::{DownloadProgress, ModelId};
use magnotia_core::error::{MagnotiaError, Result};
use magnotia_core::model_registry::{find_model, ModelFile};
use magnotia_core::types::{DownloadProgress, ModelId};
static ACTIVE_DOWNLOADS: LazyLock<Mutex<HashSet<String>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
@@ -18,9 +18,9 @@ impl DownloadReservation {
let id = id.as_str().to_string();
let mut active = ACTIVE_DOWNLOADS
.lock()
.map_err(|_| KonError::DownloadFailed("download lock poisoned".into()))?;
.map_err(|_| MagnotiaError::DownloadFailed("download lock poisoned".into()))?;
if !active.insert(id.clone()) {
return Err(KonError::DownloadFailed(format!(
return Err(MagnotiaError::DownloadFailed(format!(
"download already in progress for {id}"
)));
}
@@ -37,15 +37,15 @@ impl Drop for DownloadReservation {
}
/// Resolve the models storage directory.
/// Windows: %LOCALAPPDATA%/kon/models
/// Unix: ~/.kon/models
/// Windows: %LOCALAPPDATA%/magnotia/models
/// Unix: ~/.magnotia/models
pub fn models_dir() -> PathBuf {
kon_core::paths::app_paths().models_dir()
magnotia_core::paths::app_paths().models_dir()
}
/// Get the directory path where a specific model's files are stored.
pub fn model_dir(id: &ModelId) -> PathBuf {
kon_core::paths::app_paths().speech_model_dir(id)
magnotia_core::paths::app_paths().speech_model_dir(id)
}
/// Check whether all files for a model have been downloaded.
@@ -61,7 +61,7 @@ pub fn is_downloaded(id: &ModelId) -> bool {
/// List all downloaded model IDs.
pub fn list_downloaded() -> Vec<ModelId> {
kon_core::model_registry::all_models()
magnotia_core::model_registry::all_models()
.iter()
.filter(|m| is_downloaded(&m.id))
.map(|m| m.id.clone())
@@ -74,13 +74,13 @@ pub fn list_downloaded() -> Vec<ModelId> {
/// For files that declare a `sha256` checksum we validate an existing
/// complete file before skipping the download — a truncated or
/// tampered file gets redownloaded automatically (pattern ported from
/// `kon-llm`'s model_manager, item #8 in the Whisper ecosystem brief).
/// `magnotia-llm`'s model_manager, item #8 in the Whisper ecosystem brief).
pub async fn download(
id: &ModelId,
progress: impl Fn(DownloadProgress) + Send + 'static,
) -> Result<()> {
let _reservation = DownloadReservation::acquire(id)?;
let entry = find_model(id).ok_or_else(|| KonError::ModelNotFound(id.clone()))?;
let entry = find_model(id).ok_or_else(|| MagnotiaError::ModelNotFound(id.clone()))?;
let dir = model_dir(id);
std::fs::create_dir_all(&dir)?;
@@ -98,7 +98,7 @@ pub async fn download(
let _ = std::fs::remove_file(&dest);
}
Err(e) => {
return Err(KonError::DownloadFailed(format!(
return Err(MagnotiaError::DownloadFailed(format!(
"failed to verify existing {}: {e}",
file.filename
)));
@@ -113,10 +113,10 @@ pub async fn download(
}
fn verified_manifest_path(dir: &Path) -> PathBuf {
dir.join(".kon-verified")
dir.join(".magnotia-verified")
}
fn verified_manifest_matches(entry: &kon_core::model_registry::ModelEntry, dir: &Path) -> bool {
fn verified_manifest_matches(entry: &magnotia_core::model_registry::ModelEntry, dir: &Path) -> bool {
let manifest = match std::fs::read_to_string(verified_manifest_path(dir)) {
Ok(contents) => contents,
Err(_) => return false,
@@ -137,7 +137,7 @@ fn verified_manifest_matches(entry: &kon_core::model_registry::ModelEntry, dir:
}
fn write_verified_manifest(
entry: &kon_core::model_registry::ModelEntry,
entry: &magnotia_core::model_registry::ModelEntry,
dir: &Path,
) -> std::io::Result<()> {
let mut lines = Vec::with_capacity(entry.files.len() + 1);
@@ -193,7 +193,7 @@ async fn download_file(
let client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| KonError::DownloadFailed(e.to_string()))?;
.map_err(|e| MagnotiaError::DownloadFailed(e.to_string()))?;
// Check for existing partial download (resume support)
let existing_bytes = if part_path.exists() {
@@ -212,12 +212,12 @@ async fn download_file(
let response = request
.send()
.await
.map_err(|e| KonError::DownloadFailed(e.to_string()))?;
.map_err(|e| MagnotiaError::DownloadFailed(e.to_string()))?;
// If we requested Range but the server returned 200 (full file), the
// server does not support resume. Rather than blindly appending a
// full file on top of our partial bytes (which would produce a
// corrupt result), restart cleanly. This mirrors the kon-llm
// corrupt result), restart cleanly. This mirrors the magnotia-llm
// ResumeUnsupported branch — item #8 of the brief.
//
// For the non-resume path, we still have to validate the status:
@@ -234,14 +234,14 @@ async fn download_file(
false
}
other => {
return Err(KonError::DownloadFailed(format!(
return Err(MagnotiaError::DownloadFailed(format!(
"resume request returned unexpected status {other}"
)));
}
}
} else {
if !response.status().is_success() {
return Err(KonError::DownloadFailed(format!(
return Err(MagnotiaError::DownloadFailed(format!(
"download returned HTTP {} for {}",
response.status(),
file.filename
@@ -288,7 +288,7 @@ async fn download_file(
}
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| KonError::DownloadFailed(e.to_string()))?;
let chunk = chunk.map_err(|e| MagnotiaError::DownloadFailed(e.to_string()))?;
std::io::Write::write_all(&mut out, &chunk)?;
hasher.update(&chunk);
downloaded += chunk.len() as u64;
@@ -316,7 +316,7 @@ async fn download_file(
let actual = format!("{:x}", hasher.finalize());
if actual != file.sha256 {
let _ = std::fs::remove_file(&part_path);
return Err(KonError::DownloadFailed(format!(
return Err(MagnotiaError::DownloadFailed(format!(
"SHA256 mismatch for {}: expected {}, got {}",
file.filename, file.sha256, actual
)));
@@ -354,7 +354,7 @@ mod tests {
let list = list_downloaded();
// In test environment, no models are downloaded
// This just verifies the function doesn't panic
assert!(list.len() <= kon_core::model_registry::all_models().len());
assert!(list.len() <= magnotia_core::model_registry::all_models().len());
}
#[test]
@@ -478,7 +478,7 @@ mod tests {
let file = ModelFile {
filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
url: leak(format!("http://{addr}/fixture.bin")),
size: kon_core::types::Megabytes(0),
size: magnotia_core::types::Megabytes(0),
sha256: leak(expected_sha.clone()),
};
let id = ModelId::new("test-fixture");
@@ -513,7 +513,7 @@ mod tests {
let file = ModelFile {
filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
url: leak(format!("http://{addr}/fixture.bin")),
size: kon_core::types::Megabytes(0),
size: magnotia_core::types::Megabytes(0),
sha256: leak(expected_sha),
};
let id = ModelId::new("test-fixture");
@@ -568,7 +568,7 @@ mod tests {
let file = ModelFile {
filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
url: leak(format!("http://{addr}/fixture.bin")),
size: kon_core::types::Megabytes(0),
size: magnotia_core::types::Megabytes(0),
sha256: leak("0".repeat(64)),
};
let id = ModelId::new("test-fixture");
@@ -596,7 +596,7 @@ mod tests {
let file = ModelFile {
filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()),
url: leak(format!("http://{addr}/fixture.bin")),
size: kon_core::types::Megabytes(0),
size: magnotia_core::types::Megabytes(0),
sha256: leak("deadbeef".repeat(8)),
};
let id = ModelId::new("test-fixture");

View File

@@ -9,8 +9,8 @@
//! `whisper` feature — `WhisperRsBackend` (direct whisper-rs, the only
//! path that pipes `initial_prompt`).
use kon_core::error::Result;
use kon_core::types::{Segment, TranscriptionOptions};
use magnotia_core::error::Result;
use magnotia_core::types::{Segment, TranscriptionOptions};
/// Static capabilities a `Transcriber` advertises to callers.
///

View File

@@ -10,8 +10,8 @@ use std::path::Path;
use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters};
use kon_core::error::{KonError, Result};
use kon_core::types::{Segment, TranscriptionOptions};
use magnotia_core::error::{MagnotiaError, Result};
use magnotia_core::types::{Segment, TranscriptionOptions};
use crate::transcriber::{Transcriber, TranscriberCapabilities};
@@ -40,7 +40,7 @@ impl WhisperRsBackend {
impl Transcriber for WhisperRsBackend {
fn capabilities(&self) -> TranscriberCapabilities {
TranscriberCapabilities {
sample_rate: kon_core::constants::WHISPER_SAMPLE_RATE,
sample_rate: magnotia_core::constants::WHISPER_SAMPLE_RATE,
channels: 1,
supports_initial_prompt: true,
}
@@ -63,7 +63,7 @@ impl Transcriber for WhisperRsBackend {
);
let mut state = self.ctx.create_state().map_err(|e| {
KonError::TranscriptionFailed(WhisperBackendError::State(e.to_string()).to_string())
MagnotiaError::TranscriptionFailed(WhisperBackendError::State(e.to_string()).to_string())
})?;
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
@@ -83,7 +83,7 @@ impl Transcriber for WhisperRsBackend {
params.set_print_realtime(false);
state.full(params, samples).map_err(|e| {
KonError::TranscriptionFailed(
MagnotiaError::TranscriptionFailed(
WhisperBackendError::Transcribe(e.to_string()).to_string(),
)
})?;
@@ -98,7 +98,7 @@ impl Transcriber for WhisperRsBackend {
let text = seg
.to_str()
.map_err(|e| {
KonError::TranscriptionFailed(
MagnotiaError::TranscriptionFailed(
WhisperBackendError::Transcribe(e.to_string()).to_string(),
)
})?

View File

@@ -1,17 +1,17 @@
//! Smoke test: whisper-rs 0.16 loads a GGUF model, transcribes silence, and
//! accepts set_initial_prompt without panicking.
//!
//! Runs only when `KON_WHISPER_TEST_MODEL` is set to the path of a
//! Runs only when `MAGNOTIA_WHISPER_TEST_MODEL` is set to the path of a
//! ggml/gguf whisper model on disk. Otherwise the test exits quiet.
use std::env;
#[test]
fn whisper_rs_smoke_loads_and_transcribes() {
let model_path = match env::var("KON_WHISPER_TEST_MODEL") {
let model_path = match env::var("MAGNOTIA_WHISPER_TEST_MODEL") {
Ok(p) => p,
Err(_) => {
eprintln!("KON_WHISPER_TEST_MODEL not set — skipping");
eprintln!("MAGNOTIA_WHISPER_TEST_MODEL not set — skipping");
return;
}
};