npcrs 0.1.0

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"))
}