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:
accusys
2026-03-25 14:52:51 +08:00
parent 47e86b696f
commit 383201cacd
193 changed files with 40268 additions and 422 deletions

413
src/ui/progress/mod.rs Normal file
View 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();
}
}