1#![forbid(unsafe_code)]
2
3use std::collections::{HashMap, HashSet};
4
5use chrono::{DateTime, Utc};
6use kcode_audio_ingress::{
7 AudioIngress, AudioInput, ConfirmationState, CorrectionPacket, ErrorKind as AudioErrorKind,
8 RecordingConfirmation, RecordingState, RecordingStatus,
9};
10use kcode_session_history::{
11 NewIngressSession, RetryIngress as HistoryRetryIngress, SessionHistory, SessionRecord,
12 chatend::SessionKind,
13};
14use serde_json::{Value, json};
15use sha2::{Digest, Sha256};
16use uuid::Uuid;
17
18const ESTIMATED_CHARACTERS_PER_TOKEN: u64 = 4;
19const INGRESS_CONTEXT_DIVISOR: u64 = 4;
20const SEGMENTATION_VERSION: u64 = 1;
21
22#[derive(Clone, Debug)]
24pub struct Config {
25 pub user_id: String,
27 pub effective_context_tokens: u64,
29}
30
31#[derive(Clone, Debug)]
33pub struct RecordingInput {
34 pub bytes: Vec<u8>,
36 pub recorded_at: DateTime<Utc>,
38 pub original_filename: Option<String>,
40}
41
42#[derive(Clone, Debug)]
44pub struct RecordingSubmission {
45 pub recording: Recording,
47 pub deduplicated: bool,
49}
50
51#[derive(Clone, Debug)]
53pub struct Recording {
54 pub id: Uuid,
55 pub sha256: String,
56 pub original_filename: String,
57 pub content_type: &'static str,
58 pub size_bytes: u64,
59 pub source_created_at: String,
60 pub received_at: String,
61 pub updated_at: String,
62 pub status: String,
63 pub transcription_model: String,
64 pub reconciliation_model: String,
65 pub reconciliation_reasoning: String,
66 pub transcription_status: Option<Value>,
67 pub attempt_count: i64,
68 pub next_attempt_at: Option<String>,
69 pub last_error: Option<String>,
70 pub speaker_review: Option<SpeakerReview>,
72 pub transcript_piece_count: usize,
74 pub completed_piece_count: usize,
76}
77
78#[derive(Clone, Debug)]
80pub struct SpeakerReview {
81 pub clean: bool,
82 pub confirmation_state: ConfirmationState,
83 pub observation_count: usize,
84}
85
86#[derive(Clone, Debug)]
88pub struct IngressPiece {
89 pub id: String,
90 pub recording_id: Uuid,
91 pub sha256: String,
92 pub original_filename: String,
93 pub source_created_at: String,
94 pub piece_index: u32,
95 pub piece_count: u32,
96 pub transcript_text: String,
97 pub estimated_tokens: u64,
98 pub phase: String,
99 pub provenance_id: Option<String>,
100 pub state: Value,
101 pub version: i64,
102 pub ingress_failure_count: i64,
103 pub ingress_failures: Value,
104 pub created_at: String,
105 pub updated_at: String,
106}
107
108#[derive(Clone, Debug)]
110pub struct RecordingHistory {
111 pub recording: Recording,
112 pub final_transcript: Option<String>,
113 pub correction_packet: Option<CorrectionPacket>,
114 pub pieces: Vec<IngressPiece>,
115}
116
117#[derive(Clone, Debug)]
119pub struct RetryIngress {
120 pub piece_id: String,
121 pub expected_version: i64,
122 pub state: Option<Value>,
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum ErrorKind {
130 InvalidInput,
131 NotFound,
132 Conflict,
133 Internal,
134}
135
136#[derive(Debug)]
138pub struct Error {
139 kind: ErrorKind,
140 message: String,
141}
142
143impl Error {
144 fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
145 Self {
146 kind,
147 message: message.into(),
148 }
149 }
150
151 fn invalid(message: impl Into<String>) -> Self {
152 Self::new(ErrorKind::InvalidInput, message)
153 }
154
155 fn conflict(message: impl Into<String>) -> Self {
156 Self::new(ErrorKind::Conflict, message)
157 }
158
159 fn not_found() -> Self {
160 Self::new(
161 ErrorKind::NotFound,
162 "Audio recording or transcript piece not found.",
163 )
164 }
165
166 fn internal(error: impl std::fmt::Display) -> Self {
167 tracing::warn!(%error, "Audio session ingress operation failed");
168 Self::new(
169 ErrorKind::Internal,
170 "An unexpected audio session ingress error occurred.",
171 )
172 }
173
174 pub fn kind(&self) -> ErrorKind {
176 self.kind
177 }
178
179 pub fn message(&self) -> &str {
181 &self.message
182 }
183}
184
185impl std::fmt::Display for Error {
186 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 formatter.write_str(&self.message)
188 }
189}
190
191impl std::error::Error for Error {}
192
193#[derive(Clone)]
195pub struct Coordinator {
196 audio: AudioIngress,
197 history: SessionHistory,
198 user_id: String,
199 effective_context_tokens: u64,
200 maximum_piece_characters: usize,
201}
202
203impl Coordinator {
204 pub fn new(
206 audio: AudioIngress,
207 history: SessionHistory,
208 config: Config,
209 ) -> Result<Self, Error> {
210 if config.user_id.trim().is_empty() {
211 return Err(Error::invalid("audio user ID must not be empty"));
212 }
213 let maximum_piece_characters = maximum_piece_characters(config.effective_context_tokens)?;
214 Ok(Self {
215 audio,
216 history,
217 user_id: config.user_id,
218 effective_context_tokens: config.effective_context_tokens,
219 maximum_piece_characters,
220 })
221 }
222
223 pub fn health(&self) -> Result<(), Error> {
225 self.audio.status().map_err(audio_error)?;
226 Ok(())
227 }
228
229 pub async fn submit(&self, input: RecordingInput) -> Result<RecordingSubmission, Error> {
231 let submission = self
232 .audio
233 .submit(AudioInput {
234 user_id: self.user_id.clone(),
235 bytes: input.bytes,
236 recorded_at: input.recorded_at,
237 original_filename: input.original_filename,
238 })
239 .await
240 .map_err(audio_error)?;
241 let recording_status = self
242 .audio
243 .status()
244 .map_err(audio_error)?
245 .recordings
246 .into_iter()
247 .find(|recording| recording.id == submission.recording_id)
248 .ok_or_else(Error::not_found)?;
249 let histories = self.history.list().await.map_err(history_error)?;
250 let projection = ingress_projection(
251 &recording_status,
252 &histories,
253 self.effective_context_tokens,
254 self.maximum_piece_characters,
255 )?;
256 let recording = Recording::from_status(recording_status, &projection);
257 Ok(RecordingSubmission {
258 recording,
259 deduplicated: submission.deduplicated,
260 })
261 }
262
263 pub async fn recordings(&self) -> Result<Vec<Recording>, Error> {
265 let histories = self.history.list().await.map_err(history_error)?;
266 self.audio
267 .status()
268 .map_err(audio_error)?
269 .recordings
270 .into_iter()
271 .map(|recording| {
272 let projection = ingress_projection(
273 &recording,
274 &histories,
275 self.effective_context_tokens,
276 self.maximum_piece_characters,
277 )?;
278 Ok(Recording::from_status(recording, &projection))
279 })
280 .collect()
281 }
282
283 pub async fn recording_by_sha256(&self, sha256: &str) -> Result<Recording, Error> {
285 if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
286 return Err(Error::invalid(
287 "audio SHA-256 must contain exactly 64 hexadecimal characters",
288 ));
289 }
290 let normalized = sha256.to_ascii_lowercase();
291 self.recordings()
292 .await?
293 .into_iter()
294 .find(|recording| recording.sha256 == normalized)
295 .ok_or_else(Error::not_found)
296 }
297
298 pub async fn recording_history(&self, recording_id: Uuid) -> Result<RecordingHistory, Error> {
300 let recording_status = self
301 .audio
302 .status()
303 .map_err(audio_error)?
304 .recordings
305 .into_iter()
306 .find(|recording| recording.id == recording_id)
307 .ok_or_else(Error::not_found)?;
308 let histories = self.history.list().await.map_err(history_error)?;
309 let projection = ingress_projection(
310 &recording_status,
311 &histories,
312 self.effective_context_tokens,
313 self.maximum_piece_characters,
314 )?;
315 let final_transcript = match &recording_status.state {
316 RecordingState::Complete { transcript } => Some(transcript.clone()),
317 _ => None,
318 };
319 let correction_packet = recording_status.correction_packet.clone();
320 let recording = Recording::from_status(recording_status, &projection);
321 Ok(RecordingHistory {
322 recording,
323 final_transcript,
324 correction_packet,
325 pieces: projection.pieces,
326 })
327 }
328
329 pub fn retry_recording(&self, recording_id: Uuid) -> Result<(), Error> {
331 self.audio.retry(recording_id).map_err(audio_error)
332 }
333
334 pub async fn confirm_speakers(
337 &self,
338 confirmation: RecordingConfirmation,
339 ) -> Result<CorrectionPacket, Error> {
340 let recording_id = confirmation.recording_id;
341 let recording = self
342 .audio
343 .status()
344 .map_err(audio_error)?
345 .recordings
346 .into_iter()
347 .find(|recording| recording.id == recording_id)
348 .ok_or_else(Error::not_found)?;
349 let histories = self.history.list().await.map_err(history_error)?;
350 if let Some(packet) = recording
351 .correction_packet
352 .as_ref()
353 .filter(|packet| confirmation_matches(packet, &confirmation))
354 {
355 synchronize_recordings(
356 &self.history,
357 std::slice::from_ref(&recording),
358 self.effective_context_tokens,
359 self.maximum_piece_characters,
360 )
361 .await?;
362 return Ok(packet.clone());
363 }
364 if recording_has_ingress(recording_id, &histories) {
365 return Err(Error::conflict(
366 "speaker labels are already bound to accepted transcript ingress",
367 ));
368 }
369 let packet = self
370 .audio
371 .confirm_speakers(confirmation)
372 .map_err(audio_error)?;
373 let recording = self
374 .audio
375 .status()
376 .map_err(audio_error)?
377 .recordings
378 .into_iter()
379 .find(|recording| recording.id == recording_id)
380 .ok_or_else(Error::not_found)?;
381 synchronize_recordings(
382 &self.history,
383 std::slice::from_ref(&recording),
384 self.effective_context_tokens,
385 self.maximum_piece_characters,
386 )
387 .await?;
388 Ok(packet)
389 }
390
391 pub async fn retry_ingress(&self, input: RetryIngress) -> Result<SessionRecord, Error> {
393 let current = self
394 .history
395 .get(&input.piece_id)
396 .await
397 .map_err(history_error)?;
398 self.history
399 .retry_ingress(
400 &input.piece_id,
401 HistoryRetryIngress {
402 expected_version: input.expected_version,
403 state: input.state.unwrap_or(current.state),
404 },
405 )
406 .await
407 .map_err(history_error)
408 }
409
410 pub async fn synchronize_completed_transcripts(&self) -> Result<(), Error> {
412 let recordings = self
413 .audio
414 .status()
415 .map_err(audio_error)?
416 .recordings
417 .into_iter()
418 .collect::<Vec<_>>();
419 synchronize_recordings(
420 &self.history,
421 &recordings,
422 self.effective_context_tokens,
423 self.maximum_piece_characters,
424 )
425 .await
426 }
427}
428
429fn confirmation_matches(packet: &CorrectionPacket, confirmation: &RecordingConfirmation) -> bool {
430 if packet.confirmation_state != ConfirmationState::Confirmed {
431 return false;
432 }
433 let existing = packet
434 .chunks
435 .iter()
436 .flat_map(|chunk| &chunk.observations)
437 .filter_map(|observation| {
438 observation
439 .confirmed_full_name
440 .as_deref()
441 .map(|name| (&observation.observation_key, name))
442 })
443 .collect::<HashMap<_, _>>();
444 existing.len() == confirmation.observations.len()
445 && confirmation.observations.iter().all(|observation| {
446 existing.get(&observation.observation_key).copied()
447 == Some(observation.confirmed_full_name.trim())
448 })
449}
450
451fn recording_has_ingress(recording_id: Uuid, histories: &[SessionRecord]) -> bool {
452 let prefix = format!("audio:{recording_id}:");
453 histories.iter().any(|record| {
454 ingress_source_id(record).is_some_and(|source_id| source_id.starts_with(&prefix))
455 })
456}
457
458#[derive(Debug)]
459struct PieceSpec {
460 id: String,
461 index: u32,
462 count: u32,
463 text: String,
464}
465
466#[derive(Clone, Debug, Eq, PartialEq)]
467struct SegmentationParameters {
468 version: u64,
469 effective_context_tokens: u64,
470 maximum_piece_characters: usize,
471 transcript_sha256: String,
472 fingerprint: String,
473}
474
475#[derive(Debug)]
476struct SegmentationPlan {
477 parameters: SegmentationParameters,
478 specs: Vec<PieceSpec>,
479}
480
481#[derive(Debug)]
482struct PersistedPieceIdentity {
483 index: u32,
484 count: u32,
485 piece_characters: usize,
486 piece_text_sha256: String,
487 ingress_text_sha256: String,
488 parameters: SegmentationParameters,
489}
490
491#[derive(Debug, Default)]
492struct IngressProjection {
493 pieces: Vec<IngressPiece>,
494}
495
496impl Recording {
497 fn from_status(recording: RecordingStatus, projection: &IngressProjection) -> Self {
498 let speaker_review = recording
499 .correction_packet
500 .as_ref()
501 .map(|packet| SpeakerReview {
502 clean: packet.clean,
503 confirmation_state: packet.confirmation_state,
504 observation_count: packet
505 .chunks
506 .iter()
507 .map(|chunk| chunk.observations.len())
508 .sum(),
509 });
510 let awaiting_speaker_review = speaker_review
511 .as_ref()
512 .is_some_and(|review| review.confirmation_state != ConfirmationState::Confirmed);
513 let (mut status, transcription_status, attempt_count, last_error) = match recording.state {
514 RecordingState::Queued => ("uploaded".into(), None, 0, None),
515 RecordingState::Processing { attempt, progress } => (
516 processing_stage(&progress).into(),
517 serde_json::to_value(progress).ok(),
518 i64::from(attempt),
519 None,
520 ),
521 RecordingState::Complete { .. } if awaiting_speaker_review => {
522 ("speaker_review".into(), None, 0, None)
523 }
524 RecordingState::Complete { .. } => ("ready_for_ingress".into(), None, 0, None),
525 RecordingState::Failed {
526 attempts, error, ..
527 } => ("failed".into(), None, i64::from(attempts), Some(error)),
528 };
529 if !projection.pieces.is_empty() {
530 status = if projection
531 .pieces
532 .iter()
533 .all(|piece| piece.phase == "complete")
534 {
535 "complete".into()
536 } else if projection
537 .pieces
538 .iter()
539 .any(|piece| piece.phase == "ingress_failed")
540 {
541 "ingress_failed".into()
542 } else if projection
543 .pieces
544 .iter()
545 .any(|piece| piece.phase == "ingress_in_progress")
546 {
547 "ingressing".into()
548 } else {
549 "ready_for_ingress".into()
550 };
551 }
552 let completed_piece_count = projection
553 .pieces
554 .iter()
555 .filter(|piece| piece.phase == "complete")
556 .count();
557 Self {
558 id: recording.id,
559 sha256: recording.sha256,
560 original_filename: recording.original_filename,
561 content_type: "audio/wav",
562 size_bytes: recording.size_bytes,
563 source_created_at: recording.recorded_at.to_rfc3339(),
564 received_at: recording.received_at.to_rfc3339(),
565 updated_at: recording.received_at.to_rfc3339(),
566 status,
567 transcription_model: recording.transcription_model,
568 reconciliation_model: recording.reconciliation_model,
569 reconciliation_reasoning: recording.reconciliation_reasoning,
570 transcription_status,
571 attempt_count,
572 next_attempt_at: None,
573 last_error,
574 speaker_review,
575 transcript_piece_count: projection.pieces.len(),
576 completed_piece_count,
577 }
578 }
579}
580
581impl IngressPiece {
582 fn from_record(recording: &RecordingStatus, spec: &PieceSpec, record: &SessionRecord) -> Self {
583 Self {
584 id: record.id.clone(),
585 recording_id: recording.id,
586 sha256: recording.sha256.clone(),
587 original_filename: recording.original_filename.clone(),
588 source_created_at: recording.recorded_at.to_rfc3339(),
589 piece_index: spec.index,
590 piece_count: spec.count,
591 estimated_tokens: estimate_tokens(&spec.text),
592 transcript_text: spec.text.clone(),
593 phase: record.phase.clone(),
594 provenance_id: record.provenance_id.clone(),
595 state: record.state.clone(),
596 version: record.version,
597 ingress_failure_count: record.ingress_failure_count,
598 ingress_failures: record.ingress_failures.clone(),
599 created_at: record.started_at.clone(),
600 updated_at: record.updated_at.clone(),
601 }
602 }
603}
604
605async fn synchronize_recordings(
606 history: &SessionHistory,
607 recordings: &[RecordingStatus],
608 effective_context_tokens: u64,
609 maximum_piece_characters: usize,
610) -> Result<(), Error> {
611 let histories = history.list().await.map_err(history_error)?;
612 let mut existing = histories
613 .iter()
614 .filter_map(ingress_source_id)
615 .map(str::to_owned)
616 .collect::<HashSet<_>>();
617 for recording in recordings {
618 if !matches!(&recording.state, RecordingState::Complete { .. })
619 || !speaker_labels_authorized(recording)
620 {
621 continue;
622 }
623 let plan = segmentation_plan(
624 recording,
625 &histories,
626 effective_context_tokens,
627 maximum_piece_characters,
628 )?;
629 for spec in &plan.specs {
630 if existing.contains(&spec.id) {
631 continue;
632 }
633 let text = format_ingress_piece(recording, spec)?;
634 let created = history
635 .enqueue_ingress(NewIngressSession {
636 idempotency_id: spec.id.clone(),
637 started_at: recording.recorded_at.to_rfc3339(),
638 source_session_type: "audio".into(),
639 kind: SessionKind::AudioIngress,
640 effective_context_tokens: plan.parameters.effective_context_tokens,
641 text: text.clone(),
642 metadata: audio_piece_metadata(recording, spec, &plan.parameters, &text),
643 })
644 .await
645 .map_err(history_error)?;
646 if !created.created {
647 validate_persisted_record(recording, &plan.parameters, spec, &created.value)?;
648 }
649 existing.insert(spec.id.clone());
650 }
651 }
652 Ok(())
653}
654
655fn speaker_labels_authorized(recording: &RecordingStatus) -> bool {
656 recording
657 .correction_packet
658 .as_ref()
659 .is_none_or(|packet| packet.confirmation_state == ConfirmationState::Confirmed)
660}
661
662fn ingress_projection(
663 recording: &RecordingStatus,
664 histories: &[SessionRecord],
665 effective_context_tokens: u64,
666 maximum_piece_characters: usize,
667) -> Result<IngressProjection, Error> {
668 if !matches!(&recording.state, RecordingState::Complete { .. }) {
669 return Ok(IngressProjection::default());
670 }
671 let plan = segmentation_plan(
672 recording,
673 histories,
674 effective_context_tokens,
675 maximum_piece_characters,
676 )?;
677 let mut pieces = Vec::with_capacity(plan.specs.len());
678 for spec in &plan.specs {
679 let Some(record) = histories
680 .iter()
681 .find(|record| ingress_source_id(record) == Some(spec.id.as_str()))
682 else {
683 continue;
684 };
685 pieces.push(IngressPiece::from_record(recording, spec, record));
686 }
687 Ok(IngressProjection { pieces })
688}
689
690fn segmentation_plan(
691 recording: &RecordingStatus,
692 histories: &[SessionRecord],
693 effective_context_tokens: u64,
694 current_maximum_piece_characters: usize,
695) -> Result<SegmentationPlan, Error> {
696 let persisted = persisted_audio_records(recording.id, histories)?;
697 if persisted.is_empty() {
698 return new_segmentation_plan(
699 recording,
700 effective_context_tokens,
701 current_maximum_piece_characters,
702 );
703 }
704
705 let mut identities = Vec::with_capacity(persisted.len());
706 for (index, record) in persisted {
707 identities.push((record, persisted_piece_identity(recording, index, record)?));
708 }
709 let parameters = identities[0].1.parameters.clone();
710 if identities
711 .iter()
712 .any(|(_, identity)| identity.parameters != parameters)
713 {
714 return Err(Error::conflict(
715 "persisted audio pieces disagree about authoritative segmentation",
716 ));
717 }
718 let expected_maximum = maximum_piece_characters(parameters.effective_context_tokens)
719 .map_err(|_| Error::conflict("persisted audio segmentation parameters are invalid"))?;
720 if expected_maximum != parameters.maximum_piece_characters {
721 return Err(Error::conflict(
722 "persisted audio segmentation parameters are inconsistent",
723 ));
724 }
725 if parameters.version != SEGMENTATION_VERSION
726 || parameters.fingerprint != segmentation_fingerprint(¶meters)
727 {
728 return Err(Error::conflict(
729 "persisted audio segmentation authority is unsupported or invalid",
730 ));
731 }
732
733 let transcript = completed_transcript(recording)?;
734 if sha256_text(transcript.trim()) != parameters.transcript_sha256 {
735 return Err(Error::conflict(
736 "completed audio transcript conflicts with persisted ingress identity",
737 ));
738 }
739 let specs = transcript_piece_specs(recording, parameters.maximum_piece_characters)?;
740 for (record, identity) in &identities {
741 let spec = specs.get(identity.index as usize).ok_or_else(|| {
742 Error::conflict("persisted audio piece index is outside authoritative segmentation")
743 })?;
744 validate_persisted_identity(recording, ¶meters, spec, identity)?;
745 if ingress_source_id(record) != Some(spec.id.as_str()) {
746 return Err(Error::conflict(
747 "persisted audio piece identity does not match its segmentation index",
748 ));
749 }
750 }
751 Ok(SegmentationPlan { parameters, specs })
752}
753
754fn new_segmentation_plan(
755 recording: &RecordingStatus,
756 effective_context_tokens: u64,
757 current_maximum_piece_characters: usize,
758) -> Result<SegmentationPlan, Error> {
759 if maximum_piece_characters(effective_context_tokens)? != current_maximum_piece_characters {
760 return Err(Error::internal(
761 "audio ingress context and piece limit are inconsistent",
762 ));
763 }
764 let transcript = completed_transcript(recording)?;
765 let mut parameters = SegmentationParameters {
766 version: SEGMENTATION_VERSION,
767 effective_context_tokens,
768 maximum_piece_characters: current_maximum_piece_characters,
769 transcript_sha256: sha256_text(transcript.trim()),
770 fingerprint: String::new(),
771 };
772 parameters.fingerprint = segmentation_fingerprint(¶meters);
773 let specs = transcript_piece_specs(recording, current_maximum_piece_characters)?;
774 Ok(SegmentationPlan { parameters, specs })
775}
776
777fn persisted_audio_records(
778 recording_id: Uuid,
779 histories: &[SessionRecord],
780) -> Result<Vec<(u32, &SessionRecord)>, Error> {
781 let prefix = format!("audio:{recording_id}:");
782 let mut records = Vec::new();
783 let mut indexes = HashSet::new();
784 for record in histories {
785 let Some(source_id) = ingress_source_id(record) else {
786 continue;
787 };
788 let Some(suffix) = source_id.strip_prefix(&prefix) else {
789 continue;
790 };
791 let index = suffix.parse::<u32>().map_err(|_| {
792 Error::conflict("persisted audio piece has an invalid deterministic identity")
793 })?;
794 if audio_piece_id(recording_id, index) != source_id {
795 return Err(Error::conflict(
796 "persisted audio piece has a noncanonical deterministic identity",
797 ));
798 }
799 if !indexes.insert(index) {
800 return Err(Error::conflict(
801 "multiple persisted audio pieces claim the same deterministic identity",
802 ));
803 }
804 records.push((index, record));
805 }
806 records.sort_by_key(|(index, _)| *index);
807 Ok(records)
808}
809
810fn persisted_piece_identity(
811 recording: &RecordingStatus,
812 id_index: u32,
813 record: &SessionRecord,
814) -> Result<PersistedPieceIdentity, Error> {
815 let metadata = record
816 .state
817 .pointer("/ingressSource/metadata")
818 .and_then(Value::as_object)
819 .ok_or_else(|| {
820 Error::conflict("persisted audio piece lacks authoritative segmentation metadata")
821 })?;
822 let recording_id = recording.id.to_string();
823 let source_created_at = recording.recorded_at.to_rfc3339();
824 if metadata.get("kind").and_then(Value::as_str) != Some("audio-transcript")
825 || metadata.get("recordingId").and_then(Value::as_str) != Some(recording_id.as_str())
826 || metadata.get("sha256").and_then(Value::as_str) != Some(recording.sha256.as_str())
827 || metadata.get("originalFilename").and_then(Value::as_str)
828 != Some(recording.original_filename.as_str())
829 || metadata.get("sizeBytes").and_then(Value::as_u64) != Some(recording.size_bytes)
830 || metadata.get("sourceCreatedAt").and_then(Value::as_str)
831 != Some(source_created_at.as_str())
832 {
833 return Err(Error::conflict(
834 "persisted audio piece metadata conflicts with its recording",
835 ));
836 }
837
838 let version = required_metadata_u64(metadata, "segmentationVersion")?;
839 let effective_context_tokens = required_metadata_u64(metadata, "effectiveContextTokens")?;
840 let maximum_piece_characters =
841 usize::try_from(required_metadata_u64(metadata, "maximumPieceCharacters")?)
842 .map_err(|_| Error::conflict("persisted audio piece limit exceeds platform bounds"))?;
843 let index = u32::try_from(required_metadata_u64(metadata, "pieceIndex")?)
844 .map_err(|_| Error::conflict("persisted audio piece index exceeds u32"))?;
845 let count = u32::try_from(required_metadata_u64(metadata, "pieceCount")?)
846 .map_err(|_| Error::conflict("persisted audio piece count exceeds u32"))?;
847 let piece_characters = usize::try_from(required_metadata_u64(metadata, "pieceCharacters")?)
848 .map_err(|_| Error::conflict("persisted audio piece length exceeds platform bounds"))?;
849 if index != id_index || count == 0 || index >= count {
850 return Err(Error::conflict(
851 "persisted audio piece index or count conflicts with its identity",
852 ));
853 }
854
855 let transcript_sha256 = required_metadata_hash(metadata, "transcriptSha256")?;
856 let fingerprint = required_metadata_hash(metadata, "segmentationFingerprint")?;
857 let piece_text_sha256 = required_metadata_hash(metadata, "pieceTextSha256")?;
858 let ingress_text_sha256 = required_metadata_hash(metadata, "ingressTextSha256")?;
859 Ok(PersistedPieceIdentity {
860 index,
861 count,
862 piece_characters,
863 piece_text_sha256,
864 ingress_text_sha256,
865 parameters: SegmentationParameters {
866 version,
867 effective_context_tokens,
868 maximum_piece_characters,
869 transcript_sha256,
870 fingerprint,
871 },
872 })
873}
874
875fn validate_persisted_record(
876 recording: &RecordingStatus,
877 parameters: &SegmentationParameters,
878 spec: &PieceSpec,
879 record: &SessionRecord,
880) -> Result<(), Error> {
881 let source_id = ingress_source_id(record).ok_or_else(|| {
882 Error::conflict("replayed audio ingress omitted its deterministic identity")
883 })?;
884 if source_id != spec.id {
885 return Err(Error::conflict(
886 "replayed audio ingress returned a different deterministic identity",
887 ));
888 }
889 let identity = persisted_piece_identity(recording, spec.index, record)?;
890 validate_persisted_identity(recording, parameters, spec, &identity)
891}
892
893fn validate_persisted_identity(
894 recording: &RecordingStatus,
895 parameters: &SegmentationParameters,
896 spec: &PieceSpec,
897 identity: &PersistedPieceIdentity,
898) -> Result<(), Error> {
899 if &identity.parameters != parameters
900 || identity.index != spec.index
901 || identity.count != spec.count
902 || identity.piece_characters != spec.text.chars().count()
903 || identity.piece_text_sha256 != sha256_text(&spec.text)
904 {
905 return Err(Error::conflict(
906 "persisted audio piece conflicts with authoritative transcript segmentation",
907 ));
908 }
909 let ingress_text = format_ingress_piece(recording, spec)?;
910 if identity.ingress_text_sha256 != sha256_text(&ingress_text) {
911 return Err(Error::conflict(
912 "persisted audio ingress text conflicts with its authoritative identity",
913 ));
914 }
915 Ok(())
916}
917
918fn required_metadata_u64(
919 metadata: &serde_json::Map<String, Value>,
920 field: &str,
921) -> Result<u64, Error> {
922 metadata.get(field).and_then(Value::as_u64).ok_or_else(|| {
923 Error::conflict(format!(
924 "persisted audio piece lacks valid {field} metadata"
925 ))
926 })
927}
928
929fn required_metadata_hash(
930 metadata: &serde_json::Map<String, Value>,
931 field: &str,
932) -> Result<String, Error> {
933 let value = metadata.get(field).and_then(Value::as_str).ok_or_else(|| {
934 Error::conflict(format!(
935 "persisted audio piece lacks valid {field} metadata"
936 ))
937 })?;
938 if value.len() != 64
939 || !value
940 .bytes()
941 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
942 {
943 return Err(Error::conflict(format!(
944 "persisted audio piece has invalid {field} metadata"
945 )));
946 }
947 Ok(value.to_owned())
948}
949
950fn completed_transcript(recording: &RecordingStatus) -> Result<&str, Error> {
951 let RecordingState::Complete { transcript } = &recording.state else {
952 return Err(Error::internal(
953 "audio segmentation requested for an incomplete recording",
954 ));
955 };
956 Ok(transcript)
957}
958
959fn transcript_piece_specs(
960 recording: &RecordingStatus,
961 maximum_piece_characters: usize,
962) -> Result<Vec<PieceSpec>, Error> {
963 let RecordingState::Complete { transcript } = &recording.state else {
964 return Ok(Vec::new());
965 };
966 let pieces = split_transcript(transcript, maximum_piece_characters)?;
967 let piece_count = u32::try_from(pieces.len())
968 .map_err(|_| Error::internal("audio transcript contains too many pieces"))?;
969 pieces
970 .into_iter()
971 .enumerate()
972 .map(|(index, text)| {
973 let piece_index = u32::try_from(index)
974 .map_err(|_| Error::internal("audio transcript piece index exceeds u32"))?;
975 Ok(PieceSpec {
976 id: audio_piece_id(recording.id, piece_index),
977 index: piece_index,
978 count: piece_count,
979 text,
980 })
981 })
982 .collect()
983}
984
985fn ingress_source_id(record: &SessionRecord) -> Option<&str> {
986 record
987 .state
988 .pointer("/ingressSource/idempotencyId")
989 .and_then(Value::as_str)
990}
991
992fn audio_piece_id(recording_id: Uuid, piece_index: u32) -> String {
993 format!("audio:{recording_id}:{piece_index}")
994}
995
996fn audio_piece_metadata(
997 recording: &RecordingStatus,
998 spec: &PieceSpec,
999 parameters: &SegmentationParameters,
1000 ingress_text: &str,
1001) -> Value {
1002 json!({
1003 "kind":"audio-transcript",
1004 "recordingId":recording.id.to_string(),
1005 "sha256":recording.sha256,
1006 "originalFilename":recording.original_filename,
1007 "extension":file_name_extension(&recording.original_filename),
1008 "mimeType":"audio/wav",
1009 "sizeBytes":recording.size_bytes,
1010 "sourceCreatedAt":recording.recorded_at.to_rfc3339(),
1011 "pieceIndex":spec.index,
1012 "pieceCount":spec.count,
1013 "pieceCharacters":spec.text.chars().count(),
1014 "segmentationVersion":parameters.version,
1015 "effectiveContextTokens":parameters.effective_context_tokens,
1016 "maximumPieceCharacters":parameters.maximum_piece_characters,
1017 "transcriptSha256":parameters.transcript_sha256,
1018 "segmentationFingerprint":parameters.fingerprint,
1019 "pieceTextSha256":sha256_text(&spec.text),
1020 "ingressTextSha256":sha256_text(ingress_text),
1021 "speakerConfirmationState":recording.correction_packet.as_ref().map(|packet| match packet.confirmation_state {
1022 ConfirmationState::Unconfirmed => "unconfirmed",
1023 ConfirmationState::AutomaticallyTrained => "automatically_trained",
1024 ConfirmationState::Confirmed => "confirmed",
1025 }),
1026 })
1027}
1028
1029fn segmentation_fingerprint(parameters: &SegmentationParameters) -> String {
1030 sha256_text(&format!(
1031 "kcode-audio-session-ingress-segmentation\n{}\n{}\n{}\n{}",
1032 parameters.version,
1033 parameters.effective_context_tokens,
1034 parameters.maximum_piece_characters,
1035 parameters.transcript_sha256,
1036 ))
1037}
1038
1039fn sha256_text(value: &str) -> String {
1040 format!("{:x}", Sha256::digest(value.as_bytes()))
1041}
1042
1043fn format_ingress_piece(recording: &RecordingStatus, spec: &PieceSpec) -> Result<String, Error> {
1044 let speaker_mapping = confirmed_speaker_mapping(recording)?;
1045 Ok(format!(
1046 "Vnote final transcript piece\n\nRecording began: {}\nRecording SHA-256: {}\nOriginal filename: {}\nExtension: {}\nMIME type: audio/wav\nSize: {} bytes\nTranscript piece: {} of {}{}\n\n{}",
1047 recording.recorded_at.to_rfc3339(),
1048 recording.sha256,
1049 recording.original_filename,
1050 file_name_extension(&recording.original_filename),
1051 recording.size_bytes,
1052 spec.index + 1,
1053 spec.count,
1054 speaker_mapping,
1055 spec.text,
1056 ))
1057}
1058
1059fn confirmed_speaker_mapping(recording: &RecordingStatus) -> Result<String, Error> {
1060 let Some(packet) = recording.correction_packet.as_ref() else {
1061 return Ok(String::new());
1062 };
1063 if packet.confirmation_state != ConfirmationState::Confirmed {
1064 return Err(Error::conflict(
1065 "audio speaker labels require exact human confirmation before ingress",
1066 ));
1067 }
1068 let mut lines = Vec::new();
1069 for chunk in &packet.chunks {
1070 for observation in &chunk.observations {
1071 let confirmed = observation.confirmed_full_name.as_deref().ok_or_else(|| {
1072 Error::internal("confirmed audio packet omitted an observation label")
1073 })?;
1074 let local_label =
1075 serde_json::to_string(&observation.local_label).map_err(Error::internal)?;
1076 let confirmed = serde_json::to_string(confirmed).map_err(Error::internal)?;
1077 let candidate = observation
1078 .identified_full_name
1079 .as_deref()
1080 .map(serde_json::to_string)
1081 .transpose()
1082 .map_err(Error::internal)?
1083 .unwrap_or_else(|| "null".into());
1084 lines.push(format!(
1085 "- chunk {}/{}, source {:.3}-{:.3}s: localLabel={}, confirmedFullName={}, classifierCandidate={}",
1086 chunk.chunk_index + 1,
1087 chunk.chunk_count,
1088 chunk.audio_start_ms as f64 / 1_000.0,
1089 chunk.audio_end_ms as f64 / 1_000.0,
1090 local_label,
1091 confirmed,
1092 candidate,
1093 ));
1094 }
1095 }
1096 if lines.is_empty() {
1097 return Err(Error::internal(
1098 "confirmed audio packet contains no speaker observations",
1099 ));
1100 }
1101 Ok(format!(
1102 "\n\nHuman-confirmed speaker-label data (authoritative; do not infer alternatives):\n{}",
1103 lines.join("\n")
1104 ))
1105}
1106
1107fn file_name_extension(file_name: &str) -> String {
1108 file_name
1109 .rsplit_once('.')
1110 .and_then(|(stem, extension)| {
1111 (!stem.is_empty() && !extension.is_empty()).then_some(extension)
1112 })
1113 .map(|extension| format!(".{extension}"))
1114 .unwrap_or_else(|| "(none)".into())
1115}
1116
1117fn processing_stage(status: &kcode_audio_ingress::TranscriptionStatus) -> &'static str {
1118 let plan_complete = status.steps.iter().any(|entry| {
1119 entry.step == kcode_audio_ingress::Step::PlanChunks
1120 && entry.state == kcode_audio_ingress::StepState::Completed
1121 });
1122 if !plan_complete {
1123 return "chunking";
1124 }
1125 let chunks_complete = status
1126 .steps
1127 .iter()
1128 .filter(|entry| {
1129 matches!(
1130 entry.step,
1131 kcode_audio_ingress::Step::TranscribeChunk { .. }
1132 )
1133 })
1134 .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
1135 if chunks_complete {
1136 let analyses_complete = status
1137 .steps
1138 .iter()
1139 .filter(|entry| matches!(entry.step, kcode_audio_ingress::Step::ParseChunk { .. }))
1140 .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
1141 if !analyses_complete {
1142 return "analyzing_speakers";
1143 }
1144 let training_active = status.steps.iter().any(|entry| {
1145 entry.step == kcode_audio_ingress::Step::TrainIdentities
1146 && matches!(
1147 entry.state,
1148 kcode_audio_ingress::StepState::Running
1149 | kcode_audio_ingress::StepState::Retrying
1150 )
1151 });
1152 if training_active {
1153 "training_speakers"
1154 } else {
1155 "reconciling"
1156 }
1157 } else {
1158 "transcribing"
1159 }
1160}
1161
1162fn maximum_piece_characters(effective_context_tokens: u64) -> Result<usize, Error> {
1163 let piece_tokens = effective_context_tokens / INGRESS_CONTEXT_DIVISOR;
1164 if piece_tokens == 0 {
1165 return Err(Error::invalid(
1166 "effective ingress context must contain at least four tokens",
1167 ));
1168 }
1169 let characters = piece_tokens
1170 .checked_mul(ESTIMATED_CHARACTERS_PER_TOKEN)
1171 .ok_or_else(|| Error::invalid("effective ingress context is too large"))?;
1172 usize::try_from(characters)
1173 .map_err(|_| Error::invalid("effective ingress context exceeds platform limits"))
1174}
1175
1176fn split_transcript(
1177 transcript: &str,
1178 maximum_piece_characters: usize,
1179) -> Result<Vec<String>, Error> {
1180 let mut remaining = transcript.trim();
1181 if remaining.is_empty() {
1182 return Err(Error::internal("completed audio transcript is empty"));
1183 }
1184 let mut pieces = Vec::new();
1185 while remaining.chars().count() > maximum_piece_characters {
1186 let cutoff = remaining
1187 .char_indices()
1188 .nth(maximum_piece_characters)
1189 .map(|(index, _)| index)
1190 .unwrap_or(remaining.len());
1191 let prefix = &remaining[..cutoff];
1192 let minimum = prefix
1193 .char_indices()
1194 .nth(maximum_piece_characters / 2)
1195 .map(|(index, _)| index)
1196 .unwrap_or(0);
1197 let boundary = prefix
1198 .rfind("\n\n")
1199 .filter(|index| *index >= minimum)
1200 .or_else(|| prefix.rfind('\n').filter(|index| *index >= minimum))
1201 .unwrap_or(cutoff);
1202 let piece = remaining[..boundary].trim();
1203 if piece.is_empty() {
1204 return Err(Error::internal("could not split audio transcript"));
1205 }
1206 pieces.push(piece.to_owned());
1207 remaining = remaining[boundary..].trim();
1208 }
1209 if !remaining.is_empty() {
1210 pieces.push(remaining.to_owned());
1211 }
1212 Ok(pieces)
1213}
1214
1215fn estimate_tokens(value: &str) -> u64 {
1216 (value.chars().count() as u64).div_ceil(ESTIMATED_CHARACTERS_PER_TOKEN)
1217}
1218
1219fn audio_error(error: kcode_audio_ingress::Error) -> Error {
1220 match error.kind() {
1221 AudioErrorKind::InvalidInput => Error::new(ErrorKind::InvalidInput, error.to_string()),
1222 AudioErrorKind::NotFound => Error::new(ErrorKind::NotFound, error.to_string()),
1223 AudioErrorKind::Conflict => Error::new(ErrorKind::Conflict, error.to_string()),
1224 AudioErrorKind::Internal => Error::internal(error),
1225 }
1226}
1227
1228fn history_error(error: kcode_session_history::Error) -> Error {
1229 let kind = match error.kind {
1230 kcode_session_history::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
1231 kcode_session_history::ErrorKind::NotFound => ErrorKind::NotFound,
1232 kcode_session_history::ErrorKind::Conflict => ErrorKind::Conflict,
1233 kcode_session_history::ErrorKind::Storage => ErrorKind::Internal,
1234 };
1235 Error::new(kind, error.message)
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use std::path::{Path, PathBuf};
1241
1242 use super::*;
1243
1244 struct TestRoot(PathBuf);
1245
1246 impl TestRoot {
1247 fn new() -> Self {
1248 let path = std::env::temp_dir().join(format!(
1249 "kcode-audio-session-ingress-test-{}",
1250 Uuid::new_v4()
1251 ));
1252 std::fs::create_dir(&path).unwrap();
1253 Self(path)
1254 }
1255
1256 fn path(&self) -> &Path {
1257 &self.0
1258 }
1259 }
1260
1261 impl Drop for TestRoot {
1262 fn drop(&mut self) {
1263 let _ = std::fs::remove_dir_all(&self.0);
1264 }
1265 }
1266
1267 fn history(root: &Path) -> SessionHistory {
1268 SessionHistory::open(kcode_session_history::Config {
1269 directory: root.join("active"),
1270 completed_list: root.join("completed.txt"),
1271 })
1272 .unwrap()
1273 }
1274
1275 fn completed_recording(transcript: impl Into<String>) -> RecordingStatus {
1276 let now = Utc::now();
1277 RecordingStatus {
1278 id: Uuid::new_v4(),
1279 user_id: "user".into(),
1280 sha256: "0".repeat(64),
1281 original_filename: "meeting.final.WAV".into(),
1282 size_bytes: 42,
1283 recorded_at: now,
1284 received_at: now,
1285 transcription_model: "transcription-model".into(),
1286 reconciliation_model: "reconciliation-model".into(),
1287 reconciliation_reasoning: "xhigh".into(),
1288 state: RecordingState::Complete {
1289 transcript: transcript.into(),
1290 },
1291 correction_packet: None,
1292 }
1293 }
1294
1295 fn with_speaker_packet(
1296 mut recording: RecordingStatus,
1297 state: ConfirmationState,
1298 confirmed_name: Option<&str>,
1299 ) -> RecordingStatus {
1300 recording.correction_packet = Some(CorrectionPacket {
1301 recording_id: recording.id,
1302 user_id: recording.user_id.clone(),
1303 sha256: recording.sha256.clone(),
1304 original_filename: recording.original_filename.clone(),
1305 size_bytes: recording.size_bytes,
1306 recorded_at: recording.recorded_at,
1307 clean: true,
1308 chunk_count: 1,
1309 chunks: vec![kcode_audio_ingress::CorrectionChunk {
1310 chunk_index: 0,
1311 chunk_count: 1,
1312 audio_start_ms: 0,
1313 audio_end_ms: 1_000,
1314 raw_gemini_response: "raw".into(),
1315 parsed: kcode_audio_ingress::ParsedChunk {
1316 utterances: Vec::new(),
1317 notes: Vec::new(),
1318 clip_valid: true,
1319 clip_validity_reason: None,
1320 speakers: Vec::new(),
1321 },
1322 observations: vec![kcode_audio_ingress::CorrectionObservation {
1323 local_label: "Speaker A".into(),
1324 speaker_ordinal: 0,
1325 observation_key: kcode_audio_ingress::ObservationKey {
1326 object_id: format!(
1327 "kcode-audio-ingress/recording/{}/chunk/0",
1328 recording.id
1329 ),
1330 piece_index: 0,
1331 },
1332 candidate: Some(kcode_audio_ingress::CandidateMapping {
1333 full_name: "Classifier Candidate".into(),
1334 cost: 1.0,
1335 confidence: 3.0,
1336 runner_up_full_name: None,
1337 runner_up_cost: None,
1338 background_population_cost: 4.0,
1339 }),
1340 identified_full_name: Some("Classifier Candidate".into()),
1341 confirmed_full_name: confirmed_name.map(str::to_owned),
1342 }],
1343 clean: true,
1344 }],
1345 confirmation_state: state,
1346 });
1347 recording
1348 }
1349
1350 #[test]
1351 fn transcript_piece_limit_is_one_quarter_of_effective_context() {
1352 let effective_context_tokens = 400;
1353 let maximum_characters = maximum_piece_characters(effective_context_tokens).unwrap();
1354 let pieces = split_transcript(&"a".repeat(801), maximum_characters).unwrap();
1355 assert_eq!(maximum_characters, 400);
1356 assert_eq!(pieces.len(), 3);
1357 assert!(pieces.iter().all(|piece| {
1358 estimate_tokens(piece) <= effective_context_tokens / INGRESS_CONTEXT_DIVISOR
1359 }));
1360 }
1361
1362 #[test]
1363 fn transcript_splitting_prefers_a_late_paragraph_boundary() {
1364 let transcript = format!("{}\n\n{}", "a".repeat(250), "b".repeat(200));
1365 let pieces = split_transcript(&transcript, 400).unwrap();
1366 assert_eq!(pieces, vec!["a".repeat(250), "b".repeat(200)]);
1367 }
1368
1369 #[test]
1370 fn transcript_splitting_counts_unicode_scalars_not_bytes() {
1371 let transcript = "😀".repeat(5);
1372 let pieces = split_transcript(&transcript, 2).unwrap();
1373 assert_eq!(pieces.concat(), transcript);
1374 assert!(pieces.iter().all(|piece| piece.chars().count() <= 2));
1375 }
1376
1377 #[test]
1378 fn audio_piece_ids_are_stable_across_configuration() {
1379 let recording_id = Uuid::new_v4();
1380 assert_eq!(
1381 audio_piece_id(recording_id, 2),
1382 format!("audio:{recording_id}:2")
1383 );
1384 }
1385
1386 #[test]
1387 fn confirmed_label_retries_must_match_the_complete_bound_mapping() {
1388 let recording = with_speaker_packet(
1389 completed_recording("Transcript"),
1390 ConfirmationState::Confirmed,
1391 Some("Human Choice"),
1392 );
1393 let packet = recording.correction_packet.as_ref().unwrap();
1394 let observation_key = packet.chunks[0].observations[0].observation_key.clone();
1395 let matching = RecordingConfirmation {
1396 recording_id: recording.id,
1397 observations: vec![kcode_audio_ingress::ObservationConfirmation {
1398 observation_key: observation_key.clone(),
1399 confirmed_full_name: " Human Choice ".into(),
1400 }],
1401 };
1402 let conflicting = RecordingConfirmation {
1403 recording_id: recording.id,
1404 observations: vec![kcode_audio_ingress::ObservationConfirmation {
1405 observation_key,
1406 confirmed_full_name: "Different Choice".into(),
1407 }],
1408 };
1409
1410 assert!(confirmation_matches(packet, &matching));
1411 assert!(!confirmation_matches(packet, &conflicting));
1412 }
1413
1414 #[test]
1415 fn ingress_exposes_the_complete_file_and_segmentation_contract() {
1416 let recording = completed_recording("Transcript");
1417 let plan = new_segmentation_plan(&recording, 400, 400).unwrap();
1418 let spec = &plan.specs[0];
1419 let text = format_ingress_piece(&recording, spec).unwrap();
1420 let metadata = audio_piece_metadata(&recording, spec, &plan.parameters, &text);
1421 assert_eq!(metadata["kind"], "audio-transcript");
1422 assert_eq!(metadata["recordingId"], recording.id.to_string());
1423 assert_eq!(metadata["sha256"], recording.sha256);
1424 assert_eq!(metadata["originalFilename"], "meeting.final.WAV");
1425 assert_eq!(metadata["extension"], ".WAV");
1426 assert_eq!(metadata["mimeType"], "audio/wav");
1427 assert_eq!(metadata["sizeBytes"], 42);
1428 assert_eq!(
1429 metadata["sourceCreatedAt"],
1430 recording.recorded_at.to_rfc3339()
1431 );
1432 assert_eq!(metadata["pieceIndex"], 0);
1433 assert_eq!(metadata["pieceCount"], 1);
1434 assert_eq!(metadata["pieceCharacters"], 10);
1435 assert_eq!(metadata["segmentationVersion"], SEGMENTATION_VERSION);
1436 assert_eq!(metadata["effectiveContextTokens"], 400);
1437 assert_eq!(metadata["maximumPieceCharacters"], 400);
1438 assert_eq!(metadata["pieceTextSha256"], sha256_text("Transcript"));
1439 assert_eq!(metadata["ingressTextSha256"], sha256_text(&text));
1440 assert_eq!(metadata["speakerConfirmationState"], Value::Null);
1441 assert!(text.contains("Original filename: meeting.final.WAV"));
1442 assert!(text.contains("Extension: .WAV"));
1443 assert!(text.contains("MIME type: audio/wav"));
1444 assert!(text.contains("Size: 42 bytes"));
1445 }
1446
1447 #[test]
1448 fn reads_report_only_existing_pieces_before_worker_synchronization() {
1449 let recording = completed_recording("a".repeat(801));
1450 let projection = ingress_projection(&recording, &[], 400, 400).unwrap();
1451 assert!(projection.pieces.is_empty());
1452 let combined = Recording::from_status(recording, &projection);
1453 assert_eq!(combined.status, "ready_for_ingress");
1454 assert_eq!(combined.attempt_count, 0);
1455 assert_eq!(combined.transcript_piece_count, 0);
1456 assert_eq!(combined.completed_piece_count, 0);
1457 }
1458
1459 #[tokio::test]
1460 async fn classifier_aware_recordings_wait_for_exact_human_review() {
1461 let root = TestRoot::new();
1462 let history = history(root.path());
1463 let recording = with_speaker_packet(
1464 completed_recording("Transcript"),
1465 ConfirmationState::AutomaticallyTrained,
1466 None,
1467 );
1468
1469 let combined = Recording::from_status(recording.clone(), &IngressProjection::default());
1470 assert_eq!(combined.status, "speaker_review");
1471 assert_eq!(combined.speaker_review.unwrap().observation_count, 1);
1472
1473 synchronize_recordings(&history, &[recording], 400, 400)
1474 .await
1475 .unwrap();
1476 assert!(history.list().await.unwrap().is_empty());
1477 }
1478
1479 #[tokio::test]
1480 async fn confirmed_labels_authorize_ingress_and_accompany_the_transcript() {
1481 let root = TestRoot::new();
1482 let history = history(root.path());
1483 let recording = with_speaker_packet(
1484 completed_recording("Transcript"),
1485 ConfirmationState::Confirmed,
1486 Some("Human Choice"),
1487 );
1488
1489 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1490 .await
1491 .unwrap();
1492 let records = history.list().await.unwrap();
1493 assert_eq!(records.len(), 1);
1494 assert_eq!(
1495 records[0]
1496 .state
1497 .pointer("/ingressSource/metadata/speakerConfirmationState")
1498 .and_then(Value::as_str),
1499 Some("confirmed")
1500 );
1501 let state = serde_json::to_string(&records[0].state).unwrap();
1502 assert!(state.contains("Human-confirmed speaker-label data"));
1503 assert!(state.contains("Human Choice"));
1504 assert!(state.contains("Classifier Candidate"));
1505 }
1506
1507 #[tokio::test]
1508 async fn synchronization_is_idempotent_and_uses_quarter_window_pieces() {
1509 let root = TestRoot::new();
1510 let history = history(root.path());
1511 let recording = completed_recording("a".repeat(801));
1512
1513 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1514 .await
1515 .unwrap();
1516 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1517 .await
1518 .unwrap();
1519
1520 let records = history.list().await.unwrap();
1521 assert_eq!(records.len(), 3);
1522 let ids = records
1523 .iter()
1524 .filter_map(ingress_source_id)
1525 .collect::<HashSet<_>>();
1526 assert_eq!(ids.len(), 3);
1527 assert!(ids.contains(format!("audio:{}:0", recording.id).as_str()));
1528 assert!(ids.contains(format!("audio:{}:1", recording.id).as_str()));
1529 assert!(ids.contains(format!("audio:{}:2", recording.id).as_str()));
1530 assert!(records.iter().all(|record| {
1531 record
1532 .state
1533 .pointer("/ingressSource/metadata/pieceCount")
1534 .and_then(Value::as_u64)
1535 == Some(3)
1536 && record
1537 .state
1538 .pointer("/ingressSource/metadata/segmentationVersion")
1539 .and_then(Value::as_u64)
1540 == Some(SEGMENTATION_VERSION)
1541 }));
1542 }
1543
1544 #[tokio::test]
1545 async fn ambiguous_legacy_metadata_is_rejected_instead_of_remapping_identity() {
1546 let root = TestRoot::new();
1547 let history = history(root.path());
1548 let recording = completed_recording("Transcript");
1549 let id = audio_piece_id(recording.id, 0);
1550 history
1551 .enqueue_ingress(NewIngressSession {
1552 idempotency_id: id,
1553 started_at: recording.recorded_at.to_rfc3339(),
1554 source_session_type: "audio".into(),
1555 kind: SessionKind::AudioIngress,
1556 effective_context_tokens: 400,
1557 text: "Previously ingressed".into(),
1558 metadata: json!({
1559 "kind":"audio-transcript",
1560 "recordingId":recording.id.to_string(),
1561 "sha256":recording.sha256,
1562 "originalFilename":recording.original_filename,
1563 "sizeBytes":recording.size_bytes,
1564 "sourceCreatedAt":recording.recorded_at.to_rfc3339(),
1565 "pieceIndex":0,
1566 "pieceCount":1,
1567 }),
1568 })
1569 .await
1570 .unwrap();
1571
1572 let error = synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1573 .await
1574 .unwrap_err();
1575 assert_eq!(error.kind(), ErrorKind::Conflict);
1576 assert_eq!(history.list().await.unwrap().len(), 1);
1577 }
1578
1579 #[tokio::test]
1580 async fn context_changes_preserve_authoritative_piece_text_counts_and_ids() {
1581 let root = TestRoot::new();
1582 let history = history(root.path());
1583 let recording = completed_recording("a".repeat(801));
1584
1585 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1586 .await
1587 .unwrap();
1588 let before_records = history.list().await.unwrap();
1589 let before = ingress_projection(&recording, &before_records, 400, 400)
1590 .unwrap()
1591 .pieces
1592 .into_iter()
1593 .map(|piece| (piece.piece_index, piece.piece_count, piece.transcript_text))
1594 .collect::<Vec<_>>();
1595 let before_ids = before_records
1596 .iter()
1597 .filter_map(ingress_source_id)
1598 .map(str::to_owned)
1599 .collect::<HashSet<_>>();
1600 assert_eq!(before.len(), 3);
1601
1602 synchronize_recordings(&history, std::slice::from_ref(&recording), 800, 800)
1603 .await
1604 .unwrap();
1605 let after_records = history.list().await.unwrap();
1606 let after = ingress_projection(&recording, &after_records, 800, 800)
1607 .unwrap()
1608 .pieces
1609 .into_iter()
1610 .map(|piece| (piece.piece_index, piece.piece_count, piece.transcript_text))
1611 .collect::<Vec<_>>();
1612 let after_ids = after_records
1613 .iter()
1614 .filter_map(ingress_source_id)
1615 .map(str::to_owned)
1616 .collect::<HashSet<_>>();
1617
1618 assert_eq!(after, before);
1619 assert_eq!(after_ids, before_ids);
1620 assert_eq!(after_records.len(), 3);
1621 assert!(after_records.iter().all(|record| {
1622 record
1623 .state
1624 .pointer("/ingressSource/metadata/effectiveContextTokens")
1625 .and_then(Value::as_u64)
1626 == Some(400)
1627 }));
1628 }
1629
1630 #[tokio::test]
1631 async fn partial_synchronization_uses_the_original_authoritative_segmentation() {
1632 let root = TestRoot::new();
1633 let history = history(root.path());
1634 let recording = completed_recording("a".repeat(801));
1635 let plan = new_segmentation_plan(&recording, 400, 400).unwrap();
1636 let first = &plan.specs[0];
1637 let first_text = format_ingress_piece(&recording, first).unwrap();
1638 history
1639 .enqueue_ingress(NewIngressSession {
1640 idempotency_id: first.id.clone(),
1641 started_at: recording.recorded_at.to_rfc3339(),
1642 source_session_type: "audio".into(),
1643 kind: SessionKind::AudioIngress,
1644 effective_context_tokens: plan.parameters.effective_context_tokens,
1645 text: first_text.clone(),
1646 metadata: audio_piece_metadata(&recording, first, &plan.parameters, &first_text),
1647 })
1648 .await
1649 .unwrap();
1650
1651 synchronize_recordings(&history, std::slice::from_ref(&recording), 800, 800)
1652 .await
1653 .unwrap();
1654
1655 let records = history.list().await.unwrap();
1656 let projection = ingress_projection(&recording, &records, 800, 800).unwrap();
1657 assert_eq!(projection.pieces.len(), 3);
1658 assert_eq!(
1659 projection
1660 .pieces
1661 .iter()
1662 .map(|piece| piece.transcript_text.clone())
1663 .collect::<Vec<_>>(),
1664 plan.specs
1665 .iter()
1666 .map(|spec| spec.text.clone())
1667 .collect::<Vec<_>>()
1668 );
1669 assert!(records.iter().all(|record| {
1670 record
1671 .state
1672 .pointer("/ingressSource/metadata/maximumPieceCharacters")
1673 .and_then(Value::as_u64)
1674 == Some(400)
1675 && record
1676 .state
1677 .pointer("/ingressSource/metadata/pieceCount")
1678 .and_then(Value::as_u64)
1679 == Some(3)
1680 }));
1681 }
1682
1683 #[tokio::test]
1684 async fn changed_transcript_conflicts_with_persisted_piece_identity() {
1685 let root = TestRoot::new();
1686 let history = history(root.path());
1687 let recording = completed_recording("a".repeat(801));
1688 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1689 .await
1690 .unwrap();
1691
1692 let mut changed = recording.clone();
1693 changed.state = RecordingState::Complete {
1694 transcript: format!("{}b", "a".repeat(800)),
1695 };
1696 let error = synchronize_recordings(&history, std::slice::from_ref(&changed), 400, 400)
1697 .await
1698 .unwrap_err();
1699
1700 assert_eq!(error.kind(), ErrorKind::Conflict);
1701 assert_eq!(history.list().await.unwrap().len(), 3);
1702 }
1703
1704 #[test]
1705 fn complete_recording_projection_preserves_pre_extraction_fields() {
1706 let recording = completed_recording("Transcript");
1707 let received_at = recording.received_at.to_rfc3339();
1708 let combined = Recording::from_status(recording, &IngressProjection::default());
1709
1710 assert_eq!(combined.received_at, received_at);
1711 assert_eq!(combined.updated_at, received_at);
1712 assert_eq!(combined.status, "ready_for_ingress");
1713 assert!(combined.transcription_status.is_none());
1714 assert_eq!(combined.attempt_count, 0);
1715 assert!(combined.next_attempt_at.is_none());
1716 assert!(combined.last_error.is_none());
1717 assert_eq!(combined.transcript_piece_count, 0);
1718 }
1719}