feat: 新增 Job Worker 系統與 API 文檔全面更新
This commit is contained in:
@@ -4,7 +4,7 @@ use std::time::Duration;
|
||||
|
||||
use super::executor::PythonExecutor;
|
||||
|
||||
const ASR_TIMEOUT: Duration = Duration::from_secs(3600);
|
||||
const ASR_TIMEOUT: Duration = Duration::from_secs(1800); // 30 minutes
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AsrResult {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use libc;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
@@ -159,12 +160,16 @@ impl PythonExecutor {
|
||||
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
cmd.kill_on_drop(true);
|
||||
// Create new process group for clean termination
|
||||
cmd.process_group(0);
|
||||
|
||||
tracing::info!("[{}] Starting: {:?}", log_prefix, script_name);
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run {}", script_name))?;
|
||||
let child_pid = child.id();
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to capture stdout")?;
|
||||
let stderr = child.stderr.take().context("Failed to capture stderr")?;
|
||||
@@ -220,6 +225,13 @@ impl PythonExecutor {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => {
|
||||
// Try to kill the entire process group
|
||||
if let Some(pid) = child_pid {
|
||||
let pgid = pid as i32;
|
||||
unsafe {
|
||||
libc::killpg(pgid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
child.kill().await.context("Failed to kill process")?;
|
||||
anyhow::bail!("{} timed out after {:?}", script_name, duration);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::executor::PythonExecutor;
|
||||
@@ -31,6 +32,90 @@ pub struct YoloObject {
|
||||
pub confidence: f32,
|
||||
}
|
||||
|
||||
// New structs for parsing Python YOLO output
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct YoloPythonMetadata {
|
||||
video_path: String,
|
||||
fps: f64,
|
||||
width: i32,
|
||||
height: i32,
|
||||
total_frames: i64,
|
||||
total_duration: f64,
|
||||
processed_at: String,
|
||||
auto_save_interval: i32,
|
||||
auto_save_frames: i32,
|
||||
status: String,
|
||||
last_saved_at: String,
|
||||
last_saved_frame: i64,
|
||||
completed_at: Option<String>,
|
||||
processing_time: Option<f64>,
|
||||
total_detections: Option<i64>,
|
||||
auto_save_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct YoloPythonDetection {
|
||||
class_name: String,
|
||||
confidence: f32,
|
||||
x1: f32,
|
||||
y1: f32,
|
||||
x2: f32,
|
||||
y2: f32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
class_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct YoloPythonFrame {
|
||||
frame_number: u64,
|
||||
time_seconds: f64,
|
||||
time_formatted: String,
|
||||
detections: Vec<YoloPythonDetection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct YoloPythonResult {
|
||||
metadata: YoloPythonMetadata,
|
||||
frames: HashMap<String, YoloPythonFrame>,
|
||||
}
|
||||
|
||||
impl YoloPythonResult {
|
||||
pub fn to_yolo_result(&self) -> YoloResult {
|
||||
let mut frames = Vec::new();
|
||||
|
||||
// Sort frames by frame number (key is string, but we parse as u64)
|
||||
let mut frame_entries: Vec<_> = self.frames.iter().collect();
|
||||
frame_entries.sort_by_key(|(key, _)| key.parse::<u64>().unwrap_or(0));
|
||||
|
||||
for (_, frame) in frame_entries {
|
||||
let mut objects = Vec::new();
|
||||
for detection in &frame.detections {
|
||||
objects.push(YoloObject {
|
||||
class_name: detection.class_name.clone(),
|
||||
class_id: detection.class_id.unwrap_or(0),
|
||||
x: detection.x1 as i32,
|
||||
y: detection.y1 as i32,
|
||||
width: detection.width,
|
||||
height: detection.height,
|
||||
confidence: detection.confidence,
|
||||
});
|
||||
}
|
||||
frames.push(YoloFrame {
|
||||
frame: frame.frame_number,
|
||||
timestamp: frame.time_seconds,
|
||||
objects,
|
||||
});
|
||||
}
|
||||
|
||||
YoloResult {
|
||||
frame_count: frames.len() as u64,
|
||||
fps: self.metadata.fps,
|
||||
frames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_yolo(
|
||||
video_path: &str,
|
||||
output_path: &str,
|
||||
@@ -63,9 +148,11 @@ pub async fn process_yolo(
|
||||
|
||||
let json_str = std::fs::read_to_string(output_path).context("Failed to read YOLO output")?;
|
||||
|
||||
let result: YoloResult =
|
||||
let python_result: YoloPythonResult =
|
||||
serde_json::from_str(&json_str).context("Failed to parse YOLO output")?;
|
||||
|
||||
let result = python_result.to_yolo_result();
|
||||
|
||||
tracing::info!(
|
||||
"[YOLO] Result: {} frames, {:.2} fps",
|
||||
result.frame_count,
|
||||
@@ -150,4 +237,75 @@ mod tests {
|
||||
};
|
||||
assert!(result.frames.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_yolo_python_result_parsing() {
|
||||
// Sample JSON matching Python script output
|
||||
let json = r#"{
|
||||
"metadata": {
|
||||
"video_path": "/test/video.mp4",
|
||||
"fps": 22.0,
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"total_frames": 3512,
|
||||
"total_duration": 159.63636363636363,
|
||||
"processed_at": "2026-03-26T05:20:48.230143",
|
||||
"auto_save_interval": 30,
|
||||
"auto_save_frames": 300,
|
||||
"status": "completed",
|
||||
"last_saved_at": "2026-03-26T05:23:22.791673",
|
||||
"last_saved_frame": 0,
|
||||
"completed_at": "2026-03-26T05:23:22.791666",
|
||||
"processing_time": 154.5577518939972,
|
||||
"total_detections": 12786,
|
||||
"auto_save_count": 11
|
||||
},
|
||||
"frames": {
|
||||
"13": {
|
||||
"frame_number": 13,
|
||||
"time_seconds": 0.545,
|
||||
"time_formatted": "00:00:00",
|
||||
"detections": [
|
||||
{
|
||||
"class_id": 0,
|
||||
"class_name": "person",
|
||||
"confidence": 0.8424218893051147,
|
||||
"x1": 473.4156494140625,
|
||||
"y1": 79.5609359741211,
|
||||
"x2": 639.77783203125,
|
||||
"y2": 303.8714294433594,
|
||||
"width": 166,
|
||||
"height": 224
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let python_result: YoloPythonResult = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(python_result.metadata.fps, 22.0);
|
||||
assert_eq!(python_result.frames.len(), 1);
|
||||
let frame = python_result.frames.get("13").unwrap();
|
||||
assert_eq!(frame.frame_number, 13);
|
||||
assert_eq!(frame.detections.len(), 1);
|
||||
let detection = &frame.detections[0];
|
||||
assert_eq!(detection.class_id, Some(0));
|
||||
assert_eq!(detection.class_name, "person");
|
||||
assert!((detection.confidence - 0.8424218893051147).abs() < 0.0001);
|
||||
assert!((detection.x1 - 473.4156494140625).abs() < 0.0001);
|
||||
|
||||
// Convert to YoloResult
|
||||
let yolo_result = python_result.to_yolo_result();
|
||||
assert_eq!(yolo_result.frames.len(), 1);
|
||||
assert_eq!(yolo_result.frames[0].frame, 13);
|
||||
assert_eq!(yolo_result.frames[0].objects.len(), 1);
|
||||
let obj = &yolo_result.frames[0].objects[0];
|
||||
assert_eq!(obj.class_name, "person");
|
||||
assert_eq!(obj.class_id, 0);
|
||||
assert_eq!(obj.x, 473);
|
||||
assert_eq!(obj.y, 79);
|
||||
assert_eq!(obj.width, 166);
|
||||
assert_eq!(obj.height, 224);
|
||||
assert!((obj.confidence - 0.842421889).abs() < 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user