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

+65 -1
View File
@@ -31,6 +31,9 @@ enum Command {
},
/// Set up the required Google model in the default local cache.
Setup {
/// Set up the local speech recognition models instead of Google embeddings.
#[arg(long)]
speech: bool,
/// Override the platform-local model cache directory.
#[arg(long)]
model_dir: Option<PathBuf>,
@@ -122,6 +125,12 @@ enum Command {
/// Explicitly build only document vectors, without media.
#[arg(long)]
text_only: bool,
/// Optional bundled local speech worker; recognizes passages without replacing Google.
#[arg(long, requires = "speech_model_dir", conflicts_with = "text_only")]
speech_worker: Option<PathBuf>,
/// Directory containing verified base and Silero speech models.
#[arg(long, requires = "speech_worker", conflicts_with = "text_only")]
speech_model_dir: Option<PathBuf>,
},
/// Show the indexed file and content counts.
Stats {
@@ -143,7 +152,17 @@ fn main() -> Result<()> {
require_existing_index(&db)?;
mcp::serve(db, model_dir, embedding_worker, usize::from(cpu_threads))?;
}
Command::Setup { model_dir } => {
Command::Setup { model_dir, speech } => {
if speech {
let directory = model_dir
.map(Ok)
.unwrap_or_else(searchi_assets::speech::default_model_directory)?;
let progress = std::sync::atomic::AtomicU64::new(0);
eprintln!("Setting up local speech models; verified cached assets are reused (149 MB maximum download).");
searchi_assets::speech::provision(&directory, &progress)?;
println!("Speech models ready: {}", directory.display());
return Ok(());
}
let directory = model_dir
.map(Ok)
.unwrap_or_else(searchi_assets::default_model_directory)?;
@@ -280,10 +299,21 @@ fn main() -> Result<()> {
ffmpeg,
ffprobe,
text_only,
speech_worker,
speech_model_dir,
} => {
require_existing_index(&db)?;
let mut service =
HybridSearch::open(&db, model_dir, embedding_worker, usize::from(cpu_threads))?;
if let Some((worker, directory)) = speech_worker.zip(speech_model_dir) {
let speech = searchi_speech::SpeechWorker::load(
&worker,
&directory.join("ggml-base.bin"),
&directory.join("ggml-silero-v6.2.0.bin"),
searchi_speech::Device::Auto,
)?;
service = service.with_speech_provider(Box::new(speech))?;
}
let report = if text_only {
service.index_embeddings()
} else {
@@ -353,6 +383,40 @@ fn require_existing_index(db: &std::path::Path) -> Result<()> {
mod tests {
use super::*;
#[test]
fn speech_indexing_requires_models_and_cannot_downgrade_to_text_only() {
let base = [
"searchi",
"embed",
"--db",
"index.db",
"--model-dir",
"google",
"--embedding-worker",
"google-worker",
];
assert!(
Cli::try_parse_from(base.into_iter().chain(["--speech-worker", "speech-worker"]))
.is_err()
);
assert!(Cli::try_parse_from(base.into_iter().chain([
"--speech-worker",
"speech-worker",
"--speech-model-dir",
"speech",
"--text-only"
]))
.is_err());
assert!(Cli::try_parse_from(base.into_iter().chain([
"--speech-worker",
"speech-worker",
"--speech-model-dir",
"speech"
]))
.is_ok());
assert!(Cli::try_parse_from(["searchi", "setup", "--speech"]).is_ok());
}
#[test]
fn rejects_zero_or_excessive_result_limits() {
for limit in ["0", "1001"] {