use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use anyhow::{Context, Result};
use clap::{CommandFactory, Parser, Subcommand};
use sceptre::{OcrConfig, ReadOptions, Reader};
use crate::output::{self, OutputFormat};
use crate::overrides::OcrOverrides;
use crate::timing::StageTimer;
#[derive(Parser)]
#[command(name = "sceptre", version, about)]
pub struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true, default_value = "warn", env = "EASYOCR_LOG")]
log_level: String,
}
#[derive(Subcommand)]
pub enum ModelsAction {
List {
#[command(flatten)]
overrides: OcrOverrides,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},
Download {
#[command(flatten)]
overrides: OcrOverrides,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},
}
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(required = true, num_args = 1..)]
images: Vec<PathBuf>,
#[command(flatten)]
overrides: OcrOverrides,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
#[arg(long)]
no_detail: bool,
#[arg(long)]
timings: bool,
},
Detect {
image: PathBuf,
#[command(flatten)]
overrides: OcrOverrides,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},
Recognize {
image: PathBuf,
#[command(flatten)]
overrides: OcrOverrides,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},
Models {
#[command(subcommand)]
action: ModelsAction,
},
Completions {
shell: clap_complete::Shell,
},
#[cfg(feature = "mcp")]
Mcp {
#[command(flatten)]
overrides: OcrOverrides,
},
}
fn config_from(overrides: &OcrOverrides) -> OcrConfig {
let mut config = OcrConfig::default();
overrides.apply(&mut config);
config
}
fn build_reader(overrides: &OcrOverrides) -> Result<Reader> {
Reader::builder()
.config(config_from(overrides))
.build()
.context("building the OCR reader")
}
fn build_reader_timed(overrides: &OcrOverrides, timer: Arc<StageTimer>) -> Result<Reader> {
Reader::builder()
.config(config_from(overrides))
.progress(timer)
.build()
.context("building the OCR reader")
}
fn stdout() -> anstream::Stdout {
anstream::stdout()
}
fn stderr() -> anstream::Stderr {
anstream::stderr()
}
fn run_ocr(
images: Vec<PathBuf>,
overrides: OcrOverrides,
format: OutputFormat,
no_detail: bool,
timings: bool,
) -> Result<()> {
let timer = timings.then(|| Arc::new(StageTimer::new()));
let reader = match &timer {
Some(timer) => build_reader_timed(&overrides, timer.clone())?,
None => build_reader(&overrides)?,
};
let options = ReadOptions { detail: !no_detail };
let detail = !no_detail;
let started = Instant::now();
if let [image] = images.as_slice() {
let result = reader
.readtext(image, &options)
.with_context(|| format!("running OCR over {image:?}"))?;
report_timings(&timer, started);
output::render_result(&result, format, detail, &mut stdout()).context("writing OCR results")?;
return Ok(());
}
let mut outcomes: Vec<(PathBuf, output::ImageOutcome)> = Vec::with_capacity(images.len());
let mut failures = 0usize;
for image in &images {
match reader.readtext(image, &options) {
Ok(result) => outcomes.push((image.clone(), Ok(result))),
Err(error) => {
failures += 1;
let message = format!("{error:#}");
let _ = writeln!(stderr(), "error: {}: {message}", image.display());
outcomes.push((image.clone(), Err(message)));
}
}
}
report_timings(&timer, started);
output::render_batch(&outcomes, format, detail, &mut stdout()).context("writing OCR results")?;
if failures > 0 {
anyhow::bail!("{failures} of {} image(s) failed", images.len());
}
Ok(())
}
fn report_timings(timer: &Option<Arc<StageTimer>>, started: Instant) {
if let Some(timer) = timer {
let breakdown = timer.breakdown(started, Instant::now());
let _ = writeln!(stderr(), "{}", crate::timing::render(&breakdown));
}
}
fn run_detect(image: PathBuf, overrides: OcrOverrides, format: OutputFormat) -> Result<()> {
let reader = build_reader(&overrides)?;
let image_data = sceptre::Image::from_path(&image).with_context(|| format!("loading {image:?}"))?;
let quads = reader
.detect(&image_data, &ReadOptions::default())
.with_context(|| format!("detecting text regions in {image:?}"))?;
output::render_quads(&quads, format, &mut stdout()).context("writing detected regions")?;
Ok(())
}
fn run_recognize(image: PathBuf, overrides: OcrOverrides, format: OutputFormat) -> Result<()> {
let reader = build_reader(&overrides)?;
let image_data = sceptre::Image::from_path(&image).with_context(|| format!("loading {image:?}"))?;
let line = reader
.recognize_line(&image_data, &ReadOptions::default())
.with_context(|| format!("recognizing text in {image:?}"))?;
output::render_line(&line, format, true, &mut stdout()).context("writing recognized line")?;
Ok(())
}
fn run_models(action: ModelsAction) -> Result<()> {
match action {
ModelsAction::List { overrides, format } => {
let config = config_from(&overrides);
let models = sceptre::model_manifest(&config).context("building the model manifest")?;
output::render_models(&models, format, &mut stdout()).context("writing the model list")?;
}
ModelsAction::Download { overrides, format } => {
let config = config_from(&overrides);
let models = sceptre::download_models(&config).context("downloading models")?;
output::render_models(&models, format, &mut stdout()).context("writing the model list")?;
}
}
Ok(())
}
impl Cli {
pub fn init_tracing(&self) {
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::try_new(&self.log_level).unwrap_or_else(|_| EnvFilter::new("warn"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
pub fn run(self) -> Result<()> {
match self.command {
Commands::Run {
images,
overrides,
format,
no_detail,
timings,
} => run_ocr(images, overrides, format, no_detail, timings),
Commands::Detect {
image,
overrides,
format,
} => run_detect(image, overrides, format),
Commands::Recognize {
image,
overrides,
format,
} => run_recognize(image, overrides, format),
Commands::Models { action } => run_models(action),
Commands::Completions { shell } => {
let mut command = Cli::command();
clap_complete::generate(shell, &mut command, "sceptre", &mut std::io::stdout());
Ok(())
}
#[cfg(feature = "mcp")]
Commands::Mcp { overrides } => {
let reader = build_reader(&overrides)?;
sceptre::mcp::serve(reader).context("running the MCP server")
}
}
}
}