Add local spoken passages with Google retrieval and isolated runtime setup

This commit is contained in:
karl committed 2026-10-13 20:18:44 +02:00
1 parent bc9459785d
commit dab703413e
55 files changed
+3633 -70

No files matched your search

+2
View File
@@ -14,6 +14,8 @@ use std::{
time::Duration,
};
pub mod speech;
/// Exact bytes of the pinned text model plus its multimedia projection.
pub const TOTAL_BYTES: u64 = 864_676_480;
const ATTEMPTS: usize = 3;
+136
View File
@@ -0,0 +1,136 @@
//! Managed local speech models. This provisions model data only, never executables.
//! Ordinary search must not call provisioning; desktop setup owns progress/retry.
use super::*;
pub const TOTAL_BYTES: u64 = 148_836_563;
#[derive(Deserialize)]
struct SpeechManifest {
schema: u32,
pipeline: String,
files: Vec<SpeechArtifact>,
}
#[derive(Deserialize)]
struct SpeechArtifact {
#[serde(flatten)]
artifact: Artifact,
repository: String,
revision: String,
}
fn speech_manifest() -> Result<SpeechManifest> {
let manifest: SpeechManifest = serde_json::from_str(include_str!(
"../../../tools/speech-runtime/model-manifest.json"
))?;
if manifest.schema != 1
|| manifest.pipeline != "whisper-base-silero-v6.2.0-dtw-base-v1"
|| manifest.files.len() != 2
|| manifest
.files
.iter()
.map(|file| file.artifact.bytes)
.sum::<u64>()
!= TOTAL_BYTES
{
bail!("invalid bundled speech model manifest");
}
for (file, (name, repository)) in manifest.files.iter().zip([
("ggml-base.bin", "ggerganov/whisper.cpp"),
("ggml-silero-v6.2.0.bin", "ggml-org/whisper-vad"),
]) {
validate_artifact(&file.artifact)?;
if file.artifact.path != name
|| file.repository != repository
|| file.revision.len() != 40
|| !file.revision.bytes().all(|byte| byte.is_ascii_hexdigit())
{
bail!("unexpected speech model artifact or repository revision");
}
}
Ok(manifest)
}
/// Select the same platform-local cache family as Google models, without IO.
pub fn default_model_directory() -> Result<PathBuf> {
Ok(super::default_model_directory()?
.parent()
.context("model cache has no parent")?
.join("speech"))
}
/// Offline readiness requires exact size and SHA-256 for both pinned models.
pub fn verify_cached(directory: &Path) -> Result<bool> {
for file in speech_manifest()?.files {
if !verified(&directory.join(&file.artifact.path), &file.artifact)? {
return Ok(false);
}
}
Ok(true)
}
/// Download missing models during explicit setup; retain verified cached files.
/// Reuses Google's bounded, atomic download/retry path and numerical progress.
pub fn provision(directory: &Path, progress: &AtomicU64) -> Result<()> {
let speech = speech_manifest()?;
let sources: HashMap<_, _> = speech
.files
.iter()
.map(|file| {
(
file.artifact.path.clone(),
format!(
"https://huggingface.co/{}/resolve/{}/{}",
file.repository, file.revision, file.artifact.path
),
)
})
.collect();
let manifest = Manifest {
model_id: speech.pipeline,
export_repository: String::new(),
revision: String::new(),
files: speech.files.into_iter().map(|file| file.artifact).collect(),
};
let client = Client::builder()
.connect_timeout(Duration::from_secs(20))
.timeout(Duration::from_secs(900))
.user_agent(concat!("Searchi/", env!("CARGO_PKG_VERSION")))
.build()?;
provision_with_callback(
directory,
progress,
&manifest,
&client,
|artifact| sources[&artifact.path].clone(),
|_| {},
)
}
use std::collections::HashMap;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pinned_speech_manifest_matches_verified_diagnostic_assets() -> Result<()> {
let manifest = speech_manifest()?;
assert_eq!(manifest.files[0].artifact.bytes, 147_951_465);
assert_eq!(manifest.files[1].artifact.bytes, 885_098);
assert_eq!(
manifest.files[0].artifact.sha256,
"60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe"
);
assert_eq!(
manifest.files[1].artifact.sha256,
"2aa269b785eeb53a82983a20501ddf7c1d9c48e33ab63a41391ac6c9f7fb6987"
);
let missing = tempfile::tempdir()?;
assert!(!verify_cached(missing.path())?);
// Length-only or wrong cached input never establishes model readiness.
fs::write(missing.path().join("ggml-base.bin"), b"unverified")?;
assert!(!verify_cached(missing.path())?);
Ok(())
}
}