Isolate speech transport failures and keep experimental media usable
This commit is contained in:
1 parent
f15ee07ed0
commit
296a39a6c2
7 files changed
+147
-3
No files matched your search
@@ -484,6 +484,7 @@ async function refreshSemanticStatus() {
|
|||||||
? "Speech models ready · Spoken passages are waiting to be indexed."
|
? "Speech models ready · Spoken passages are waiting to be indexed."
|
||||||
: "Local speech passages ready. Recognized words and timings can contain errors."
|
: "Local speech passages ready. Recognized words and timings can contain errors."
|
||||||
: "Local speech passages are preparing. Filename and existing content search remain available.";
|
: "Local speech passages are preparing. Filename and existing content search remain available.";
|
||||||
|
speechStatus.textContent = `Experimental speech search · ${speechStatus.textContent}`;
|
||||||
}
|
}
|
||||||
const retry = element<HTMLButtonElement>("retry-model");
|
const retry = element<HTMLButtonElement>("retry-model");
|
||||||
retry.hidden =
|
retry.hidden =
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ pub trait SpeechProvider: Send {
|
|||||||
fn admission_us(&self) -> u64 {
|
fn admission_us(&self) -> u64 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
/// Starts a new source without retrying a failed window from the previous source.
|
||||||
|
fn begin_source(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription>;
|
fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +35,9 @@ impl SpeechProvider for searchi_speech::SpeechWorker {
|
|||||||
fn fingerprint(&self) -> &str {
|
fn fingerprint(&self) -> &str {
|
||||||
searchi_speech::SpeechWorker::fingerprint(self)
|
searchi_speech::SpeechWorker::fingerprint(self)
|
||||||
}
|
}
|
||||||
|
fn begin_source(&mut self) -> Result<()> {
|
||||||
|
searchi_speech::SpeechWorker::begin_source(self)
|
||||||
|
}
|
||||||
fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription> {
|
fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription> {
|
||||||
searchi_speech::SpeechWorker::transcribe(self, pcm, sample_rate)
|
searchi_speech::SpeechWorker::transcribe(self, pcm, sample_rate)
|
||||||
}
|
}
|
||||||
@@ -58,6 +65,13 @@ impl SpeechProvider for BudgetedSpeechProvider {
|
|||||||
fn admission_us(&self) -> u64 {
|
fn admission_us(&self) -> u64 {
|
||||||
self.admission_us
|
self.admission_us
|
||||||
}
|
}
|
||||||
|
fn begin_source(&mut self) -> Result<()> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let permit = self.budget.acquire(InferenceRole::Background, 1);
|
||||||
|
self.admission_us = start.elapsed().as_micros().min(u64::MAX as u128) as u64;
|
||||||
|
let _permit = permit?;
|
||||||
|
self.inner.begin_source()
|
||||||
|
}
|
||||||
fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription> {
|
fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let permit = self.budget.acquire(InferenceRole::Background, 1);
|
let permit = self.budget.acquire(InferenceRole::Background, 1);
|
||||||
@@ -275,6 +289,10 @@ impl HybridSearch {
|
|||||||
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
||||||
let mut ordinal = 0usize;
|
let mut ordinal = 0usize;
|
||||||
if metadata.has_audio {
|
if metadata.has_audio {
|
||||||
|
self.speech
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Speech provider unavailable"))?
|
||||||
|
.begin_source()?;
|
||||||
for window in speech_windows(metadata.audio_start_ms, metadata.audio_end_ms)? {
|
for window in speech_windows(metadata.audio_start_ms, metadata.audio_end_ms)? {
|
||||||
let pcm = decode_audio_aligned(tools, path, window, limits)?;
|
let pcm = decode_audio_aligned(tools, path, window, limits)?;
|
||||||
let provider = self
|
let provider = self
|
||||||
@@ -372,6 +390,49 @@ impl HybridSearch {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
|
fn new_source_recovers_provider_without_retrying_failed_window() -> Result<()> {
|
||||||
|
struct SourceProvider {
|
||||||
|
sources: usize,
|
||||||
|
calls: usize,
|
||||||
|
failed: bool,
|
||||||
|
}
|
||||||
|
impl SpeechProvider for SourceProvider {
|
||||||
|
fn fingerprint(&self) -> &str {
|
||||||
|
"lifecycle-test"
|
||||||
|
}
|
||||||
|
fn begin_source(&mut self) -> Result<()> {
|
||||||
|
self.sources += 1;
|
||||||
|
self.failed = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn transcribe(&mut self, _: &[f32], _: u32) -> Result<SpeechTranscription> {
|
||||||
|
self.calls += 1;
|
||||||
|
if self.sources == 1 || self.failed {
|
||||||
|
self.failed = true;
|
||||||
|
anyhow::bail!("first source transport failure");
|
||||||
|
}
|
||||||
|
Ok(SpeechTranscription {
|
||||||
|
passages: Vec::new(),
|
||||||
|
metrics: SpeechMetrics::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut provider = SourceProvider {
|
||||||
|
sources: 0,
|
||||||
|
calls: 0,
|
||||||
|
failed: false,
|
||||||
|
};
|
||||||
|
provider.begin_source()?;
|
||||||
|
let first_failure = provider.transcribe(&[0.], 16000).unwrap_err().to_string();
|
||||||
|
assert_eq!(provider.calls, 1, "failed PCM is never retried");
|
||||||
|
provider.begin_source()?;
|
||||||
|
provider.transcribe(&[0.], 16000)?;
|
||||||
|
assert_eq!(provider.calls, 2);
|
||||||
|
assert_eq!(provider.sources, 2);
|
||||||
|
assert_eq!(first_failure, "first source transport failure");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
fn point_spans_are_legal_and_invalid_native_evidence_is_rejected() -> Result<()> {
|
fn point_spans_are_legal_and_invalid_native_evidence_is_rejected() -> Result<()> {
|
||||||
let mut result = SpeechTranscription {
|
let mut result = SpeechTranscription {
|
||||||
passages: vec![SpeechPassage {
|
passages: vec![SpeechPassage {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ pub struct SpeechWorker {
|
|||||||
base_path: PathBuf,
|
base_path: PathBuf,
|
||||||
vad_path: PathBuf,
|
vad_path: PathBuf,
|
||||||
device: Device,
|
device: Device,
|
||||||
|
reset_on_next_source: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verified_file(path: &Path, bytes: u64, expected: &str) -> Result<String> {
|
fn verified_file(path: &Path, bytes: u64, expected: &str) -> Result<String> {
|
||||||
@@ -114,11 +115,31 @@ impl SpeechWorker {
|
|||||||
base_path: base.to_owned(),
|
base_path: base.to_owned(),
|
||||||
vad_path: vad.to_owned(),
|
vad_path: vad.to_owned(),
|
||||||
device,
|
device,
|
||||||
|
reset_on_next_source: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn fingerprint(&self) -> &str {
|
pub fn fingerprint(&self) -> &str {
|
||||||
&self.fingerprint
|
&self.fingerprint
|
||||||
}
|
}
|
||||||
|
/// Recover a failed transport only at admission of a different source.
|
||||||
|
/// Never retries the previous source or malformed native output.
|
||||||
|
pub fn begin_source(&mut self) -> Result<()> {
|
||||||
|
if !std::mem::take(&mut self.reset_on_next_source) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let replacement = Self::load(
|
||||||
|
&self.worker_path,
|
||||||
|
&self.base_path,
|
||||||
|
&self.vad_path,
|
||||||
|
self.device,
|
||||||
|
)?;
|
||||||
|
ensure!(
|
||||||
|
replacement.fingerprint == self.fingerprint,
|
||||||
|
"Speech assets changed before source recovery"
|
||||||
|
);
|
||||||
|
*self = replacement;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
pub fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription> {
|
pub fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription> {
|
||||||
ensure!(
|
ensure!(
|
||||||
sample_rate == SAMPLE_RATE && !pcm.is_empty() && pcm.len() <= MAX_SAMPLES,
|
sample_rate == SAMPLE_RATE && !pcm.is_empty() && pcm.len() <= MAX_SAMPLES,
|
||||||
@@ -126,7 +147,7 @@ impl SpeechWorker {
|
|||||||
);
|
);
|
||||||
ensure!(pcm.iter().all(|v| v.is_finite()), "Nonfinite speech input");
|
ensure!(pcm.iter().all(|v| v.is_finite()), "Nonfinite speech input");
|
||||||
let fallback_started = Instant::now();
|
let fallback_started = Instant::now();
|
||||||
match self.transcribe_once(pcm) {
|
let result = (|| match self.transcribe_once(pcm) {
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
Err(error) if self.device == Device::Auto && process::recoverable(&error) => {
|
Err(error) if self.device == Device::Auto && process::recoverable(&error) => {
|
||||||
// Retry only initialization/compute/transport failures. Reverify
|
// Retry only initialization/compute/transport failures. Reverify
|
||||||
@@ -148,7 +169,9 @@ impl SpeechWorker {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
Err(error) => Err(error),
|
Err(error) => Err(error),
|
||||||
}
|
})();
|
||||||
|
self.reset_on_next_source = result.as_ref().err().is_some_and(process::failed_transport);
|
||||||
|
result
|
||||||
}
|
}
|
||||||
fn transcribe_once(&mut self, pcm: &[f32]) -> Result<SpeechTranscription> {
|
fn transcribe_once(&mut self, pcm: &[f32]) -> Result<SpeechTranscription> {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ pub(crate) fn recoverable(error: &anyhow::Error) -> bool {
|
|||||||
.downcast_ref::<NativeFailure>()
|
.downcast_ref::<NativeFailure>()
|
||||||
.is_some_and(NativeFailure::recoverable)
|
.is_some_and(NativeFailure::recoverable)
|
||||||
}
|
}
|
||||||
|
pub(crate) fn failed_transport(error: &anyhow::Error) -> bool {
|
||||||
|
error.is::<TransportFailure>()
|
||||||
|
}
|
||||||
|
|
||||||
fn read_response<R: Read>(reader: &mut R) -> Result<Value> {
|
fn read_response<R: Read>(reader: &mut R) -> Result<Value> {
|
||||||
let mut prefix = [0; 4];
|
let mut prefix = [0; 4];
|
||||||
@@ -259,6 +262,7 @@ mod tests {
|
|||||||
.exchange(vec![1], Duration::from_millis(50))
|
.exchange(vec![1], Duration::from_millis(50))
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(recoverable(&error));
|
assert!(recoverable(&error));
|
||||||
|
assert!(failed_transport(&error));
|
||||||
assert!(process.child.try_wait().unwrap().is_some());
|
assert!(process.child.try_wait().unwrap().is_some());
|
||||||
assert!(process.failed);
|
assert!(process.failed);
|
||||||
let mut command = Command::new(&python);
|
let mut command = Command::new(&python);
|
||||||
@@ -268,6 +272,7 @@ mod tests {
|
|||||||
.exchange(vec![1], Duration::from_secs(1))
|
.exchange(vec![1], Duration::from_secs(1))
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(!recoverable(&error));
|
assert!(!recoverable(&error));
|
||||||
|
assert!(!failed_transport(&error));
|
||||||
assert!(process.child.try_wait().unwrap().is_some());
|
assert!(process.child.try_wait().unwrap().is_some());
|
||||||
assert!(process.failed);
|
assert!(process.failed);
|
||||||
}
|
}
|
||||||
@@ -279,9 +284,12 @@ mod tests {
|
|||||||
anyhow::Error::new(ProtocolFailure),
|
anyhow::Error::new(ProtocolFailure),
|
||||||
] {
|
] {
|
||||||
assert!(!recoverable(&error));
|
assert!(!recoverable(&error));
|
||||||
|
assert!(!failed_transport(&error));
|
||||||
}
|
}
|
||||||
assert!(recoverable(&NativeFailure::Initialization.into()));
|
assert!(recoverable(&NativeFailure::Initialization.into()));
|
||||||
assert!(recoverable(&NativeFailure::Compute.into()));
|
assert!(recoverable(&NativeFailure::Compute.into()));
|
||||||
|
assert!(!failed_transport(&NativeFailure::Initialization.into()));
|
||||||
|
assert!(!failed_transport(&NativeFailure::Compute.into()));
|
||||||
assert!(!recoverable(
|
assert!(!recoverable(
|
||||||
&read_response(&mut &[2, 0, 0, 0, 0xff, 0xff][..]).unwrap_err()
|
&read_response(&mut &[2, 0, 0, 0, 0xff, 0xff][..]).unwrap_err()
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ Raw log `.searchi/qa/service-speech-google-cpu-v3.log`: 1292 bytes, SHA
|
|||||||
The first normal auto compilation reached Vulkan setup but failed on Windows
|
The first normal auto compilation reached Vulkan setup but failed on Windows
|
||||||
nested MSBuild path length. Its log is retained, SHA
|
nested MSBuild path length. Its log is retained, SHA
|
||||||
`59f1820d6ac6adac6f525d8084c442c1ed261753eb34fcce916dc157041f777c`.
|
`59f1820d6ac6adac6f525d8084c442c1ed261753eb34fcce916dc157041f777c`.
|
||||||
An unchanged build in a shorter owned output directory is underway; no CPU-only
|
An unchanged build in a shorter owned output directory was started; no CPU-only
|
||||||
distribution was substituted.
|
distribution was substituted.
|
||||||
|
|
||||||
The shorter-path normal auto build subsequently completed. Worker SHA
|
The shorter-path normal auto build subsequently completed. Worker SHA
|
||||||
@@ -122,3 +122,24 @@ The complete report is 174868 bytes, SHA
|
|||||||
It uses the internal v3 CPU artifact and does not establish normal auto runtime
|
It uses the internal v3 CPU artifact and does not establish normal auto runtime
|
||||||
quality parity or GPU performance. Details and the retained comparison are in
|
quality parity or GPU performance. Details and the retained comparison are in
|
||||||
[the evaluator](../tools/speech-eval/native-validation/README.md).
|
[the evaluator](../tools/speech-eval/native-validation/README.md).
|
||||||
|
|
||||||
|
## Longer-source failure and release recovery
|
||||||
|
|
||||||
|
The normal-runtime timing regression retained all five original sources and
|
||||||
|
all eight queries, but the speech session returned a transport failure on the
|
||||||
|
first source and later files inherited the unavailable session. Raw Google
|
||||||
|
indexing completed with 79 chunks and no media failures. Recognized timing
|
||||||
|
scores remain ineligible; no historical 6/8 score is claimed for this run.
|
||||||
|
|
||||||
|
An isolated native reproduction using the **exact failed PCM hash**
|
||||||
|
`03ef72d2a4cb926b7d31fbaef757a6cb954a55d6d6961085ea6992728c8d9e37`
|
||||||
|
completed all four actual VAD intervals, valid recognition responses and exit
|
||||||
|
zero. The original transport failure is unexplained; no model or memory tuning
|
||||||
|
was introduced to hide it.
|
||||||
|
|
||||||
|
The release now restarts a verified session once at the next new source after
|
||||||
|
a terminal recoverable transport failure. The original failed source remains
|
||||||
|
reported; compatible donors return before restarting and raw search stays
|
||||||
|
usable. Malformed protocol/text and invalid token points never arm this reset.
|
||||||
|
Five focused service tests and native transport/lifecycle checks passed with
|
||||||
|
strict Clippy. Media and spoken passages remain visibly experimental.
|
||||||
@@ -152,3 +152,28 @@ Seven Rust tests, strict all-target Clippy and five timing-adapter tests passed.
|
|||||||
A retry must use a fresh output directory after the corrected evaluator is
|
A retry must use a fresh output directory after the corrected evaluator is
|
||||||
frozen and the CPU inference slot is granted again; the failed attempt remains
|
frozen and the CPU inference slot is granted again; the failed attempt remains
|
||||||
preserved and has no quality score.
|
preserved and has no quality score.
|
||||||
|
|
||||||
|
The corrected frozen attempt (commit `f15ee07`, session 39812) completed on CPU
|
||||||
|
at `.searchi/qa/speech-live-timing-v1-coverage`, but is **not timing-eligible**.
|
||||||
|
All five source checks passed; raw indexing produced 79 chunks (16 audio and
|
||||||
|
63 image calls), with no media failures, in 377691.6842 ms. The first speech
|
||||||
|
request for the video's initial 30-second window failed with
|
||||||
|
`Speech worker transport exited or timed out` after 2382.0169 ms. The four
|
||||||
|
remaining sources reported `Unavailable speech session or unbounded PCM`.
|
||||||
|
Five failed calls were recorded against 16 expected windows; zero recognized
|
||||||
|
speech chunks or citations were published. The public client discards stderr,
|
||||||
|
so these artifacts cannot identify the native failure cause. The request
|
||||||
|
deadline is 600 seconds, longer than the observed first-call failure.
|
||||||
|
|
||||||
|
All eight first files were `clip04`, but their actual origins were raw audio or
|
||||||
|
video, not recognized speech. The unchanged scorer therefore records
|
||||||
|
`comparison_eligible: false`, `strict_point_score: null` and
|
||||||
|
`correct_first_file: null`; no historical score carries over.
|
||||||
|
Immutable report SHA-256:
|
||||||
|
`7261223ba479ff1c319ded187fcd6e7cf37d351ec9d62437e26b63017d1bccc2`;
|
||||||
|
scored artifact SHA-256:
|
||||||
|
`361d5fbaf917363bfedd9c3ba2ea368d1714cf28f1bc34544a9ed324761eb4db`;
|
||||||
|
preflight SHA-256:
|
||||||
|
`97c603b3078831ce0fcf19f0ebb9e15160ee2363b488946c992e881d5509ce6b`.
|
||||||
|
This failed speech lifecycle requires investigation before any live-window
|
||||||
|
timing claim; no policy, labels or sources were changed after this result.
|
||||||
@@ -57,6 +57,11 @@ path; the failed log SHA-256 is
|
|||||||
An unchanged-source build using a shorter owned output path completed. Prefer a
|
An unchanged-source build using a shorter owned output path completed. Prefer a
|
||||||
short build output directory on Windows; no CPU-only distribution was substituted.
|
short build output directory on Windows; no CPU-only distribution was substituted.
|
||||||
|
|
||||||
|
If a worker transport dies, the failing source retains its error. `begin_source`
|
||||||
|
permits one verified same-fingerprint restart at the next source boundary, keeping
|
||||||
|
the current CPU/device choice. Malformed protocol, UTF-8 or alignment outputs do
|
||||||
|
not authorize recovery or repeated inference of the failing source.
|
||||||
|
|
||||||
The persistent worker receives binary requests: operation byte (1 live VAD,
|
The persistent worker receives binary requests: operation byte (1 live VAD,
|
||||||
2 contiguous recognition), little-endian u32 sample count and mono little-endian
|
2 contiguous recognition), little-endian u32 sample count and mono little-endian
|
||||||
f32 PCM, at most 480000 finite samples. Responses have a four-byte little-endian
|
f32 PCM, at most 480000 finite samples. Responses have a four-byte little-endian
|
||||||
|
|||||||
Reference in new issue
Block a user