bookforge-cli 1.8.0

CLI-first EPUB translation engine with deterministic structure rebuild and review loop.
use anyhow::Result;
use bookforge_core::config::SegmentationConfig;
use bookforge_core::segment::build_segments;
use bookforge_epub::read_epub;
use clap::Args;
use std::path::PathBuf;

use crate::{
    LanguageArgs, ProviderArgs,
    cost::{estimate_cost_usd_with_pricing, load_pricing},
};

#[derive(Debug, Args)]
pub struct EstimateArgs {
    pub input: PathBuf,

    #[command(flatten)]
    pub language: LanguageArgs,

    #[command(flatten)]
    pub provider: ProviderArgs,

    /// Override the bundled pricing catalog. BOOKFORGE_PRICING_PATH is
    /// used when this flag is omitted.
    #[arg(long)]
    pub pricing: Option<PathBuf>,
}

pub async fn run(args: EstimateArgs) -> Result<()> {
    let book = read_epub(&args.input)?;
    let segments = build_segments(&book, &SegmentationConfig::default())?;
    let input_tokens = segments
        .iter()
        .map(|segment| segment.source.token_estimate as u64)
        .sum::<u64>();
    let output_tokens = (input_tokens as f64 * 1.15).ceil() as u64;
    let model = args
        .provider
        .model
        .as_deref()
        .unwrap_or_else(|| default_model(&args.provider.provider));
    let pricing = load_pricing(args.pricing.as_deref())?;

    println!("Input: {}", args.input.display());
    println!("Target: {}", args.language.target);
    println!("Provider: {}", args.provider.provider);
    println!("Model: {model}");
    println!("Segments: {}", segments.len());
    println!("Estimated input tokens: {input_tokens}");
    println!("Estimated output tokens: {output_tokens}");
    println!("Pricing: {}", pricing.source_label());

    match estimate_cost_usd_with_pricing(
        &pricing,
        &args.provider.provider,
        model,
        input_tokens,
        0,
        output_tokens,
    ) {
        Some(cost) => println!("Estimated cost: ${cost:.6}"),
        None => println!("Estimated cost: unavailable for this provider/model"),
    }

    Ok(())
}

fn default_model(provider: &str) -> &str {
    match provider {
        "mock" => "mock-prefix-target",
        "deepseek" => "deepseek-v4-flash",
        "openrouter" => "openrouter/auto",
        _ => "unknown",
    }
}