update: pipeline, search, clip, embedding fixes

This commit is contained in:
Accusys
2026-05-17 19:46:35 +08:00
parent eec2eea880
commit 3164a65554
36 changed files with 4313 additions and 4061 deletions

View File

@@ -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())
}