chore: stabilize current head before Phase 10a QC
This commit is contained in:
@@ -15,5 +15,7 @@ pub async fn decode_and_resample(path: &Path) -> Result<AudioSamples> {
|
|||||||
resample_to_16khz(&audio)
|
resample_to_16khz(&audio)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| magnotia_core::error::MagnotiaError::AudioDecodeFailed(format!("Task join error: {e}")))?
|
.map_err(|e| {
|
||||||
|
magnotia_core::error::MagnotiaError::AudioDecodeFailed(format!("Task join error: {e}"))
|
||||||
|
})?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ fn decode_media_stream(
|
|||||||
.ok_or_else(|| MagnotiaError::AudioDecodeFailed("Unknown sample rate".into()))?;
|
.ok_or_else(|| MagnotiaError::AudioDecodeFailed("Unknown sample rate".into()))?;
|
||||||
|
|
||||||
if sample_rate == 0 {
|
if sample_rate == 0 {
|
||||||
return Err(MagnotiaError::AudioDecodeFailed("Invalid sample rate: 0".into()));
|
return Err(MagnotiaError::AudioDecodeFailed(
|
||||||
|
"Invalid sample rate: 0".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let track_id = track.id;
|
let track_id = track.id;
|
||||||
@@ -166,7 +168,9 @@ fn decode_media_stream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if samples.is_empty() {
|
if samples.is_empty() {
|
||||||
return Err(MagnotiaError::AudioDecodeFailed("No audio data decoded".into()));
|
return Err(MagnotiaError::AudioDecodeFailed(
|
||||||
|
"No audio data decoded".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AudioSamples::new(samples, sample_rate, 1))
|
Ok(AudioSamples::new(samples, sample_rate, 1))
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ impl StreamingResampler {
|
|||||||
INPUT_CHUNK,
|
INPUT_CHUNK,
|
||||||
1, // mono
|
1, // mono
|
||||||
)
|
)
|
||||||
.map_err(|e| MagnotiaError::AudioDecodeFailed(format!("StreamingResampler init failed: {e}")))?;
|
.map_err(|e| {
|
||||||
|
MagnotiaError::AudioDecodeFailed(format!("StreamingResampler init failed: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(Self::Sinc {
|
Ok(Self::Sinc {
|
||||||
resampler,
|
resampler,
|
||||||
@@ -142,7 +144,9 @@ impl StreamingResampler {
|
|||||||
|
|
||||||
let input = vec![chunk];
|
let input = vec![chunk];
|
||||||
let result = resampler.process(&input, None).map_err(|e| {
|
let result = resampler.process(&input, None).map_err(|e| {
|
||||||
MagnotiaError::AudioDecodeFailed(format!("StreamingResampler flush failed: {e}"))
|
MagnotiaError::AudioDecodeFailed(format!(
|
||||||
|
"StreamingResampler flush failed: {e}"
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let Some(mut out) = result.into_iter().next() else {
|
let Some(mut out) = result.into_iter().next() else {
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ impl WavWriter {
|
|||||||
};
|
};
|
||||||
let file = std::fs::File::create(path).map_err(MagnotiaError::Io)?;
|
let file = std::fs::File::create(path).map_err(MagnotiaError::Io)?;
|
||||||
let buffered = BufWriter::new(file);
|
let buffered = BufWriter::new(file);
|
||||||
let inner = hound::WavWriter::new(buffered, spec)
|
let inner = hound::WavWriter::new(buffered, spec).map_err(|e| {
|
||||||
.map_err(|e| MagnotiaError::Io(std::io::Error::other(format!("WAV create failed: {e}"))))?;
|
MagnotiaError::Io(std::io::Error::other(format!("WAV create failed: {e}")))
|
||||||
|
})?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner,
|
inner,
|
||||||
samples_since_flush: 0,
|
samples_since_flush: 0,
|
||||||
@@ -76,9 +77,9 @@ impl WavWriter {
|
|||||||
/// `Self::DEFAULT_FLUSH_EVERY_SAMPLES` — but may do so at natural
|
/// `Self::DEFAULT_FLUSH_EVERY_SAMPLES` — but may do so at natural
|
||||||
/// boundaries (end-of-utterance, UI events) for tighter recovery.
|
/// boundaries (end-of-utterance, UI events) for tighter recovery.
|
||||||
pub fn flush(&mut self) -> Result<()> {
|
pub fn flush(&mut self) -> Result<()> {
|
||||||
self.inner
|
self.inner.flush().map_err(|e| {
|
||||||
.flush()
|
MagnotiaError::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;
|
self.samples_since_flush = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -110,14 +111,14 @@ pub fn write_wav(path: &Path, audio: &AudioSamples) -> Result<()> {
|
|||||||
for &sample in audio.samples() {
|
for &sample in audio.samples() {
|
||||||
let clamped = sample.clamp(-1.0, 1.0);
|
let clamped = sample.clamp(-1.0, 1.0);
|
||||||
let int_sample = (clamped * i16::MAX as f32) as i16;
|
let int_sample = (clamped * i16::MAX as f32) as i16;
|
||||||
writer
|
writer.write_sample(int_sample).map_err(|e| {
|
||||||
.write_sample(int_sample)
|
MagnotiaError::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
|
writer.finalize().map_err(|e| {
|
||||||
.finalize()
|
MagnotiaError::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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ fn resolve_app_data_dir() -> PathBuf {
|
|||||||
return PathBuf::from(xdg).join("magnotia");
|
return PathBuf::from(xdg).join("magnotia");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PathBuf::from(home).join(".local").join("share").join("magnotia")
|
PathBuf::from(home)
|
||||||
|
.join(".local")
|
||||||
|
.join("share")
|
||||||
|
.join("magnotia")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||||
@@ -112,7 +115,10 @@ mod tests {
|
|||||||
let paths = AppPaths {
|
let paths = AppPaths {
|
||||||
app_data_dir: PathBuf::from("/tmp/magnotia-test"),
|
app_data_dir: PathBuf::from("/tmp/magnotia-test"),
|
||||||
};
|
};
|
||||||
assert_eq!(paths.database_path(), PathBuf::from("/tmp/magnotia-test/magnotia.db"));
|
assert_eq!(
|
||||||
|
paths.database_path(),
|
||||||
|
PathBuf::from("/tmp/magnotia-test/magnotia.db")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
paths.speech_model_dir(&ModelId::new("whisper-base-en")),
|
paths.speech_model_dir(&ModelId::new("whisper-base-en")),
|
||||||
PathBuf::from("/tmp/magnotia-test/models/whisper-base-en")
|
PathBuf::from("/tmp/magnotia-test/models/whisper-base-en")
|
||||||
|
|||||||
@@ -162,7 +162,9 @@ static TEST_OVERRIDE: Mutex<Option<PowerState>> = Mutex::new(None);
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn test_get_override() -> Option<PowerState> {
|
fn test_get_override() -> Option<PowerState> {
|
||||||
*TEST_OVERRIDE.lock().expect("power test override mutex poisoned")
|
*TEST_OVERRIDE
|
||||||
|
.lock()
|
||||||
|
.expect("power test override mutex poisoned")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop-guard that resets `TEST_OVERRIDE` to `None` on drop (including on unwind).
|
/// Drop-guard that resets `TEST_OVERRIDE` to `None` on drop (including on unwind).
|
||||||
@@ -174,7 +176,9 @@ struct OverrideGuard;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
impl Drop for OverrideGuard {
|
impl Drop for OverrideGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
*TEST_OVERRIDE.lock().expect("power test override mutex poisoned") = None;
|
*TEST_OVERRIDE
|
||||||
|
.lock()
|
||||||
|
.expect("power test override mutex poisoned") = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +192,9 @@ impl Drop for OverrideGuard {
|
|||||||
pub(crate) fn with_override<R>(state: Option<PowerState>, body: impl FnOnce() -> R) -> R {
|
pub(crate) fn with_override<R>(state: Option<PowerState>, body: impl FnOnce() -> R) -> R {
|
||||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||||
let _lock = TEST_LOCK.lock().expect("power TEST_LOCK poisoned");
|
let _lock = TEST_LOCK.lock().expect("power TEST_LOCK poisoned");
|
||||||
*TEST_OVERRIDE.lock().expect("power test override mutex poisoned") = state;
|
*TEST_OVERRIDE
|
||||||
|
.lock()
|
||||||
|
.expect("power test override mutex poisoned") = state;
|
||||||
let _guard = OverrideGuard;
|
let _guard = OverrideGuard;
|
||||||
body()
|
body()
|
||||||
}
|
}
|
||||||
@@ -226,7 +232,10 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
write_supply(dir.path(), "AC", "Mains", "0");
|
write_supply(dir.path(), "AC", "Mains", "0");
|
||||||
write_supply(dir.path(), "BAT0", "Battery", "0");
|
write_supply(dir.path(), "BAT0", "Battery", "0");
|
||||||
assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnBattery);
|
assert_eq!(
|
||||||
|
parse_power_state_from_dir(dir.path()),
|
||||||
|
PowerState::OnBattery
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ impl ProcessLister {
|
|||||||
/// Refresh the process table in place and return the current
|
/// Refresh the process table in place and return the current
|
||||||
/// lowercased executable names.
|
/// lowercased executable names.
|
||||||
pub fn snapshot(&mut self) -> Vec<String> {
|
pub fn snapshot(&mut self) -> Vec<String> {
|
||||||
self.system
|
self.system.refresh_processes(ProcessesToUpdate::All, true);
|
||||||
.refresh_processes(ProcessesToUpdate::All, true);
|
|
||||||
self.system
|
self.system
|
||||||
.processes()
|
.processes()
|
||||||
.values()
|
.values()
|
||||||
|
|||||||
@@ -113,7 +113,9 @@ mod tests {
|
|||||||
fn with_thread_env_lock<R>(body: impl FnOnce() -> R) -> R {
|
fn with_thread_env_lock<R>(body: impl FnOnce() -> R) -> R {
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
static THREAD_ENV_LOCK: Mutex<()> = Mutex::new(());
|
static THREAD_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||||
let _lock = THREAD_ENV_LOCK.lock().expect("tuning thread-env lock poisoned");
|
let _lock = THREAD_ENV_LOCK
|
||||||
|
.lock()
|
||||||
|
.expect("tuning thread-env lock poisoned");
|
||||||
body()
|
body()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,8 +128,10 @@ mod tests {
|
|||||||
with_thread_env_lock(|| {
|
with_thread_env_lock(|| {
|
||||||
std::env::remove_var("MAGNOTIA_INFERENCE_THREADS");
|
std::env::remove_var("MAGNOTIA_INFERENCE_THREADS");
|
||||||
let n = inference_thread_count(Workload::Llm, false);
|
let n = inference_thread_count(Workload::Llm, false);
|
||||||
assert!(n >= MIN_INFERENCE_THREADS && n <= MAX_INFERENCE_THREADS,
|
assert!(
|
||||||
"got {n}, expected within [{MIN_INFERENCE_THREADS}, {MAX_INFERENCE_THREADS}]");
|
(MIN_INFERENCE_THREADS..=MAX_INFERENCE_THREADS).contains(&n),
|
||||||
|
"got {n}, expected within [{MIN_INFERENCE_THREADS}, {MAX_INFERENCE_THREADS}]"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,8 +179,10 @@ mod tests {
|
|||||||
std::env::remove_var("MAGNOTIA_INFERENCE_THREADS");
|
std::env::remove_var("MAGNOTIA_INFERENCE_THREADS");
|
||||||
crate::power::with_override(Some(PowerState::OnAc), || {
|
crate::power::with_override(Some(PowerState::OnAc), || {
|
||||||
let n = inference_thread_count(Workload::Llm, true);
|
let n = inference_thread_count(Workload::Llm, true);
|
||||||
assert!(n <= GPU_FLOOR_LLM.max(MIN_INFERENCE_THREADS),
|
assert!(
|
||||||
"Llm with GPU offload should clamp to {GPU_FLOOR_LLM}, got {n}");
|
n <= GPU_FLOOR_LLM.max(MIN_INFERENCE_THREADS),
|
||||||
|
"Llm with GPU offload should clamp to {GPU_FLOOR_LLM}, got {n}"
|
||||||
|
);
|
||||||
assert!(n >= MIN_INFERENCE_THREADS);
|
assert!(n >= MIN_INFERENCE_THREADS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -196,11 +202,12 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _: () = assert!(GPU_FLOOR_WHISPER >= GPU_FLOOR_LLM);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn whisper_gpu_floor_is_at_least_llm_gpu_floor() {
|
fn whisper_gpu_floor_is_at_least_llm_gpu_floor() {
|
||||||
// Architectural invariant: whisper retains CPU work even when
|
// Architectural invariant: whisper retains CPU work even when
|
||||||
// GPU-offloaded; its floor must not be lower than the LLM's.
|
// GPU-offloaded; its floor must not be lower than the LLM's.
|
||||||
assert!(GPU_FLOOR_WHISPER >= GPU_FLOOR_LLM);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ impl LlmEngine {
|
|||||||
// follow-up in docs/superpowers/specs/2026-05-09-battery-gpu-aware-
|
// follow-up in docs/superpowers/specs/2026-05-09-battery-gpu-aware-
|
||||||
// thread-tuning-design.md (§ Out of scope).
|
// thread-tuning-design.md (§ Out of scope).
|
||||||
let gpu_offloaded = use_gpu && gpu_layers >= model.n_layer();
|
let gpu_offloaded = use_gpu && gpu_layers >= model.n_layer();
|
||||||
let thread_count = i32::try_from(inference_thread_count(Workload::Llm, gpu_offloaded))
|
let thread_count =
|
||||||
.unwrap_or(4);
|
i32::try_from(inference_thread_count(Workload::Llm, gpu_offloaded)).unwrap_or(4);
|
||||||
let ctx_params = LlamaContextParams::default()
|
let ctx_params = LlamaContextParams::default()
|
||||||
.with_n_ctx(Some(
|
.with_n_ctx(Some(
|
||||||
NonZeroU32::new(n_ctx).expect("n_ctx must be non-zero"),
|
NonZeroU32::new(n_ctx).expect("n_ctx must be non-zero"),
|
||||||
|
|||||||
@@ -182,7 +182,9 @@ pub async fn update_transcript(
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::StorageError(format!("Update transcript failed: {e}")))?;
|
.map_err(|e| {
|
||||||
|
MagnotiaError::StorageError(format!("Update transcript failed: {e}"))
|
||||||
|
})?;
|
||||||
Ok(res.rows_affected())
|
Ok(res.rows_affected())
|
||||||
}
|
}
|
||||||
(Some(t), None) => {
|
(Some(t), None) => {
|
||||||
@@ -191,7 +193,9 @@ pub async fn update_transcript(
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::StorageError(format!("Update transcript failed: {e}")))?;
|
.map_err(|e| {
|
||||||
|
MagnotiaError::StorageError(format!("Update transcript failed: {e}"))
|
||||||
|
})?;
|
||||||
Ok(res.rows_affected())
|
Ok(res.rows_affected())
|
||||||
}
|
}
|
||||||
(None, Some(ttl)) => {
|
(None, Some(ttl)) => {
|
||||||
@@ -200,7 +204,9 @@ pub async fn update_transcript(
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::StorageError(format!("Update transcript failed: {e}")))?;
|
.map_err(|e| {
|
||||||
|
MagnotiaError::StorageError(format!("Update transcript failed: {e}"))
|
||||||
|
})?;
|
||||||
Ok(res.rows_affected())
|
Ok(res.rows_affected())
|
||||||
}
|
}
|
||||||
(None, None) => Ok(0),
|
(None, None) => Ok(0),
|
||||||
@@ -484,7 +490,9 @@ pub async fn complete_subtask_and_check_parent(pool: &SqlitePool, subtask_id: &s
|
|||||||
.bind(&pid)
|
.bind(&pid)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::StorageError(format!("Auto-complete parent failed: {e}")))?;
|
.map_err(|e| {
|
||||||
|
MagnotiaError::StorageError(format!("Auto-complete parent failed: {e}"))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,7 +716,9 @@ pub async fn set_implementation_rule_enabled(
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::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(|| {
|
get_implementation_rule(pool, id).await?.ok_or_else(|| {
|
||||||
MagnotiaError::StorageError(format!(
|
MagnotiaError::StorageError(format!(
|
||||||
@@ -731,7 +741,9 @@ pub async fn mark_implementation_rule_fired(
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::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(|| {
|
get_implementation_rule(pool, id).await?.ok_or_else(|| {
|
||||||
MagnotiaError::StorageError(format!(
|
MagnotiaError::StorageError(format!(
|
||||||
@@ -745,7 +757,9 @@ pub async fn delete_implementation_rule(pool: &SqlitePool, id: &str) -> Result<(
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::StorageError(format!("Delete implementation rule failed: {e}")))?;
|
.map_err(|e| {
|
||||||
|
MagnotiaError::StorageError(format!("Delete implementation rule failed: {e}"))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2511,24 +2525,21 @@ mod tests {
|
|||||||
let removed = prune_error_log(&pool, 90).await.unwrap();
|
let removed = prune_error_log(&pool, 90).await.unwrap();
|
||||||
assert_eq!(removed, 1, "only the 200-day row should be pruned");
|
assert_eq!(removed, 1, "only the 200-day row should be pruned");
|
||||||
|
|
||||||
let remaining: Vec<String> = sqlx::query_scalar(
|
let remaining: Vec<String> =
|
||||||
"SELECT context FROM error_log ORDER BY id ASC",
|
sqlx::query_scalar("SELECT context FROM error_log ORDER BY id ASC")
|
||||||
)
|
.fetch_all(&pool)
|
||||||
.fetch_all(&pool)
|
.await
|
||||||
.await
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
assert_eq!(remaining, vec!["recent".to_string(), "older".to_string()]);
|
assert_eq!(remaining, vec!["recent".to_string(), "older".to_string()]);
|
||||||
|
|
||||||
// A 14-day window prunes the 30-day row too.
|
// A 14-day window prunes the 30-day row too.
|
||||||
let removed = prune_error_log(&pool, 14).await.unwrap();
|
let removed = prune_error_log(&pool, 14).await.unwrap();
|
||||||
assert_eq!(removed, 1);
|
assert_eq!(removed, 1);
|
||||||
|
|
||||||
let remaining: Vec<String> = sqlx::query_scalar(
|
let remaining: Vec<String> = sqlx::query_scalar("SELECT context FROM error_log")
|
||||||
"SELECT context FROM error_log",
|
.fetch_all(&pool)
|
||||||
)
|
.await
|
||||||
.fetch_all(&pool)
|
.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(remaining, vec!["recent".to_string()]);
|
assert_eq!(remaining, vec!["recent".to_string()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ pub use database::{
|
|||||||
list_profiles, list_recent_completions, list_recent_errors, list_subtasks, list_tasks,
|
list_profiles, list_recent_completions, list_recent_errors, list_subtasks, list_tasks,
|
||||||
list_transcripts, list_transcripts_paged, log_error, mark_implementation_rule_fired,
|
list_transcripts, list_transcripts_paged, log_error, mark_implementation_rule_fired,
|
||||||
prune_error_log, record_feedback, search_transcripts, set_implementation_rule_enabled,
|
prune_error_log, record_feedback, search_transcripts, set_implementation_rule_enabled,
|
||||||
set_setting,
|
set_setting, set_task_energy, uncomplete_task, update_profile, update_task, update_transcript,
|
||||||
set_task_energy, uncomplete_task, update_profile, update_task, update_transcript,
|
|
||||||
update_transcript_meta, DailyCompletionCount, ErrorLogRow, FeedbackRow, FeedbackTargetType,
|
update_transcript_meta, DailyCompletionCount, ErrorLogRow, FeedbackRow, FeedbackTargetType,
|
||||||
ImplementationRuleRow, InsertTranscriptParams, ProfileRow, ProfileTermRow,
|
ImplementationRuleRow, InsertTranscriptParams, ProfileRow, ProfileTermRow,
|
||||||
RecordFeedbackParams, TaskRow, TranscriptRow,
|
RecordFeedbackParams, TaskRow, TranscriptRow,
|
||||||
|
|||||||
@@ -553,7 +553,9 @@ async fn run_migrations_slice(pool: &SqlitePool, migrations: &[(i64, &str, &str)
|
|||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MagnotiaError::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")
|
let current: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(version), 0) FROM schema_version")
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
@@ -1178,7 +1180,8 @@ mod tests {
|
|||||||
.map(|r| r.get::<String, _>("detail"))
|
.map(|r| r.get::<String, _>("detail"))
|
||||||
.collect();
|
.collect();
|
||||||
assert!(
|
assert!(
|
||||||
plan.iter().any(|row| row.contains("idx_transcripts_profile_created")),
|
plan.iter()
|
||||||
|
.any(|row| row.contains("idx_transcripts_profile_created")),
|
||||||
"planner should use the composite index, got plan: {plan:?}",
|
"planner should use the composite index, got plan: {plan:?}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,10 @@ fn verified_manifest_path(dir: &Path) -> PathBuf {
|
|||||||
dir.join(".magnotia-verified")
|
dir.join(".magnotia-verified")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verified_manifest_matches(entry: &magnotia_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)) {
|
let manifest = match std::fs::read_to_string(verified_manifest_path(dir)) {
|
||||||
Ok(contents) => contents,
|
Ok(contents) => contents,
|
||||||
Err(_) => return false,
|
Err(_) => return false,
|
||||||
|
|||||||
@@ -70,9 +70,7 @@ impl TranscriptionProvider for LocalProviderAdapter {
|
|||||||
.map(|c| c.sample_rate)
|
.map(|c| c.sample_rate)
|
||||||
.unwrap_or(magnotia_core::constants::WHISPER_SAMPLE_RATE),
|
.unwrap_or(magnotia_core::constants::WHISPER_SAMPLE_RATE),
|
||||||
channels: local.map(|c| c.channels).unwrap_or(1),
|
channels: local.map(|c| c.channels).unwrap_or(1),
|
||||||
initial_prompt_supported: local
|
initial_prompt_supported: local.map(|c| c.supports_initial_prompt).unwrap_or(false),
|
||||||
.map(|c| c.supports_initial_prompt)
|
|
||||||
.unwrap_or(false),
|
|
||||||
language_hint_supported: true,
|
language_hint_supported: true,
|
||||||
streaming_supported: false,
|
streaming_supported: false,
|
||||||
cost_class: CostClass::Free,
|
cost_class: CostClass::Free,
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ impl Transcriber for WhisperRsBackend {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut state = self.ctx.create_state().map_err(|e| {
|
let mut state = self.ctx.create_state().map_err(|e| {
|
||||||
MagnotiaError::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 });
|
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ fn jfk_transcription_benchmark() {
|
|||||||
let sample_rate = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
|
let sample_rate = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
|
||||||
let channels = u16::from_le_bytes(bytes[22..24].try_into().unwrap());
|
let channels = u16::from_le_bytes(bytes[22..24].try_into().unwrap());
|
||||||
let bits = u16::from_le_bytes(bytes[34..36].try_into().unwrap());
|
let bits = u16::from_le_bytes(bytes[34..36].try_into().unwrap());
|
||||||
eprintln!("[bench] wav spec: {} Hz, {} ch, {}-bit", sample_rate, channels, bits);
|
eprintln!(
|
||||||
|
"[bench] wav spec: {} Hz, {} ch, {}-bit",
|
||||||
|
sample_rate, channels, bits
|
||||||
|
);
|
||||||
assert_eq!(sample_rate, 16_000, "expected 16 kHz wav");
|
assert_eq!(sample_rate, 16_000, "expected 16 kHz wav");
|
||||||
assert_eq!(channels, 1, "expected mono");
|
assert_eq!(channels, 1, "expected mono");
|
||||||
assert_eq!(bits, 16, "expected 16-bit PCM");
|
assert_eq!(bits, 16, "expected 16-bit PCM");
|
||||||
@@ -48,7 +51,10 @@ fn jfk_transcription_benchmark() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let rss_before_load_kb = read_rss_kb();
|
let rss_before_load_kb = read_rss_kb();
|
||||||
eprintln!("[bench] RSS before model load: {} MB", rss_before_load_kb / 1024);
|
eprintln!(
|
||||||
|
"[bench] RSS before model load: {} MB",
|
||||||
|
rss_before_load_kb / 1024
|
||||||
|
);
|
||||||
|
|
||||||
let load_start = Instant::now();
|
let load_start = Instant::now();
|
||||||
let ctx = WhisperContext::new_with_params(&model_path, WhisperContextParameters::default())
|
let ctx = WhisperContext::new_with_params(&model_path, WhisperContextParameters::default())
|
||||||
@@ -57,9 +63,11 @@ fn jfk_transcription_benchmark() {
|
|||||||
eprintln!("[bench] model load: {:.2}s", load_dur.as_secs_f64());
|
eprintln!("[bench] model load: {:.2}s", load_dur.as_secs_f64());
|
||||||
|
|
||||||
let rss_after_load_kb = read_rss_kb();
|
let rss_after_load_kb = read_rss_kb();
|
||||||
eprintln!("[bench] RSS after model load: {} MB (delta +{} MB)",
|
eprintln!(
|
||||||
|
"[bench] RSS after model load: {} MB (delta +{} MB)",
|
||||||
rss_after_load_kb / 1024,
|
rss_after_load_kb / 1024,
|
||||||
(rss_after_load_kb.saturating_sub(rss_before_load_kb)) / 1024);
|
(rss_after_load_kb.saturating_sub(rss_before_load_kb)) / 1024
|
||||||
|
);
|
||||||
|
|
||||||
let mut state = ctx.create_state().expect("whisper state");
|
let mut state = ctx.create_state().expect("whisper state");
|
||||||
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
||||||
@@ -110,12 +118,20 @@ fn jfk_transcription_benchmark() {
|
|||||||
let rss_final_kb = read_rss_kb();
|
let rss_final_kb = read_rss_kb();
|
||||||
eprintln!("[bench] RSS final: {} MB", rss_final_kb / 1024);
|
eprintln!("[bench] RSS final: {} MB", rss_final_kb / 1024);
|
||||||
|
|
||||||
eprintln!("");
|
eprintln!();
|
||||||
eprintln!("=== SUMMARY ===");
|
eprintln!("=== SUMMARY ===");
|
||||||
eprintln!("audio: {:.2}s", audio_secs);
|
eprintln!("audio: {:.2}s", audio_secs);
|
||||||
eprintln!("model_load: {:.2}s", load_dur.as_secs_f64());
|
eprintln!("model_load: {:.2}s", load_dur.as_secs_f64());
|
||||||
eprintln!("cold xc: {:.2}s RTF={:.3}", cold_dur.as_secs_f64(), cold_dur.as_secs_f64() / audio_secs);
|
eprintln!(
|
||||||
eprintln!("warm xc: {:.2}s RTF={:.3}", warm_dur.as_secs_f64(), warm_dur.as_secs_f64() / audio_secs);
|
"cold xc: {:.2}s RTF={:.3}",
|
||||||
|
cold_dur.as_secs_f64(),
|
||||||
|
cold_dur.as_secs_f64() / audio_secs
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"warm xc: {:.2}s RTF={:.3}",
|
||||||
|
warm_dur.as_secs_f64(),
|
||||||
|
warm_dur.as_secs_f64() / audio_secs
|
||||||
|
);
|
||||||
eprintln!("RSS peak: {} MB", rss_final_kb / 1024);
|
eprintln!("RSS peak: {} MB", rss_final_kb / 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +140,9 @@ fn read_rss_kb() -> u64 {
|
|||||||
let s = std::fs::read_to_string(format!("/proc/{pid}/status")).unwrap_or_default();
|
let s = std::fs::read_to_string(format!("/proc/{pid}/status")).unwrap_or_default();
|
||||||
for line in s.lines() {
|
for line in s.lines() {
|
||||||
if let Some(rest) = line.strip_prefix("VmRSS:") {
|
if let Some(rest) = line.strip_prefix("VmRSS:") {
|
||||||
return rest.trim().split_whitespace().next()
|
return rest
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
.and_then(|n| n.parse::<u64>().ok())
|
.and_then(|n| n.parse::<u64>().ok())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,14 +82,7 @@ fn whisper_thread_count_sweep() {
|
|||||||
for (label, power, gpu_offloaded_for_helper) in panels {
|
for (label, power, gpu_offloaded_for_helper) in panels {
|
||||||
env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", power);
|
env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", power);
|
||||||
let helper_pick = inference_thread_count(Workload::Whisper, gpu_offloaded_for_helper);
|
let helper_pick = inference_thread_count(Workload::Whisper, gpu_offloaded_for_helper);
|
||||||
run_sweep_panel(
|
run_sweep_panel(label, helper_pick, &ctx, &samples, audio_secs, &targets);
|
||||||
label,
|
|
||||||
helper_pick,
|
|
||||||
&ctx,
|
|
||||||
&samples,
|
|
||||||
audio_secs,
|
|
||||||
&targets,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE");
|
env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE");
|
||||||
}
|
}
|
||||||
@@ -102,10 +95,8 @@ fn run_sweep_panel(
|
|||||||
audio_secs: f64,
|
audio_secs: f64,
|
||||||
targets: &[i32],
|
targets: &[i32],
|
||||||
) {
|
) {
|
||||||
eprintln!("");
|
eprintln!();
|
||||||
eprintln!(
|
eprintln!("=== n_threads scaling: {label} (helper picks: {helper_pick}) ===");
|
||||||
"=== n_threads scaling: {label} (helper picks: {helper_pick}) ==="
|
|
||||||
);
|
|
||||||
eprintln!("n_threads | xc_time | RTF | speedup_vs_1");
|
eprintln!("n_threads | xc_time | RTF | speedup_vs_1");
|
||||||
eprintln!("----------|---------|--------|-------------");
|
eprintln!("----------|---------|--------|-------------");
|
||||||
let mut baseline_dur: Option<f64> = None;
|
let mut baseline_dur: Option<f64> = None;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
name: Phase 10a stabilization gate after post-handover commits
|
||||||
|
description: Current-head stabilization checkpoint before resuming Phase 10a QC/dogfood. Covers the two commits ahead of origin/main, verification results, and scope decision for dormant Phase A orchestrator work.
|
||||||
|
type: audit
|
||||||
|
tags: [phase10a, stabilization, release, orchestrator, verification]
|
||||||
|
created: 2026/05/10
|
||||||
|
status: green-with-manual-qc-remaining
|
||||||
|
author: Hermes Agent on behalf of Jake Sames
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 10a — Stabilization Gate
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This checkpoint updates the 2026/04/25 handover and static Phase 10a audit for the current local `main`, which is two commits ahead of `origin/main`.
|
||||||
|
|
||||||
|
Current local-only commits inspected:
|
||||||
|
|
||||||
|
- `c95a5da` — `docs(roadmap): bookmark SIE + adjacent embeddings/rerank/extract servers for PKM phase`
|
||||||
|
- `6bc8acc` — `feat(transcription): Phase A — engine protocol + registry + orchestrator (clean-room)`
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### PKM roadmap bookmark
|
||||||
|
|
||||||
|
Low release risk. The SIE / embeddings / rerank / extraction note is documentation-only and explicitly marked deferred. It should not affect v0.1 runtime or QA scope.
|
||||||
|
|
||||||
|
### Phase A transcription orchestrator
|
||||||
|
|
||||||
|
Medium architectural scope, low immediate runtime risk.
|
||||||
|
|
||||||
|
The commit adds:
|
||||||
|
|
||||||
|
- `crates/cloud-providers/src/provider.rs`
|
||||||
|
- `crates/transcription/src/registry.rs`
|
||||||
|
- `crates/transcription/src/orchestrator.rs`
|
||||||
|
- wiring exports/dependencies plus `KNOWN-ISSUES.md` KI-06
|
||||||
|
|
||||||
|
The implementation is deliberately dormant for current app paths. Existing `transcribe_file`, live transcription, and meeting flows still call the existing local engine path directly. KI-06 documents the required follow-up: build the registry at app boot, inject `Arc<Orchestrator>` into `AppState`, and migrate command call sites through the orchestrator with mock-provider coverage.
|
||||||
|
|
||||||
|
Decision for v0.1 release flow: keep the Phase A commit only if the dormant boundary remains green; do not expand scope into Phase A.1 before Phase 10a unless a direct release blocker requires it.
|
||||||
|
|
||||||
|
## Verification run
|
||||||
|
|
||||||
|
Command run from repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --check && \
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings && \
|
||||||
|
cargo test --workspace && \
|
||||||
|
npm run check && \
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Final result: PASS.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Initial run failed `cargo fmt --check`; `cargo fmt` was applied.
|
||||||
|
- Current Rust/Clippy toolchain also surfaced a few warning-as-error issues unrelated to the new orchestrator surface:
|
||||||
|
- `manual_range_contains` in `crates/core/src/tuning.rs`
|
||||||
|
- `assertions_on_constants` in `crates/core/src/tuning.rs`
|
||||||
|
- `println_empty_string` in transcription benchmark tests
|
||||||
|
- `trim_split_whitespace` in transcription benchmark helper
|
||||||
|
- Those were fixed minimally to restore `-D warnings` cleanliness.
|
||||||
|
|
||||||
|
## Files changed by stabilization
|
||||||
|
|
||||||
|
Rust formatting touched multiple existing files. Semantic clippy fixes were limited to:
|
||||||
|
|
||||||
|
- `crates/core/src/tuning.rs`
|
||||||
|
- `crates/transcription/tests/jfk_bench.rs`
|
||||||
|
- `crates/transcription/tests/thread_sweep.rs`
|
||||||
|
|
||||||
|
This audit file records why those changes exist.
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Resume the original Phase 10a plan now, with this added gate satisfied:
|
||||||
|
|
||||||
|
1. Current HEAD is green.
|
||||||
|
2. Post-handover commits are inspected.
|
||||||
|
3. Phase A orchestrator work is classified as dormant / post-v0.1 follow-up, not a reason to expand release scope.
|
||||||
|
|
||||||
|
Next work remains the manual/runtime Phase 10a checklist:
|
||||||
|
|
||||||
|
- Dogfood walkthrough from `HANDOVER.md`
|
||||||
|
- Runtime keyboard / focus / contrast / reduced-motion checks
|
||||||
|
- RB-08 macOS power-assertion verification by Rachmann
|
||||||
|
- Clean-install test once release artefacts are available
|
||||||
|
- Manual GitHub Actions build workflow before tagging v0.1.0
|
||||||
@@ -59,4 +59,3 @@ pub async fn record_feedback(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1677,7 +1677,10 @@ mod tests {
|
|||||||
&message,
|
&message,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(!delivered, "send must report not delivered when result_channel errors");
|
assert!(
|
||||||
|
!delivered,
|
||||||
|
"send must report not delivered when result_channel errors"
|
||||||
|
);
|
||||||
assert!(result_listener_lost, "result_listener_lost must be set");
|
assert!(result_listener_lost, "result_listener_lost must be set");
|
||||||
assert!(
|
assert!(
|
||||||
stop_flag.load(Ordering::Relaxed),
|
stop_flag.load(Ordering::Relaxed),
|
||||||
|
|||||||
@@ -319,7 +319,6 @@ pub struct LanguageSupportInfo {
|
|||||||
pub language_count: u16,
|
pub language_count: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Compile-time target signalling used by `compose_accelerators`.
|
/// Compile-time target signalling used by `compose_accelerators`.
|
||||||
/// Split out so the pure-function behaviour is testable without `cfg!`
|
/// Split out so the pure-function behaviour is testable without `cfg!`
|
||||||
/// appearing in the test matrix.
|
/// appearing in the test matrix.
|
||||||
|
|||||||
@@ -249,4 +249,3 @@ pub async fn transcribe_file(
|
|||||||
"raw_text": raw_text,
|
"raw_text": raw_text,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ pub async fn check_for_update(window: tauri::WebviewWindow) -> Result<Option<Str
|
|||||||
ensure_main_window(&window)?;
|
ensure_main_window(&window)?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ use tauri::Manager;
|
|||||||
|
|
||||||
use magnotia_core::types::EngineName;
|
use magnotia_core::types::EngineName;
|
||||||
use magnotia_llm::LlmEngine;
|
use magnotia_llm::LlmEngine;
|
||||||
use magnotia_storage::{
|
use magnotia_storage::{database_path, get_setting, init as init_db, prune_error_log, set_setting};
|
||||||
database_path, get_setting, init as init_db, prune_error_log, set_setting,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// How long to retain `error_log` rows. Pruned once on startup.
|
/// How long to retain `error_log` rows. Pruned once on startup.
|
||||||
/// 90 days is long enough to triage "this happened a few weeks ago"
|
/// 90 days is long enough to triage "this happened a few weeks ago"
|
||||||
@@ -117,7 +115,11 @@ fn ensure_x11_on_wayland() {
|
|||||||
// magnotia idle CPU 12.30% → 2.80% of one core and idle GPU 17% → 10%.
|
// magnotia idle CPU 12.30% → 2.80% of one core and idle GPU 17% → 10%.
|
||||||
// Apples to both X11 and Wayland sessions; users can opt back in by
|
// Apples to both X11 and Wayland sessions; users can opt back in by
|
||||||
// setting WEBKIT_DISABLE_DMABUF_RENDERER=0 explicitly.
|
// setting WEBKIT_DISABLE_DMABUF_RENDERER=0 explicitly.
|
||||||
set_if_unset("WEBKIT_DISABLE_DMABUF_RENDERER", "1", "iGPU idle-cost workaround");
|
set_if_unset(
|
||||||
|
"WEBKIT_DISABLE_DMABUF_RENDERER",
|
||||||
|
"1",
|
||||||
|
"iGPU idle-cost workaround",
|
||||||
|
);
|
||||||
|
|
||||||
// If the user is on a Wayland session, also force WebKitGTK + GDK
|
// If the user is on a Wayland session, also force WebKitGTK + GDK
|
||||||
// onto X11 via XWayland. Idempotent: if a value is already set, keep
|
// onto X11 via XWayland. Idempotent: if a value is already set, keep
|
||||||
|
|||||||
Reference in New Issue
Block a user