car-inference 0.13.0

Local model inference for CAR — Candle backend with Qwen3 models
Documentation
//! External Flux image backend — shells out to `mflux-generate`.
//!
//! TEMPORARY BRIDGE. Parallels `external_ltx` for `car image`. The native
//! Rust MLX Flux port (`mlx_flux.rs`) runs end-to-end but the denoising
//! path collapses every prompt to near-white output regardless of seed.
//! Until I can parity-debug each stage, route image generation through
//! the mflux Python CLI so `car image` is usable.
//!
//! Same policy as `external_ltx`: subprocess call is at the feature
//! boundary (command → PNG path on disk), env var `CAR_IMAGE_BACKEND`
//! controls dispatch, all logs are loud about which path is live.

use std::process::Command;

use crate::tasks::generate_image::{GenerateImageRequest, GenerateImageResult};
use crate::InferenceError;

const CLI_BINARY: &str = "mflux-generate";

/// Default Flux checkpoint to pass via `--model`. Same quantized weights
/// the native `mlx_flux` backend loads — so no additional download cost
/// when swapping backends.
const DEFAULT_MODEL: &str = "mlx-community/Flux-1.lite-8B-MLX-Q4";
/// mflux needs a `--base-model` hint for custom `--model` paths; the
/// Flux.1-lite checkpoint is a distilled variant of Flux.1-dev, so `dev`
/// is the right architecture family.
const DEFAULT_BASE_MODEL: &str = "dev";

pub fn is_available() -> bool {
    Command::new(CLI_BINARY)
        .arg("--help")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

pub fn generate_image(req: &GenerateImageRequest) -> Result<GenerateImageResult, InferenceError> {
    let output_path = req
        .output_path
        .clone()
        .unwrap_or_else(|| "output.png".to_string());

    let mut cmd = Command::new(CLI_BINARY);
    cmd.arg("--base-model")
        .arg(DEFAULT_BASE_MODEL)
        .arg("--model")
        .arg(req.model.as_deref().unwrap_or(DEFAULT_MODEL))
        .arg("--prompt")
        .arg(&req.prompt)
        .arg("--output")
        .arg(&output_path);

    if let Some(w) = req.width {
        cmd.arg("--width").arg(w.to_string());
    }
    if let Some(h) = req.height {
        cmd.arg("--height").arg(h.to_string());
    }
    if let Some(s) = req.steps {
        cmd.arg("--steps").arg(s.to_string());
    }
    if let Some(g) = req.guidance {
        cmd.arg("--guidance").arg(g.to_string());
    }
    if let Some(seed) = req.seed {
        cmd.arg("--seed").arg(seed.to_string());
    }

    tracing::info!(prompt = %req.prompt, output = %output_path, "external mflux: invoking");
    let output = cmd.output().map_err(|e| {
        InferenceError::InferenceFailed(format!(
            "failed to spawn `{CLI_BINARY}`: {e}. \
             Install with `uv pip install mflux` and put its venv's bin on PATH."
        ))
    })?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(InferenceError::InferenceFailed(format!(
            "mflux-generate exited with status {}: stderr={stderr}",
            output.status
        )));
    }

    Ok(GenerateImageResult {
        image_path: output_path,
        media_type: "image/png".to_string(),
        model_used: Some(format!(
            "external:{}",
            req.model.as_deref().unwrap_or(DEFAULT_MODEL)
        )),
    })
}