video2slides: 重构为纯客户端 app(浏览器抽帧 + IndexedDB),后端归零
deploy music / build-and-deploy (push) Canceled after 0s
deploy webgl / build-and-deploy (push) Canceled after 0s
deploy karaoke / build-and-deploy (push) Canceled after 0s
deploy articulate / build-and-deploy (push) Canceled after 0s
deploy cube / build-and-deploy (push) Canceled after 0s
deploy llm-proxy / build-and-deploy (push) Canceled after 0s
deploy notes / build-and-deploy (push) Canceled after 0s
deploy simpleasm / build-and-deploy (push) Canceled after 0s
deploy video2slides / build-and-deploy (push) Canceled after 0s
deploy werewolf / build-and-deploy (push) Canceled after 0s
deploy write / build-and-deploy (push) Canceled after 0s

- 不再上传视频:<video>+canvas 原生解码按时间戳 seek 抽帧,逐帧 256px 灰度
  MAE 算差异,缩略图(320)+大图(1280) 随抽随写 IndexedDB,带进度条+ETA
- 阈值/手动 保留弃用/缩放偏好 持久化到 IndexedDB,刷新仍在
- PDF 导出回到客户端 jsPDF,保留帧逐张 base64 嵌入、单帧处理防 OOM
- 后端删光业务逻辑(core.rs/handlers.rs),main.rs 缩成 cube_core::base 静态服务
- 不再需要 ffmpeg → Dockerfile 回归 FROM scratch;k8s 去掉 hostPath 卷、降资源
- 真浏览器(Playwright)验证:抽帧/差异/阈值/持久化/导出 全通过
This commit is contained in:
fam
2026-06-14 22:25:20 +01:00
parent ef29561b28
commit a0253e118f
10 changed files with 648 additions and 1709 deletions
+7 -39
View File
@@ -1,46 +1,14 @@
//! video2slides.famzheng.me — 长视频按固定间隔抽帧,逐帧差异比较,
//! 拖动阈值挑出关键画面/幻灯片。
//! video2slides.famzheng.me — 长视频抽帧、逐帧差异、拖阈值挑关键画面/幻灯片。
//!
//! - 静态 SPA + SPA fallback 由 cube-core::base 处理。
//! - `/api/*` 一组 REST:上传(multipart 流式落盘)→ 分析(后台线程跑 ffmpeg
//! 抽帧 + image crate 逐帧灰度差异,前端轮询进度)→ 取帧元数据 / 缩略图 / 大图。
//! - 按 `X-Client-Id` 头分目录,每个浏览器只看到自己上传的。
//! - 存储靠 hostPath 挂进来的目录(默认 /data),pod 重启不丢。
mod core;
mod handlers;
use axum::extract::DefaultBodyLimit;
use axum::routing::{delete, get, post};
use axum::Router;
use handlers::AppState;
//! **纯客户端 app**:视频不上传,浏览器用 `<video>` + canvas 原生解码抽帧,
//! 逐帧差异、缩略图、PDF 导出全在前端,结果存 IndexedDB(每浏览器各自持久化)。
//!
//! 所以这里没有任何业务后端 —— 这个进程只是个静态文件服务器,把前端发出去。
//! `cube_core::base` 已经包含 healthz + 静态目录 + SPA fallback。
#[tokio::main]
async fn main() -> std::io::Result<()> {
cube_core::init_tracing();
let dist = std::env::var("VIDEO2SLIDES_DIST_DIR").unwrap_or_else(|_| "/dist".into());
let data_dir = std::env::var("VIDEO2SLIDES_DATA_DIR").unwrap_or_else(|_| "/data".into());
std::fs::create_dir_all(&data_dir)?;
tracing::info!(%data_dir, "video2slides data dir");
let state = AppState::new(data_dir.into());
let api = Router::new()
.route("/videos", post(handlers::upload_video).get(handlers::list_videos))
.route("/videos/:id", delete(handlers::delete_video))
.route("/videos/:id/analyze", post(handlers::analyze))
.route("/videos/:id/job", get(handlers::job_status))
.route("/videos/:id/frames", get(handlers::frames))
.route("/videos/:id/thumb/:idx", get(handlers::thumb))
.route("/videos/:id/frame/:idx", get(handlers::frame))
.route("/videos/:id/pdf", post(handlers::export_start))
.route("/videos/:id/pdf/status", get(handlers::export_status))
.route("/videos/:id/pdf/download", get(handlers::export_download))
.layer(DefaultBodyLimit::disable()) // 视频上传可能很大,关掉默认 2MB 限制
.with_state(state);
let app = cube_core::base(dist).nest("/api", api);
cube_core::serve(app, 8080).await
cube_core::serve(cube_core::base(dist), 8080).await
}