feat: add Vision LLM integration (CLIP + Qwen3-VL cascade)
- Add Qwen3-VL dynamic management (start/stop/status CLI) - Add CLIP + Qwen3-VL cascade detection strategy - Add Vision CLI commands (vision start/stop/status, detect) - Add cascade_vision processor module - Add clip processor module - Add qwen_vl_manager module Changes: - scripts/start_qwen3vl.sh, stop_qwen3vl.sh: Qwen3-VL management scripts - src/core/vision/: Qwen3-VL manager module - src/core/processor/cascade_vision.rs: CLIP + Qwen3-VL cascade logic - src/core/processor/clip.rs: CLIP classification and detection - src/api/clip_api.rs: CLIP API endpoints - src/cli/vision.rs: Vision CLI implementation - src/cli/args.rs: Add Vision and Detect commands - src/main.rs: Integrate Vision CLI - src/core/mod.rs: Add vision module - src/core/processor/mod.rs: Add cascade_vision module
This commit is contained in:
@@ -44,10 +44,7 @@ pub async fn process_asrx(
|
||||
let executor = PythonExecutor::new()?;
|
||||
let script_path = executor.script_path("asrx_processor.py");
|
||||
|
||||
tracing::info!(
|
||||
"[ASRX] Starting hybrid speaker diarization: {}",
|
||||
video_path
|
||||
);
|
||||
tracing::info!("[ASRX] Starting hybrid speaker diarization: {}", video_path);
|
||||
|
||||
if !script_path.exists() {
|
||||
tracing::error!("[ASRX] Script not found: {:?}", script_path);
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::core::processor::clip::{ClipPrediction, detect_objects};
|
||||
use crate::core::vision::qwen_vl_manager::QwenVLManager;
|
||||
|
||||
const DEFAULT_CLIP_THRESHOLD: f32 = 0.7;
|
||||
const QWENVL_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CascadeDetectionResult {
|
||||
pub detections: Vec<ClipPrediction>,
|
||||
pub model_used: String,
|
||||
pub clip_confidence: f32,
|
||||
pub qwenvl_used: bool,
|
||||
pub processing_time_ms: u64,
|
||||
}
|
||||
|
||||
pub struct CascadeVisionProcessor {
|
||||
clip_threshold: f32,
|
||||
qwen_vl_manager: QwenVLManager,
|
||||
}
|
||||
|
||||
impl CascadeVisionProcessor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clip_threshold: DEFAULT_CLIP_THRESHOLD,
|
||||
qwen_vl_manager: QwenVLManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_threshold(threshold: f32) -> Self {
|
||||
Self {
|
||||
clip_threshold: threshold,
|
||||
qwen_vl_manager: QwenVLManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn detect_objects(&self, image_path: &Path, objects: &[&str]) -> Result<CascadeDetectionResult> {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
info!(
|
||||
"[Cascade] Starting detection for {:?} with {} object classes (threshold: {:.2})",
|
||||
image_path,
|
||||
objects.len(),
|
||||
self.clip_threshold
|
||||
);
|
||||
|
||||
let clip_result = self.run_clip_detection(image_path, objects).await?;
|
||||
|
||||
let max_clip_confidence = clip_result
|
||||
.iter()
|
||||
.map(|p| p.confidence)
|
||||
.fold(0.0_f32, |max, val| if val > max { val } else { max });
|
||||
|
||||
debug!(
|
||||
"[Cascade] CLIP max confidence: {:.3} (threshold: {:.2})",
|
||||
max_clip_confidence,
|
||||
self.clip_threshold
|
||||
);
|
||||
|
||||
if max_clip_confidence > self.clip_threshold {
|
||||
info!(
|
||||
"[Cascade] High confidence ({:.3} > {:.2}) → triggering Qwen3-VL",
|
||||
max_clip_confidence,
|
||||
self.clip_threshold
|
||||
);
|
||||
|
||||
let qwenvl_result = self.run_qwenvl_detection(image_path, objects).await?;
|
||||
|
||||
let processing_time = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
return Ok(CascadeDetectionResult {
|
||||
detections: qwenvl_result,
|
||||
model_used: "qwen3vl".to_string(),
|
||||
clip_confidence: max_clip_confidence,
|
||||
qwenvl_used: true,
|
||||
processing_time_ms: processing_time,
|
||||
});
|
||||
}
|
||||
|
||||
info!(
|
||||
"[Cascade] Low confidence ({:.3} <= {:.2}) → using CLIP results only",
|
||||
max_clip_confidence,
|
||||
self.clip_threshold
|
||||
);
|
||||
|
||||
let processing_time = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
Ok(CascadeDetectionResult {
|
||||
detections: clip_result,
|
||||
model_used: "clip".to_string(),
|
||||
clip_confidence: max_clip_confidence,
|
||||
qwenvl_used: false,
|
||||
processing_time_ms: processing_time,
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_clip_detection(&self, image_path: &Path, objects: &[&str]) -> Result<Vec<ClipPrediction>> {
|
||||
let image_path_str = image_path.display().to_string();
|
||||
|
||||
debug!("[Cascade] Running CLIP detection for {:?}", image_path);
|
||||
|
||||
let predictions = detect_objects(&image_path_str, objects, None, None)
|
||||
.await
|
||||
.context("CLIP detection failed")?;
|
||||
|
||||
debug!(
|
||||
"[Cascade] CLIP detected {} objects",
|
||||
predictions.len()
|
||||
);
|
||||
|
||||
Ok(predictions)
|
||||
}
|
||||
|
||||
async fn run_qwenvl_detection(&self, image_path: &Path, objects: &[&str]) -> Result<Vec<ClipPrediction>> {
|
||||
let image_path_str = image_path.display().to_string();
|
||||
|
||||
debug!("[Cascade] Running Qwen3-VL detection for {:?}", image_path);
|
||||
|
||||
self.qwen_vl_manager.ensure_running().await?;
|
||||
|
||||
let prompt = self.build_detection_prompt(objects);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("http://localhost:{}/v1/chat/completions", self.qwen_vl_manager.get_port());
|
||||
|
||||
let request_body = serde_json::json!({
|
||||
"model": "Qwen3VL-8B-Instruct-Q8_0",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": prompt
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": format!("file://{}", image_path_str)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"max_tokens": 500,
|
||||
"temperature": 0.1
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.json(&request_body)
|
||||
.timeout(QWENVL_TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.context("Qwen3-VL API request failed")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
warn!("[Cascade] Qwen3-VL API error: {}", response.status());
|
||||
anyhow::bail!("Qwen3-VL API returned error: {}", response.status());
|
||||
}
|
||||
|
||||
let response_json: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse Qwen3-VL response")?;
|
||||
|
||||
let content = response_json
|
||||
.get("choices")
|
||||
.and_then(|choices| choices.get(0))
|
||||
.and_then(|choice| choice.get("message"))
|
||||
.and_then(|message| message.get("content"))
|
||||
.and_then(|content| content.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
debug!("[Cascade] Qwen3-VL response: {}", content);
|
||||
|
||||
let detections = self.parse_qwenvl_response(content, objects);
|
||||
|
||||
self.qwen_vl_manager.update_last_request_time().await;
|
||||
|
||||
info!(
|
||||
"[Cascade] Qwen3-VL detected {} objects",
|
||||
detections.len()
|
||||
);
|
||||
|
||||
Ok(detections)
|
||||
}
|
||||
|
||||
fn build_detection_prompt(&self, objects: &[&str]) -> String {
|
||||
let object_list = objects.join(", ");
|
||||
|
||||
format!(
|
||||
"Analyze this image and detect the following objects: {}.\n\
|
||||
For each detected object, provide:\n\
|
||||
1. The object name\n\
|
||||
2. A confidence score (0.0 to 1.0)\n\
|
||||
3. A brief description of what you see\n\
|
||||
\n\
|
||||
Format your response as JSON:\n\
|
||||
{{\"detections\": [{{\"label\": \"object_name\", \"confidence\": 0.95, \"description\": \"brief description\"}}]}}\n\
|
||||
\n\
|
||||
If no objects are detected, return: {{\"detections\": []}}\n\
|
||||
\n\
|
||||
IMPORTANT: Only detect objects that are clearly visible and identifiable. Do not guess or hallucinate.",
|
||||
object_list
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_qwenvl_response(&self, content: &str, _objects: &[&str]) -> Vec<ClipPrediction> {
|
||||
let json_start = content.find('{');
|
||||
let json_end = content.rfind('}');
|
||||
|
||||
if json_start.is_none() || json_end.is_none() {
|
||||
debug!("[Cascade] No JSON found in Qwen3-VL response");
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let json_str = &content[json_start.unwrap()..=json_end.unwrap()];
|
||||
|
||||
let parsed: serde_json::Value = serde_json::from_str(json_str)
|
||||
.unwrap_or(serde_json::json!({"detections": []}));
|
||||
|
||||
let detections = parsed
|
||||
.get("detections")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|arr| arr.clone())
|
||||
.unwrap_or_else(|| Vec::new());
|
||||
|
||||
detections
|
||||
.iter()
|
||||
.filter_map(|d| {
|
||||
let label = d.get("label").and_then(|l| l.as_str()).unwrap_or("");
|
||||
let confidence = d.get("confidence").and_then(|c| c.as_f64()).unwrap_or(0.0) as f32;
|
||||
|
||||
if !label.is_empty() && confidence > 0.0 {
|
||||
Some(ClipPrediction {
|
||||
label: label.to_string(),
|
||||
confidence,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CascadeVisionProcessor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_detection_prompt() {
|
||||
let processor = CascadeVisionProcessor::new();
|
||||
let objects = vec!["gun", "weapon", "person"];
|
||||
let prompt = processor.build_detection_prompt(&objects);
|
||||
|
||||
assert!(prompt.contains("gun, weapon, person"));
|
||||
assert!(prompt.contains("confidence score"));
|
||||
assert!(prompt.contains("JSON"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_qwenvl_response() {
|
||||
let processor = CascadeVisionProcessor::new();
|
||||
let response = "{\"detections\": [{\"label\": \"gun\", \"confidence\": 0.95, \"description\": \"a handgun\"}]}";
|
||||
let objects = vec!["gun"];
|
||||
|
||||
let detections = processor.parse_qwenvl_response(response, &objects);
|
||||
|
||||
assert_eq!(detections.len(), 1);
|
||||
assert_eq!(detections[0].label, "gun");
|
||||
assert!((detections[0].confidence - 0.95).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_response() {
|
||||
let processor = CascadeVisionProcessor::new();
|
||||
let response = "{\"detections\": []}";
|
||||
let objects = vec!["gun"];
|
||||
|
||||
let detections = processor.parse_qwenvl_response(response, &objects);
|
||||
|
||||
assert_eq!(detections.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_json() {
|
||||
let processor = CascadeVisionProcessor::new();
|
||||
let response = "This is not JSON";
|
||||
let objects = vec!["gun"];
|
||||
|
||||
let detections = processor.parse_qwenvl_response(response, &objects);
|
||||
|
||||
assert_eq!(detections.len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::executor::PythonExecutor;
|
||||
|
||||
const CLIP_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// CLIP classification prediction
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ClipPrediction {
|
||||
pub label: String,
|
||||
pub confidence: f32,
|
||||
}
|
||||
|
||||
/// CLIP classification result for a single image
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ClipImageResult {
|
||||
pub image_path: String,
|
||||
pub predictions: Vec<ClipPrediction>,
|
||||
}
|
||||
|
||||
/// CLIP object detection result
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ClipDetectionResult {
|
||||
pub image_path: String,
|
||||
pub detected_objects: Vec<ClipPrediction>,
|
||||
}
|
||||
|
||||
/// Classify a single image with given labels
|
||||
pub async fn classify_image(
|
||||
image_path: &str,
|
||||
labels: &[&str],
|
||||
top_k: Option<usize>,
|
||||
model_name: Option<&str>,
|
||||
) -> Result<Vec<ClipPrediction>> {
|
||||
let executor = PythonExecutor::new()?;
|
||||
let script_path = executor.script_path("clip_classifier.py");
|
||||
|
||||
if !script_path.exists() {
|
||||
anyhow::bail!("clip_classifier.py not found at {:?}", script_path);
|
||||
}
|
||||
|
||||
let top_k = top_k.unwrap_or(5);
|
||||
let model = model_name.unwrap_or("openai/clip-vit-base-patch32");
|
||||
|
||||
let mut args = vec![
|
||||
image_path.to_string(),
|
||||
"--labels".to_string(),
|
||||
labels.join(","),
|
||||
"--top-k".to_string(),
|
||||
top_k.to_string(),
|
||||
"--model".to_string(),
|
||||
model.to_string(),
|
||||
];
|
||||
|
||||
let output_path = format!("{}.clip.json", image_path);
|
||||
args.push("--output".to_string());
|
||||
args.push(output_path.clone());
|
||||
|
||||
tracing::info!(
|
||||
"[CLIP] Classifying image: {} with {} labels",
|
||||
image_path,
|
||||
labels.len()
|
||||
);
|
||||
|
||||
executor
|
||||
.run(
|
||||
"clip_classifier.py",
|
||||
&args.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
||||
None,
|
||||
"CLIP",
|
||||
Some(CLIP_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
.context("Failed to run CLIP classifier")?;
|
||||
|
||||
let json_str = std::fs::read_to_string(&output_path)
|
||||
.context("Failed to read CLIP output")?;
|
||||
|
||||
let results: std::collections::HashMap<String, Vec<ClipPrediction>> =
|
||||
serde_json::from_str(&json_str)
|
||||
.context("Failed to parse CLIP output")?;
|
||||
|
||||
let predictions = results
|
||||
.get(image_path)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
tracing::info!(
|
||||
"[CLIP] Top prediction: {} ({:.3})",
|
||||
predictions.first().map(|p| p.label.as_str()).unwrap_or("none"),
|
||||
predictions.first().map(|p| p.confidence).unwrap_or(0.0)
|
||||
);
|
||||
|
||||
Ok(predictions)
|
||||
}
|
||||
|
||||
/// Detect objects in an image
|
||||
pub async fn detect_objects(
|
||||
image_path: &str,
|
||||
objects: &[&str],
|
||||
threshold: Option<f32>,
|
||||
model_name: Option<&str>,
|
||||
) -> Result<Vec<ClipPrediction>> {
|
||||
let executor = PythonExecutor::new()?;
|
||||
let script_path = executor.script_path("clip_classifier.py");
|
||||
|
||||
if !script_path.exists() {
|
||||
anyhow::bail!("clip_classifier.py not found at {:?}", script_path);
|
||||
}
|
||||
|
||||
let threshold = threshold.unwrap_or(0.15);
|
||||
let model = model_name.unwrap_or("openai/clip-vit-base-patch32");
|
||||
|
||||
let mut args = vec![
|
||||
image_path.to_string(),
|
||||
"--detect".to_string(),
|
||||
objects.join(","),
|
||||
"--threshold".to_string(),
|
||||
threshold.to_string(),
|
||||
"--model".to_string(),
|
||||
model.to_string(),
|
||||
];
|
||||
|
||||
let output_path = format!("{}.clip.json", image_path);
|
||||
args.push("--output".to_string());
|
||||
args.push(output_path.clone());
|
||||
|
||||
tracing::info!(
|
||||
"[CLIP] Detecting {} objects in: {} (threshold: {:.2})",
|
||||
objects.len(),
|
||||
image_path,
|
||||
threshold
|
||||
);
|
||||
|
||||
executor
|
||||
.run(
|
||||
"clip_classifier.py",
|
||||
&args.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
||||
None,
|
||||
"CLIP",
|
||||
Some(CLIP_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
.context("Failed to run CLIP object detection")?;
|
||||
|
||||
let json_str = std::fs::read_to_string(&output_path)
|
||||
.context("Failed to read CLIP output")?;
|
||||
|
||||
let results: std::collections::HashMap<String, Vec<ClipPrediction>> =
|
||||
serde_json::from_str(&json_str)
|
||||
.context("Failed to parse CLIP output")?;
|
||||
|
||||
let detected = results
|
||||
.get(image_path)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
if !detected.is_empty() {
|
||||
tracing::info!(
|
||||
"[CLIP] Detected {} objects: {}",
|
||||
detected.len(),
|
||||
detected.iter().map(|p| p.label.as_str()).collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
} else {
|
||||
tracing::info!("[CLIP] No objects detected above threshold {:.2}", threshold);
|
||||
}
|
||||
|
||||
Ok(detected)
|
||||
}
|
||||
|
||||
/// Batch classify multiple images
|
||||
pub async fn classify_images(
|
||||
image_paths: &[&str],
|
||||
labels: &[&str],
|
||||
top_k: Option<usize>,
|
||||
model_name: Option<&str>,
|
||||
) -> Result<Vec<ClipImageResult>> {
|
||||
let executor = PythonExecutor::new()?;
|
||||
let script_path = executor.script_path("clip_classifier.py");
|
||||
|
||||
if !script_path.exists() {
|
||||
anyhow::bail!("clip_classifier.py not found at {:?}", script_path);
|
||||
}
|
||||
|
||||
let top_k = top_k.unwrap_or(5);
|
||||
let model = model_name.unwrap_or("openai/clip-vit-base-patch32");
|
||||
|
||||
// Create temp file with image paths
|
||||
let temp_file = format!("/tmp/clip_batch_{}.txt", uuid::Uuid::new_v4());
|
||||
std::fs::write(&temp_file, image_paths.join("\n"))
|
||||
.context("Failed to write batch file")?;
|
||||
|
||||
let mut args = vec![
|
||||
temp_file.clone(),
|
||||
"--batch".to_string(),
|
||||
"--labels".to_string(),
|
||||
labels.join(","),
|
||||
"--top-k".to_string(),
|
||||
top_k.to_string(),
|
||||
"--model".to_string(),
|
||||
model.to_string(),
|
||||
];
|
||||
|
||||
let output_path = format!("/tmp/clip_batch_{}.json", uuid::Uuid::new_v4());
|
||||
args.push("--output".to_string());
|
||||
args.push(output_path.clone());
|
||||
|
||||
tracing::info!(
|
||||
"[CLIP] Batch classifying {} images with {} labels",
|
||||
image_paths.len(),
|
||||
labels.len()
|
||||
);
|
||||
|
||||
executor
|
||||
.run(
|
||||
"clip_classifier.py",
|
||||
&args.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
||||
None,
|
||||
"CLIP",
|
||||
Some(CLIP_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
.context("Failed to run batch CLIP classification")?;
|
||||
|
||||
let json_str = std::fs::read_to_string(&output_path)
|
||||
.context("Failed to read CLIP batch output")?;
|
||||
|
||||
let results_map: std::collections::HashMap<String, Vec<ClipPrediction>> =
|
||||
serde_json::from_str(&json_str)
|
||||
.context("Failed to parse CLIP batch output")?;
|
||||
|
||||
let results: Vec<ClipImageResult> = image_paths
|
||||
.iter()
|
||||
.map(|path| ClipImageResult {
|
||||
image_path: path.to_string(),
|
||||
predictions: results_map.get(*path).cloned().unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Cleanup temp files
|
||||
let _ = std::fs::remove_file(&temp_file);
|
||||
let _ = std::fs::remove_file(&output_path);
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clip_prediction_serialization() {
|
||||
let pred = ClipPrediction {
|
||||
label: "person in room".to_string(),
|
||||
confidence: 0.876,
|
||||
};
|
||||
let json = serde_json::to_string(&pred).unwrap();
|
||||
assert!(json.contains("person in room"));
|
||||
assert!(json.contains("0.876"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clip_prediction_deserialization() {
|
||||
let json = r#"{"label":"outdoor scene","confidence":0.945}"#;
|
||||
let pred: ClipPrediction = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(pred.label, "outdoor scene");
|
||||
assert!((pred.confidence - 0.945).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clip_image_result() {
|
||||
let result = ClipImageResult {
|
||||
image_path: "/test/image.jpg".to_string(),
|
||||
predictions: vec![
|
||||
ClipPrediction {
|
||||
label: "indoor".to_string(),
|
||||
confidence: 0.92,
|
||||
},
|
||||
ClipPrediction {
|
||||
label: "outdoor".to_string(),
|
||||
confidence: 0.08,
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(result.predictions.len(), 2);
|
||||
assert_eq!(result.predictions[0].label, "indoor");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod asr;
|
||||
pub mod asrx;
|
||||
pub mod caption;
|
||||
pub mod cascade_vision;
|
||||
pub mod clip;
|
||||
pub mod cut;
|
||||
pub mod executor;
|
||||
pub mod face;
|
||||
@@ -16,6 +18,8 @@ pub mod yolo;
|
||||
pub use asr::{process_asr, AsrResult, AsrSegment};
|
||||
pub use asrx::{process_asrx, AsrxResult, AsrxSegment};
|
||||
pub use caption::{process_caption, CaptionResult, CaptionSummary, FrameCaption};
|
||||
pub use cascade_vision::{CascadeDetectionResult, CascadeVisionProcessor};
|
||||
pub use clip::{classify_image, classify_images, detect_objects, ClipDetectionResult, ClipImageResult, ClipPrediction};
|
||||
pub use cut::{process_cut, CutResult, CutScene};
|
||||
pub use executor::{validate_python_env, PythonExecutor, RetryConfig};
|
||||
pub use face::{process_face, Face, FaceFrame, FaceResult};
|
||||
|
||||
Reference in New Issue
Block a user