update: pipeline, search, clip, embedding fixes
This commit is contained in:
@@ -51,6 +51,7 @@ pub fn bbox_routes() -> Router<crate::api::server::AppState> {
|
||||
)
|
||||
.route("/api/v1/file/:file_uuid/video", get(stream_video))
|
||||
.route("/api/v1/file/:file_uuid/thumbnail", get(face_thumbnail))
|
||||
.route("/api/v1/file/:file_uuid/clip", get(video_clip))
|
||||
}
|
||||
|
||||
/// 5×7 bitmap font — each character 5 wide × 7 tall
|
||||
@@ -198,35 +199,18 @@ async fn bbox_overlay_video(
|
||||
.fetch_all(state.db.pool()).await
|
||||
.unwrap_or_else(|e| { tracing::error!("bbox query error: {}", e); vec![] });
|
||||
|
||||
// Build filters
|
||||
// Build filters — each bbox enabled only on its frame
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
let mut is_first = true;
|
||||
for (frame, x, y, w, h, trace_id, _) in &rows {
|
||||
let text = format!("t{}", trace_id.unwrap_or(0));
|
||||
|
||||
if is_first {
|
||||
is_first = false;
|
||||
// Persistent bbox: thin pale red border
|
||||
parts.push(format!(
|
||||
"drawbox=x={}:y={}:w={}:h={}:color=red@0.3:thickness=4",
|
||||
x, y, w, h
|
||||
));
|
||||
// Always-on text: top-left of bbox with padding
|
||||
let tx = *x + 6;
|
||||
let ty = *y + 6;
|
||||
render_text(&mut parts, &text, tx, ty, None);
|
||||
} else {
|
||||
let offset = frame - start_f;
|
||||
// Per-frame bbox: thick bright red
|
||||
parts.push(format!(
|
||||
"drawbox=x={}:y={}:w={}:h={}:color=red@0.8:thickness=8:enable='eq(n,{})'",
|
||||
x, y, w, h, offset
|
||||
));
|
||||
// Per-frame text
|
||||
let tx = *x + 6;
|
||||
let ty = *y + 6;
|
||||
render_text(&mut parts, &text, tx, ty, Some(offset));
|
||||
}
|
||||
let offset = frame - start_f;
|
||||
parts.push(format!(
|
||||
"drawbox=x={}:y={}:w={}:h={}:color=red@0.8:thickness=4:enable='eq(n,{})'",
|
||||
x, y, w, h, offset
|
||||
));
|
||||
let tx = *x + 6;
|
||||
let ty = *y + 6;
|
||||
render_text(&mut parts, &text, tx, ty, Some(offset));
|
||||
}
|
||||
|
||||
let bbox_mode = p.mode.as_deref().unwrap_or("normal");
|
||||
@@ -671,3 +655,78 @@ async fn face_thumbnail(
|
||||
.body(Body::from(output.stdout))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct ClipQuery {
|
||||
start_frame: Option<i64>,
|
||||
end_frame: Option<i64>,
|
||||
start_time: Option<f64>,
|
||||
end_time: Option<f64>,
|
||||
fps: Option<f64>,
|
||||
mode: Option<String>,
|
||||
audio: Option<String>,
|
||||
}
|
||||
|
||||
async fn video_clip(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(file_uuid): Path<String>,
|
||||
Query(q): Query<ClipQuery>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let videos_table = schema::table_name("videos");
|
||||
let row: Option<(String, f64)> = sqlx::query_as(&format!(
|
||||
"SELECT file_path, COALESCE(fps, 30.0) FROM {} WHERE file_uuid = $1",
|
||||
videos_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let (file_path, db_fps) = row.ok_or(StatusCode::NOT_FOUND)?;
|
||||
let fps = q.fps.unwrap_or(db_fps);
|
||||
|
||||
let (s, e) = if let (Some(sf), Some(ef)) = (q.start_frame, q.end_frame) {
|
||||
(sf as f64 / fps, ef as f64 / fps)
|
||||
} else if let (Some(st), Some(et)) = (q.start_time, q.end_time) {
|
||||
(st, et)
|
||||
} else {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
};
|
||||
if e <= s {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
let mode = q.mode.as_deref().unwrap_or("normal").to_string();
|
||||
let audio = q.audio.as_deref().unwrap_or("on");
|
||||
|
||||
let mut cmd = ffmpeg_cmd();
|
||||
cmd.args(["-ss", &s.to_string(), "-i", &file_path]);
|
||||
if q.start_frame.is_some() {
|
||||
let frame_count = ((e - s) * fps) as i64;
|
||||
cmd.args(["-vframes", &frame_count.to_string()]);
|
||||
} else {
|
||||
cmd.args(["-to", &e.to_string()]);
|
||||
}
|
||||
if mode == "debug" {
|
||||
let debug_text = if let (Some(sf), Some(ef)) = (q.start_frame, q.end_frame) {
|
||||
format!("drawtext=text='Frame %{{n}} FRAMES {}-{}':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=10", sf, ef)
|
||||
} else {
|
||||
"drawtext=text='Frame %{n} CLIP':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=10".to_string()
|
||||
};
|
||||
cmd.args(["-vf", &debug_text]);
|
||||
}
|
||||
if audio == "off" {
|
||||
cmd.args(["-an"]);
|
||||
}
|
||||
cmd.args(["-c:v", "libx264", "-c:a", "aac", "-f", "mpegts", "-"]);
|
||||
let output = cmd.output().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
if !output.status.success() {
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "video/mp2t")
|
||||
.header(header::CACHE_CONTROL, "public, max-age=86400")
|
||||
.body(Body::from(output.stdout))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user