agent: lumotia — pin rust toolchain + workspace clippy/fmt sweep
rust-toolchain.toml pins to stable 1.94.1 so contributors and CI runners share the exact rustc / rustfmt / clippy versions. Without the pin, every machine surfaces a different lint set depending on its local install — six pre-existing lints showed up on 1.94.1 that 1.93-era HANDOVER reported clean. Clippy fixes (all pre-existing, not introduced by feature work): - crates/storage/src/database.rs: std::iter::repeat().take() -> repeat_n() - crates/llm/src/lib.rs (docs): "+ frontends" was parsed as a markdown bullet continuation by rustdoc, breaking doc-lazy-continuation. Reworded to "and". - crates/llm/src/lib.rs (loop): while-let-on-iterator -> for-loop. - src-tauri/src/commands/security.rs: .iter().any(|a| *a == x) -> .contains(&x). - src-tauri/src/lib.rs: io::Error::new(Other, e) -> io::Error::other(e). - src-tauri/src/tauri_app_data_migration.rs: drop function-tail `return`s inside cfg blocks; each platform's block now ends with a tail expression. cargo fmt sweep across the workspace. Mechanical layout-only changes; no semantics affected. Workspace gates after this commit: - cargo fmt --check: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo test --workspace: 405/0 (will become 409/0 with Phase A.1+A.2)
This commit is contained in:
@@ -370,7 +370,11 @@ fn open_and_validate(
|
|||||||
device: cpal::Device,
|
device: cpal::Device,
|
||||||
name: &str,
|
name: &str,
|
||||||
require_audio: bool,
|
require_audio: bool,
|
||||||
) -> Result<(MicrophoneCapture, VecDeque<AudioChunk>, mpsc::Receiver<AudioChunk>)> {
|
) -> Result<(
|
||||||
|
MicrophoneCapture,
|
||||||
|
VecDeque<AudioChunk>,
|
||||||
|
mpsc::Receiver<AudioChunk>,
|
||||||
|
)> {
|
||||||
let config = device
|
let config = device
|
||||||
.default_input_config()
|
.default_input_config()
|
||||||
.map_err(|e| Error::AudioCaptureFailed(format!("default_input_config: {e}")))?;
|
.map_err(|e| Error::AudioCaptureFailed(format!("default_input_config: {e}")))?;
|
||||||
|
|||||||
@@ -15,7 +15,5 @@ pub async fn decode_and_resample(path: &Path) -> Result<AudioSamples> {
|
|||||||
resample_to_16khz(&audio)
|
resample_to_16khz(&audio)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| lumotia_core::error::Error::AudioDecodeFailed(format!("Task join error: {e}")))?
|
||||||
lumotia_core::error::Error::AudioDecodeFailed(format!("Task join error: {e}"))
|
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ pub fn decode_audio_file_limited(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
max_duration_secs: Option<f64>,
|
max_duration_secs: Option<f64>,
|
||||||
) -> Result<AudioSamples> {
|
) -> Result<AudioSamples> {
|
||||||
let file = File::open(path)
|
let file =
|
||||||
.map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
|
File::open(path).map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
|
||||||
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||||
|
|
||||||
let mut hint = Hint::new();
|
let mut hint = Hint::new();
|
||||||
@@ -41,8 +41,8 @@ pub fn decode_audio_file_limited(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn probe_audio_duration_secs(path: &Path) -> Result<Option<f64>> {
|
pub fn probe_audio_duration_secs(path: &Path) -> Result<Option<f64>> {
|
||||||
let file = File::open(path)
|
let file =
|
||||||
.map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
|
File::open(path).map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
|
||||||
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||||
let mut hint = Hint::new();
|
let mut hint = Hint::new();
|
||||||
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||||
@@ -99,9 +99,7 @@ fn decode_media_stream(
|
|||||||
.ok_or_else(|| Error::AudioDecodeFailed("Unknown sample rate".into()))?;
|
.ok_or_else(|| Error::AudioDecodeFailed("Unknown sample rate".into()))?;
|
||||||
|
|
||||||
if sample_rate == 0 {
|
if sample_rate == 0 {
|
||||||
return Err(Error::AudioDecodeFailed(
|
return Err(Error::AudioDecodeFailed("Invalid sample rate: 0".into()));
|
||||||
"Invalid sample rate: 0".into(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let track_id = track.id;
|
let track_id = track.id;
|
||||||
@@ -128,9 +126,7 @@ fn decode_media_stream(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(Error::AudioDecodeFailed(format!(
|
return Err(Error::AudioDecodeFailed(format!("packet read failed: {e}")));
|
||||||
"packet read failed: {e}"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,9 +164,7 @@ fn decode_media_stream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if samples.is_empty() {
|
if samples.is_empty() {
|
||||||
return Err(Error::AudioDecodeFailed(
|
return Err(Error::AudioDecodeFailed("No audio data decoded".into()));
|
||||||
"No audio data decoded".into(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AudioSamples::new(samples, sample_rate, 1))
|
Ok(AudioSamples::new(samples, sample_rate, 1))
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ impl StreamingResampler {
|
|||||||
INPUT_CHUNK,
|
INPUT_CHUNK,
|
||||||
1, // mono
|
1, // mono
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::AudioDecodeFailed(format!("StreamingResampler init failed: {e}")))?;
|
||||||
Error::AudioDecodeFailed(format!("StreamingResampler init failed: {e}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Self::Sinc {
|
Ok(Self::Sinc {
|
||||||
resampler,
|
resampler,
|
||||||
@@ -110,9 +108,7 @@ impl StreamingResampler {
|
|||||||
let chunk: Vec<f32> = residual.drain(..INPUT_CHUNK).collect();
|
let chunk: Vec<f32> = residual.drain(..INPUT_CHUNK).collect();
|
||||||
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| {
|
||||||
Error::AudioDecodeFailed(format!(
|
Error::AudioDecodeFailed(format!("StreamingResampler process failed: {e}"))
|
||||||
"StreamingResampler process failed: {e}"
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
if let Some(channel) = result.into_iter().next() {
|
if let Some(channel) = result.into_iter().next() {
|
||||||
out.extend_from_slice(&channel);
|
out.extend_from_slice(&channel);
|
||||||
@@ -144,9 +140,7 @@ 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| {
|
||||||
Error::AudioDecodeFailed(format!(
|
Error::AudioDecodeFailed(format!("StreamingResampler flush failed: {e}"))
|
||||||
"StreamingResampler flush failed: {e}"
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let Some(mut out) = result.into_iter().next() else {
|
let Some(mut out) = result.into_iter().next() else {
|
||||||
|
|||||||
@@ -42,9 +42,8 @@ impl WavWriter {
|
|||||||
};
|
};
|
||||||
let file = std::fs::File::create(path).map_err(Error::from)?;
|
let file = std::fs::File::create(path).map_err(Error::from)?;
|
||||||
let buffered = BufWriter::new(file);
|
let buffered = BufWriter::new(file);
|
||||||
let inner = hound::WavWriter::new(buffered, spec).map_err(|e| {
|
let inner = hound::WavWriter::new(buffered, spec)
|
||||||
Error::from(std::io::Error::other(format!("WAV create failed: {e}")))
|
.map_err(|e| Error::from(std::io::Error::other(format!("WAV create failed: {e}"))))?;
|
||||||
})?;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner,
|
inner,
|
||||||
samples_since_flush: 0,
|
samples_since_flush: 0,
|
||||||
@@ -77,9 +76,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.flush().map_err(|e| {
|
self.inner
|
||||||
Error::from(std::io::Error::other(format!("WAV flush failed: {e}")))
|
.flush()
|
||||||
})?;
|
.map_err(|e| Error::from(std::io::Error::other(format!("WAV flush failed: {e}"))))?;
|
||||||
self.samples_since_flush = 0;
|
self.samples_since_flush = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -89,9 +88,9 @@ impl WavWriter {
|
|||||||
/// writer leaves a playable file up to the last flush; callers
|
/// writer leaves a playable file up to the last flush; callers
|
||||||
/// that care about the unflushed tail should always finalise.
|
/// that care about the unflushed tail should always finalise.
|
||||||
pub fn finalize(self) -> Result<()> {
|
pub fn finalize(self) -> Result<()> {
|
||||||
self.inner.finalize().map_err(|e| {
|
self.inner
|
||||||
Error::from(std::io::Error::other(format!("WAV finalize failed: {e}")))
|
.finalize()
|
||||||
})?;
|
.map_err(|e| Error::from(std::io::Error::other(format!("WAV finalize failed: {e}"))))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,21 +104,20 @@ pub fn write_wav(path: &Path, audio: &AudioSamples) -> Result<()> {
|
|||||||
sample_format: hound::SampleFormat::Int,
|
sample_format: hound::SampleFormat::Int,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut writer = hound::WavWriter::create(path, spec).map_err(|e| {
|
let mut writer = hound::WavWriter::create(path, spec)
|
||||||
Error::from(std::io::Error::other(format!("WAV create failed: {e}")))
|
.map_err(|e| Error::from(std::io::Error::other(format!("WAV create failed: {e}"))))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
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.write_sample(int_sample).map_err(|e| {
|
writer
|
||||||
Error::from(std::io::Error::other(format!("WAV write failed: {e}")))
|
.write_sample(int_sample)
|
||||||
})?;
|
.map_err(|e| Error::from(std::io::Error::other(format!("WAV write failed: {e}"))))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.finalize().map_err(|e| {
|
writer
|
||||||
Error::from(std::io::Error::other(format!("WAV finalize failed: {e}")))
|
.finalize()
|
||||||
})?;
|
.map_err(|e| Error::from(std::io::Error::other(format!("WAV finalize failed: {e}"))))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -146,17 +144,14 @@ pub fn read_wav(path: &Path) -> Result<AudioSamples> {
|
|||||||
.map(|sample| {
|
.map(|sample| {
|
||||||
sample
|
sample
|
||||||
.map(|s| s as f32 / (1 << (bits_per_sample - 1)) as f32)
|
.map(|s| s as f32 / (1 << (bits_per_sample - 1)) as f32)
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::AudioDecodeFailed(format!("WAV sample decode failed: {e}")))
|
||||||
Error::AudioDecodeFailed(format!("WAV sample decode failed: {e}"))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<f32>>>()?,
|
.collect::<Result<Vec<f32>>>()?,
|
||||||
hound::SampleFormat::Float => reader
|
hound::SampleFormat::Float => reader
|
||||||
.into_samples::<f32>()
|
.into_samples::<f32>()
|
||||||
.map(|sample| {
|
.map(|sample| {
|
||||||
sample.map_err(|e| {
|
sample
|
||||||
Error::AudioDecodeFailed(format!("WAV sample decode failed: {e}"))
|
.map_err(|e| Error::AudioDecodeFailed(format!("WAV sample decode failed: {e}")))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<f32>>>()?,
|
.collect::<Result<Vec<f32>>>()?,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -411,7 +411,9 @@ async fn try_attach_device(
|
|||||||
// Register with the supervisor. This await is brief — it just locks
|
// Register with the supervisor. This await is brief — it just locks
|
||||||
// the supervisor inner Vec and pushes — and happens outside the
|
// the supervisor inner Vec and pushes — and happens outside the
|
||||||
// tracked-map lock.
|
// tracked-map lock.
|
||||||
supervisor.register("device-listener", listener_handle).await;
|
supervisor
|
||||||
|
.register("device-listener", listener_handle)
|
||||||
|
.await;
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,8 +143,7 @@ async fn reconfigure_does_not_leak_forwarder() {
|
|||||||
// (orphaned listener tasks still holding sender clones), the
|
// (orphaned listener tasks still holding sender clones), the
|
||||||
// forwarder would never see `None` from recv() and this timeout
|
// forwarder would never see `None` from recv() and this timeout
|
||||||
// would fire.
|
// would fire.
|
||||||
let join_result =
|
let join_result = tokio::time::timeout(Duration::from_secs(5), forwarder_1).await;
|
||||||
tokio::time::timeout(Duration::from_secs(5), forwarder_1).await;
|
|
||||||
assert!(
|
assert!(
|
||||||
join_result.is_ok(),
|
join_result.is_ok(),
|
||||||
"old forwarder did not join after listener.stop() — Race-2 regressed: \
|
"old forwarder did not join after listener.stop() — Race-2 regressed: \
|
||||||
@@ -176,8 +175,7 @@ async fn reconfigure_does_not_leak_forwarder() {
|
|||||||
|
|
||||||
// ---- Cleanup ----
|
// ---- Cleanup ----
|
||||||
listener_2.stop().await;
|
listener_2.stop().await;
|
||||||
let cleanup_join =
|
let cleanup_join = tokio::time::timeout(Duration::from_secs(5), forwarder_2).await;
|
||||||
tokio::time::timeout(Duration::from_secs(5), forwarder_2).await;
|
|
||||||
assert!(
|
assert!(
|
||||||
cleanup_join.is_ok(),
|
cleanup_join.is_ok(),
|
||||||
"second forwarder also failed to drain after stop()"
|
"second forwarder also failed to drain after stop()"
|
||||||
@@ -189,5 +187,8 @@ async fn reconfigure_does_not_leak_forwarder() {
|
|||||||
// count. The counters exist so the test compiles as a real
|
// count. The counters exist so the test compiles as a real
|
||||||
// forwarder pattern matching what commands::hotkey does in
|
// forwarder pattern matching what commands::hotkey does in
|
||||||
// production.
|
// production.
|
||||||
let _ = (received_first.load(Ordering::SeqCst), received_second.load(Ordering::SeqCst));
|
let _ = (
|
||||||
|
received_first.load(Ordering::SeqCst),
|
||||||
|
received_second.load(Ordering::SeqCst),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ impl LlmEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// True iff a model load is currently in flight. Exposed for tests
|
/// True iff a model load is currently in flight. Exposed for tests
|
||||||
/// + frontends that want to render a "loading…" state without
|
/// and frontends that want to render a "loading…" state without
|
||||||
/// polling `is_loaded()` (which returns false during a swap).
|
/// polling `is_loaded()` (which returns false during a swap).
|
||||||
pub fn is_loading(&self) -> bool {
|
pub fn is_loading(&self) -> bool {
|
||||||
self.loading.load(Ordering::Acquire)
|
self.loading.load(Ordering::Acquire)
|
||||||
@@ -630,7 +630,7 @@ fn extract_json_envelope(text: &str) -> Option<&str> {
|
|||||||
let mut in_string = false;
|
let mut in_string = false;
|
||||||
let mut escaped = false;
|
let mut escaped = false;
|
||||||
|
|
||||||
while let Some((offset, ch)) = chars.next() {
|
for (offset, ch) in chars {
|
||||||
if in_string {
|
if in_string {
|
||||||
if escaped {
|
if escaped {
|
||||||
escaped = false;
|
escaped = false;
|
||||||
@@ -933,8 +933,6 @@ mod tests {
|
|||||||
loader_handle.join().unwrap();
|
loader_handle.join().unwrap();
|
||||||
|
|
||||||
// After the first load completes, a fresh attempt is allowed.
|
// After the first load completes, a fresh attempt is allowed.
|
||||||
assert!(engine
|
assert!(engine.__test_run_with_lock_discipline(|| {}).is_ok());
|
||||||
.__test_run_with_lock_discipline(|| {})
|
|
||||||
.is_ok());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,15 +181,13 @@ pub async fn list_transcripts_paged(
|
|||||||
/// Excludes soft-deleted (deleted_at IS NOT NULL) rows — they only count
|
/// Excludes soft-deleted (deleted_at IS NOT NULL) rows — they only count
|
||||||
/// toward the trash view.
|
/// toward the trash view.
|
||||||
pub async fn count_transcripts(pool: &SqlitePool) -> Result<i64> {
|
pub async fn count_transcripts(pool: &SqlitePool) -> Result<i64> {
|
||||||
let n: i64 = sqlx::query_scalar(
|
let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM transcripts WHERE deleted_at IS NULL")
|
||||||
"SELECT COUNT(*) FROM transcripts WHERE deleted_at IS NULL",
|
.fetch_one(pool)
|
||||||
)
|
.await
|
||||||
.fetch_one(pool)
|
.map_err(|source| Error::Query {
|
||||||
.await
|
operation: "count_transcripts".into(),
|
||||||
.map_err(|source| Error::Query {
|
source,
|
||||||
operation: "count_transcripts".into(),
|
})?;
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,17 +335,16 @@ pub async fn delete_transcript(pool: &SqlitePool, id: &str) -> Result<()> {
|
|||||||
// (which filters deleted_at IS NULL) so a double-delete still finds
|
// (which filters deleted_at IS NULL) so a double-delete still finds
|
||||||
// the row and the second call is a no-op cleanup rather than an
|
// the row and the second call is a no-op cleanup rather than an
|
||||||
// error.
|
// error.
|
||||||
let audio_path: Option<String> = sqlx::query_scalar(
|
let audio_path: Option<String> =
|
||||||
"SELECT audio_path FROM transcripts WHERE id = ?",
|
sqlx::query_scalar("SELECT audio_path FROM transcripts WHERE id = ?")
|
||||||
)
|
.bind(id)
|
||||||
.bind(id)
|
.fetch_optional(pool)
|
||||||
.fetch_optional(pool)
|
.await
|
||||||
.await
|
.map_err(|source| Error::Query {
|
||||||
.map_err(|source| Error::Query {
|
operation: "delete_transcript".into(),
|
||||||
operation: "delete_transcript".into(),
|
source,
|
||||||
source,
|
})?
|
||||||
})?
|
.flatten();
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let res = sqlx::query(
|
let res = sqlx::query(
|
||||||
"UPDATE transcripts SET deleted_at = datetime('now') \
|
"UPDATE transcripts SET deleted_at = datetime('now') \
|
||||||
@@ -390,10 +387,7 @@ pub async fn delete_transcript(pool: &SqlitePool, id: &str) -> Result<()> {
|
|||||||
///
|
///
|
||||||
/// Audio files are also best-effort removed here in case the original
|
/// Audio files are also best-effort removed here in case the original
|
||||||
/// soft-delete failed at the filesystem layer (Rev-3 belt-and-braces).
|
/// soft-delete failed at the filesystem layer (Rev-3 belt-and-braces).
|
||||||
pub async fn purge_deleted_transcripts(
|
pub async fn purge_deleted_transcripts(pool: &SqlitePool, older_than_days: i64) -> Result<u64> {
|
||||||
pool: &SqlitePool,
|
|
||||||
older_than_days: i64,
|
|
||||||
) -> Result<u64> {
|
|
||||||
// Collect (id, audio_path) BEFORE the DELETE so we still have the
|
// Collect (id, audio_path) BEFORE the DELETE so we still have the
|
||||||
// paths to clean up after the row is gone.
|
// paths to clean up after the row is gone.
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
@@ -429,8 +423,7 @@ pub async fn purge_deleted_transcripts(
|
|||||||
// SQLITE_MAX_VARIABLE_NUMBER (default 999).
|
// SQLITE_MAX_VARIABLE_NUMBER (default 999).
|
||||||
let mut total: u64 = 0;
|
let mut total: u64 = 0;
|
||||||
for chunk in ids.chunks(200) {
|
for chunk in ids.chunks(200) {
|
||||||
let placeholders = std::iter::repeat("?")
|
let placeholders = std::iter::repeat_n("?", chunk.len())
|
||||||
.take(chunk.len())
|
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let sql = format!("DELETE FROM transcripts WHERE id IN ({placeholders})");
|
let sql = format!("DELETE FROM transcripts WHERE id IN ({placeholders})");
|
||||||
@@ -3067,7 +3060,10 @@ mod tests {
|
|||||||
assert_eq!(second, (0, 0));
|
assert_eq!(second, (0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn minimal_transcript(id: &'static str, audio_path: Option<&'static str>) -> InsertTranscriptParams<'static> {
|
fn minimal_transcript(
|
||||||
|
id: &'static str,
|
||||||
|
audio_path: Option<&'static str>,
|
||||||
|
) -> InsertTranscriptParams<'static> {
|
||||||
InsertTranscriptParams {
|
InsertTranscriptParams {
|
||||||
id,
|
id,
|
||||||
text: "soft-delete fixture",
|
text: "soft-delete fixture",
|
||||||
@@ -3100,14 +3096,16 @@ mod tests {
|
|||||||
|
|
||||||
delete_transcript(&pool, "t-soft").await.unwrap();
|
delete_transcript(&pool, "t-soft").await.unwrap();
|
||||||
|
|
||||||
let deleted_at: Option<String> = sqlx::query_scalar(
|
let deleted_at: Option<String> =
|
||||||
"SELECT deleted_at FROM transcripts WHERE id = ?",
|
sqlx::query_scalar("SELECT deleted_at FROM transcripts WHERE id = ?")
|
||||||
)
|
.bind("t-soft")
|
||||||
.bind("t-soft")
|
.fetch_one(&pool)
|
||||||
.fetch_one(&pool)
|
.await
|
||||||
.await
|
.unwrap();
|
||||||
.unwrap();
|
assert!(
|
||||||
assert!(deleted_at.is_some(), "row should still exist with deleted_at set");
|
deleted_at.is_some(),
|
||||||
|
"row should still exist with deleted_at set"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -3116,10 +3114,7 @@ mod tests {
|
|||||||
// soft-delete actually flipped a row. A repeat delete is a
|
// soft-delete actually flipped a row. A repeat delete is a
|
||||||
// no-op and does NOT fail when the file is already gone.
|
// no-op and does NOT fail when the file is already gone.
|
||||||
let pool = test_pool().await;
|
let pool = test_pool().await;
|
||||||
let tmp = std::env::temp_dir().join(format!(
|
let tmp = std::env::temp_dir().join(format!("lumotia-test-{}.wav", std::process::id()));
|
||||||
"lumotia-test-{}.wav",
|
|
||||||
std::process::id()
|
|
||||||
));
|
|
||||||
std::fs::write(&tmp, b"fake wav").unwrap();
|
std::fs::write(&tmp, b"fake wav").unwrap();
|
||||||
let path_owned = tmp.to_string_lossy().to_string();
|
let path_owned = tmp.to_string_lossy().to_string();
|
||||||
let path_static: &'static str = Box::leak(path_owned.into_boxed_str());
|
let path_static: &'static str = Box::leak(path_owned.into_boxed_str());
|
||||||
@@ -3130,7 +3125,10 @@ mod tests {
|
|||||||
assert!(tmp.exists(), "fixture file should exist pre-delete");
|
assert!(tmp.exists(), "fixture file should exist pre-delete");
|
||||||
|
|
||||||
delete_transcript(&pool, "t-audio").await.unwrap();
|
delete_transcript(&pool, "t-audio").await.unwrap();
|
||||||
assert!(!tmp.exists(), "audio file should be removed by delete_transcript");
|
assert!(
|
||||||
|
!tmp.exists(),
|
||||||
|
"audio file should be removed by delete_transcript"
|
||||||
|
);
|
||||||
|
|
||||||
// Repeat delete must not surface an error even though both the
|
// Repeat delete must not surface an error even though both the
|
||||||
// soft-delete UPDATE is a no-op AND the audio file is already gone.
|
// soft-delete UPDATE is a no-op AND the audio file is already gone.
|
||||||
@@ -3184,33 +3182,32 @@ mod tests {
|
|||||||
delete_transcript(&pool, "t-new").await.unwrap();
|
delete_transcript(&pool, "t-new").await.unwrap();
|
||||||
|
|
||||||
// Backdate t-old past the 30-day window. t-new keeps "now".
|
// Backdate t-old past the 30-day window. t-new keeps "now".
|
||||||
sqlx::query(
|
sqlx::query("UPDATE transcripts SET deleted_at = datetime('now', '-60 days') WHERE id = ?")
|
||||||
"UPDATE transcripts SET deleted_at = datetime('now', '-60 days') WHERE id = ?",
|
.bind("t-old")
|
||||||
)
|
.execute(&pool)
|
||||||
.bind("t-old")
|
.await
|
||||||
.execute(&pool)
|
.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let purged = purge_deleted_transcripts(&pool, 30).await.unwrap();
|
let purged = purge_deleted_transcripts(&pool, 30).await.unwrap();
|
||||||
assert_eq!(purged, 1, "only t-old should be hard-deleted");
|
assert_eq!(purged, 1, "only t-old should be hard-deleted");
|
||||||
|
|
||||||
let old_exists: Option<String> = sqlx::query_scalar(
|
let old_exists: Option<String> =
|
||||||
"SELECT id FROM transcripts WHERE id = ?",
|
sqlx::query_scalar("SELECT id FROM transcripts WHERE id = ?")
|
||||||
)
|
.bind("t-old")
|
||||||
.bind("t-old")
|
.fetch_optional(&pool)
|
||||||
.fetch_optional(&pool)
|
.await
|
||||||
.await
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
assert!(old_exists.is_none(), "t-old should be hard-gone");
|
assert!(old_exists.is_none(), "t-old should be hard-gone");
|
||||||
|
|
||||||
let new_exists: Option<String> = sqlx::query_scalar(
|
let new_exists: Option<String> =
|
||||||
"SELECT id FROM transcripts WHERE id = ?",
|
sqlx::query_scalar("SELECT id FROM transcripts WHERE id = ?")
|
||||||
)
|
.bind("t-new")
|
||||||
.bind("t-new")
|
.fetch_optional(&pool)
|
||||||
.fetch_optional(&pool)
|
.await
|
||||||
.await
|
.unwrap();
|
||||||
.unwrap();
|
assert!(
|
||||||
assert!(new_exists.is_some(), "t-new still inside the retention window");
|
new_exists.is_some(),
|
||||||
|
"t-new still inside the retention window"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ pub use database::{
|
|||||||
list_transcripts, list_transcripts_paged, list_trashed_transcripts, log_error,
|
list_transcripts, list_transcripts_paged, list_trashed_transcripts, log_error,
|
||||||
mark_implementation_rule_fired, migrate_legacy_setting_keys, prune_error_log,
|
mark_implementation_rule_fired, migrate_legacy_setting_keys, prune_error_log,
|
||||||
purge_deleted_transcripts, record_feedback, restore_transcript, search_transcripts,
|
purge_deleted_transcripts, record_feedback, restore_transcript, search_transcripts,
|
||||||
set_implementation_rule_enabled, set_setting, set_task_energy, uncomplete_task,
|
set_implementation_rule_enabled, set_setting, set_task_energy, uncomplete_task, update_profile,
|
||||||
update_profile, update_task, update_transcript, update_transcript_meta, DailyCompletionCount, ErrorLogRow, FeedbackRow, FeedbackTargetType,
|
update_task, update_transcript, update_transcript_meta, DailyCompletionCount, ErrorLogRow,
|
||||||
ImplementationRuleRow, InsertTranscriptParams, ProfileRow, ProfileTermRow,
|
FeedbackRow, FeedbackTargetType, ImplementationRuleRow, InsertTranscriptParams, ProfileRow,
|
||||||
RecordFeedbackParams, TaskRow, TranscriptRow,
|
ProfileTermRow, RecordFeedbackParams, TaskRow, TranscriptRow,
|
||||||
};
|
};
|
||||||
pub use file_storage::{app_data_dir, crashes_dir, database_path, logs_dir, recordings_dir};
|
pub use file_storage::{app_data_dir, crashes_dir, database_path, logs_dir, recordings_dir};
|
||||||
|
|||||||
@@ -1237,7 +1237,9 @@ mod tests {
|
|||||||
// rows so the migration doesn't accidentally mark old transcripts
|
// rows so the migration doesn't accidentally mark old transcripts
|
||||||
// as deleted.
|
// as deleted.
|
||||||
let pool = fk_test_pool().await;
|
let pool = fk_test_pool().await;
|
||||||
run_migrations_up_to(&pool, 15).await.expect("migrate to v15");
|
run_migrations_up_to(&pool, 15)
|
||||||
|
.await
|
||||||
|
.expect("migrate to v15");
|
||||||
|
|
||||||
// Seed a pre-v16 row to verify backfill preserves NULL.
|
// Seed a pre-v16 row to verify backfill preserves NULL.
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -1290,7 +1292,9 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("read indexes");
|
.expect("read indexes");
|
||||||
assert!(
|
assert!(
|
||||||
index_names.iter().any(|n| n == "idx_transcripts_deleted_at"),
|
index_names
|
||||||
|
.iter()
|
||||||
|
.any(|n| n == "idx_transcripts_deleted_at"),
|
||||||
"expected idx_transcripts_deleted_at, got {index_names:?}",
|
"expected idx_transcripts_deleted_at, got {index_names:?}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,8 +199,7 @@ impl LocalEngine {
|
|||||||
let backend = guard.as_mut().ok_or(Error::EngineNotLoaded)?;
|
let backend = guard.as_mut().ok_or(Error::EngineNotLoaded)?;
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let segments =
|
let segments = backend.transcribe_sync_with_abort(audio.samples(), options, abort_flag)?;
|
||||||
backend.transcribe_sync_with_abort(audio.samples(), options, abort_flag)?;
|
|
||||||
let inference_ms = start.elapsed().as_millis() as u64;
|
let inference_ms = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
Ok(TimedTranscript {
|
Ok(TimedTranscript {
|
||||||
@@ -272,7 +271,9 @@ pub fn load_whisper(model_path: &Path) -> Result<Box<dyn Transcriber + Send>> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use transcribe_rs::{ModelCapabilities, TranscribeError, TranscribeOptions, TranscriptionResult};
|
use transcribe_rs::{
|
||||||
|
ModelCapabilities, TranscribeError, TranscribeOptions, TranscriptionResult,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn engine_reports_not_available_before_loading() {
|
fn engine_reports_not_available_before_loading() {
|
||||||
|
|||||||
@@ -130,10 +130,7 @@ fn verified_manifest_path(dir: &Path) -> PathBuf {
|
|||||||
dir.join(".lumotia-verified")
|
dir.join(".lumotia-verified")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verified_manifest_matches(
|
fn verified_manifest_matches(entry: &lumotia_core::model_registry::ModelEntry, dir: &Path) -> bool {
|
||||||
entry: &lumotia_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,
|
||||||
@@ -760,10 +757,7 @@ mod tests {
|
|||||||
manifest_path.exists(),
|
manifest_path.exists(),
|
||||||
"final manifest must exist after atomic write"
|
"final manifest must exist after atomic write"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(!tmp_path.exists(), "stale .tmp must be removed by rename");
|
||||||
!tmp_path.exists(),
|
|
||||||
"stale .tmp must be removed by rename"
|
|
||||||
);
|
|
||||||
|
|
||||||
let body = std::fs::read_to_string(&manifest_path).unwrap();
|
let body = std::fs::read_to_string(&manifest_path).unwrap();
|
||||||
assert!(body.starts_with("version\t1"));
|
assert!(body.starts_with("version\t1"));
|
||||||
|
|||||||
@@ -92,9 +92,7 @@ impl WhisperRsBackend {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut state = self.ctx.create_state().map_err(|e| {
|
let mut state = self.ctx.create_state().map_err(|e| {
|
||||||
Error::TranscriptionFailed(
|
Error::TranscriptionFailed(WhisperBackendError::State(e.to_string()).to_string())
|
||||||
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 });
|
||||||
@@ -124,9 +122,7 @@ impl WhisperRsBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.full(params, samples).map_err(|e| {
|
state.full(params, samples).map_err(|e| {
|
||||||
Error::TranscriptionFailed(
|
Error::TranscriptionFailed(WhisperBackendError::Transcribe(e.to_string()).to_string())
|
||||||
WhisperBackendError::Transcribe(e.to_string()).to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let n = state.full_n_segments();
|
let n = state.full_n_segments();
|
||||||
|
|||||||
10
rust-toolchain.toml
Normal file
10
rust-toolchain.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[toolchain]
|
||||||
|
# Pinned to stop rustc / rustfmt / clippy drift across contributor machines
|
||||||
|
# and CI runners. Bumping this version requires:
|
||||||
|
# - cargo fmt --check clean on the new toolchain
|
||||||
|
# - cargo clippy --workspace --all-targets -- -D warnings clean
|
||||||
|
# - cargo test --workspace green
|
||||||
|
# in that order, committed as a hygiene sweep separate from feature work.
|
||||||
|
channel = "1.94.1"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
|
profile = "minimal"
|
||||||
@@ -16,8 +16,7 @@ pub(crate) const MAX_CLIPBOARD_BYTES: usize = 1024 * 1024;
|
|||||||
/// buttons; mirror the secondary-windows capability grant in
|
/// buttons; mirror the secondary-windows capability grant in
|
||||||
/// `src-tauri/capabilities/secondary-windows.json` so the IPC trust
|
/// `src-tauri/capabilities/secondary-windows.json` so the IPC trust
|
||||||
/// boundary and the permission set stay in lock-step.
|
/// boundary and the permission set stay in lock-step.
|
||||||
const CLIPBOARD_ALLOWED_WINDOWS: &[&str] =
|
const CLIPBOARD_ALLOWED_WINDOWS: &[&str] = &["main", "transcript-viewer", "transcription-preview"];
|
||||||
&["main", "transcript-viewer", "transcription-preview"];
|
|
||||||
|
|
||||||
/// Copy text to the system clipboard via arboard. Restricted to the
|
/// Copy text to the system clipboard via arboard. Restricted to the
|
||||||
/// documented "clipboard-capable" windows (main + transcript-viewer +
|
/// documented "clipboard-capable" windows (main + transcript-viewer +
|
||||||
|
|||||||
@@ -88,9 +88,12 @@ pub(crate) fn resolve_export_path(path: &Path, bases: &[PathBuf]) -> Result<Path
|
|||||||
path.display()
|
path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let file_name = path
|
let file_name = path.file_name().ok_or_else(|| {
|
||||||
.file_name()
|
format!(
|
||||||
.ok_or_else(|| format!("Refusing to write {}: path has no filename.", path.display()))?;
|
"Refusing to write {}: path has no filename.",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let canon_parent = std::fs::canonicalize(parent).map_err(|e| {
|
let canon_parent = std::fs::canonicalize(parent).map_err(|e| {
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -427,9 +427,7 @@ impl LiveSessionRuntime {
|
|||||||
// the dimensions are real. Validation-window drops can fire
|
// the dimensions are real. Validation-window drops can fire
|
||||||
// before any chunk reaches `process_audio_chunk`; they get
|
// before any chunk reaches `process_audio_chunk`; they get
|
||||||
// attributed at the next reconciliation once we know the rate.
|
// attributed at the next reconciliation once we know the rate.
|
||||||
if self.state.last_chunk_sample_rate == 0
|
if self.state.last_chunk_sample_rate == 0 || self.state.last_chunk_samples_per_chan == 0 {
|
||||||
|| self.state.last_chunk_samples_per_chan == 0
|
|
||||||
{
|
|
||||||
// Roll back the consumed delta so we re-observe it once
|
// Roll back the consumed delta so we re-observe it once
|
||||||
// we have dimensions to convert it with.
|
// we have dimensions to convert it with.
|
||||||
self.state.last_dropped_chunks = self.state.last_dropped_chunks.saturating_sub(delta);
|
self.state.last_dropped_chunks = self.state.last_dropped_chunks.saturating_sub(delta);
|
||||||
@@ -1111,11 +1109,8 @@ fn maybe_dispatch_chunk(
|
|||||||
let parent_span = tracing::Span::current();
|
let parent_span = tracing::Span::current();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let _parent = parent_span.enter();
|
let _parent = parent_span.enter();
|
||||||
let inference_span = tracing::info_span!(
|
let inference_span =
|
||||||
"inference",
|
tracing::info_span!("inference", chunk_id = current_chunk_id, duration_secs,);
|
||||||
chunk_id = current_chunk_id,
|
|
||||||
duration_secs,
|
|
||||||
);
|
|
||||||
let _enter = inference_span.enter();
|
let _enter = inference_span.enter();
|
||||||
let audio = AudioSamples::mono_16khz(chunk_samples);
|
let audio = AudioSamples::mono_16khz(chunk_samples);
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
|
|||||||
@@ -21,15 +21,12 @@ pub fn ensure_main_window_label(label: &str) -> Result<(), String> {
|
|||||||
/// `&[&'static str]` and should mirror an entry in
|
/// `&[&'static str]` and should mirror an entry in
|
||||||
/// `src-tauri/capabilities/*.json` — keeping the IPC trust boundary and the
|
/// `src-tauri/capabilities/*.json` — keeping the IPC trust boundary and the
|
||||||
/// permission grant in lock-step.
|
/// permission grant in lock-step.
|
||||||
pub fn ensure_window_in_set(
|
pub fn ensure_window_in_set(window: &tauri::WebviewWindow, allowed: &[&str]) -> Result<(), String> {
|
||||||
window: &tauri::WebviewWindow,
|
|
||||||
allowed: &[&str],
|
|
||||||
) -> Result<(), String> {
|
|
||||||
ensure_window_in_set_label(window.label(), allowed)
|
ensure_window_in_set_label(window.label(), allowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_window_in_set_label(label: &str, allowed: &[&str]) -> Result<(), String> {
|
pub fn ensure_window_in_set_label(label: &str, allowed: &[&str]) -> Result<(), String> {
|
||||||
if allowed.iter().any(|a| *a == label) {
|
if allowed.contains(&label) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ const MAX_TRANSCRIBE_BYTES: u64 = 1024 * 1024 * 1024;
|
|||||||
/// rejects most obvious attacks), then size (one stat call), then
|
/// rejects most obvious attacks), then size (one stat call), then
|
||||||
/// path canonicalisation (slowest, only meaningful once the cheaper
|
/// path canonicalisation (slowest, only meaningful once the cheaper
|
||||||
/// gates pass).
|
/// gates pass).
|
||||||
pub(crate) fn validate_transcribe_input(
|
pub(crate) fn validate_transcribe_input(path: &Path, metadata_len: u64) -> Result<(), String> {
|
||||||
path: &Path,
|
|
||||||
metadata_len: u64,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let ext = path
|
let ext = path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|s| s.to_str())
|
.and_then(|s| s.to_str())
|
||||||
@@ -67,7 +64,10 @@ pub(crate) fn validate_transcribe_input(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !ALLOWED_AUDIO_EXTENSIONS.iter().any(|allowed| *allowed == ext) {
|
if !ALLOWED_AUDIO_EXTENSIONS
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| *allowed == ext)
|
||||||
|
{
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Refusing to transcribe {}: extension '.{}' is not in the \
|
"Refusing to transcribe {}: extension '.{}' is not in the \
|
||||||
allowlist. Supported: {}.",
|
allowlist. Supported: {}.",
|
||||||
@@ -237,8 +237,7 @@ pub async fn transcribe_file(
|
|||||||
// Trust-5: extension + size gate before we hand the path to the
|
// Trust-5: extension + size gate before we hand the path to the
|
||||||
// audio decoder. `std::fs::metadata` resolves symlinks so the size
|
// audio decoder. `std::fs::metadata` resolves symlinks so the size
|
||||||
// check sees the actual blob, not a symlink-target lie.
|
// check sees the actual blob, not a symlink-target lie.
|
||||||
let metadata = std::fs::metadata(&path)
|
let metadata = std::fs::metadata(&path).map_err(|e| format!("Cannot stat {path}: {e}"))?;
|
||||||
.map_err(|e| format!("Cannot stat {path}: {e}"))?;
|
|
||||||
validate_transcribe_input(Path::new(&path), metadata.len())?;
|
validate_transcribe_input(Path::new(&path), metadata.len())?;
|
||||||
|
|
||||||
let resolved_profile_id =
|
let resolved_profile_id =
|
||||||
@@ -347,7 +346,9 @@ mod tests_trust5 {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accepts_each_allowed_extension() {
|
fn accepts_each_allowed_extension() {
|
||||||
for ext in ["wav", "mp3", "m4a", "mp4", "flac", "ogg", "opus", "webm", "aac"] {
|
for ext in [
|
||||||
|
"wav", "mp3", "m4a", "mp4", "flac", "ogg", "opus", "webm", "aac",
|
||||||
|
] {
|
||||||
let path_string = format!("/tmp/clip.{ext}");
|
let path_string = format!("/tmp/clip.{ext}");
|
||||||
let path = Path::new(&path_string);
|
let path = Path::new(&path_string);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -376,10 +377,8 @@ mod tests_trust5 {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_oversize_file() {
|
fn rejects_oversize_file() {
|
||||||
let result = validate_transcribe_input(
|
let result =
|
||||||
Path::new("/tmp/huge.wav"),
|
validate_transcribe_input(Path::new("/tmp/huge.wav"), MAX_TRANSCRIBE_BYTES + 1);
|
||||||
MAX_TRANSCRIBE_BYTES + 1,
|
|
||||||
);
|
|
||||||
let err = result.expect_err("must reject oversize file");
|
let err = result.expect_err("must reject oversize file");
|
||||||
assert!(err.contains("1 GiB"), "unexpected error: {err}");
|
assert!(err.contains("1 GiB"), "unexpected error: {err}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ use lumotia_storage::{
|
|||||||
delete_transcript as db_delete_transcript, get_transcript as db_get_transcript,
|
delete_transcript as db_delete_transcript, get_transcript as db_get_transcript,
|
||||||
insert_transcript as db_insert_transcript, list_transcripts_paged,
|
insert_transcript as db_insert_transcript, list_transcripts_paged,
|
||||||
list_trashed_transcripts as db_list_trashed_transcripts,
|
list_trashed_transcripts as db_list_trashed_transcripts,
|
||||||
restore_transcript as db_restore_transcript,
|
restore_transcript as db_restore_transcript, search_transcripts as db_search_transcripts,
|
||||||
search_transcripts as db_search_transcripts, update_transcript as db_update_transcript,
|
update_transcript as db_update_transcript, update_transcript_meta as db_update_transcript_meta,
|
||||||
update_transcript_meta as db_update_transcript_meta, InsertTranscriptParams, TranscriptRow,
|
InsertTranscriptParams, TranscriptRow,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
|||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
use tracing_subscriber::{EnvFilter, Layer};
|
use tracing_subscriber::{EnvFilter, Layer};
|
||||||
|
|
||||||
use lumotia_core::paths::{
|
use lumotia_core::paths::{check_target_ambiguity, migrate_legacy_data_dir, MigrationStatus};
|
||||||
check_target_ambiguity, migrate_legacy_data_dir, MigrationStatus,
|
|
||||||
};
|
|
||||||
use lumotia_core::types::EngineName;
|
use lumotia_core::types::EngineName;
|
||||||
use lumotia_llm::LlmEngine;
|
use lumotia_llm::LlmEngine;
|
||||||
use lumotia_storage::{
|
use lumotia_storage::{
|
||||||
@@ -239,7 +237,7 @@ fn build_rolling_appender(logs_dir: &Path) -> std::io::Result<RollingFileAppende
|
|||||||
.filename_suffix("log")
|
.filename_suffix("log")
|
||||||
.max_log_files(7)
|
.max_log_files(7)
|
||||||
.build(logs_dir)
|
.build(logs_dir)
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
.map_err(std::io::Error::other)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install a `tracing` subscriber that writes to stderr (developer
|
/// Install a `tracing` subscriber that writes to stderr (developer
|
||||||
@@ -278,9 +276,7 @@ pub fn install_subscriber(logs_dir: &Path) -> Option<WorkerGuard> {
|
|||||||
// Best-effort: keep stderr logging even if the file path is
|
// Best-effort: keep stderr logging even if the file path is
|
||||||
// unwritable, and surface the failure to stderr so dogfooders
|
// unwritable, and surface the failure to stderr so dogfooders
|
||||||
// notice the missing forensic stream.
|
// notice the missing forensic stream.
|
||||||
let _ = tracing_subscriber::registry()
|
let _ = tracing_subscriber::registry().with(stderr_layer).try_init();
|
||||||
.with(stderr_layer)
|
|
||||||
.try_init();
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"lumotia: failed to install rolling file log appender at {}: {e}",
|
"lumotia: failed to install rolling file log appender at {}: {e}",
|
||||||
logs_dir.display()
|
logs_dir.display()
|
||||||
|
|||||||
@@ -41,16 +41,10 @@ pub enum AppDataMigrationStatus {
|
|||||||
/// after the first successful migration (we preserve legacy as a
|
/// after the first successful migration (we preserve legacy as a
|
||||||
/// backup), so the warning is informational rather than an
|
/// backup), so the warning is informational rather than an
|
||||||
/// indication of trouble.
|
/// indication of trouble.
|
||||||
BothExistLegacyPreserved {
|
BothExistLegacyPreserved { old: PathBuf, new: PathBuf },
|
||||||
old: PathBuf,
|
|
||||||
new: PathBuf,
|
|
||||||
},
|
|
||||||
/// Migration succeeded: legacy copied to new path via atomic
|
/// Migration succeeded: legacy copied to new path via atomic
|
||||||
/// staging rename, legacy preserved as a backup.
|
/// staging rename, legacy preserved as a backup.
|
||||||
Migrated {
|
Migrated { old: PathBuf, new: PathBuf },
|
||||||
old: PathBuf,
|
|
||||||
new: PathBuf,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the OLD Tauri `app_data_dir` from platform conventions. The
|
/// Resolve the OLD Tauri `app_data_dir` from platform conventions. The
|
||||||
@@ -67,6 +61,10 @@ pub fn legacy_tauri_app_data_dir() -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn legacy_tauri_app_data_dir_for(identifier: &str) -> Option<PathBuf> {
|
fn legacy_tauri_app_data_dir_for(identifier: &str) -> Option<PathBuf> {
|
||||||
|
// Exactly one of the four cfg blocks below is present per target
|
||||||
|
// compile. Each is a tail expression that becomes the function's
|
||||||
|
// return value. Avoiding explicit `return` keeps clippy's
|
||||||
|
// needless_return lint happy on every platform.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
// XDG_DATA_HOME wins when set and non-empty, per the XDG Base
|
// XDG_DATA_HOME wins when set and non-empty, per the XDG Base
|
||||||
@@ -79,29 +77,29 @@ fn legacy_tauri_app_data_dir_for(identifier: &str) -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let home = std::env::var("HOME").ok().filter(|s| !s.is_empty())?;
|
let home = std::env::var("HOME").ok().filter(|s| !s.is_empty())?;
|
||||||
return Some(
|
Some(
|
||||||
PathBuf::from(home)
|
PathBuf::from(home)
|
||||||
.join(".local")
|
.join(".local")
|
||||||
.join("share")
|
.join("share")
|
||||||
.join(identifier),
|
.join(identifier),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let home = std::env::var("HOME").ok().filter(|s| !s.is_empty())?;
|
let home = std::env::var("HOME").ok().filter(|s| !s.is_empty())?;
|
||||||
return Some(
|
Some(
|
||||||
PathBuf::from(home)
|
PathBuf::from(home)
|
||||||
.join("Library")
|
.join("Library")
|
||||||
.join("Application Support")
|
.join("Application Support")
|
||||||
.join(identifier),
|
.join(identifier),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
let appdata = std::env::var("APPDATA").ok().filter(|s| !s.is_empty())?;
|
let appdata = std::env::var("APPDATA").ok().filter(|s| !s.is_empty())?;
|
||||||
return Some(PathBuf::from(appdata).join(identifier));
|
Some(PathBuf::from(appdata).join(identifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||||
@@ -123,10 +121,7 @@ fn legacy_tauri_app_data_dir_for(identifier: &str) -> Option<PathBuf> {
|
|||||||
/// `paths.rs` migration already covers the user's transcripts and
|
/// `paths.rs` migration already covers the user's transcripts and
|
||||||
/// models, so even a total-failure here only loses webview-keyed state
|
/// models, so even a total-failure here only loses webview-keyed state
|
||||||
/// (preferences, session storage, plugin geometry).
|
/// (preferences, session storage, plugin geometry).
|
||||||
pub fn migrate_tauri_app_data_dir_with_paths(
|
pub fn migrate_tauri_app_data_dir_with_paths(old: &Path, new: &Path) -> AppDataMigrationStatus {
|
||||||
old: &Path,
|
|
||||||
new: &Path,
|
|
||||||
) -> AppDataMigrationStatus {
|
|
||||||
let old_exists = old.exists();
|
let old_exists = old.exists();
|
||||||
let new_exists = new.exists();
|
let new_exists = new.exists();
|
||||||
|
|
||||||
@@ -265,7 +260,10 @@ mod tests {
|
|||||||
assert!(leveldb.exists());
|
assert!(leveldb.exists());
|
||||||
|
|
||||||
// Staging directory is cleaned up.
|
// Staging directory is cleaned up.
|
||||||
assert!(!tmp.path().join("consulting.corbel.lumotia.migrating").exists());
|
assert!(!tmp
|
||||||
|
.path()
|
||||||
|
.join("consulting.corbel.lumotia.migrating")
|
||||||
|
.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -329,6 +327,9 @@ mod tests {
|
|||||||
AppDataMigrationStatus::BothExistLegacyPreserved { .. }
|
AppDataMigrationStatus::BothExistLegacyPreserved { .. }
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(fs::read(new.join("file.txt")).unwrap(), b"v2-edited-by-user");
|
assert_eq!(
|
||||||
|
fs::read(new.join("file.txt")).unwrap(),
|
||||||
|
b"v2-edited-by-user"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,11 +56,7 @@ fn init_tracing_creates_log_file() {
|
|||||||
let entries: Vec<_> = fs::read_dir(&logs_dir)
|
let entries: Vec<_> = fs::read_dir(&logs_dir)
|
||||||
.expect("read tempdir")
|
.expect("read tempdir")
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.filter(|e| {
|
.filter(|e| e.file_name().to_string_lossy().starts_with("lumotia"))
|
||||||
e.file_name()
|
|
||||||
.to_string_lossy()
|
|
||||||
.starts_with("lumotia")
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
Reference in New Issue
Block a user