1#![forbid(unsafe_code)]
2
3use std::collections::HashSet;
4
5use chrono::{DateTime, Utc};
6use kcode_audio_ingress::{
7 AudioIngress, AudioInput, ErrorKind as AudioErrorKind, RecordingState, RecordingStatus,
8};
9use kcode_session_history::{
10 NewIngressSession, RetryIngress as HistoryRetryIngress, SessionHistory, SessionRecord,
11 chatend::SessionKind,
12};
13use serde_json::{Value, json};
14use uuid::Uuid;
15
16const ESTIMATED_CHARACTERS_PER_TOKEN: u64 = 4;
17const INGRESS_CONTEXT_DIVISOR: u64 = 4;
18const SEGMENTATION_VERSION: u64 = 1;
19
20#[derive(Clone, Debug)]
22pub struct Config {
23 pub user_id: String,
25 pub effective_context_tokens: u64,
27}
28
29#[derive(Clone, Debug)]
31pub struct RecordingInput {
32 pub bytes: Vec<u8>,
34 pub recorded_at: DateTime<Utc>,
36 pub original_filename: Option<String>,
38}
39
40#[derive(Clone, Debug)]
42pub struct RecordingSubmission {
43 pub recording: Recording,
45 pub deduplicated: bool,
47}
48
49#[derive(Clone, Debug)]
51pub struct Recording {
52 pub id: Uuid,
53 pub sha256: String,
54 pub original_filename: String,
55 pub content_type: &'static str,
56 pub size_bytes: u64,
57 pub source_created_at: String,
58 pub received_at: String,
59 pub status: String,
60 pub transcription_model: String,
61 pub reconciliation_model: String,
62 pub reconciliation_reasoning: String,
63 pub attempt_count: Option<u8>,
65 pub transcript_piece_count: usize,
67 pub completed_piece_count: usize,
69}
70
71#[derive(Clone, Debug)]
73pub struct IngressPiece {
74 pub id: String,
75 pub recording_id: Uuid,
76 pub sha256: String,
77 pub original_filename: String,
78 pub source_created_at: String,
79 pub piece_index: u32,
80 pub piece_count: u32,
81 pub transcript_text: String,
82 pub estimated_tokens: u64,
83 pub phase: String,
84 pub provenance_id: Option<String>,
85 pub state: Value,
86 pub version: i64,
87 pub ingress_failure_count: i64,
88 pub ingress_failures: Value,
89 pub created_at: String,
90 pub updated_at: String,
91}
92
93#[derive(Clone, Debug)]
95pub struct RecordingHistory {
96 pub recording: Recording,
97 pub final_transcript: Option<String>,
98 pub pieces: Vec<IngressPiece>,
99}
100
101#[derive(Clone, Debug)]
103pub struct RetryIngress {
104 pub piece_id: String,
105 pub expected_version: i64,
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum ErrorKind {
111 InvalidInput,
112 NotFound,
113 Conflict,
114 Internal,
115}
116
117#[derive(Debug)]
119pub struct Error {
120 kind: ErrorKind,
121 message: String,
122}
123
124impl Error {
125 fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
126 Self {
127 kind,
128 message: message.into(),
129 }
130 }
131
132 fn invalid(message: impl Into<String>) -> Self {
133 Self::new(ErrorKind::InvalidInput, message)
134 }
135
136 fn not_found() -> Self {
137 Self::new(
138 ErrorKind::NotFound,
139 "Audio recording or transcript piece not found.",
140 )
141 }
142
143 fn conflict(message: impl Into<String>) -> Self {
144 Self::new(ErrorKind::Conflict, message)
145 }
146
147 fn internal(error: impl std::fmt::Display) -> Self {
148 tracing::warn!(%error, "Audio session ingress operation failed");
149 Self::new(
150 ErrorKind::Internal,
151 "An unexpected audio session ingress error occurred.",
152 )
153 }
154
155 pub fn kind(&self) -> ErrorKind {
157 self.kind
158 }
159
160 pub fn message(&self) -> &str {
162 &self.message
163 }
164}
165
166impl std::fmt::Display for Error {
167 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 formatter.write_str(&self.message)
169 }
170}
171
172impl std::error::Error for Error {}
173
174#[derive(Clone)]
176pub struct Coordinator {
177 audio: AudioIngress,
178 history: SessionHistory,
179 user_id: String,
180 effective_context_tokens: u64,
181 maximum_piece_characters: usize,
182}
183
184impl Coordinator {
185 pub fn new(
187 audio: AudioIngress,
188 history: SessionHistory,
189 config: Config,
190 ) -> Result<Self, Error> {
191 if config.user_id.trim().is_empty() {
192 return Err(Error::invalid("audio user ID must not be empty"));
193 }
194 let maximum_piece_characters = maximum_piece_characters(config.effective_context_tokens)?;
195 Ok(Self {
196 audio,
197 history,
198 user_id: config.user_id,
199 effective_context_tokens: config.effective_context_tokens,
200 maximum_piece_characters,
201 })
202 }
203
204 pub fn health(&self) -> Result<(), Error> {
206 self.audio.status().map_err(audio_error)?;
207 self.history.health().map_err(history_error)?;
208 Ok(())
209 }
210
211 pub async fn submit(&self, input: RecordingInput) -> Result<RecordingSubmission, Error> {
213 let submission = self
214 .audio
215 .submit(AudioInput {
216 user_id: self.user_id.clone(),
217 bytes: input.bytes,
218 recorded_at: input.recorded_at,
219 original_filename: input.original_filename,
220 })
221 .await
222 .map_err(audio_error)?;
223 let recording_status = self
224 .audio
225 .status()
226 .map_err(audio_error)?
227 .recordings
228 .into_iter()
229 .find(|recording| recording.id == submission.recording_id)
230 .ok_or_else(Error::not_found)?;
231 if recording_status.user_id != self.user_id {
232 return Err(Error::conflict(
233 "The submitted audio already belongs to a different configured user.",
234 ));
235 }
236 let histories = self.history.list().await.map_err(history_error)?;
237 let projection = ingress_projection(
238 &recording_status,
239 &histories,
240 self.effective_context_tokens,
241 self.maximum_piece_characters,
242 )?;
243 let recording = Recording::from_status(recording_status, &projection);
244 Ok(RecordingSubmission {
245 recording,
246 deduplicated: submission.deduplicated,
247 })
248 }
249
250 pub async fn recordings(&self) -> Result<Vec<Recording>, Error> {
252 let histories = self.history.list().await.map_err(history_error)?;
253 self.audio
254 .status()
255 .map_err(audio_error)?
256 .recordings
257 .into_iter()
258 .filter(|recording| recording.user_id == self.user_id)
259 .map(|recording| {
260 let projection = ingress_projection(
261 &recording,
262 &histories,
263 self.effective_context_tokens,
264 self.maximum_piece_characters,
265 )?;
266 Ok(Recording::from_status(recording, &projection))
267 })
268 .collect()
269 }
270
271 pub async fn recording_by_sha256(&self, sha256: &str) -> Result<Recording, Error> {
273 if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
274 return Err(Error::invalid(
275 "audio SHA-256 must contain exactly 64 hexadecimal characters",
276 ));
277 }
278 let normalized = sha256.to_ascii_lowercase();
279 self.recordings()
280 .await?
281 .into_iter()
282 .find(|recording| recording.sha256 == normalized)
283 .ok_or_else(Error::not_found)
284 }
285
286 pub async fn recording_history(&self, recording_id: Uuid) -> Result<RecordingHistory, Error> {
288 let recording_status = self
289 .audio
290 .status()
291 .map_err(audio_error)?
292 .recordings
293 .into_iter()
294 .find(|recording| recording.id == recording_id && recording.user_id == self.user_id)
295 .ok_or_else(Error::not_found)?;
296 let histories = self.history.list().await.map_err(history_error)?;
297 let projection = ingress_projection(
298 &recording_status,
299 &histories,
300 self.effective_context_tokens,
301 self.maximum_piece_characters,
302 )?;
303 let final_transcript = match &recording_status.state {
304 RecordingState::Complete { transcript } => Some(transcript.clone()),
305 _ => None,
306 };
307 let recording = Recording::from_status(recording_status, &projection);
308 Ok(RecordingHistory {
309 recording,
310 final_transcript,
311 pieces: projection.pieces,
312 })
313 }
314
315 pub fn retry_recording(&self, recording_id: Uuid) -> Result<(), Error> {
317 let owned = self
318 .audio
319 .status()
320 .map_err(audio_error)?
321 .recordings
322 .into_iter()
323 .any(|recording| recording.id == recording_id && recording.user_id == self.user_id);
324 if !owned {
325 return Err(Error::not_found());
326 }
327 self.audio.retry(recording_id).map_err(audio_error)
328 }
329
330 pub async fn retry_ingress(&self, input: RetryIngress) -> Result<SessionRecord, Error> {
332 let current = self
333 .history
334 .get(&input.piece_id)
335 .await
336 .map_err(history_error)?;
337 let recordings = self
338 .audio
339 .status()
340 .map_err(audio_error)?
341 .recordings
342 .into_iter()
343 .filter(|recording| recording.user_id == self.user_id)
344 .collect::<Vec<_>>();
345 validate_retry_target(
346 ¤t,
347 &recordings,
348 self.effective_context_tokens,
349 self.maximum_piece_characters,
350 )?;
351 self.history
352 .retry_ingress(
353 &input.piece_id,
354 HistoryRetryIngress {
355 expected_version: input.expected_version,
356 state: current.state,
357 },
358 )
359 .await
360 .map_err(history_error)
361 }
362
363 pub async fn synchronize_completed_transcripts(&self) -> Result<(), Error> {
365 let recordings = self
366 .audio
367 .status()
368 .map_err(audio_error)?
369 .recordings
370 .into_iter()
371 .filter(|recording| recording.user_id == self.user_id)
372 .collect::<Vec<_>>();
373 synchronize_recordings(
374 &self.history,
375 &recordings,
376 self.effective_context_tokens,
377 self.maximum_piece_characters,
378 )
379 .await
380 }
381}
382
383#[derive(Debug)]
384struct PieceSpec {
385 id: String,
386 index: u32,
387 count: u32,
388 text: String,
389 fingerprint: String,
390}
391
392#[derive(Debug, Default)]
393struct IngressProjection {
394 expected_piece_count: usize,
395 pieces: Vec<IngressPiece>,
396}
397
398impl Recording {
399 fn from_status(recording: RecordingStatus, projection: &IngressProjection) -> Self {
400 let (mut status, attempt_count) = match recording.state {
401 RecordingState::Queued => ("uploaded".into(), Some(0)),
402 RecordingState::Processing { attempt, progress } => {
403 (processing_stage(&progress).into(), Some(attempt))
404 }
405 RecordingState::Complete { .. } => ("ready_for_ingress".into(), None),
406 RecordingState::Failed { attempts, .. } => ("failed".into(), Some(attempts)),
407 };
408 if projection.expected_piece_count != 0 {
409 status = if projection
410 .pieces
411 .iter()
412 .any(|piece| piece.phase == "ingress_failed")
413 {
414 "ingress_failed".into()
415 } else if projection
416 .pieces
417 .iter()
418 .any(|piece| piece.phase == "ingress_in_progress")
419 {
420 "ingressing".into()
421 } else if projection.pieces.len() == projection.expected_piece_count
422 && projection
423 .pieces
424 .iter()
425 .all(|piece| piece.phase == "complete")
426 {
427 "complete".into()
428 } else {
429 "ready_for_ingress".into()
430 };
431 }
432 let completed_piece_count = projection
433 .pieces
434 .iter()
435 .filter(|piece| piece.phase == "complete")
436 .count();
437 Self {
438 id: recording.id,
439 sha256: recording.sha256,
440 original_filename: recording.original_filename,
441 content_type: "audio/wav",
442 size_bytes: recording.size_bytes,
443 source_created_at: recording.recorded_at.to_rfc3339(),
444 received_at: recording.received_at.to_rfc3339(),
445 status,
446 transcription_model: recording.transcription_model,
447 reconciliation_model: recording.reconciliation_model,
448 reconciliation_reasoning: recording.reconciliation_reasoning,
449 attempt_count,
450 transcript_piece_count: projection.expected_piece_count,
451 completed_piece_count,
452 }
453 }
454}
455
456impl IngressPiece {
457 fn from_record(recording: &RecordingStatus, spec: &PieceSpec, record: &SessionRecord) -> Self {
458 Self {
459 id: record.id.clone(),
460 recording_id: recording.id,
461 sha256: recording.sha256.clone(),
462 original_filename: recording.original_filename.clone(),
463 source_created_at: recording.recorded_at.to_rfc3339(),
464 piece_index: spec.index,
465 piece_count: spec.count,
466 estimated_tokens: estimate_tokens(&spec.text),
467 transcript_text: spec.text.clone(),
468 phase: record.phase.clone(),
469 provenance_id: record.provenance_id.clone(),
470 state: record.state.clone(),
471 version: record.version,
472 ingress_failure_count: record.ingress_failure_count,
473 ingress_failures: record.ingress_failures.clone(),
474 created_at: record.started_at.clone(),
475 updated_at: record.updated_at.clone(),
476 }
477 }
478}
479
480async fn synchronize_recordings(
481 history: &SessionHistory,
482 recordings: &[RecordingStatus],
483 effective_context_tokens: u64,
484 maximum_piece_characters: usize,
485) -> Result<(), Error> {
486 let histories = history.list().await.map_err(history_error)?;
487 for recording in recordings {
488 if !matches!(&recording.state, RecordingState::Complete { .. }) {
489 continue;
490 }
491 let specs = transcript_piece_specs(recording, maximum_piece_characters)?;
492 validate_existing_piece_records(
493 recording,
494 &histories,
495 &specs,
496 effective_context_tokens,
497 maximum_piece_characters,
498 )?;
499 let mut existing = histories
500 .iter()
501 .filter_map(ingress_source_id)
502 .map(str::to_owned)
503 .collect::<HashSet<_>>();
504 for spec in specs {
505 if existing.contains(&spec.id) {
506 continue;
507 }
508 let created = history
509 .enqueue_ingress(NewIngressSession {
510 idempotency_id: spec.id.clone(),
511 started_at: recording.recorded_at.to_rfc3339(),
512 source_session_type: "audio".into(),
513 kind: SessionKind::AudioIngress,
514 effective_context_tokens,
515 text: format_ingress_piece(recording, &spec),
516 metadata: audio_piece_metadata(
517 recording,
518 &spec,
519 effective_context_tokens,
520 maximum_piece_characters,
521 ),
522 })
523 .await
524 .map_err(history_error)?;
525 validate_piece_record(
526 &created.value,
527 recording,
528 &spec,
529 effective_context_tokens,
530 maximum_piece_characters,
531 )?;
532 existing.insert(spec.id);
533 }
534 }
535 Ok(())
536}
537
538fn ingress_projection(
539 recording: &RecordingStatus,
540 histories: &[SessionRecord],
541 effective_context_tokens: u64,
542 maximum_piece_characters: usize,
543) -> Result<IngressProjection, Error> {
544 if !matches!(&recording.state, RecordingState::Complete { .. }) {
545 return Ok(IngressProjection::default());
546 }
547 let specs = transcript_piece_specs(recording, maximum_piece_characters)?;
548 validate_existing_piece_records(
549 recording,
550 histories,
551 &specs,
552 effective_context_tokens,
553 maximum_piece_characters,
554 )?;
555 let mut pieces = Vec::with_capacity(specs.len());
556 for spec in &specs {
557 let Some(record) = histories
558 .iter()
559 .find(|record| ingress_source_id(record) == Some(spec.id.as_str()))
560 else {
561 continue;
562 };
563 pieces.push(IngressPiece::from_record(recording, spec, record));
564 }
565 Ok(IngressProjection {
566 expected_piece_count: specs.len(),
567 pieces,
568 })
569}
570
571fn transcript_piece_specs(
572 recording: &RecordingStatus,
573 maximum_piece_characters: usize,
574) -> Result<Vec<PieceSpec>, Error> {
575 let RecordingState::Complete { transcript } = &recording.state else {
576 return Ok(Vec::new());
577 };
578 let pieces = split_transcript(transcript, maximum_piece_characters)?;
579 let piece_count = u32::try_from(pieces.len())
580 .map_err(|_| Error::internal("audio transcript contains too many pieces"))?;
581 pieces
582 .into_iter()
583 .enumerate()
584 .map(|(index, text)| {
585 let piece_index = u32::try_from(index)
586 .map_err(|_| Error::internal("audio transcript piece index exceeds u32"))?;
587 Ok(PieceSpec {
588 id: audio_piece_id(recording.id, piece_index),
589 index: piece_index,
590 count: piece_count,
591 fingerprint: piece_fingerprint(&text),
592 text,
593 })
594 })
595 .collect()
596}
597
598fn validate_existing_piece_records(
599 recording: &RecordingStatus,
600 histories: &[SessionRecord],
601 specs: &[PieceSpec],
602 effective_context_tokens: u64,
603 maximum_piece_characters: usize,
604) -> Result<(), Error> {
605 for record in histories {
606 if let Some(spec) = ingress_source_id(record)
607 .and_then(|source_id| specs.iter().find(|spec| spec.id == source_id))
608 {
609 validate_piece_record(
610 record,
611 recording,
612 spec,
613 effective_context_tokens,
614 maximum_piece_characters,
615 )?;
616 } else if metadata_recording_id(record) == Some(recording.id) {
617 return Err(segmentation_conflict());
618 }
619 }
620 Ok(())
621}
622
623fn validate_retry_target(
624 record: &SessionRecord,
625 recordings: &[RecordingStatus],
626 effective_context_tokens: u64,
627 maximum_piece_characters: usize,
628) -> Result<(), Error> {
629 let Some(source_id) = ingress_source_id(record) else {
630 return Err(Error::not_found());
631 };
632 for recording in recordings {
633 if !matches!(&recording.state, RecordingState::Complete { .. }) {
634 continue;
635 }
636 let specs = transcript_piece_specs(recording, maximum_piece_characters)?;
637 if let Some(spec) = specs.iter().find(|spec| spec.id == source_id) {
638 return validate_piece_record(
639 record,
640 recording,
641 spec,
642 effective_context_tokens,
643 maximum_piece_characters,
644 );
645 }
646 if metadata_recording_id(record) == Some(recording.id) {
647 return Err(segmentation_conflict());
648 }
649 }
650 Err(Error::not_found())
651}
652
653fn validate_piece_record(
654 record: &SessionRecord,
655 recording: &RecordingStatus,
656 spec: &PieceSpec,
657 effective_context_tokens: u64,
658 maximum_piece_characters: usize,
659) -> Result<(), Error> {
660 let Some(metadata) = ingress_metadata(record) else {
661 return Err(segmentation_conflict());
662 };
663 let maximum_piece_characters = u64::try_from(maximum_piece_characters)
664 .map_err(|_| Error::internal("piece limit exceeds u64"))?;
665 let piece_characters = u64::try_from(spec.text.chars().count())
666 .map_err(|_| Error::internal("audio transcript piece exceeds u64 characters"))?;
667 let recording_id = recording.id.to_string();
668 let valid = ingress_source_id(record) == Some(spec.id.as_str())
669 && metadata.get("kind").and_then(Value::as_str) == Some("audio-transcript")
670 && metadata.get("recordingId").and_then(Value::as_str) == Some(recording_id.as_str())
671 && metadata.get("sha256").and_then(Value::as_str) == Some(recording.sha256.as_str())
672 && metadata.get("segmentationVersion").and_then(Value::as_u64)
673 == Some(SEGMENTATION_VERSION)
674 && metadata
675 .get("effectiveContextTokens")
676 .and_then(Value::as_u64)
677 == Some(effective_context_tokens)
678 && metadata
679 .get("maximumPieceCharacters")
680 .and_then(Value::as_u64)
681 == Some(maximum_piece_characters)
682 && metadata.get("pieceIndex").and_then(Value::as_u64) == Some(u64::from(spec.index))
683 && metadata.get("pieceCount").and_then(Value::as_u64) == Some(u64::from(spec.count))
684 && metadata.get("pieceCharacters").and_then(Value::as_u64) == Some(piece_characters)
685 && metadata.get("pieceFingerprint").and_then(Value::as_str)
686 == Some(spec.fingerprint.as_str());
687 if valid {
688 Ok(())
689 } else {
690 Err(segmentation_conflict())
691 }
692}
693
694fn segmentation_conflict() -> Error {
695 Error::conflict(
696 "Existing Session History audio pieces do not match the configured transcript segmentation.",
697 )
698}
699
700fn ingress_source_id(record: &SessionRecord) -> Option<&str> {
701 record
702 .state
703 .pointer("/ingressSource/idempotencyId")
704 .and_then(Value::as_str)
705}
706
707fn ingress_metadata(record: &SessionRecord) -> Option<&Value> {
708 record.state.pointer("/ingressSource/metadata")
709}
710
711fn metadata_recording_id(record: &SessionRecord) -> Option<Uuid> {
712 let metadata = ingress_metadata(record)?;
713 if metadata.get("kind").and_then(Value::as_str) != Some("audio-transcript") {
714 return None;
715 }
716 metadata
717 .get("recordingId")
718 .and_then(Value::as_str)
719 .and_then(|value| Uuid::parse_str(value).ok())
720}
721
722fn audio_piece_id(recording_id: Uuid, piece_index: u32) -> String {
723 format!("audio:{recording_id}:{piece_index}")
724}
725
726fn audio_piece_metadata(
727 recording: &RecordingStatus,
728 spec: &PieceSpec,
729 effective_context_tokens: u64,
730 maximum_piece_characters: usize,
731) -> Value {
732 json!({
733 "kind":"audio-transcript",
734 "recordingId":recording.id.to_string(),
735 "sha256":recording.sha256,
736 "originalFilename":recording.original_filename,
737 "extension":file_name_extension(&recording.original_filename),
738 "mimeType":"audio/wav",
739 "sizeBytes":recording.size_bytes,
740 "sourceCreatedAt":recording.recorded_at.to_rfc3339(),
741 "segmentationVersion":SEGMENTATION_VERSION,
742 "effectiveContextTokens":effective_context_tokens,
743 "maximumPieceCharacters":maximum_piece_characters,
744 "pieceIndex":spec.index,
745 "pieceCount":spec.count,
746 "pieceCharacters":spec.text.chars().count(),
747 "pieceFingerprint":spec.fingerprint,
748 })
749}
750
751fn format_ingress_piece(recording: &RecordingStatus, spec: &PieceSpec) -> String {
752 format!(
753 "Vnote final transcript piece\n\nRecording began: {}\nRecording SHA-256: {}\nOriginal filename: {}\nExtension: {}\nMIME type: audio/wav\nSize: {} bytes\nTranscript piece: {} of {}\n\n{}",
754 recording.recorded_at.to_rfc3339(),
755 recording.sha256,
756 recording.original_filename,
757 file_name_extension(&recording.original_filename),
758 recording.size_bytes,
759 spec.index + 1,
760 spec.count,
761 spec.text,
762 )
763}
764
765fn piece_fingerprint(value: &str) -> String {
766 let mut hash = 0xcbf29ce484222325_u64;
767 for byte in value.as_bytes() {
768 hash ^= u64::from(*byte);
769 hash = hash.wrapping_mul(0x100000001b3);
770 }
771 format!("fnv1a64:{hash:016x}")
772}
773
774fn file_name_extension(file_name: &str) -> String {
775 file_name
776 .rsplit_once('.')
777 .and_then(|(stem, extension)| {
778 (!stem.is_empty() && !extension.is_empty()).then_some(extension)
779 })
780 .map(|extension| format!(".{extension}"))
781 .unwrap_or_else(|| "(none)".into())
782}
783
784fn processing_stage(status: &kcode_audio_ingress::TranscriptionStatus) -> &'static str {
785 let plan_complete = status.steps.iter().any(|entry| {
786 entry.step == kcode_audio_ingress::Step::PlanChunks
787 && entry.state == kcode_audio_ingress::StepState::Completed
788 });
789 if !plan_complete {
790 return "chunking";
791 }
792 let mut saw_chunk = false;
793 for entry in &status.steps {
794 if matches!(
795 entry.step,
796 kcode_audio_ingress::Step::TranscribeChunk { .. }
797 ) {
798 saw_chunk = true;
799 if entry.state != kcode_audio_ingress::StepState::Completed {
800 return "transcribing";
801 }
802 }
803 }
804 if saw_chunk {
805 "reconciling"
806 } else {
807 "transcribing"
808 }
809}
810
811fn maximum_piece_characters(effective_context_tokens: u64) -> Result<usize, Error> {
812 let piece_tokens = effective_context_tokens / INGRESS_CONTEXT_DIVISOR;
813 if piece_tokens == 0 {
814 return Err(Error::invalid(
815 "effective ingress context must contain at least four tokens",
816 ));
817 }
818 let characters = piece_tokens
819 .checked_mul(ESTIMATED_CHARACTERS_PER_TOKEN)
820 .ok_or_else(|| Error::invalid("effective ingress context is too large"))?;
821 usize::try_from(characters)
822 .map_err(|_| Error::invalid("effective ingress context exceeds platform limits"))
823}
824
825fn split_transcript(
826 transcript: &str,
827 maximum_piece_characters: usize,
828) -> Result<Vec<String>, Error> {
829 let mut remaining = transcript.trim();
830 if remaining.is_empty() {
831 return Err(Error::internal("completed audio transcript is empty"));
832 }
833 let mut pieces = Vec::new();
834 while remaining.chars().count() > maximum_piece_characters {
835 let cutoff = remaining
836 .char_indices()
837 .nth(maximum_piece_characters)
838 .map(|(index, _)| index)
839 .unwrap_or(remaining.len());
840 let prefix = &remaining[..cutoff];
841 let minimum = prefix
842 .char_indices()
843 .nth(maximum_piece_characters / 2)
844 .map(|(index, _)| index)
845 .unwrap_or(0);
846 let boundary = prefix
847 .rfind("\n\n")
848 .filter(|index| *index >= minimum)
849 .or_else(|| prefix.rfind('\n').filter(|index| *index >= minimum))
850 .unwrap_or(cutoff);
851 let piece = remaining[..boundary].trim();
852 if piece.is_empty() {
853 return Err(Error::internal("could not split audio transcript"));
854 }
855 pieces.push(piece.to_owned());
856 remaining = remaining[boundary..].trim();
857 }
858 if !remaining.is_empty() {
859 pieces.push(remaining.to_owned());
860 }
861 Ok(pieces)
862}
863
864fn estimate_tokens(value: &str) -> u64 {
865 (value.chars().count() as u64).div_ceil(ESTIMATED_CHARACTERS_PER_TOKEN)
866}
867
868fn audio_error(error: kcode_audio_ingress::Error) -> Error {
869 match error.kind() {
870 AudioErrorKind::InvalidInput => Error::new(ErrorKind::InvalidInput, error.to_string()),
871 AudioErrorKind::NotFound => Error::new(ErrorKind::NotFound, error.to_string()),
872 AudioErrorKind::Conflict => Error::new(ErrorKind::Conflict, error.to_string()),
873 AudioErrorKind::Internal => Error::internal(error),
874 }
875}
876
877fn history_error(error: kcode_session_history::Error) -> Error {
878 let kind = match error.kind {
879 kcode_session_history::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
880 kcode_session_history::ErrorKind::NotFound => ErrorKind::NotFound,
881 kcode_session_history::ErrorKind::Conflict => ErrorKind::Conflict,
882 kcode_session_history::ErrorKind::Storage => ErrorKind::Internal,
883 };
884 Error::new(kind, error.message)
885}
886
887#[cfg(test)]
888mod tests {
889 use std::path::{Path, PathBuf};
890
891 use super::*;
892
893 struct TestRoot(PathBuf);
894
895 impl TestRoot {
896 fn new() -> Self {
897 let path = std::env::temp_dir().join(format!(
898 "kcode-audio-session-ingress-test-{}",
899 Uuid::new_v4()
900 ));
901 std::fs::create_dir(&path).unwrap();
902 Self(path)
903 }
904
905 fn path(&self) -> &Path {
906 &self.0
907 }
908 }
909
910 impl Drop for TestRoot {
911 fn drop(&mut self) {
912 let _ = std::fs::remove_dir_all(&self.0);
913 }
914 }
915
916 fn history(root: &Path) -> SessionHistory {
917 SessionHistory::open(kcode_session_history::Config {
918 directory: root.join("active"),
919 completed_list: root.join("completed.txt"),
920 })
921 .unwrap()
922 }
923
924 fn completed_recording(transcript: impl Into<String>) -> RecordingStatus {
925 let now = Utc::now();
926 RecordingStatus {
927 id: Uuid::new_v4(),
928 user_id: "user".into(),
929 sha256: "0".repeat(64),
930 original_filename: "meeting.final.WAV".into(),
931 size_bytes: 42,
932 recorded_at: now,
933 received_at: now,
934 transcription_model: "transcription-model".into(),
935 reconciliation_model: "reconciliation-model".into(),
936 reconciliation_reasoning: "xhigh".into(),
937 state: RecordingState::Complete {
938 transcript: transcript.into(),
939 },
940 }
941 }
942
943 #[test]
944 fn transcript_piece_limit_is_one_quarter_of_effective_context() {
945 let effective_context_tokens = 400;
946 let maximum_characters = maximum_piece_characters(effective_context_tokens).unwrap();
947 let pieces = split_transcript(&"a".repeat(801), maximum_characters).unwrap();
948 assert_eq!(maximum_characters, 400);
949 assert_eq!(pieces.len(), 3);
950 assert!(pieces.iter().all(|piece| {
951 estimate_tokens(piece) <= effective_context_tokens / INGRESS_CONTEXT_DIVISOR
952 }));
953 }
954
955 #[test]
956 fn transcript_splitting_prefers_a_late_paragraph_boundary() {
957 let transcript = format!("{}\n\n{}", "a".repeat(250), "b".repeat(200));
958 let pieces = split_transcript(&transcript, 400).unwrap();
959 assert_eq!(pieces, vec!["a".repeat(250), "b".repeat(200)]);
960 }
961
962 #[test]
963 fn transcript_splitting_counts_unicode_scalars_not_bytes() {
964 let transcript = "😀".repeat(5);
965 let pieces = split_transcript(&transcript, 2).unwrap();
966 assert_eq!(pieces.concat(), transcript);
967 assert!(pieces.iter().all(|piece| piece.chars().count() <= 2));
968 }
969
970 #[test]
971 fn audio_piece_ids_are_stable_across_configuration() {
972 let recording_id = Uuid::new_v4();
973 assert_eq!(
974 audio_piece_id(recording_id, 2),
975 format!("audio:{recording_id}:2")
976 );
977 }
978
979 #[test]
980 fn ingress_exposes_the_complete_file_metadata_contract() {
981 let recording = completed_recording("Transcript");
982 let spec = transcript_piece_specs(&recording, 400).unwrap().remove(0);
983 let metadata = audio_piece_metadata(&recording, &spec, 400, 400);
984 assert_eq!(metadata["originalFilename"], "meeting.final.WAV");
985 assert_eq!(metadata["extension"], ".WAV");
986 assert_eq!(metadata["mimeType"], "audio/wav");
987 assert_eq!(metadata["sizeBytes"], 42);
988 assert_eq!(metadata["segmentationVersion"], SEGMENTATION_VERSION);
989 assert_eq!(metadata["effectiveContextTokens"], 400);
990 assert_eq!(metadata["maximumPieceCharacters"], 400);
991 assert_eq!(
992 metadata["pieceFingerprint"],
993 piece_fingerprint("Transcript")
994 );
995 let text = format_ingress_piece(&recording, &spec);
996 assert!(text.contains("Original filename: meeting.final.WAV"));
997 assert!(text.contains("Extension: .WAV"));
998 assert!(text.contains("MIME type: audio/wav"));
999 assert!(text.contains("Size: 42 bytes"));
1000 }
1001
1002 #[test]
1003 fn reads_report_expected_pieces_before_worker_synchronization() {
1004 let recording = completed_recording("a".repeat(801));
1005 let projection = ingress_projection(&recording, &[], 400, 400).unwrap();
1006 assert_eq!(projection.expected_piece_count, 3);
1007 assert!(projection.pieces.is_empty());
1008 let combined = Recording::from_status(recording, &projection);
1009 assert_eq!(combined.status, "ready_for_ingress");
1010 assert_eq!(combined.transcript_piece_count, 3);
1011 assert_eq!(combined.completed_piece_count, 0);
1012 }
1013
1014 #[tokio::test]
1015 async fn synchronization_is_idempotent_and_uses_quarter_window_pieces() {
1016 let root = TestRoot::new();
1017 let history = history(root.path());
1018 let recording = completed_recording("a".repeat(801));
1019
1020 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1021 .await
1022 .unwrap();
1023 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1024 .await
1025 .unwrap();
1026
1027 let records = history.list().await.unwrap();
1028 assert_eq!(records.len(), 3);
1029 let ids = records
1030 .iter()
1031 .filter_map(ingress_source_id)
1032 .collect::<HashSet<_>>();
1033 assert_eq!(ids.len(), 3);
1034 assert!(ids.contains(format!("audio:{}:0", recording.id).as_str()));
1035 assert!(ids.contains(format!("audio:{}:1", recording.id).as_str()));
1036 assert!(ids.contains(format!("audio:{}:2", recording.id).as_str()));
1037 assert!(records.iter().all(|record| {
1038 record
1039 .state
1040 .pointer("/ingressSource/metadata/pieceCount")
1041 .and_then(Value::as_u64)
1042 == Some(3)
1043 }));
1044 }
1045
1046 #[tokio::test]
1047 async fn changed_segmentation_conflicts_without_creating_new_work() {
1048 let root = TestRoot::new();
1049 let history = history(root.path());
1050 let recording = completed_recording("a".repeat(801));
1051
1052 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1053 .await
1054 .unwrap();
1055 let error = synchronize_recordings(&history, std::slice::from_ref(&recording), 800, 800)
1056 .await
1057 .unwrap_err();
1058
1059 assert_eq!(error.kind(), ErrorKind::Conflict);
1060 assert_eq!(history.list().await.unwrap().len(), 3);
1061 }
1062
1063 #[tokio::test]
1064 async fn retry_validation_rejects_unrelated_history_records() {
1065 let root = TestRoot::new();
1066 let history = history(root.path());
1067 let unrelated = history
1068 .enqueue_ingress(NewIngressSession {
1069 idempotency_id: "unrelated".into(),
1070 started_at: Utc::now().to_rfc3339(),
1071 source_session_type: "other".into(),
1072 kind: SessionKind::AudioIngress,
1073 effective_context_tokens: 400,
1074 text: "Unrelated".into(),
1075 metadata: json!({"kind":"other"}),
1076 })
1077 .await
1078 .unwrap()
1079 .value;
1080 let recording = completed_recording("Transcript");
1081
1082 let error = validate_retry_target(&unrelated, &[recording], 400, 400).unwrap_err();
1083 assert_eq!(error.kind(), ErrorKind::NotFound);
1084 }
1085
1086 #[tokio::test]
1087 async fn retry_validation_accepts_only_current_correlated_pieces() {
1088 let root = TestRoot::new();
1089 let history = history(root.path());
1090 let recording = completed_recording("Transcript");
1091 synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1092 .await
1093 .unwrap();
1094 let record = history.list().await.unwrap().remove(0);
1095
1096 validate_retry_target(&record, &[recording], 400, 400).unwrap();
1097 }
1098}