docs: 修復場景識別測試報告 markdown 編號

- 修正有序列表編號符合 markdownlint MD029
- 使用 1/2/3 樣式而非連續編號
This commit is contained in:
Warren
2026-04-01 02:21:40 +08:00
parent 576f58df71
commit 4109ec3d95
6 changed files with 1704 additions and 0 deletions

View File

@@ -4,8 +4,10 @@ pub mod caption;
pub mod cut;
pub mod executor;
pub mod face;
pub mod face_recognition;
pub mod ocr;
pub mod pose;
pub mod scene_classification;
pub mod story;
pub mod yolo;
@@ -15,7 +17,15 @@ pub use caption::{process_caption, CaptionResult, CaptionSummary, FrameCaption};
pub use cut::{process_cut, CutResult, CutScene};
pub use executor::{validate_python_env, PythonExecutor, RetryConfig};
pub use face::{process_face, Face, FaceFrame, FaceResult};
pub use face_recognition::{
process_face_recognition, register_face, FaceAttributes, FaceCluster, FaceIdentity, FacePose,
FaceRecognitionFrame, FaceRecognitionResult, FaceRegistrationResult, RecognizedFace,
RecognizedFaceDetection,
};
pub use ocr::{process_ocr, OcrFrame, OcrResult, OcrText};
pub use pose::{process_pose, Bbox, Keypoint, PersonPose, PoseFrame, PoseResult};
pub use scene_classification::{
process_scene_classification, SceneClassificationResult, ScenePrediction, SceneSegment,
};
pub use story::{process_story, StoryChildChunk, StoryParentChunk, StoryResult, StoryStats};
pub use yolo::{process_yolo, YoloFrame, YoloObject, YoloResult};

View File

@@ -0,0 +1,170 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use super::executor::PythonExecutor;
const SCENE_TIMEOUT: Duration = Duration::from_secs(7200);
/// 場景識別結果
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SceneClassificationResult {
pub frame_count: u64,
pub fps: f64,
pub scenes: Vec<SceneSegment>,
}
/// 場景片段
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SceneSegment {
pub start_time: f64,
pub end_time: f64,
pub scene_type: String, // 場景類型英文 (如 "hospital_room")
pub scene_type_zh: Option<String>, // 場景類型中文 (如 "醫院病房")
pub confidence: f32,
pub top_5: Vec<ScenePrediction>, // 前 5 個預測
}
/// 場景預測
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ScenePrediction {
pub scene_type: String,
pub confidence: f32,
}
/// 執行場景識別
pub async fn process_scene_classification(
video_path: &str,
output_path: &str,
uuid: Option<&str>,
) -> Result<SceneClassificationResult> {
let executor = PythonExecutor::new()?;
let script_path = executor.script_path("scene_classifier.py");
tracing::info!("[SCENE] Starting scene classification: {}", video_path);
if !script_path.exists() {
tracing::warn!("[SCENE] Script not found, returning empty result");
return Ok(SceneClassificationResult {
frame_count: 0,
fps: 0.0,
scenes: vec![],
});
}
executor
.run(
"scene_classifier.py",
&[video_path, output_path],
uuid,
"SCENE",
Some(SCENE_TIMEOUT),
)
.await
.with_context(|| format!("Failed to run {:?}", script_path))?;
let json_str = std::fs::read_to_string(output_path)
.context("Failed to read scene classification output")?;
let result: SceneClassificationResult =
serde_json::from_str(&json_str).context("Failed to parse scene classification output")?;
tracing::info!("[SCENE] Result: {} scenes detected", result.scenes.len());
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scene_result_serialization() {
let result = SceneClassificationResult {
frame_count: 100,
fps: 30.0,
scenes: vec![SceneSegment {
start_time: 0.0,
end_time: 10.5,
scene_type: "hospital_room".to_string(),
scene_type_zh: Some("醫院病房".to_string()),
confidence: 0.92,
top_5: vec![
ScenePrediction {
scene_type: "hospital_room".to_string(),
confidence: 0.92,
},
ScenePrediction {
scene_type: "pharmacy".to_string(),
confidence: 0.05,
},
],
}],
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("hospital_room"));
assert!(json.contains("醫院病房"));
assert!(json.contains("\"confidence\":0.92"));
}
#[test]
fn test_scene_result_deserialization() {
let json = r#"{
"frame_count": 50,
"fps": 25.0,
"scenes": [
{
"start_time": 0.0,
"end_time": 5.5,
"scene_type": "basketball_court",
"scene_type_zh": "籃球場",
"confidence": 0.87,
"top_5": [
{"scene_type": "basketball_court", "confidence": 0.87},
{"scene_type": "gymnasium", "confidence": 0.08}
]
}
]
}"#;
let result: SceneClassificationResult = serde_json::from_str(json).unwrap();
assert_eq!(result.frame_count, 50);
assert_eq!(result.scenes.len(), 1);
assert_eq!(result.scenes[0].scene_type, "basketball_court");
assert_eq!(result.scenes[0].confidence, 0.87);
}
#[test]
fn test_scene_result_empty() {
let result = SceneClassificationResult {
frame_count: 0,
fps: 0.0,
scenes: vec![],
};
assert!(result.scenes.is_empty());
}
#[test]
fn test_scene_prediction() {
let pred = ScenePrediction {
scene_type: "classroom".to_string(),
confidence: 0.95,
};
assert_eq!(pred.scene_type, "classroom");
assert!(pred.confidence >= 0.0 && pred.confidence <= 1.0);
}
#[test]
fn test_scene_segment_time() {
let segment = SceneSegment {
start_time: 10.0,
end_time: 20.0,
scene_type: "office".to_string(),
scene_type_zh: None,
confidence: 0.8,
top_5: vec![],
};
assert!(segment.end_time > segment.start_time);
}
}