npcrs 0.1.13

Rust core for the NPC system — agent kernel, jinx executor, LLM client
Documentation
use crate::error::Result;

pub fn process_video(file_path: &str, _table_name: &str) -> Result<(Vec<String>, Vec<String>)> {
    let output = std::process::Command::new("ffprobe")
        .args([
            "-v",
            "error",
            "-select_streams",
            "v:0",
            "-show_entries",
            "stream=width,height,nb_frames,r_frame_rate",
            "-show_entries",
            "format=duration",
            "-of",
            "json",
            file_path,
        ])
        .output();
    let mut texts = Vec::new();
    match output {
        Ok(out) if out.status.success() => {
            let json_str = String::from_utf8_lossy(&out.stdout);
            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&json_str) {
                texts.push(format!(
                    "Video: {}x{}, {} frames",
                    json["streams"][0]["width"].as_u64().unwrap_or(0),
                    json["streams"][0]["height"].as_u64().unwrap_or(0),
                    json["streams"][0]["nb_frames"].as_str().unwrap_or("?")
                ));
            }
        }
        _ => {
            texts.push(format!(
                "Video file: {}",
                std::path::Path::new(file_path)
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or(file_path)
            ));
        }
    }
    Ok((Vec::new(), texts))
}

pub fn summarize_video_file(
    file_path: &str,
    language: Option<&str>,
    max_audio_seconds: u32,
) -> Result<String> {
    let mut meta = Vec::new();
    let output = std::process::Command::new("ffprobe")
        .args([
            "-v",
            "error",
            "-select_streams",
            "v:0",
            "-show_entries",
            "stream=width,height,nb_frames,r_frame_rate",
            "-show_entries",
            "format=duration",
            "-of",
            "json",
            file_path,
        ])
        .output();
    match output {
        Ok(out) if out.status.success() => {
            let json_str = String::from_utf8_lossy(&out.stdout);
            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&json_str) {
                let basename = std::path::Path::new(file_path)
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or(file_path);
                meta.push(format!(
                    "Video file: {} | {}x{} | {} fps | {} frames | ~{}s",
                    basename,
                    json["streams"][0]["width"].as_u64().unwrap_or(0),
                    json["streams"][0]["height"].as_u64().unwrap_or(0),
                    json["streams"][0]["r_frame_rate"].as_str().unwrap_or("?"),
                    json["streams"][0]["nb_frames"].as_str().unwrap_or("?"),
                    json["format"]["duration"].as_str().unwrap_or("?")
                ));
            }
        }
        _ => {
            meta.push(format!(
                "Video file: {}",
                std::path::Path::new(file_path)
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or(file_path)
            ));
        }
    }
    let tmp_audio = std::env::temp_dir().join(format!("npc_va_{}.wav", std::process::id()));
    let extract = std::process::Command::new("ffmpeg")
        .args([
            "-y",
            "-i",
            file_path,
            "-vn",
            "-ac",
            "1",
            "-ar",
            "16000",
            "-t",
            &max_audio_seconds.to_string(),
            tmp_audio.to_str().unwrap(),
        ])
        .output();
    let audio_ok = extract.map(|o| o.status.success()).unwrap_or(false) && tmp_audio.exists();
    let mut transcript = String::new();
    if audio_ok {
        if let Ok(t) = super::audio::transcribe_audio_file(tmp_audio.to_str().unwrap(), language) {
            if !t.is_empty() {
                transcript = t;
            }
        }
        let _ = std::fs::remove_file(&tmp_audio);
    }
    if !transcript.is_empty() {
        meta.push("Audio transcript:".into());
        meta.push(transcript);
    } else {
        meta.push("[No transcript; ensure ffmpeg and transcription backend installed]".into());
    }
    Ok(meta.join("\n"))
}