kcode-audio-ingress 0.7.4

Durable automatic audio transcription with restart recovery
Documentation
//! Provider parsing and read-only speaker classification for audio ingress.

use std::sync::Arc;

use anyhow::ensure;
use chrono::{DateTime, Utc};
use kcode_audio_speaker_review::{ConfirmationState, ParsedSpeaker};
pub(crate) use kcode_audio_speaker_review::{
    CorrectionChunk, CorrectionObservation, CorrectionPacket, ParsedChunk,
};
use kcode_speaker_extract::ExtractionOutcome;
use kcode_speaker_system::SpeechClassifier;
use uuid::Uuid;

#[derive(Clone)]
pub(crate) struct ClassificationContext {
    pub(crate) recording_id: Uuid,
    pub(crate) user_id: String,
    pub(crate) sha256: String,
    pub(crate) original_filename: String,
    pub(crate) size_bytes: u64,
    pub(crate) recorded_at: DateTime<Utc>,
    pub(crate) classifier: Arc<SpeechClassifier>,
}

pub(crate) fn parsed_chunk_from_extraction(
    extraction: &ExtractionOutcome,
) -> anyhow::Result<ParsedChunk> {
    let (count, clip_valid, clip_validity_reason) = match extraction {
        ExtractionOutcome::Scored(scored) if scored.additional_speakers.is_empty() => {
            (scored.speakers.len(), true, None)
        }
        ExtractionOutcome::Scored(scored) => (
            scored.speakers.len() + scored.additional_speakers.len(),
            false,
            Some("One or more speakers lacked a complete feature profile.".to_owned()),
        ),
        ExtractionOutcome::Unscorable {
            reason,
            additional_speakers,
        } => (additional_speakers.len(), false, Some(reason.clone())),
    };
    let speakers = (0..count)
        .map(|ordinal| {
            let profile = match extraction {
                ExtractionOutcome::Scored(scored) => scored
                    .speakers
                    .iter()
                    .find(|profile| usize::from(profile.speaker_ordinal) == ordinal),
                ExtractionOutcome::Unscorable { .. } => None,
            };
            ParsedSpeaker {
                local_label: format!("Speaker {}", ordinal + 1),
                primary_language: profile.map(|value| value.primary_language.as_ref().to_owned()),
                feature_row: profile.map(|value| value.features),
            }
        })
        .collect();
    Ok(ParsedChunk {
        clip_valid,
        clip_validity_reason,
        speakers,
    })
}

pub(crate) fn classify_speakers(
    context: &ClassificationContext,
    chunk_index: usize,
    parsed: &ParsedChunk,
) -> anyhow::Result<Vec<CorrectionObservation>> {
    kcode_audio_speaker_candidates::classify_chunk(
        &context.classifier,
        context.recording_id,
        chunk_index,
        parsed,
    )
}

pub(crate) fn unclassified_observations(
    recording_id: Uuid,
    chunk_index: usize,
    parsed: &ParsedChunk,
) -> anyhow::Result<Vec<CorrectionObservation>> {
    kcode_audio_speaker_candidates::observations_without_candidates(
        recording_id,
        chunk_index,
        parsed,
    )
}

pub(crate) fn build_packet(
    context: &ClassificationContext,
    chunks: Vec<CorrectionChunk>,
) -> anyhow::Result<CorrectionPacket> {
    ensure!(!chunks.is_empty(), "correction packet has no chunks");
    let chunk_count = chunks.len();
    ensure!(
        chunks.iter().enumerate().all(|(index, chunk)| {
            chunk.chunk_index == index
                && chunk.chunk_count == chunk_count
                && chunk.audio_end_ms > chunk.audio_start_ms
                && chunk.observations.len() == chunk.parsed.speakers.len()
                && !chunk.signed_off
        }),
        "correction packet chunks are not one complete chronological plan"
    );
    Ok(CorrectionPacket {
        recording_id: context.recording_id,
        user_id: context.user_id.clone(),
        sha256: context.sha256.clone(),
        original_filename: context.original_filename.clone(),
        size_bytes: context.size_bytes,
        recorded_at: context.recorded_at,
        chunk_count,
        chunks,
        confirmation_state: ConfirmationState::Unconfirmed,
    })
}