feat: Initial v0.9 release with API Key authentication
## v0.9.20260325_144654 ### Features - API Key Authentication System - Job Worker System - V2 Backup Versioning ### Bug Fixes - get_processor_results_by_job column mapping Co-authored-by: OpenCode
This commit is contained in:
1
src/ui/mod.rs
Normal file
1
src/ui/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod progress;
|
||||
413
src/ui/progress/mod.rs
Normal file
413
src/ui/progress/mod.rs
Normal file
@@ -0,0 +1,413 @@
|
||||
use ratatui::prelude::Stylize;
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Paragraph, Row, Table},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ProcessorType {
|
||||
Asr,
|
||||
Cut,
|
||||
Asrx,
|
||||
Yolo,
|
||||
Ocr,
|
||||
Face,
|
||||
Pose,
|
||||
Story,
|
||||
Caption,
|
||||
}
|
||||
|
||||
impl ProcessorType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ProcessorType::Asr => "ASR",
|
||||
ProcessorType::Cut => "CUT",
|
||||
ProcessorType::Asrx => "ASRX",
|
||||
ProcessorType::Yolo => "YOLO",
|
||||
ProcessorType::Ocr => "OCR",
|
||||
ProcessorType::Face => "Face",
|
||||
ProcessorType::Pose => "Pose",
|
||||
ProcessorType::Story => "Story",
|
||||
ProcessorType::Caption => "Caption",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProcessorType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ProcessorStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProcessorProgress {
|
||||
pub processor_type: ProcessorType,
|
||||
pub status: ProcessorStatus,
|
||||
pub current: u32,
|
||||
pub total: u32,
|
||||
pub message: String,
|
||||
pub elapsed_secs: u64,
|
||||
}
|
||||
|
||||
impl ProcessorProgress {
|
||||
pub fn new(processor_type: ProcessorType) -> Self {
|
||||
Self {
|
||||
processor_type,
|
||||
status: ProcessorStatus::Pending,
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: String::new(),
|
||||
elapsed_secs: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, total: u32) {
|
||||
self.status = ProcessorStatus::Running;
|
||||
self.total = total;
|
||||
self.current = 0;
|
||||
self.elapsed_secs = 0;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, current: u32, message: &str) {
|
||||
self.current = current;
|
||||
self.message = message.to_string();
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, message: &str) {
|
||||
self.status = ProcessorStatus::Completed;
|
||||
self.current = self.total;
|
||||
self.message = message.to_string();
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, message: &str) {
|
||||
self.status = ProcessorStatus::Failed;
|
||||
self.message = message.to_string();
|
||||
}
|
||||
|
||||
pub fn progress_ratio(&self) -> f64 {
|
||||
if self.total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.current as f64 / self.total as f64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eta(&self) -> Option<std::time::Duration> {
|
||||
if self.status == ProcessorStatus::Completed || self.total == 0 || self.current == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let elapsed = std::time::Duration::from_secs(self.elapsed_secs);
|
||||
let ratio = self.current as f64 / self.total as f64;
|
||||
if ratio <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let total_estimated = elapsed.div_f64(ratio);
|
||||
Some(total_estimated - elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgressState {
|
||||
pub processors: Vec<ProcessorProgress>,
|
||||
pub video_name: String,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl ProgressState {
|
||||
pub fn new(video_name: &str) -> Self {
|
||||
Self {
|
||||
processors: vec![
|
||||
ProcessorProgress::new(ProcessorType::Asr),
|
||||
ProcessorProgress::new(ProcessorType::Cut),
|
||||
ProcessorProgress::new(ProcessorType::Asrx),
|
||||
ProcessorProgress::new(ProcessorType::Yolo),
|
||||
ProcessorProgress::new(ProcessorType::Ocr),
|
||||
ProcessorProgress::new(ProcessorType::Face),
|
||||
ProcessorProgress::new(ProcessorType::Pose),
|
||||
ProcessorProgress::new(ProcessorType::Story),
|
||||
ProcessorProgress::new(ProcessorType::Caption),
|
||||
],
|
||||
video_name: video_name.to_string(),
|
||||
is_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_processor(&mut self, processor_type: ProcessorType) -> &mut ProcessorProgress {
|
||||
self.processors
|
||||
.iter_mut()
|
||||
.find(|p| p.processor_type == processor_type)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn completed_count(&self) -> usize {
|
||||
self.processors
|
||||
.iter()
|
||||
.filter(|p| p.status == ProcessorStatus::Completed)
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn total_count(&self) -> usize {
|
||||
self.processors.len()
|
||||
}
|
||||
|
||||
pub fn overall_progress(&self) -> f64 {
|
||||
let total: f64 = self.processors.iter().map(|p| p.progress_ratio()).sum();
|
||||
total / self.processors.len() as f64
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.is_active = true;
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.is_active = false;
|
||||
}
|
||||
|
||||
pub fn update_from_redis(
|
||||
&mut self,
|
||||
msg_type: &str,
|
||||
processor: &str,
|
||||
current: Option<i32>,
|
||||
total: Option<i32>,
|
||||
message: Option<&str>,
|
||||
) {
|
||||
let proc_type = match processor.to_uppercase().as_str() {
|
||||
"ASR" => ProcessorType::Asr,
|
||||
"CUT" => ProcessorType::Cut,
|
||||
"ASRX" => ProcessorType::Asrx,
|
||||
"YOLO" => ProcessorType::Yolo,
|
||||
"OCR" => ProcessorType::Ocr,
|
||||
"FACE" => ProcessorType::Face,
|
||||
"POSE" => ProcessorType::Pose,
|
||||
"STORY" => ProcessorType::Story,
|
||||
"CAPTION" => ProcessorType::Caption,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let p = self.get_processor(proc_type);
|
||||
|
||||
match msg_type {
|
||||
"START" | "INFO" => {
|
||||
p.status = ProcessorStatus::Running;
|
||||
if let Some(m) = message {
|
||||
p.message = m.to_string();
|
||||
}
|
||||
}
|
||||
"PROGRESS" => {
|
||||
p.status = ProcessorStatus::Running;
|
||||
if let Some(c) = current {
|
||||
p.current = c as u32;
|
||||
}
|
||||
if let Some(t) = total {
|
||||
p.total = t as u32;
|
||||
}
|
||||
if let Some(m) = message {
|
||||
p.message = m.to_string();
|
||||
}
|
||||
}
|
||||
"COMPLETE" => {
|
||||
p.status = ProcessorStatus::Completed;
|
||||
p.current = p.total;
|
||||
if let Some(m) = message {
|
||||
p.message = m.to_string();
|
||||
}
|
||||
}
|
||||
"ERROR" => {
|
||||
p.status = ProcessorStatus::Failed;
|
||||
if let Some(m) = message {
|
||||
p.message = m.to_string();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProgressUi {
|
||||
terminal: Terminal<CrosstermBackend<io::Stderr>>,
|
||||
state: std::sync::Mutex<ProgressState>,
|
||||
}
|
||||
|
||||
impl ProgressUi {
|
||||
pub fn new(video_name: &str) -> io::Result<Self> {
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{enable_raw_mode, EnterAlternateScreen};
|
||||
|
||||
let mut stderr = io::stderr();
|
||||
|
||||
enable_raw_mode()?;
|
||||
execute!(stderr, EnterAlternateScreen)?;
|
||||
|
||||
let backend = CrosstermBackend::new(stderr);
|
||||
let terminal = Terminal::new(backend)?;
|
||||
|
||||
let state = std::sync::Mutex::new(ProgressState::new(video_name));
|
||||
|
||||
Ok(Self { terminal, state })
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &std::sync::Mutex<ProgressState> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> io::Result<()> {
|
||||
let state = self.state.lock().unwrap().clone();
|
||||
let video_name = state.video_name.clone();
|
||||
let is_active = state.is_active;
|
||||
let processors = state.processors.clone();
|
||||
let completed = state.completed_count();
|
||||
let total = state.total_count();
|
||||
let overall = state.overall_progress();
|
||||
|
||||
self.terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(10),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
Self::render_header_static(f, chunks[0], &video_name);
|
||||
Self::render_processors_static(f, chunks[1], &processors);
|
||||
Self::render_footer_static(f, chunks[2], completed, total, overall, is_active);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self) -> io::Result<()> {
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen};
|
||||
|
||||
execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_header_static(f: &mut Frame, area: Rect, video_name: &str) {
|
||||
let title = format!(" Processing: {} ", video_name);
|
||||
let block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
|
||||
fn render_processors_static(f: &mut Frame, area: Rect, processors: &[ProcessorProgress]) {
|
||||
let rows: Vec<Row> = processors
|
||||
.iter()
|
||||
.map(|p| Self::processor_to_row_static(p))
|
||||
.collect();
|
||||
|
||||
let widths = [
|
||||
Constraint::Length(8),
|
||||
Constraint::Length(10),
|
||||
Constraint::Min(20),
|
||||
Constraint::Length(12),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.block(Block::default().borders(Borders::ALL).title(" Processors "))
|
||||
.column_spacing(1);
|
||||
|
||||
f.render_widget(table, area);
|
||||
}
|
||||
|
||||
fn processor_to_row_static(p: &ProcessorProgress) -> Row<'_> {
|
||||
let status_color = match p.status {
|
||||
ProcessorStatus::Pending => Color::DarkGray,
|
||||
ProcessorStatus::Running => Color::Yellow,
|
||||
ProcessorStatus::Completed => Color::Green,
|
||||
ProcessorStatus::Failed => Color::Red,
|
||||
};
|
||||
|
||||
let progress_bar = if p.total > 0 {
|
||||
let filled = (p.progress_ratio() * 20.0) as usize;
|
||||
let bar: String = format!(
|
||||
"[{}{}]",
|
||||
"█".repeat(filled.min(20)),
|
||||
"░".repeat((20 - filled).min(20))
|
||||
);
|
||||
bar
|
||||
} else {
|
||||
"[--------------------]".to_string()
|
||||
};
|
||||
|
||||
let percentage = format!("{:5.1}%", p.progress_ratio() * 100.0);
|
||||
|
||||
let detail = if p.total > 0 {
|
||||
format!("{}/{}", p.current, p.total)
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
||||
let eta = match p.eta() {
|
||||
Some(d) => {
|
||||
let secs = d.as_secs();
|
||||
if secs > 60 {
|
||||
format!("{}m", secs / 60)
|
||||
} else {
|
||||
format!("{}s", secs)
|
||||
}
|
||||
}
|
||||
None => "-".to_string(),
|
||||
};
|
||||
|
||||
Row::new(vec![
|
||||
Span::raw(format!(" {} ", p.processor_type.as_str())),
|
||||
Span::raw(progress_bar).fg(status_color),
|
||||
Span::raw(format!(" {} {}", detail, eta)),
|
||||
Span::raw(format!(" {} ", percentage)),
|
||||
])
|
||||
}
|
||||
|
||||
fn render_footer_static(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
completed: usize,
|
||||
total: usize,
|
||||
overall: f64,
|
||||
is_active: bool,
|
||||
) {
|
||||
let status_text = if is_active {
|
||||
format!(
|
||||
" Progress: {}/{} ({:.1}%) | Press Ctrl+C to cancel ",
|
||||
completed,
|
||||
total,
|
||||
overall * 100.0
|
||||
)
|
||||
} else if completed == total {
|
||||
" ✓ All processors completed! ".to_string()
|
||||
} else {
|
||||
" Ready ".to_string()
|
||||
};
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::Green));
|
||||
let paragraph = Paragraph::new(status_text).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProgressUi {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.cleanup();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user