1use crate::audio::{estimate_audio_memory_bytes, Audio};
12use crate::execution::{ReceiptPublicKey, ReceiptSecretKey, ReceiptSignature};
13#[cfg(feature = "onnx")]
14use crate::{
15 AcceleratorPreference, AcceleratorSelection, Backend, BackendOptions, RuntimeModelPackage,
16};
17use serde::{Deserialize, Serialize};
18#[cfg(feature = "onnx")]
19use sha2::{Digest as _, Sha256};
20use std::collections::{BTreeMap, BTreeSet};
21use std::io::Read as _;
22use std::path::Path;
23#[cfg(feature = "onnx")]
24use zeroize::{Zeroize, Zeroizing};
25
26pub const TARGET_SPEAKER_REPORT_SCHEMA: &str = "denoize-target-speaker-report-v1";
27pub const TARGET_SPEAKER_PROMOTION_EVIDENCE_SCHEMA: &str =
28 "denoize-target-speaker-promotion-evidence-v1";
29pub const TARGET_SPEAKER_SCHEMA_VERSION: u32 = 1;
30pub const MAX_TARGET_SPEAKER_EVIDENCE_STRATA: usize = 256;
31pub const MAX_TARGET_SPEAKER_EVIDENCE_METRICS: usize = 64;
32pub const MIN_TARGET_SPEAKER_ENROLLMENT_MILLIS: u64 = 500;
33pub const MAX_TARGET_SPEAKER_ENROLLMENT_MILLIS: u64 = 30_000;
34pub const MAX_TARGET_SPEAKER_MIXTURE_SECONDS: u64 = 3_600;
35
36#[cfg(feature = "onnx")]
37const MAX_CHANNELS: usize = 64;
38const MAX_EVIDENCE_JSON_BYTES: u64 = 16 * 1024 * 1024;
39const PROMOTION_SIGNATURE_DOMAIN: &[u8] = b"denoize-target-speaker-promotion-evidence-v1";
40#[cfg(feature = "onnx")]
41const MIXTURE_PCM_DIGEST_DOMAIN: &[u8] = b"denoize-target-speaker-mixture-pcm-v1\0";
42#[cfg(feature = "onnx")]
43const OUTPUT_PCM_DIGEST_DOMAIN: &[u8] = b"denoize-target-speaker-output-pcm-v1\0";
44#[cfg(feature = "onnx")]
45const SILENCE_FLOOR: f64 = 1e-12;
46
47const REQUIRED_STRATA: &[(&str, TargetSpeakerStratumKind)] = &[
48 ("channel-mismatch", TargetSpeakerStratumKind::TargetPresent),
49 ("child-speaker", TargetSpeakerStratumKind::TargetPresent),
50 ("code-switching", TargetSpeakerStratumKind::TargetPresent),
51 ("codec-enrollment", TargetSpeakerStratumKind::TargetPresent),
52 ("different-sex", TargetSpeakerStratumKind::TargetPresent),
53 ("many-interferers", TargetSpeakerStratumKind::TargetPresent),
54 ("noisy-enrollment", TargetSpeakerStratumKind::TargetPresent),
55 ("one-interferer", TargetSpeakerStratumKind::TargetPresent),
56 (
57 "real-t-conversation",
58 TargetSpeakerStratumKind::TargetPresent,
59 ),
60 (
61 "reverberant-enrollment",
62 TargetSpeakerStratumKind::TargetPresent,
63 ),
64 ("same-sex", TargetSpeakerStratumKind::TargetPresent),
65 ("same-words", TargetSpeakerStratumKind::TargetPresent),
66 ("similar-voices", TargetSpeakerStratumKind::TargetPresent),
67 ("singing", TargetSpeakerStratumKind::TargetPresent),
68 ("speech-absent", TargetSpeakerStratumKind::TargetAbsent),
69 ("target-absent", TargetSpeakerStratumKind::TargetAbsent),
70 (
71 "target-absent-same-words",
72 TargetSpeakerStratumKind::TargetAbsent,
73 ),
74 (
75 "target-absent-similar-interferer",
76 TargetSpeakerStratumKind::TargetAbsent,
77 ),
78 (
79 "target-present-clean",
80 TargetSpeakerStratumKind::TargetPresent,
81 ),
82 ("ts-superb", TargetSpeakerStratumKind::TargetPresent),
83 ("unseen-domain", TargetSpeakerStratumKind::TargetPresent),
84 ("whisper", TargetSpeakerStratumKind::TargetPresent),
85];
86
87const PRESENT_METRICS: &[MetricPolicy] = &[
88 MetricPolicy::at_most("content.target-word-error-rate", 0.35),
89 MetricPolicy::at_least("extraction.si-sdr-improvement-db", 3.0),
90 MetricPolicy::at_most("interferer.speaker-similarity", 0.30),
91 MetricPolicy::at_most("interferer.word-leakage-rate", 0.02),
92 MetricPolicy::at_most("output.duration-error-frames", 0.0),
93 MetricPolicy::at_most("output.non-finite-samples", 0.0),
94 MetricPolicy::at_least("perceptual.dnsmos-p808", 3.0),
95 MetricPolicy::at_least("presence.recall", 0.95),
96 MetricPolicy::at_least("speaker.target-similarity", 0.70),
97];
98
99const ABSENT_METRICS: &[MetricPolicy] = &[
100 MetricPolicy::at_most("interferer.speaker-similarity", 0.30),
101 MetricPolicy::at_most("interferer.word-leakage-rate", 0.01),
102 MetricPolicy::at_most("output.duration-error-frames", 0.0),
103 MetricPolicy::at_most("output.non-finite-samples", 0.0),
104 MetricPolicy::at_most("output.rms-dbfs", -60.0),
105 MetricPolicy::at_most("presence.false-positive-rate", 0.01),
106];
107
108#[derive(Clone, Copy)]
109struct MetricPolicy {
110 name: &'static str,
111 operator: TargetSpeakerMetricOperator,
112 hard_limit: f64,
113}
114
115impl MetricPolicy {
116 const fn at_least(name: &'static str, hard_limit: f64) -> Self {
117 Self {
118 name,
119 operator: TargetSpeakerMetricOperator::GreaterOrEqual,
120 hard_limit,
121 }
122 }
123
124 const fn at_most(name: &'static str, hard_limit: f64) -> Self {
125 Self {
126 name,
127 operator: TargetSpeakerMetricOperator::LessOrEqual,
128 hard_limit,
129 }
130 }
131}
132
133#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
134#[serde(rename_all = "kebab-case")]
135pub enum TargetSpeakerStratumKind {
136 TargetPresent,
137 TargetAbsent,
138}
139
140#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
141#[serde(rename_all = "kebab-case")]
142pub enum TargetSpeakerMetricOperator {
143 GreaterOrEqual,
144 LessOrEqual,
145}
146
147#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
148#[serde(deny_unknown_fields)]
149pub struct TargetSpeakerMetricOutcome {
150 pub metric: String,
151 pub value: f64,
152 pub operator: TargetSpeakerMetricOperator,
153 pub limit: f64,
154 pub passed: bool,
155}
156
157#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
158#[serde(deny_unknown_fields)]
159pub struct TargetSpeakerStratumEvidence {
160 pub id: String,
161 pub kind: TargetSpeakerStratumKind,
162 pub cases: u32,
163 pub metrics: Vec<TargetSpeakerMetricOutcome>,
164}
165
166#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
167#[serde(deny_unknown_fields)]
168pub struct TargetSpeakerPromotionEvidencePayload {
169 pub completed_at_unix_seconds: u64,
170 pub model_package_sha256: String,
171 pub source_revision: String,
172 pub source_sha256: String,
173 pub checkpoint_sha256: String,
174 pub corpus_manifest_sha256: String,
175 pub evaluation_result_sha256: String,
176 pub real_t_result_sha256: String,
177 pub ts_superb_result_sha256: String,
178 pub strata: Vec<TargetSpeakerStratumEvidence>,
179 pub target_speaker_count: u32,
180 pub interferer_speaker_count: u32,
181 pub language_count: u32,
182 pub presence_expected_calibration_error: f64,
183 pub presence_expected_calibration_error_limit: f64,
184 pub minimum_listeners: u32,
185 pub listener_count: u32,
186 pub listener_preference: f64,
187 pub listener_preference_limit: f64,
188 pub accepted: bool,
189}
190
191impl TargetSpeakerPromotionEvidencePayload {
192 pub fn validate(&self) -> Result<(), String> {
193 for (label, digest) in [
194 ("model package", self.model_package_sha256.as_str()),
195 ("source", self.source_sha256.as_str()),
196 ("checkpoint", self.checkpoint_sha256.as_str()),
197 ("corpus manifest", self.corpus_manifest_sha256.as_str()),
198 ("evaluation result", self.evaluation_result_sha256.as_str()),
199 ("REAL-T result", self.real_t_result_sha256.as_str()),
200 ("TS-SUPERB result", self.ts_superb_result_sha256.as_str()),
201 ] {
202 validate_sha256(label, digest)?;
203 }
204 validate_identifier("source revision", &self.source_revision)?;
205 if self.completed_at_unix_seconds > (1_u64 << 53) - 1 {
206 return Err(
207 "target-speaker evidence timestamp exceeds the JSON safe-integer limit".into(),
208 );
209 }
210 if self.strata.is_empty() || self.strata.len() > MAX_TARGET_SPEAKER_EVIDENCE_STRATA {
211 return Err(format!(
212 "target-speaker evidence must contain 1..={MAX_TARGET_SPEAKER_EVIDENCE_STRATA} strata"
213 ));
214 }
215 let required: BTreeMap<_, _> = REQUIRED_STRATA.iter().copied().collect();
216 let mut observed_strata = BTreeSet::new();
217 let mut previous = None;
218 let mut all_metrics_passed = true;
219 for stratum in &self.strata {
220 validate_identifier("target-speaker evidence stratum", &stratum.id)?;
221 if previous.is_some_and(|value: &str| value >= stratum.id.as_str()) {
222 return Err(
223 "target-speaker evidence strata must be unique and strictly sorted".into(),
224 );
225 }
226 previous = Some(&stratum.id);
227 observed_strata.insert(stratum.id.as_str());
228 if required
229 .get(stratum.id.as_str())
230 .is_some_and(|expected| *expected != stratum.kind)
231 {
232 return Err(format!(
233 "target-speaker evidence stratum {} has the wrong presence kind",
234 stratum.id
235 ));
236 }
237 if !(10..=1_000_000).contains(&stratum.cases) {
238 return Err("target-speaker evidence stratum cases must be in 10..=1000000".into());
239 }
240 if stratum.metrics.is_empty()
241 || stratum.metrics.len() > MAX_TARGET_SPEAKER_EVIDENCE_METRICS
242 {
243 return Err(format!(
244 "target-speaker evidence stratum metrics must be in 1..={MAX_TARGET_SPEAKER_EVIDENCE_METRICS}"
245 ));
246 }
247 let policies = match stratum.kind {
248 TargetSpeakerStratumKind::TargetPresent => PRESENT_METRICS,
249 TargetSpeakerStratumKind::TargetAbsent => ABSENT_METRICS,
250 };
251 let policy_by_name: BTreeMap<_, _> = policies
252 .iter()
253 .map(|policy| (policy.name, policy))
254 .collect();
255 let mut observed_metrics = BTreeSet::new();
256 let mut previous_metric = None;
257 for metric in &stratum.metrics {
258 validate_identifier("target-speaker evidence metric", &metric.metric)?;
259 if previous_metric.is_some_and(|value: &str| value >= metric.metric.as_str()) {
260 return Err(
261 "target-speaker evidence metrics must be unique and strictly sorted".into(),
262 );
263 }
264 previous_metric = Some(&metric.metric);
265 observed_metrics.insert(metric.metric.as_str());
266 if !metric.value.is_finite() || !metric.limit.is_finite() {
267 return Err("target-speaker evidence metric values must be finite".into());
268 }
269 let expected = match metric.operator {
270 TargetSpeakerMetricOperator::GreaterOrEqual => metric.value >= metric.limit,
271 TargetSpeakerMetricOperator::LessOrEqual => metric.value <= metric.limit,
272 };
273 if metric.passed != expected {
274 return Err(format!(
275 "target-speaker evidence metric {} has an inconsistent passed flag",
276 metric.metric
277 ));
278 }
279 if let Some(policy) = policy_by_name.get(metric.metric.as_str()) {
280 validate_metric_policy(metric, policy)?;
281 }
282 all_metrics_passed &= metric.passed;
283 }
284 for policy in policies {
285 if !observed_metrics.contains(policy.name) {
286 return Err(format!(
287 "target-speaker evidence stratum {} omits required metric {}",
288 stratum.id, policy.name
289 ));
290 }
291 }
292 }
293 for (id, _) in REQUIRED_STRATA {
294 if !observed_strata.contains(id) {
295 return Err(format!(
296 "target-speaker evidence omits required stratum {id}"
297 ));
298 }
299 }
300 if self.target_speaker_count < 100
301 || self.target_speaker_count > 1_000_000
302 || self.interferer_speaker_count < 100
303 || self.interferer_speaker_count > 1_000_000
304 || !(2..=1_000).contains(&self.language_count)
305 {
306 return Err(
307 "target-speaker evidence requires at least 100 target and interferer speakers and two languages"
308 .into(),
309 );
310 }
311 if !self.presence_expected_calibration_error.is_finite()
312 || !self.presence_expected_calibration_error_limit.is_finite()
313 || !(0.0..=1.0).contains(&self.presence_expected_calibration_error)
314 || !(0.0..=0.05).contains(&self.presence_expected_calibration_error_limit)
315 {
316 return Err("target-speaker evidence presence calibration values are invalid".into());
317 }
318 if self.minimum_listeners < 20
319 || self.minimum_listeners > 100_000
320 || self.listener_count < self.minimum_listeners
321 || self.listener_count > 100_000
322 || !self.listener_preference.is_finite()
323 || !self.listener_preference_limit.is_finite()
324 || !(0.0..=1.0).contains(&self.listener_preference)
325 || !(0.5..=1.0).contains(&self.listener_preference_limit)
326 {
327 return Err("target-speaker evidence listening values are invalid".into());
328 }
329 let expected_accepted = all_metrics_passed
330 && self.presence_expected_calibration_error
331 <= self.presence_expected_calibration_error_limit
332 && self.listener_count >= self.minimum_listeners
333 && self.listener_preference >= self.listener_preference_limit;
334 if self.accepted != expected_accepted {
335 return Err("target-speaker evidence accepted flag is inconsistent".into());
336 }
337 let bytes = serde_json::to_vec(self)
338 .map_err(|error| format!("serialize target-speaker evidence payload: {error}"))?;
339 if bytes.len() as u64 >= MAX_EVIDENCE_JSON_BYTES {
340 return Err("target-speaker evidence payload exceeds the bounded JSON limit".into());
341 }
342 Ok(())
343 }
344}
345
346#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
347#[serde(deny_unknown_fields)]
348pub struct SignedTargetSpeakerPromotionEvidence {
349 pub schema: String,
350 pub schema_version: u32,
351 pub payload: TargetSpeakerPromotionEvidencePayload,
352 pub signature: ReceiptSignature,
353}
354
355impl SignedTargetSpeakerPromotionEvidence {
356 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, String> {
357 let path = path.as_ref();
358 let (file, length) =
359 crate::input::open_regular_file(path, "target-speaker promotion evidence")?;
360 if length >= MAX_EVIDENCE_JSON_BYTES {
361 return Err(format!(
362 "target-speaker promotion evidence {} exceeds the {MAX_EVIDENCE_JSON_BYTES}-byte limit",
363 path.display()
364 ));
365 }
366 let mut bytes = Vec::new();
367 bytes
368 .try_reserve_exact(length as usize)
369 .map_err(|_| "unable to reserve target-speaker evidence JSON".to_string())?;
370 file.take(MAX_EVIDENCE_JSON_BYTES)
371 .read_to_end(&mut bytes)
372 .map_err(|error| format!("read target-speaker promotion evidence: {error}"))?;
373 if bytes.len() as u64 != length {
374 return Err("target-speaker promotion evidence changed while reading".into());
375 }
376 let evidence: Self = serde_json::from_slice(&bytes)
377 .map_err(|error| format!("parse target-speaker promotion evidence: {error}"))?;
378 evidence.validate_structure()?;
379 Ok(evidence)
380 }
381
382 pub fn validate_structure(&self) -> Result<(), String> {
383 if self.schema != TARGET_SPEAKER_PROMOTION_EVIDENCE_SCHEMA
384 || self.schema_version != TARGET_SPEAKER_SCHEMA_VERSION
385 {
386 return Err("unsupported target-speaker promotion evidence schema".into());
387 }
388 self.payload.validate()?;
389 if self.signature.algorithm != "ed25519" {
390 return Err("target-speaker promotion evidence signature must use ed25519".into());
391 }
392 validate_sha256("evidence key ID", &self.signature.key_id)?;
393 let bytes = serde_json::to_vec(self)
394 .map_err(|error| format!("serialize target-speaker promotion evidence: {error}"))?;
395 if bytes.len() as u64 >= MAX_EVIDENCE_JSON_BYTES {
396 return Err("target-speaker promotion evidence exceeds the bounded JSON limit".into());
397 }
398 Ok(())
399 }
400
401 pub fn verify_signature(&self, key: &ReceiptPublicKey) -> Result<(), String> {
402 self.validate_structure()?;
403 let document = serde_json::to_vec(&self.payload).map_err(|error| {
404 format!("serialize target-speaker evidence for verification: {error}")
405 })?;
406 key.verify_domain_document(
407 PROMOTION_SIGNATURE_DOMAIN,
408 &document,
409 &self.signature,
410 "target-speaker promotion evidence",
411 )
412 }
413
414 pub fn to_pretty_json(&self) -> Result<String, String> {
415 self.validate_structure()?;
416 serde_json::to_string_pretty(self)
417 .map_err(|error| format!("serialize target-speaker promotion evidence: {error}"))
418 }
419}
420
421pub fn sign_target_speaker_promotion_evidence(
422 payload: TargetSpeakerPromotionEvidencePayload,
423 key: &ReceiptSecretKey,
424) -> Result<SignedTargetSpeakerPromotionEvidence, String> {
425 payload.validate()?;
426 let document = serde_json::to_vec(&payload)
427 .map_err(|error| format!("serialize target-speaker evidence for signing: {error}"))?;
428 let signature = key.sign_domain_document(
429 PROMOTION_SIGNATURE_DOMAIN,
430 &document,
431 "target-speaker promotion evidence",
432 )?;
433 let evidence = SignedTargetSpeakerPromotionEvidence {
434 schema: TARGET_SPEAKER_PROMOTION_EVIDENCE_SCHEMA.into(),
435 schema_version: TARGET_SPEAKER_SCHEMA_VERSION,
436 payload,
437 signature,
438 };
439 evidence.validate_structure()?;
440 Ok(evidence)
441}
442
443#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
444#[serde(rename_all = "kebab-case")]
445pub enum TargetSpeakerPresence {
446 Present,
447 Absent,
448 Uncertain,
449}
450
451#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
452#[serde(rename_all = "kebab-case")]
453pub enum TargetSpeakerDecision {
454 AcceptedPresent,
455 WithheldAbsent,
456 WithheldUncertain,
457 WithheldSafetyGate,
458}
459
460#[derive(Clone, Debug, Deserialize, Serialize)]
461#[serde(deny_unknown_fields)]
462pub struct TargetSpeakerExtractionConfig {
463 pub minimum_present_probability: f64,
464 pub minimum_absent_probability: f64,
465 pub maximum_energy_gain_db: f64,
466 pub maximum_peak_gain_db: f64,
467 pub maximum_new_clipping_ratio: f64,
468}
469
470impl Default for TargetSpeakerExtractionConfig {
471 fn default() -> Self {
472 Self {
473 minimum_present_probability: 0.90,
474 minimum_absent_probability: 0.90,
475 maximum_energy_gain_db: 3.0,
476 maximum_peak_gain_db: 3.0,
477 maximum_new_clipping_ratio: 0.0001,
478 }
479 }
480}
481
482impl TargetSpeakerExtractionConfig {
483 pub fn validate(&self) -> Result<(), String> {
484 validate_range(
485 "minimum_present_probability",
486 self.minimum_present_probability,
487 0.5,
488 1.0,
489 )?;
490 validate_range(
491 "minimum_absent_probability",
492 self.minimum_absent_probability,
493 0.5,
494 1.0,
495 )?;
496 validate_range(
497 "maximum_energy_gain_db",
498 self.maximum_energy_gain_db,
499 0.0,
500 12.0,
501 )?;
502 validate_range("maximum_peak_gain_db", self.maximum_peak_gain_db, 0.0, 12.0)?;
503 validate_range(
504 "maximum_new_clipping_ratio",
505 self.maximum_new_clipping_ratio,
506 0.0,
507 0.01,
508 )
509 }
510}
511
512#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
513#[serde(deny_unknown_fields)]
514pub struct TargetSpeakerModelIdentity {
515 pub package_sha256: String,
516 pub public_key_sha256: String,
517 pub package_id: String,
518 pub package_revision: String,
519 pub precision_profile: String,
520 pub source_revision: String,
521 pub source_sha256: String,
522 pub source_license_spdx: String,
523 pub checkpoint_sha256: String,
524 pub checkpoint_license_spdx: String,
525 pub accelerator: String,
526}
527
528#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
529#[serde(deny_unknown_fields)]
530pub struct TargetSpeakerEvidenceIdentity {
531 pub signing_key_id: String,
532 pub corpus_manifest_sha256: String,
533 pub evaluation_result_sha256: String,
534 pub real_t_result_sha256: String,
535 pub ts_superb_result_sha256: String,
536 pub strata: u32,
537 pub target_speakers: u32,
538 pub interferer_speakers: u32,
539 pub languages: u32,
540 pub accepted: bool,
541}
542
543#[derive(Clone, Debug, Deserialize, Serialize)]
544#[serde(deny_unknown_fields)]
545pub struct TargetSpeakerEnrollmentSummary {
546 pub input_sample_rate: u32,
547 pub input_channels: usize,
548 pub input_frames: usize,
549 pub model_sample_rate: u32,
550 pub model_samples: usize,
551 pub mixdown_policy: String,
552 pub raw_audio_retained: bool,
553 pub embedding_retained: bool,
554 pub digest_recorded: bool,
555}
556
557#[derive(Clone, Debug, Deserialize, Serialize)]
558#[serde(deny_unknown_fields)]
559pub struct TargetSpeakerPresenceAssessment {
560 pub state: TargetSpeakerPresence,
561 pub absent_probability: f64,
562 pub uncertain_probability: f64,
563 pub present_probability: f64,
564 pub minimum_absent_probability: f64,
565 pub minimum_present_probability: f64,
566}
567
568#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
569#[serde(rename_all = "kebab-case")]
570pub enum TargetSpeakerSafetyGateKind {
571 Geometry,
572 FiniteNormalizedSamples,
573 EnergyGain,
574 PeakGain,
575 NewClipping,
576 TargetPresence,
577 PromotionEvidence,
578}
579
580#[derive(Clone, Debug, Deserialize, Serialize)]
581#[serde(deny_unknown_fields)]
582pub struct TargetSpeakerSafetyGate {
583 pub kind: TargetSpeakerSafetyGateKind,
584 pub observed: f64,
585 pub limit: f64,
586 pub passed: bool,
587}
588
589#[derive(Clone, Debug, Deserialize, Serialize)]
590#[serde(deny_unknown_fields)]
591pub struct TargetSpeakerSafetyMeasurements {
592 pub mixture_rms_dbfs: f64,
593 pub candidate_rms_dbfs: f64,
594 pub mixture_peak_dbfs: f64,
595 pub candidate_peak_dbfs: f64,
596 pub energy_delta_db: f64,
597 pub mixture_clipping_ratio: f64,
598 pub candidate_clipping_ratio: f64,
599}
600
601#[derive(Clone, Debug, Deserialize, Serialize)]
602#[serde(deny_unknown_fields)]
603pub struct TargetSpeakerExtractionReport {
604 pub schema: String,
605 pub schema_version: u32,
606 pub denoize_version: String,
607 pub network_accessed: bool,
608 pub deterministic: bool,
609 pub model: TargetSpeakerModelIdentity,
610 pub promotion_evidence: TargetSpeakerEvidenceIdentity,
611 pub decision: TargetSpeakerDecision,
612 pub model_invoked: bool,
613 pub candidate_accepted: bool,
614 pub output_published: bool,
615 pub candidate_retained: bool,
616 pub source_sample_rate: u32,
617 pub source_channels: usize,
618 pub source_frames: usize,
619 pub output_channels: usize,
620 pub output_frames: Option<usize>,
621 pub mixture_mixdown_policy: String,
622 pub mixture_pcm_sha256: String,
623 pub candidate_pcm_sha256: Option<String>,
624 pub output_pcm_sha256: Option<String>,
625 pub enrollment: TargetSpeakerEnrollmentSummary,
626 pub presence: TargetSpeakerPresenceAssessment,
627 pub measurements: TargetSpeakerSafetyMeasurements,
628 pub safety_gates: Vec<TargetSpeakerSafetyGate>,
629 pub runtime_speaker_identity_verified: bool,
630 pub interferer_leakage_measured_at_runtime: bool,
631 pub limitations: Vec<String>,
632 pub warnings: Vec<String>,
633}
634
635impl TargetSpeakerExtractionReport {
636 pub fn to_json(&self) -> Result<String, String> {
637 serde_json::to_string(self)
638 .map_err(|error| format!("serialize target-speaker extraction report: {error}"))
639 }
640
641 pub fn to_pretty_json(&self) -> Result<String, String> {
642 serde_json::to_string_pretty(self)
643 .map_err(|error| format!("serialize target-speaker extraction report: {error}"))
644 }
645}
646
647#[derive(Clone, Debug)]
648pub struct TargetSpeakerExtractionResult {
649 pub audio: Option<Audio>,
652 pub report: TargetSpeakerExtractionReport,
653}
654
655pub fn estimate_target_speaker_memory_bytes(mixture: &Audio, enrollment: &Audio) -> u64 {
659 estimate_audio_memory_bytes(mixture)
660 .saturating_mul(7)
661 .saturating_add(estimate_audio_memory_bytes(enrollment).saturating_mul(5))
662 .max(1024 * 1024)
663}
664
665#[cfg(feature = "onnx")]
666pub struct TargetSpeakerSession {
667 package: RuntimeModelPackage,
668 model: crate::backend::target_speaker::TargetSpeakerModel,
669 accelerator: AcceleratorSelection,
670 evidence: TargetSpeakerEvidenceIdentity,
671}
672
673#[cfg(feature = "onnx")]
674impl std::fmt::Debug for TargetSpeakerSession {
675 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676 formatter
677 .debug_struct("TargetSpeakerSession")
678 .field("package_sha256", &self.package.package_sha256())
679 .field("accelerator", &self.accelerator)
680 .field("evidence", &self.evidence)
681 .finish_non_exhaustive()
682 }
683}
684
685#[cfg(feature = "onnx")]
686impl TargetSpeakerSession {
687 pub fn prepare(
690 package: RuntimeModelPackage,
691 evidence: &SignedTargetSpeakerPromotionEvidence,
692 evidence_key: &ReceiptPublicKey,
693 requested: AcceleratorPreference,
694 ) -> Result<Self, String> {
695 evidence.verify_signature(evidence_key)?;
696 if !evidence.payload.accepted {
697 return Err(
698 "target-speaker promotion evidence is authentic but does not pass promotion gates"
699 .into(),
700 );
701 }
702 let manifest = package
703 .manifest_v2()
704 .ok_or("target-speaker extraction rejects runtime model package v1")?;
705 for (label, observed, expected) in [
706 (
707 "model package SHA-256",
708 evidence.payload.model_package_sha256.as_str(),
709 package.package_sha256(),
710 ),
711 (
712 "source revision",
713 evidence.payload.source_revision.as_str(),
714 manifest.provenance.source_revision.as_str(),
715 ),
716 (
717 "source SHA-256",
718 evidence.payload.source_sha256.as_str(),
719 manifest.provenance.source_sha256.as_str(),
720 ),
721 (
722 "checkpoint SHA-256",
723 evidence.payload.checkpoint_sha256.as_str(),
724 manifest.provenance.checkpoint_sha256.as_str(),
725 ),
726 ] {
727 if observed != expected {
728 return Err(format!(
729 "target-speaker promotion evidence {label} does not match the authenticated package"
730 ));
731 }
732 }
733 let mut options = BackendOptions::default().with_runtime_model_package(package.clone());
734 options.deterministic = true;
735 options.accelerator = requested;
736 let accelerator = crate::select_accelerator_for_options(Backend::Onnx, &options)?;
737 if !package.supports_accelerator(accelerator.effective()) {
738 return Err(format!(
739 "target-speaker package does not permit the {} accelerator",
740 accelerator.effective().name()
741 ));
742 }
743 let model = crate::backend::target_speaker::TargetSpeakerModel::load_runtime_package(
744 &package,
745 accelerator.effective(),
746 )?;
747 let payload = &evidence.payload;
748 Ok(Self {
749 package,
750 model,
751 accelerator,
752 evidence: TargetSpeakerEvidenceIdentity {
753 signing_key_id: evidence.signature.key_id.clone(),
754 corpus_manifest_sha256: payload.corpus_manifest_sha256.clone(),
755 evaluation_result_sha256: payload.evaluation_result_sha256.clone(),
756 real_t_result_sha256: payload.real_t_result_sha256.clone(),
757 ts_superb_result_sha256: payload.ts_superb_result_sha256.clone(),
758 strata: payload.strata.len() as u32,
759 target_speakers: payload.target_speaker_count,
760 interferer_speakers: payload.interferer_speaker_count,
761 languages: payload.language_count,
762 accepted: true,
763 },
764 })
765 }
766
767 #[must_use]
768 pub const fn accelerator(&self) -> AcceleratorSelection {
769 self.accelerator
770 }
771
772 pub fn model_working_set_bytes(&self) -> Result<u64, String> {
773 let profile = self
774 .package
775 .precision_profile_for(self.accelerator.effective())?
776 .expect("target-speaker packages use v2 precision profiles");
777 Ok(profile
778 .resources
779 .max_session_memory_bytes
780 .saturating_add(profile.resources.max_worker_memory_bytes))
781 }
782
783 pub fn extract(
784 &self,
785 mixture: &Audio,
786 enrollment: Audio,
787 config: &TargetSpeakerExtractionConfig,
788 ) -> Result<TargetSpeakerExtractionResult, String> {
789 config.validate()?;
790 validate_audio(mixture, "mixture", false)?;
791 let enrollment = SensitiveEnrollment::new(enrollment);
792 validate_audio(enrollment.audio(), "enrollment", false)?;
793 let model_rate = self
794 .package
795 .manifest_v2()
796 .expect("target-speaker session requires v2")
797 .runtime
798 .sample_rate_hz;
799 let source_frames = mixture.frames();
800 if source_frames as u64
801 > u64::from(mixture.sample_rate).saturating_mul(MAX_TARGET_SPEAKER_MIXTURE_SECONDS)
802 {
803 return Err(format!(
804 "target-speaker mixture exceeds the {MAX_TARGET_SPEAKER_MIXTURE_SECONDS}-second limit"
805 ));
806 }
807 let enrollment_summary_input = (
808 enrollment.audio().sample_rate,
809 enrollment.audio().channels(),
810 enrollment.audio().frames(),
811 );
812 let mixture_mono = mono_mix(mixture, "mixture")?;
813 let enrollment_mono = Zeroizing::new(mono_mix(enrollment.audio(), "enrollment")?);
814 let enrollment_model_f64 = Zeroizing::new(crate::resample::resample(
815 &enrollment_mono,
816 enrollment.audio().sample_rate,
817 model_rate,
818 )?);
819 let enrollment_model = Zeroizing::new(
820 enrollment_model_f64
821 .iter()
822 .map(|sample| *sample as f32)
823 .collect::<Vec<_>>(),
824 );
825 validate_enrollment_duration(enrollment_model.len(), model_rate)?;
826 if let Some(required) = self.model.fixed_enrollment_samples() {
827 if enrollment_model.len() != required {
828 return Err(format!(
829 "target-speaker package requires exactly {required} enrollment samples at {model_rate} Hz, got {}",
830 enrollment_model.len()
831 ));
832 }
833 }
834 let enrollment_model_samples = enrollment_model.len();
835 let mixture_model_f64 =
836 crate::resample::resample(&mixture_mono, mixture.sample_rate, model_rate)?;
837 if mixture_model_f64.is_empty() {
838 return Err("target-speaker mixture becomes empty at the model sample rate".into());
839 }
840 let mixture_model = mixture_model_f64
841 .iter()
842 .map(|sample| *sample as f32)
843 .collect::<Vec<_>>();
844 let inference = self.model.process(&mixture_model, &enrollment_model)?;
845 drop(enrollment_model);
846 drop(enrollment_model_f64);
847 drop(enrollment_mono);
848 drop(enrollment);
849 let presence_values = inference.presence_probabilities;
850 let candidate_model = Zeroizing::new(inference.audio);
851 let candidate_resampled = Zeroizing::new(crate::resample::resample(
852 &candidate_model
853 .iter()
854 .map(|sample| f64::from(*sample))
855 .collect::<Vec<_>>(),
856 model_rate,
857 mixture.sample_rate,
858 )?);
859 let mut candidate = Zeroizing::new(Vec::new());
860 candidate
861 .try_reserve_exact(source_frames)
862 .map_err(|_| "unable to reserve target-speaker candidate".to_string())?;
863 candidate.extend(candidate_resampled.iter().copied().take(source_frames));
864 candidate.resize(source_frames, 0.0);
865
866 let presence = classify_presence(presence_values, config);
867 let mixture_measurements = signal_measurements(&mixture_mono);
868 let candidate_measurements = signal_measurements(&candidate);
869 let geometry_passed = candidate.len() == source_frames;
870 let finite_normalized_passed = candidate
871 .iter()
872 .all(|sample| sample.is_finite() && (-1.0..=1.0).contains(sample));
873 let energy_delta_db = candidate_measurements.rms_dbfs - mixture_measurements.rms_dbfs;
874 let peak_delta_db = candidate_measurements.peak_dbfs - mixture_measurements.peak_dbfs;
875 let new_clipping =
876 (candidate_measurements.clipping_ratio - mixture_measurements.clipping_ratio).max(0.0);
877 let gates = vec![
878 safety_gate(
879 TargetSpeakerSafetyGateKind::Geometry,
880 bool_value(geometry_passed),
881 1.0,
882 geometry_passed,
883 ),
884 safety_gate(
885 TargetSpeakerSafetyGateKind::FiniteNormalizedSamples,
886 bool_value(finite_normalized_passed),
887 1.0,
888 finite_normalized_passed,
889 ),
890 safety_gate(
891 TargetSpeakerSafetyGateKind::EnergyGain,
892 energy_delta_db,
893 config.maximum_energy_gain_db,
894 energy_delta_db <= config.maximum_energy_gain_db,
895 ),
896 safety_gate(
897 TargetSpeakerSafetyGateKind::PeakGain,
898 peak_delta_db,
899 config.maximum_peak_gain_db,
900 peak_delta_db <= config.maximum_peak_gain_db,
901 ),
902 safety_gate(
903 TargetSpeakerSafetyGateKind::NewClipping,
904 new_clipping,
905 config.maximum_new_clipping_ratio,
906 new_clipping <= config.maximum_new_clipping_ratio,
907 ),
908 safety_gate(
909 TargetSpeakerSafetyGateKind::TargetPresence,
910 f64::from(presence_values[2]),
911 config.minimum_present_probability,
912 presence == TargetSpeakerPresence::Present,
913 ),
914 safety_gate(
915 TargetSpeakerSafetyGateKind::PromotionEvidence,
916 1.0,
917 1.0,
918 true,
919 ),
920 ];
921 let signal_gates_passed = gates
922 .iter()
923 .filter(|gate| gate.kind != TargetSpeakerSafetyGateKind::TargetPresence)
924 .all(|gate| gate.passed);
925 let decision = match presence {
926 TargetSpeakerPresence::Absent => TargetSpeakerDecision::WithheldAbsent,
927 TargetSpeakerPresence::Uncertain => TargetSpeakerDecision::WithheldUncertain,
928 TargetSpeakerPresence::Present if !signal_gates_passed => {
929 TargetSpeakerDecision::WithheldSafetyGate
930 }
931 TargetSpeakerPresence::Present => TargetSpeakerDecision::AcceptedPresent,
932 };
933 let accepted = decision == TargetSpeakerDecision::AcceptedPresent;
934 let output = if accepted {
935 Some(Audio {
936 sample_rate: mixture.sample_rate,
937 channels: vec![candidate.iter().copied().collect()],
938 bits_per_sample: mixture.bits_per_sample,
939 sample_format: mixture.sample_format,
940 channel_mask: None,
941 })
942 } else {
943 None
944 };
945 let output_digest = output
946 .as_ref()
947 .map(|audio| pcm_digest(audio, OUTPUT_PCM_DIGEST_DOMAIN));
948 let mut warnings = Vec::new();
949 match decision {
950 TargetSpeakerDecision::AcceptedPresent => {}
951 TargetSpeakerDecision::WithheldAbsent => warnings.push(
952 "the calibrated presence head classified the target as absent; no audio was published"
953 .into(),
954 ),
955 TargetSpeakerDecision::WithheldUncertain => warnings.push(
956 "target presence was uncertain; no mixture or candidate fallback was published"
957 .into(),
958 ),
959 TargetSpeakerDecision::WithheldSafetyGate => {
960 let failed = gates
961 .iter()
962 .filter(|gate| !gate.passed)
963 .map(|gate| format!("{:?}", gate.kind).to_ascii_lowercase())
964 .collect::<Vec<_>>()
965 .join(", ");
966 warnings.push(format!(
967 "target-speaker candidate failed safety gates ({failed}); no audio was published"
968 ));
969 }
970 }
971 let manifest = self
972 .package
973 .manifest_v2()
974 .expect("target-speaker session requires v2");
975 let profile = self
976 .package
977 .precision_profile_for(self.accelerator.effective())?
978 .expect("target-speaker session selects one v2 profile");
979 let report = TargetSpeakerExtractionReport {
980 schema: TARGET_SPEAKER_REPORT_SCHEMA.into(),
981 schema_version: TARGET_SPEAKER_SCHEMA_VERSION,
982 denoize_version: env!("CARGO_PKG_VERSION").into(),
983 network_accessed: false,
984 deterministic: true,
985 model: TargetSpeakerModelIdentity {
986 package_sha256: self.package.package_sha256().into(),
987 public_key_sha256: self.package.public_key_sha256().into(),
988 package_id: manifest.package_id.clone(),
989 package_revision: manifest.package_revision.clone(),
990 precision_profile: profile.id.clone(),
991 source_revision: manifest.provenance.source_revision.clone(),
992 source_sha256: manifest.provenance.source_sha256.clone(),
993 source_license_spdx: manifest.provenance.source_license_spdx.clone(),
994 checkpoint_sha256: manifest.provenance.checkpoint_sha256.clone(),
995 checkpoint_license_spdx: manifest.provenance.checkpoint_license_spdx.clone(),
996 accelerator: self.accelerator.effective().name().into(),
997 },
998 promotion_evidence: self.evidence.clone(),
999 decision,
1000 model_invoked: true,
1001 candidate_accepted: accepted,
1002 output_published: accepted,
1003 candidate_retained: accepted,
1004 source_sample_rate: mixture.sample_rate,
1005 source_channels: mixture.channels(),
1006 source_frames,
1007 output_channels: 1,
1008 output_frames: accepted.then_some(source_frames),
1009 mixture_mixdown_policy: "arithmetic-mean-mono-v1".into(),
1010 mixture_pcm_sha256: pcm_digest(mixture, MIXTURE_PCM_DIGEST_DOMAIN),
1011 candidate_pcm_sha256: output_digest.clone(),
1012 output_pcm_sha256: output_digest,
1013 enrollment: TargetSpeakerEnrollmentSummary {
1014 input_sample_rate: enrollment_summary_input.0,
1015 input_channels: enrollment_summary_input.1,
1016 input_frames: enrollment_summary_input.2,
1017 model_sample_rate: model_rate,
1018 model_samples: enrollment_model_samples,
1019 mixdown_policy: "arithmetic-mean-mono-v1".into(),
1020 raw_audio_retained: false,
1021 embedding_retained: false,
1022 digest_recorded: false,
1023 },
1024 presence: TargetSpeakerPresenceAssessment {
1025 state: presence,
1026 absent_probability: f64::from(presence_values[0]),
1027 uncertain_probability: f64::from(presence_values[1]),
1028 present_probability: f64::from(presence_values[2]),
1029 minimum_absent_probability: config.minimum_absent_probability,
1030 minimum_present_probability: config.minimum_present_probability,
1031 },
1032 measurements: TargetSpeakerSafetyMeasurements {
1033 mixture_rms_dbfs: mixture_measurements.rms_dbfs,
1034 candidate_rms_dbfs: candidate_measurements.rms_dbfs,
1035 mixture_peak_dbfs: mixture_measurements.peak_dbfs,
1036 candidate_peak_dbfs: candidate_measurements.peak_dbfs,
1037 energy_delta_db,
1038 mixture_clipping_ratio: mixture_measurements.clipping_ratio,
1039 candidate_clipping_ratio: candidate_measurements.clipping_ratio,
1040 },
1041 safety_gates: gates,
1042 runtime_speaker_identity_verified: false,
1043 interferer_leakage_measured_at_runtime: false,
1044 limitations: limitations(),
1045 warnings,
1046 };
1047 Ok(TargetSpeakerExtractionResult {
1048 audio: output,
1049 report,
1050 })
1051 }
1052}
1053
1054#[cfg(feature = "onnx")]
1055struct SensitiveEnrollment(Audio);
1056
1057#[cfg(feature = "onnx")]
1058impl SensitiveEnrollment {
1059 fn new(audio: Audio) -> Self {
1060 Self(audio)
1061 }
1062
1063 fn audio(&self) -> &Audio {
1064 &self.0
1065 }
1066}
1067
1068#[cfg(feature = "onnx")]
1069impl Drop for SensitiveEnrollment {
1070 fn drop(&mut self) {
1071 for channel in &mut self.0.channels {
1072 channel.zeroize();
1073 }
1074 }
1075}
1076
1077#[cfg(feature = "onnx")]
1078fn validate_audio(audio: &Audio, label: &str, allow_empty: bool) -> Result<(), String> {
1079 if audio.sample_rate == 0 {
1080 return Err(format!("target-speaker {label} sample rate is invalid"));
1081 }
1082 if audio.channels.is_empty() || audio.channels.len() > MAX_CHANNELS {
1083 return Err(format!(
1084 "target-speaker {label} channels must be in 1..={MAX_CHANNELS}"
1085 ));
1086 }
1087 let frames = audio.channels[0].len();
1088 if !allow_empty && frames == 0 {
1089 return Err(format!("target-speaker {label} must not be empty"));
1090 }
1091 if audio.channels.iter().any(|channel| channel.len() != frames) {
1092 return Err(format!(
1093 "target-speaker {label} channels must have equal lengths"
1094 ));
1095 }
1096 if audio
1097 .channels
1098 .iter()
1099 .flatten()
1100 .any(|sample| !sample.is_finite() || !(-1.0..=1.0).contains(sample))
1101 {
1102 return Err(format!(
1103 "target-speaker {label} contains an invalid normalized sample"
1104 ));
1105 }
1106 Ok(())
1107}
1108
1109#[cfg(feature = "onnx")]
1110fn mono_mix(audio: &Audio, label: &str) -> Result<Vec<f64>, String> {
1111 let frames = audio.frames();
1112 let mut mono = Vec::new();
1113 mono.try_reserve_exact(frames)
1114 .map_err(|_| format!("unable to reserve target-speaker {label} mono mix"))?;
1115 let scale = 1.0 / audio.channels() as f64;
1116 for frame in 0..frames {
1117 let value = audio
1118 .channels
1119 .iter()
1120 .map(|channel| channel[frame])
1121 .sum::<f64>()
1122 * scale;
1123 mono.push(value);
1124 }
1125 Ok(mono)
1126}
1127
1128#[cfg(feature = "onnx")]
1129fn validate_enrollment_duration(samples: usize, sample_rate: u32) -> Result<(), String> {
1130 let millis = (samples as u64)
1131 .saturating_mul(1000)
1132 .checked_div(u64::from(sample_rate))
1133 .unwrap_or(0);
1134 if !(MIN_TARGET_SPEAKER_ENROLLMENT_MILLIS..=MAX_TARGET_SPEAKER_ENROLLMENT_MILLIS)
1135 .contains(&millis)
1136 {
1137 return Err(format!(
1138 "target-speaker enrollment must be {MIN_TARGET_SPEAKER_ENROLLMENT_MILLIS}..={MAX_TARGET_SPEAKER_ENROLLMENT_MILLIS} ms after resampling, got {millis} ms"
1139 ));
1140 }
1141 Ok(())
1142}
1143
1144#[cfg(feature = "onnx")]
1145fn classify_presence(
1146 probabilities: [f32; 3],
1147 config: &TargetSpeakerExtractionConfig,
1148) -> TargetSpeakerPresence {
1149 let absent = f64::from(probabilities[0]);
1150 let uncertain = f64::from(probabilities[1]);
1151 let present = f64::from(probabilities[2]);
1152 if present >= config.minimum_present_probability && present > absent && present > uncertain {
1153 TargetSpeakerPresence::Present
1154 } else if absent >= config.minimum_absent_probability && absent > present && absent > uncertain
1155 {
1156 TargetSpeakerPresence::Absent
1157 } else {
1158 TargetSpeakerPresence::Uncertain
1159 }
1160}
1161
1162#[cfg(feature = "onnx")]
1163#[derive(Clone, Copy)]
1164struct SignalMeasurements {
1165 rms_dbfs: f64,
1166 peak_dbfs: f64,
1167 clipping_ratio: f64,
1168}
1169
1170#[cfg(feature = "onnx")]
1171fn signal_measurements(samples: &[f64]) -> SignalMeasurements {
1172 if samples.is_empty() {
1173 return SignalMeasurements {
1174 rms_dbfs: -240.0,
1175 peak_dbfs: -240.0,
1176 clipping_ratio: 0.0,
1177 };
1178 }
1179 let energy = samples.iter().fold(0.0, |sum, sample| {
1180 if sample.is_finite() {
1181 sum + sample * sample
1182 } else {
1183 f64::INFINITY
1184 }
1185 });
1186 let rms = (energy / samples.len() as f64).sqrt();
1187 let peak = samples
1188 .iter()
1189 .map(|sample| sample.abs())
1190 .fold(0.0_f64, f64::max);
1191 let clipping =
1192 samples.iter().filter(|sample| sample.abs() >= 1.0).count() as f64 / samples.len() as f64;
1193 SignalMeasurements {
1194 rms_dbfs: amplitude_dbfs(rms),
1195 peak_dbfs: amplitude_dbfs(peak),
1196 clipping_ratio: clipping,
1197 }
1198}
1199
1200#[cfg(feature = "onnx")]
1201fn amplitude_dbfs(amplitude: f64) -> f64 {
1202 if !amplitude.is_finite() {
1203 240.0
1204 } else {
1205 (20.0 * amplitude.max(SILENCE_FLOOR).log10()).clamp(-240.0, 240.0)
1206 }
1207}
1208
1209#[cfg(feature = "onnx")]
1210fn safety_gate(
1211 kind: TargetSpeakerSafetyGateKind,
1212 observed: f64,
1213 limit: f64,
1214 passed: bool,
1215) -> TargetSpeakerSafetyGate {
1216 TargetSpeakerSafetyGate {
1217 kind,
1218 observed: observed.clamp(-240.0, 240.0),
1219 limit: limit.clamp(-240.0, 240.0),
1220 passed,
1221 }
1222}
1223
1224#[cfg(feature = "onnx")]
1225const fn bool_value(value: bool) -> f64 {
1226 if value {
1227 1.0
1228 } else {
1229 0.0
1230 }
1231}
1232
1233#[cfg(feature = "onnx")]
1234fn pcm_digest(audio: &Audio, domain: &[u8]) -> String {
1235 let mut digest = Sha256::new();
1236 digest.update(domain);
1237 digest.update(audio.sample_rate.to_be_bytes());
1238 digest.update((audio.channels() as u64).to_be_bytes());
1239 digest.update((audio.frames() as u64).to_be_bytes());
1240 for channel in &audio.channels {
1241 for sample in channel {
1242 digest.update(sample.to_bits().to_be_bytes());
1243 }
1244 }
1245 format!("{:x}", digest.finalize())
1246}
1247
1248#[cfg(feature = "onnx")]
1249fn limitations() -> Vec<String> {
1250 vec![
1251 "the runtime presence head is not an independent speaker-verification system".into(),
1252 "interferer leakage and target identity are promotion-time measurements, not runtime measurements"
1253 .into(),
1254 "a valid evidence signature authenticates the evaluator's claim but cannot prove the underlying recordings or labels are truthful"
1255 .into(),
1256 "the v1 adapter mixes program channels to mono and does not preserve or infer spatial position"
1257 .into(),
1258 "enrollment buffers are zeroized on ordinary drop, but operating-system caches, allocator copies, swap, and crash dumps are outside this guarantee"
1259 .into(),
1260 "denoize does not bundle a target-speaker checkpoint until artifact-level redistribution and protected-stratum gates are independently satisfied"
1261 .into(),
1262 ]
1263}
1264
1265fn validate_metric_policy(
1266 metric: &TargetSpeakerMetricOutcome,
1267 policy: &MetricPolicy,
1268) -> Result<(), String> {
1269 if metric.operator != policy.operator {
1270 return Err(format!(
1271 "target-speaker evidence metric {} uses the wrong operator",
1272 metric.metric
1273 ));
1274 }
1275 let strong_enough = match policy.operator {
1276 TargetSpeakerMetricOperator::GreaterOrEqual => metric.limit >= policy.hard_limit,
1277 TargetSpeakerMetricOperator::LessOrEqual => metric.limit <= policy.hard_limit,
1278 };
1279 if !strong_enough {
1280 return Err(format!(
1281 "target-speaker evidence metric {} uses a weaker limit than the release policy {}",
1282 metric.metric, policy.hard_limit
1283 ));
1284 }
1285 Ok(())
1286}
1287
1288fn validate_range(label: &str, value: f64, minimum: f64, maximum: f64) -> Result<(), String> {
1289 if !value.is_finite() || !(minimum..=maximum).contains(&value) {
1290 Err(format!(
1291 "target-speaker {label} must be finite and in {minimum}..={maximum}"
1292 ))
1293 } else {
1294 Ok(())
1295 }
1296}
1297
1298fn validate_sha256(label: &str, value: &str) -> Result<(), String> {
1299 if value.len() != 64
1300 || value
1301 .bytes()
1302 .any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
1303 {
1304 return Err(format!(
1305 "target-speaker evidence {label} must be lowercase SHA-256"
1306 ));
1307 }
1308 Ok(())
1309}
1310
1311fn validate_identifier(label: &str, value: &str) -> Result<(), String> {
1312 if value.is_empty()
1313 || value.len() > 256
1314 || !value.bytes().all(|byte| {
1315 byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"._+-".contains(&byte)
1316 })
1317 {
1318 return Err(format!(
1319 "{label} must use 1..=256 lowercase ASCII identifier characters"
1320 ));
1321 }
1322 Ok(())
1323}
1324
1325#[cfg(test)]
1326mod tests {
1327 use super::*;
1328
1329 #[test]
1330 fn promotion_evidence_enforces_protected_strata_and_hard_limits() {
1331 let payload = passing_payload();
1332 payload.validate().unwrap();
1333
1334 let mut missing = payload.clone();
1335 missing.strata.remove(0);
1336 assert!(missing
1337 .validate()
1338 .unwrap_err()
1339 .contains("omits required stratum"));
1340
1341 let mut weak = payload.clone();
1342 let metric = weak
1343 .strata
1344 .iter_mut()
1345 .find(|stratum| stratum.kind == TargetSpeakerStratumKind::TargetPresent)
1346 .unwrap()
1347 .metrics
1348 .iter_mut()
1349 .find(|metric| metric.metric == "speaker.target-similarity")
1350 .unwrap();
1351 metric.limit = 0.1;
1352 metric.value = 0.1;
1353 assert!(weak.validate().unwrap_err().contains("weaker limit"));
1354
1355 let mut uncalibrated = payload;
1356 uncalibrated.presence_expected_calibration_error = 0.051;
1357 assert!(uncalibrated
1358 .validate()
1359 .unwrap_err()
1360 .contains("accepted flag"));
1361 }
1362
1363 #[test]
1364 fn extraction_config_is_closed_and_conservative() {
1365 let config = TargetSpeakerExtractionConfig::default();
1366 config.validate().unwrap();
1367 assert_eq!(config.minimum_present_probability, 0.90);
1368 assert_eq!(config.minimum_absent_probability, 0.90);
1369 let encoded = serde_json::to_string(&config).unwrap();
1370 let unknown = encoded.replace('{', "{\"unknown\":true,");
1371 assert!(serde_json::from_str::<TargetSpeakerExtractionConfig>(&unknown).is_err());
1372 }
1373
1374 #[cfg(feature = "onnx")]
1375 #[test]
1376 fn three_state_presence_never_promotes_ambiguous_probabilities() {
1377 let config = TargetSpeakerExtractionConfig::default();
1378 assert_eq!(
1379 classify_presence([0.01, 0.01, 0.98], &config),
1380 TargetSpeakerPresence::Present
1381 );
1382 assert_eq!(
1383 classify_presence([0.98, 0.01, 0.01], &config),
1384 TargetSpeakerPresence::Absent
1385 );
1386 assert_eq!(
1387 classify_presence([0.40, 0.20, 0.40], &config),
1388 TargetSpeakerPresence::Uncertain
1389 );
1390 }
1391
1392 fn passing_payload() -> TargetSpeakerPromotionEvidencePayload {
1393 let strata = REQUIRED_STRATA
1394 .iter()
1395 .map(|(id, kind)| TargetSpeakerStratumEvidence {
1396 id: (*id).into(),
1397 kind: *kind,
1398 cases: 10,
1399 metrics: match kind {
1400 TargetSpeakerStratumKind::TargetPresent => metric_outcomes(PRESENT_METRICS),
1401 TargetSpeakerStratumKind::TargetAbsent => metric_outcomes(ABSENT_METRICS),
1402 },
1403 })
1404 .collect();
1405 TargetSpeakerPromotionEvidencePayload {
1406 completed_at_unix_seconds: 1_800_000_000,
1407 model_package_sha256: "0".repeat(64),
1408 source_revision: "0123456789abcdef".into(),
1409 source_sha256: "1".repeat(64),
1410 checkpoint_sha256: "2".repeat(64),
1411 corpus_manifest_sha256: "3".repeat(64),
1412 evaluation_result_sha256: "4".repeat(64),
1413 real_t_result_sha256: "5".repeat(64),
1414 ts_superb_result_sha256: "6".repeat(64),
1415 strata,
1416 target_speaker_count: 100,
1417 interferer_speaker_count: 100,
1418 language_count: 2,
1419 presence_expected_calibration_error: 0.05,
1420 presence_expected_calibration_error_limit: 0.05,
1421 minimum_listeners: 20,
1422 listener_count: 20,
1423 listener_preference: 0.5,
1424 listener_preference_limit: 0.5,
1425 accepted: true,
1426 }
1427 }
1428
1429 fn metric_outcomes(policies: &[MetricPolicy]) -> Vec<TargetSpeakerMetricOutcome> {
1430 policies
1431 .iter()
1432 .map(|policy| TargetSpeakerMetricOutcome {
1433 metric: policy.name.into(),
1434 value: policy.hard_limit,
1435 operator: policy.operator,
1436 limit: policy.hard_limit,
1437 passed: true,
1438 })
1439 .collect()
1440 }
1441}