paracletics-hypercube 0.1.0

General-purpose paracletic hyper cube compression toolkit.
Documentation
use std::env;
use std::error::Error;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

use paracletics_hypercube::compression::{Codec, compress_with, decompress_with};
use paracletics_hypercube::cross_modal::{
    byte_from_token, decode_from_hue, decode_from_note, encode_byte,
};
use paracletics_hypercube::media_samples::synth_y4m_musical_light;

const DEFAULT_FRAME_LIMIT: usize = 4096;

fn main() -> Result<(), Box<dyn Error>> {
    let config = parse_args()?;
    let (label, input) = match config.input_path {
        Some(path) => {
            let bytes = fs::read(&path)?;
            let label = path
                .file_name()
                .and_then(|s| s.to_str())
                .unwrap_or("input")
                .to_string();
            (label, bytes)
        }
        None => (
            "generated-video-y4m".to_string(),
            synth_y4m_musical_light(192, 108, 75, 25),
        ),
    };

    let compressed = compress_with(Codec::Chromoharmonic, &input);
    let decompressed = decompress_with(Codec::Chromoharmonic, &compressed)?;
    let storage_roundtrip_ok = decompressed == input;

    let frames_to_map = compressed.len().min(config.frame_limit);
    let mapped_slice = &compressed[..frames_to_map];

    let mut note_recovered = Vec::with_capacity(mapped_slice.len());
    let mut hue_recovered = Vec::with_capacity(mapped_slice.len());
    let mut token_recovered = Vec::with_capacity(mapped_slice.len());

    let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/cross_modal_demo");
    fs::create_dir_all(&out_dir)?;

    let csv_path = out_dir.join("frames.csv");
    let score_path = out_dir.join("score.txt");
    let ppm_path = out_dir.join("palette.ppm");
    let source_path = out_dir.join("source.bin");
    let compressed_path = out_dir.join("encoded.chromoharmonic.bin");
    let restored_path = out_dir.join("restored.bin");

    let mut csv = fs::File::create(&csv_path)?;
    writeln!(csv, "index,byte,note,register,hue_index,r,g,b,token")?;

    let mut score = fs::File::create(&score_path)?;

    let mut colors = Vec::with_capacity(mapped_slice.len());

    for (i, &b) in mapped_slice.iter().enumerate() {
        let frame = encode_byte(b);
        note_recovered.push(decode_from_note(frame.note_name, frame.register)?);
        hue_recovered.push(decode_from_hue(frame.hue_index, frame.register)?);
        token_recovered.push(byte_from_token(&frame.token)?);

        colors.push(frame.rgb);
        let hex = format!(
            "#{:02X}{:02X}{:02X}",
            frame.rgb[0], frame.rgb[1], frame.rgb[2]
        );

        writeln!(
            csv,
            "{},{},{},{},{},{},{},{},{}",
            i,
            frame.byte,
            frame.note_name,
            frame.register,
            frame.hue_index,
            frame.rgb[0],
            frame.rgb[1],
            frame.rgb[2],
            frame.token
        )?;
        writeln!(
            score,
            "{:06} {:>3}{} {:<11} {}",
            i, frame.note_name, frame.register, frame.token, hex
        )?;
    }

    write_palette_ppm(&ppm_path, &colors)?;

    fs::write(&source_path, &input)?;
    fs::write(&compressed_path, &compressed)?;
    fs::write(&restored_path, &decompressed)?;

    let note_ok = note_recovered == mapped_slice;
    let hue_ok = hue_recovered == mapped_slice;
    let token_ok = token_recovered == mapped_slice;

    println!("Cross-modal PHC demonstration");
    println!("dataset: {}", label);
    println!("input bytes: {}", input.len());
    println!("chromoharmonic bytes: {}", compressed.len());
    println!(
        "compression ratio: {:.3}",
        compressed.len() as f64 / input.len().max(1) as f64
    );
    println!("storage roundtrip: {}", yes_no(storage_roundtrip_ok));
    println!(
        "mapped frames: {} (first {} compressed bytes)",
        frames_to_map, frames_to_map
    );
    println!("music decode roundtrip: {}", yes_no(note_ok));
    println!("light decode roundtrip: {}", yes_no(hue_ok));
    println!("word decode roundtrip: {}", yes_no(token_ok));
    println!();
    println!("Artifacts:");
    println!("  {}", csv_path.display());
    println!("  {}", score_path.display());
    println!("  {}", ppm_path.display());
    println!("  {}", source_path.display());
    println!("  {}", compressed_path.display());
    println!("  {}", restored_path.display());

    if !(storage_roundtrip_ok && note_ok && hue_ok && token_ok) {
        return Err(io::Error::other("cross-modal verification failed").into());
    }

    Ok(())
}

fn parse_args() -> Result<Config, Box<dyn Error>> {
    let mut args = env::args().skip(1);
    let mut input_path: Option<PathBuf> = None;
    let mut frame_limit = DEFAULT_FRAME_LIMIT;

    while let Some(arg) = args.next() {
        if arg == "--input" {
            let value = args.next().ok_or_else(|| {
                io::Error::new(io::ErrorKind::InvalidInput, "--input requires a path")
            })?;
            input_path = Some(PathBuf::from(value));
            continue;
        }
        if let Some(v) = arg.strip_prefix("--input=") {
            input_path = Some(PathBuf::from(v));
            continue;
        }
        if arg == "--frames" {
            let value = args.next().ok_or_else(|| {
                io::Error::new(io::ErrorKind::InvalidInput, "--frames requires a number")
            })?;
            frame_limit = value.parse::<usize>()?.max(1);
            continue;
        }
        if let Some(v) = arg.strip_prefix("--frames=") {
            frame_limit = v.parse::<usize>()?.max(1);
            continue;
        }
        return Err(
            io::Error::new(io::ErrorKind::InvalidInput, format!("unknown arg: {arg}")).into(),
        );
    }

    Ok(Config {
        input_path,
        frame_limit,
    })
}

fn write_palette_ppm(path: &Path, colors: &[[u8; 3]]) -> Result<(), Box<dyn Error>> {
    let width = colors.len().max(1);
    let height = 64usize;
    let mut bytes = Vec::with_capacity(width * height * 3 + 64);
    bytes.extend_from_slice(format!("P6\n{} {}\n255\n", width, height).as_bytes());
    for _y in 0..height {
        for x in 0..width {
            let rgb = colors.get(x).copied().unwrap_or([0, 0, 0]);
            bytes.extend_from_slice(&rgb);
        }
    }
    fs::write(path, bytes)?;
    Ok(())
}

fn yes_no(v: bool) -> &'static str {
    if v { "yes" } else { "no" }
}

struct Config {
    input_path: Option<PathBuf>,
    frame_limit: usize,
}