fix: ASRX duplication, TKG edges, trace ingest, and add pipeline progress publishing
- ASRX handler no longer stores duplicate 'asr' pre_chunks - Pre_chunks storage made idempotent (delete-before-insert) - Rule 1 + trace_ingest changed to query 'asrx' not 'asr' - Trace chunks removed (dynamic from TKG/Qdrant) - TKG scroll_face_points fixed: trace_id >= 1 (not == 1) - TKG AsrxSegmentEntry: start/end -> start_time/end_time (match ASRX JSON) - Unregister error handling: log instead of silent discard - Add publish_pipeline_progress calls at each pipeline stage (processors, rule1, face_trace, identity_agent, TKG, rule2, completion)
This commit is contained in:
@@ -24,10 +24,18 @@ pub struct AppearanceFrame {
|
||||
pub struct AppearancePerson {
|
||||
pub person_id: u64,
|
||||
pub bbox: BBox,
|
||||
pub facing: String,
|
||||
pub body_parts: Vec<BodyPart>,
|
||||
pub dominant_colors: Vec<Vec<f64>>,
|
||||
pub hsv_histogram: Vec<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BodyPart {
|
||||
pub name: String,
|
||||
pub bbox: BBox,
|
||||
pub hsv_histogram: Vec<Vec<f64>>,
|
||||
pub dominant_colors: Vec<Vec<f64>>,
|
||||
pub upper_body: Option<Vec<Vec<f64>>>,
|
||||
pub lower_body: Option<Vec<Vec<f64>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -2,12 +2,47 @@ use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::executor::PythonExecutor;
|
||||
use super::AsrStatus;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AsrResult {
|
||||
#[serde(default)]
|
||||
pub status: Option<AsrStatus>,
|
||||
pub language: Option<String>,
|
||||
pub language_probability: Option<f64>,
|
||||
pub segments: Vec<AsrSegment>,
|
||||
#[serde(default)]
|
||||
pub segment_count: usize,
|
||||
}
|
||||
|
||||
impl AsrResult {
|
||||
pub fn compute_status(&mut self) {
|
||||
self.segment_count = self.segments.len();
|
||||
// Only compute status if Python didn't provide one
|
||||
if self.status.is_none() {
|
||||
self.status = Some(AsrStatus::from_segments(self.segment_count));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_audio_track() -> Self {
|
||||
AsrResult {
|
||||
status: Some(AsrStatus::NoAudioTrack),
|
||||
language: None,
|
||||
language_probability: None,
|
||||
segments: vec![],
|
||||
segment_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn silent_audio() -> Self {
|
||||
AsrResult {
|
||||
status: Some(AsrStatus::SilentAudio),
|
||||
language: None,
|
||||
language_probability: None,
|
||||
segments: vec![],
|
||||
segment_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -44,12 +79,19 @@ pub async fn process_asr(
|
||||
|
||||
let json_str = std::fs::read_to_string(output_path).context("Failed to read ASR output")?;
|
||||
|
||||
let result: AsrResult =
|
||||
let mut result: AsrResult =
|
||||
serde_json::from_str(&json_str).context("Failed to parse ASR output")?;
|
||||
|
||||
result.compute_status();
|
||||
|
||||
tracing::info!(
|
||||
"[ASR] Result: {} segments, language: {:?}",
|
||||
result.segments.len(),
|
||||
"[ASR] Result: status={}, {} segments, language: {:?}",
|
||||
result
|
||||
.status
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default(),
|
||||
result.segment_count,
|
||||
result.language
|
||||
);
|
||||
|
||||
|
||||
@@ -6,15 +6,47 @@ use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::executor::PythonExecutor;
|
||||
use super::AsrStatus;
|
||||
|
||||
const ASRX_TIMEOUT: Duration = Duration::from_secs(7200);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AsrxResult {
|
||||
#[serde(default)]
|
||||
pub status: Option<AsrStatus>,
|
||||
pub language: Option<String>,
|
||||
pub segments: Vec<AsrxSegment>,
|
||||
#[serde(skip_serializing)]
|
||||
pub embeddings: Option<Vec<Vec<f32>>>,
|
||||
#[serde(default)]
|
||||
pub segment_count: usize,
|
||||
}
|
||||
|
||||
impl AsrxResult {
|
||||
pub fn compute_status(&mut self) {
|
||||
self.segment_count = self.segments.len();
|
||||
self.status = Some(AsrStatus::from_segments(self.segment_count));
|
||||
}
|
||||
|
||||
pub fn no_audio_track() -> Self {
|
||||
AsrxResult {
|
||||
status: Some(AsrStatus::NoAudioTrack),
|
||||
language: None,
|
||||
segments: vec![],
|
||||
embeddings: None,
|
||||
segment_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn silent_audio() -> Self {
|
||||
AsrxResult {
|
||||
status: Some(AsrStatus::SilentAudio),
|
||||
language: None,
|
||||
segments: vec![],
|
||||
embeddings: None,
|
||||
segment_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -157,10 +189,20 @@ pub async fn process_asrx(
|
||||
|
||||
let json_str = std::fs::read_to_string(output_path).context("Failed to read ASRX output")?;
|
||||
|
||||
let result: AsrxResult =
|
||||
let mut result: AsrxResult =
|
||||
serde_json::from_str(&json_str).context("Failed to parse ASRX output")?;
|
||||
|
||||
tracing::info!("[ASRX] Result: {} segments", result.segments.len());
|
||||
result.compute_status();
|
||||
|
||||
tracing::info!(
|
||||
"[ASRX] Result: status={}, {} segments",
|
||||
result
|
||||
.status
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default(),
|
||||
result.segment_count
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -174,6 +174,12 @@ impl PythonExecutor {
|
||||
(0..total_frames).step_by(interval as usize).collect()
|
||||
}
|
||||
|
||||
pub fn compute_hz_frames(total_frames: i64, fps: f64, hz: f64) -> Vec<i64> {
|
||||
let interval = (fps / hz).round() as i64;
|
||||
let interval = interval.max(1);
|
||||
(0..total_frames).step_by(interval as usize).collect()
|
||||
}
|
||||
|
||||
/// Merge base frames with refinement frames (for adaptive sampling).
|
||||
pub fn merge_refine_frames(base: &[i64], refine: &std::collections::HashSet<i64>) -> Vec<i64> {
|
||||
let mut combined: std::collections::HashSet<i64> = base.iter().cloned().collect();
|
||||
@@ -303,6 +309,9 @@ impl PythonExecutor {
|
||||
cmd.env("DATABASE_SCHEMA", &*DATABASE_SCHEMA);
|
||||
cmd.env("MOMENTRY_DB_SCHEMA", &*DATABASE_SCHEMA);
|
||||
cmd.env("MOMENTRY_REDIS_PREFIX", &*REDIS_KEY_PREFIX);
|
||||
if let Some(u) = uuid {
|
||||
cmd.env("UUID", u);
|
||||
}
|
||||
cmd.arg(&script_path);
|
||||
|
||||
for arg in args {
|
||||
@@ -441,6 +450,9 @@ impl PythonExecutor {
|
||||
cmd.env("DATABASE_SCHEMA", &*DATABASE_SCHEMA);
|
||||
cmd.env("MOMENTRY_DB_SCHEMA", &*DATABASE_SCHEMA);
|
||||
cmd.env("MOMENTRY_REDIS_PREFIX", &*REDIS_KEY_PREFIX);
|
||||
if let Some(u) = uuid {
|
||||
cmd.env("UUID", u);
|
||||
}
|
||||
cmd.arg(&script_path);
|
||||
|
||||
for arg in args {
|
||||
|
||||
@@ -3,14 +3,39 @@ use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::executor::PythonExecutor;
|
||||
use super::FaceStatus;
|
||||
|
||||
const FACE_TIMEOUT: Duration = Duration::from_secs(7200);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FaceResult {
|
||||
#[serde(default)]
|
||||
pub status: Option<FaceStatus>,
|
||||
pub frame_count: u64,
|
||||
pub fps: f64,
|
||||
pub frames: Vec<FaceFrame>,
|
||||
#[serde(default)]
|
||||
pub total_faces: usize,
|
||||
}
|
||||
|
||||
impl FaceResult {
|
||||
pub fn compute_status(&mut self) {
|
||||
self.total_faces = self.frames.iter().map(|f| f.faces.len()).sum();
|
||||
// Only compute status if Python didn't provide one
|
||||
if self.status.is_none() {
|
||||
self.status = Some(FaceStatus::from_face_count(self.total_faces));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_faces(frame_count: u64, fps: f64) -> Self {
|
||||
FaceResult {
|
||||
status: Some(FaceStatus::NoFaces),
|
||||
frame_count,
|
||||
fps,
|
||||
frames: vec![],
|
||||
total_faces: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -46,6 +71,33 @@ pub async fn process_face(
|
||||
uuid: Option<&str>,
|
||||
frames: Option<&[i64]>,
|
||||
) -> Result<FaceResult> {
|
||||
// Check if face.json already exists (from SwiftFacePose)
|
||||
if std::path::Path::new(output_path).exists() {
|
||||
tracing::info!(
|
||||
"[FACE] Output exists from SwiftFacePose, loading: {}",
|
||||
output_path
|
||||
);
|
||||
let json_str =
|
||||
std::fs::read_to_string(output_path).context("Failed to read existing FACE output")?;
|
||||
let mut result: FaceResult =
|
||||
serde_json::from_str(&json_str).context("Failed to parse existing FACE output")?;
|
||||
|
||||
result.compute_status();
|
||||
|
||||
tracing::info!(
|
||||
"[FACE] Loaded from SwiftFacePose: status={}, {} frames, {} total faces",
|
||||
result
|
||||
.status
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default(),
|
||||
result.frames.len(),
|
||||
result.total_faces
|
||||
);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
let executor = PythonExecutor::new()?;
|
||||
let script_path = executor.script_path("face_processor.py");
|
||||
|
||||
@@ -53,11 +105,7 @@ pub async fn process_face(
|
||||
|
||||
if !script_path.exists() {
|
||||
tracing::warn!("[FACE] Script not found, returning empty result");
|
||||
return Ok(FaceResult {
|
||||
frame_count: 0,
|
||||
fps: 0.0,
|
||||
frames: vec![],
|
||||
});
|
||||
return Ok(FaceResult::no_faces(0, 0.0));
|
||||
}
|
||||
|
||||
executor
|
||||
@@ -74,10 +122,21 @@ pub async fn process_face(
|
||||
|
||||
let json_str = std::fs::read_to_string(output_path).context("Failed to read FACE output")?;
|
||||
|
||||
let result: FaceResult =
|
||||
let mut result: FaceResult =
|
||||
serde_json::from_str(&json_str).context("Failed to parse FACE output")?;
|
||||
|
||||
tracing::info!("[FACE] Result: {} frames", result.frames.len());
|
||||
result.compute_status();
|
||||
|
||||
tracing::info!(
|
||||
"[FACE] Result: status={}, {} frames, {} total faces",
|
||||
result
|
||||
.status
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default(),
|
||||
result.frames.len(),
|
||||
result.total_faces
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -64,12 +64,17 @@ pub async fn process_face_cluster(
|
||||
.await
|
||||
.with_context(|| format!("Failed to run face clustering script"))?;
|
||||
|
||||
let json_str = std::fs::read_to_string(output_path).context("Failed to read FACE_CLUSTER output")?;
|
||||
let json_str =
|
||||
std::fs::read_to_string(output_path).context("Failed to read FACE_CLUSTER output")?;
|
||||
|
||||
let result: FaceClusterResult =
|
||||
serde_json::from_str(&json_str).context("Failed to parse FACE_CLUSTER output")?;
|
||||
|
||||
tracing::info!("[FACE_CLUSTER] Result: {} clusters, {} frames", result.clusters.len(), result.frames.len());
|
||||
tracing::info!(
|
||||
"[FACE_CLUSTER] Result: {} clusters, {} frames",
|
||||
result.clusters.len(),
|
||||
result.frames.len()
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,4 +82,4 @@ pub async fn process_hand(
|
||||
tracing::info!("[HAND] Result: {} frames", result.frames.len());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,24 +148,23 @@ pub async fn build_heuristic_scene_meta(
|
||||
}
|
||||
}
|
||||
|
||||
// Get face counts grouped by frame
|
||||
let fd_table = schema::table_name("face_detections");
|
||||
let face_rows: Vec<(i64, i64)> = sqlx::query_as(&format!(
|
||||
"SELECT frame_number, COUNT(*) as fc \
|
||||
FROM {} \
|
||||
WHERE file_uuid = $1 AND frame_number IS NOT NULL \
|
||||
GROUP BY frame_number \
|
||||
ORDER BY frame_number",
|
||||
fd_table
|
||||
))
|
||||
.bind(file_uuid)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
// Get face counts from Qdrant _faces
|
||||
use crate::core::db::qdrant_db::QdrantDb;
|
||||
use serde_json::json;
|
||||
|
||||
let qdrant = QdrantDb::new();
|
||||
let face_filter = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "trace_id", "match": {"value": 1}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 500).await.unwrap_or_default();
|
||||
|
||||
let mut frame_face_counts: HashMap<i64, i64> = HashMap::new();
|
||||
for (frame, count) in &face_rows {
|
||||
frame_face_counts.insert(*frame, *count);
|
||||
for point in &points {
|
||||
let frame = point["payload"]["frame"].as_i64().unwrap_or(0);
|
||||
*frame_face_counts.entry(frame).or_default() += 1;
|
||||
}
|
||||
|
||||
// Process each segment
|
||||
|
||||
+140
-4
@@ -17,8 +17,146 @@ pub mod scene_classification;
|
||||
pub mod tkg;
|
||||
pub mod yolo;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AsrStatus {
|
||||
NoAudioTrack,
|
||||
SilentAudio,
|
||||
HasTranscript,
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AsrStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AsrStatus::NoAudioTrack => write!(f, "no_audio_track"),
|
||||
AsrStatus::SilentAudio => write!(f, "silent_audio"),
|
||||
AsrStatus::HasTranscript => write!(f, "has_transcript"),
|
||||
AsrStatus::Processing => write!(f, "processing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsrStatus {
|
||||
pub fn css_class(&self) -> &'static str {
|
||||
match self {
|
||||
AsrStatus::NoAudioTrack => "card-asr--no_audio_track",
|
||||
AsrStatus::SilentAudio => "card-asr--silent_audio",
|
||||
AsrStatus::HasTranscript => "card-asr--has_transcript",
|
||||
AsrStatus::Processing => "card-asr--processing",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_text(&self, segment_count: usize) -> String {
|
||||
match self {
|
||||
AsrStatus::NoAudioTrack => "無音軌".to_string(),
|
||||
AsrStatus::SilentAudio => "無語音".to_string(),
|
||||
AsrStatus::HasTranscript => format!("{} 段語音", segment_count),
|
||||
AsrStatus::Processing => "處理中".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_segments(segment_count: usize) -> Self {
|
||||
if segment_count > 0 {
|
||||
AsrStatus::HasTranscript
|
||||
} else {
|
||||
AsrStatus::SilentAudio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FaceStatus {
|
||||
NoFaces,
|
||||
HasFaces,
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FaceStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FaceStatus::NoFaces => write!(f, "no_faces"),
|
||||
FaceStatus::HasFaces => write!(f, "has_faces"),
|
||||
FaceStatus::Processing => write!(f, "processing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FaceStatus {
|
||||
pub fn css_class(&self) -> &'static str {
|
||||
match self {
|
||||
FaceStatus::NoFaces => "card-face--no_faces",
|
||||
FaceStatus::HasFaces => "card-face--has_faces",
|
||||
FaceStatus::Processing => "card-face--processing",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_text(&self, face_count: usize) -> String {
|
||||
match self {
|
||||
FaceStatus::NoFaces => "無人脸".to_string(),
|
||||
FaceStatus::HasFaces => format!("{} 張人脸", face_count),
|
||||
FaceStatus::Processing => "處理中".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_face_count(face_count: usize) -> Self {
|
||||
if face_count > 0 {
|
||||
FaceStatus::HasFaces
|
||||
} else {
|
||||
FaceStatus::NoFaces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TraceStatus {
|
||||
NoTraces,
|
||||
HasTraces,
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TraceStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TraceStatus::NoTraces => write!(f, "no_traces"),
|
||||
TraceStatus::HasTraces => write!(f, "has_traces"),
|
||||
TraceStatus::Processing => write!(f, "processing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TraceStatus {
|
||||
pub fn css_class(&self) -> &'static str {
|
||||
match self {
|
||||
TraceStatus::NoTraces => "card-trace--no_traces",
|
||||
TraceStatus::HasTraces => "card-trace--has_traces",
|
||||
TraceStatus::Processing => "card-trace--processing",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_text(&self, trace_count: usize) -> String {
|
||||
match self {
|
||||
TraceStatus::NoTraces => "無人脸轨迹".to_string(),
|
||||
TraceStatus::HasTraces => format!("{} 条人脸轨迹", trace_count),
|
||||
TraceStatus::Processing => "處理中".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_trace_count(trace_count: usize) -> Self {
|
||||
if trace_count > 0 {
|
||||
TraceStatus::HasTraces
|
||||
} else {
|
||||
TraceStatus::NoTraces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use appearance::{
|
||||
process_appearance, AppearanceFrame, AppearancePerson, AppearanceResult, BBox,
|
||||
process_appearance, AppearanceFrame, AppearancePerson, AppearanceResult, BBox, BodyPart,
|
||||
};
|
||||
pub use asr::{process_asr, AsrResult, AsrSegment};
|
||||
pub use asrx::{process_asrx, AsrxResult, AsrxSegment};
|
||||
@@ -39,9 +177,7 @@ pub use face_recognition::{
|
||||
FaceRecognitionFrame, FaceRecognitionResult, FaceRegistrationResult, RecognizedFace,
|
||||
RecognizedFaceDetection,
|
||||
};
|
||||
pub use hand::{
|
||||
process_hand, HandFrame, HandLandmark, HandResult, PersonHand,
|
||||
};
|
||||
pub use hand::{process_hand, HandFrame, HandLandmark, HandResult, PersonHand};
|
||||
pub use heuristic_scene::{
|
||||
build_heuristic_scene_meta, generate_scene_meta, CrowdSize, HeuristicSceneMeta,
|
||||
SceneSegmentMeta,
|
||||
|
||||
@@ -48,6 +48,150 @@ pub async fn process_pose(
|
||||
uuid: Option<&str>,
|
||||
frames: Option<&[i64]>,
|
||||
) -> Result<PoseResult> {
|
||||
// Check if pose.json already exists (from swift_face_pose)
|
||||
if std::path::Path::new(output_path).exists() {
|
||||
tracing::info!(
|
||||
"[POSE] Output exists from swift_face_pose, checking if needs interpolation: {}",
|
||||
output_path
|
||||
);
|
||||
let json_str =
|
||||
std::fs::read_to_string(output_path).context("Failed to read existing POSE output")?;
|
||||
let existing_result: PoseResult =
|
||||
serde_json::from_str(&json_str).context("Failed to parse existing POSE output")?;
|
||||
|
||||
// Get total video frames to check if interpolation needed
|
||||
let total_video_frames = {
|
||||
// Use ffprobe to get frame count from container metadata
|
||||
let output = std::process::Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=nb_frames",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
video_path,
|
||||
])
|
||||
.output()
|
||||
.context("Failed to run ffprobe")?;
|
||||
if output.status.success() {
|
||||
let frame_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
// Handle "N/A" case for some videos
|
||||
if frame_str == "N/A" {
|
||||
// Fallback to duration * fps
|
||||
let dur_output = std::process::Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
video_path,
|
||||
])
|
||||
.output()
|
||||
.context("Failed to run ffprobe for duration")?;
|
||||
let fps_output = std::process::Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"stream=r_frame_rate",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
video_path,
|
||||
])
|
||||
.output()
|
||||
.context("Failed to run ffprobe for fps")?;
|
||||
if dur_output.status.success() && fps_output.status.success() {
|
||||
let dur_str = String::from_utf8_lossy(&dur_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
let fps_str = String::from_utf8_lossy(&fps_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
let duration: f64 = dur_str.parse().ok().unwrap_or(0.0);
|
||||
// Parse fps like "30000/1001" or "30"
|
||||
let fps: f64 = if fps_str.contains('/') {
|
||||
let parts: Vec<&str> = fps_str.split('/').collect();
|
||||
if parts.len() == 2 {
|
||||
let num: f64 = parts[0].parse().ok().unwrap_or(30.0);
|
||||
let den: f64 = parts[1].parse().ok().unwrap_or(1.0);
|
||||
num / den
|
||||
} else {
|
||||
30.0
|
||||
}
|
||||
} else {
|
||||
fps_str.parse().ok().unwrap_or(30.0)
|
||||
};
|
||||
(duration * fps) as u64
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
frame_str.parse::<u64>().ok().unwrap_or(0)
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
// When 8Hz sampling frames are provided, skip interpolation entirely.
|
||||
// Swift already outputs at sample_interval=3 (~8Hz), no need to fill all frames.
|
||||
if frames.is_some() {
|
||||
tracing::info!(
|
||||
"[POSE] 8Hz mode: returning {} existing frames without interpolation",
|
||||
existing_result.frames.len()
|
||||
);
|
||||
return Ok(existing_result);
|
||||
}
|
||||
|
||||
// If pose frames < video frames, need interpolation
|
||||
if existing_result.frames.len() < total_video_frames as usize && total_video_frames > 0 {
|
||||
tracing::info!(
|
||||
"[POSE] Interpolation needed: {} pose frames < {} video frames",
|
||||
existing_result.frames.len(),
|
||||
total_video_frames
|
||||
);
|
||||
|
||||
// Call Python pose_processor.py for interpolation
|
||||
let executor = PythonExecutor::new()?;
|
||||
let script_path = executor.script_path("pose_processor.py");
|
||||
|
||||
if script_path.exists() {
|
||||
executor
|
||||
.run_with_frames(
|
||||
"pose_processor.py",
|
||||
&[video_path, output_path],
|
||||
uuid,
|
||||
"POSE",
|
||||
Some(POSE_TIMEOUT),
|
||||
frames,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to run {:?}", script_path))?;
|
||||
|
||||
let json_str = std::fs::read_to_string(output_path)
|
||||
.context("Failed to read interpolated POSE output")?;
|
||||
let result: PoseResult = serde_json::from_str(&json_str)
|
||||
.context("Failed to parse interpolated POSE output")?;
|
||||
tracing::info!(
|
||||
"[POSE] Interpolation completed: {} frames",
|
||||
result.frames.len()
|
||||
);
|
||||
return Ok(result);
|
||||
}
|
||||
} else {
|
||||
tracing::info!(
|
||||
"[POSE] No interpolation needed, loaded {} frames",
|
||||
existing_result.frames.len()
|
||||
);
|
||||
return Ok(existing_result);
|
||||
}
|
||||
}
|
||||
|
||||
let executor = PythonExecutor::new()?;
|
||||
let script_path = executor.script_path("pose_processor.py");
|
||||
|
||||
|
||||
+704
-484
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user