Add local spoken passages with Google retrieval and isolated runtime setup
This commit is contained in:
1 parent
bc9459785d
commit
dab703413e
55 files changed
+3633
-70
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user