Remove rust-video keyframe extraction API and related files

Deleted the entire src/chat/utils/rust-video directory, including Rust and Python source files, configuration, and documentation. Updated utils_video.py, official_configs.py, and bot_config_template.toml to remove or adjust references to the removed rust-video module. This cleans up the codebase by removing the integrated Rust-based keyframe extraction API and its supporting infrastructure.
This commit is contained in:
雅诺狐
2025-08-29 19:13:42 +08:00
parent f33bb57c75
commit 0a647376f7
12 changed files with 328 additions and 2790 deletions

View File

@@ -1 +0,0 @@
/target

View File

@@ -1,610 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cc"
version = "1.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rust-video"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"rayon",
"serde",
"serde_json",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"

View File

@@ -1,24 +0,0 @@
[package]
name = "rust-video"
version = "0.1.0"
edition = "2021"
authors = ["VideoAnalysis Team"]
description = "Ultra-fast video keyframe extraction tool in Rust"
license = "GPL-3.0"
[dependencies]
anyhow = "1.0"
clap = { version = "4.0", features = ["derive"] }
rayon = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true

View File

@@ -1,221 +0,0 @@
# 🎯 Rust Video Keyframe Extraction API
高性能视频关键帧提取API服务基于Rust后端 + Python FastAPI。
## 📁 项目结构
```
rust-video/
├── outputs/ # 关键帧输出目录
├── src/ # Rust源码
│ └── main.rs
├── target/ # Rust编译文件
├── api_server.py # 🚀 主API服务器 (整合版)
├── start_server.py # 生产启动脚本
├── config.py # 配置管理
├── config.toml # 配置文件
├── Cargo.toml # Rust项目配置
├── Cargo.lock # Rust依赖锁定
├── .gitignore # Git忽略文件
└── README.md # 项目文档
```
## 快速开始
### 1. 安装依赖
```bash
pip install fastapi uvicorn python-multipart aiofiles
```
### 2. 启动服务
```bash
# 开发模式
python api_server.py
# 生产模式
python start_server.py --mode prod --port 8050
```
### 3. 访问API
- **服务地址**: http://localhost:8050
- **API文档**: http://localhost:8050/docs
- **健康检查**: http://localhost:8050/health
- **性能指标**: http://localhost:8050/metrics
## API使用方法
### 主要端点
#### 1. 提取关键帧 (JSON响应)
```http
POST /extract-keyframes
Content-Type: multipart/form-data
- video: 视频文件 (.mp4, .avi, .mov, .mkv)
- scene_threshold: 场景变化阈值 (0.1-1.0, 默认0.3)
- max_frames: 最大关键帧数 (1-200, 默认50)
- resize_width: 调整宽度 (可选, 100-1920)
- time_interval: 时间间隔秒数 (可选, 0.1-60.0)
```
#### 2. 提取关键帧 (ZIP下载)
```http
POST /extract-keyframes-zip
Content-Type: multipart/form-data
参数同上返回包含所有关键帧的ZIP文件
```
#### 3. 健康检查
```http
GET /health
```
#### 4. 性能指标
```http
GET /metrics
```
### Python客户端示例
```python
import requests
# 上传视频并提取关键帧
files = {'video': open('video.mp4', 'rb')}
data = {
'scene_threshold': 0.3,
'max_frames': 50,
'resize_width': 800
}
response = requests.post(
'http://localhost:8050/extract-keyframes',
files=files,
data=data
)
result = response.json()
print(f"提取了 {result['keyframe_count']} 个关键帧")
print(f"处理时间: {result['performance']['total_api_time']:.2f}秒")
```
### JavaScript客户端示例
```javascript
const formData = new FormData();
formData.append('video', videoFile);
formData.append('scene_threshold', '0.3');
formData.append('max_frames', '50');
fetch('http://localhost:8050/extract-keyframes', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log(`提取了 ${data.keyframe_count} 个关键帧`);
console.log(`处理时间: ${data.performance.total_api_time}秒`);
});
```
### cURL示例
```bash
curl -X POST "http://localhost:8050/extract-keyframes" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
-F "video=@video.mp4" \
-F "scene_threshold=0.3" \
-F "max_frames=50"
```
## ⚙️ 配置
编辑 `config.toml` 文件:
```toml
[server]
host = "0.0.0.0"
port = 8050
debug = false
[processing]
default_scene_threshold = 0.3
default_max_frames = 50
timeout_seconds = 300
[performance]
async_workers = 4
max_file_size_mb = 500
```
## 性能特性
- **异步I/O**: 文件上传/下载异步处理
- **多线程处理**: 视频处理在独立线程池
- **内存优化**: 流式处理,减少内存占用
- **智能清理**: 自动临时文件管理
- **性能监控**: 实时处理时间和吞吐量统计
总之就是非常快()
## 响应格式
```json
{
"status": "success",
"processing_time": 4.5,
"output_directory": "/tmp/output_xxx",
"keyframe_count": 15,
"keyframes": [
"/tmp/output_xxx/frame_001.jpg",
"/tmp/output_xxx/frame_002.jpg"
],
"performance": {
"file_size_mb": 209.7,
"upload_time": 0.23,
"processing_time": 4.5,
"total_api_time": 4.73,
"upload_speed_mbps": 912.2
},
"rust_output": "处理完成",
"command": "rust-video input.mp4 output/ --scene-threshold 0.3 --max-frames 50"
}
```
## 故障排除
### 常见问题
1. **Rust binary not found**
```bash
cargo build # 重新构建Rust项目
```
2. **端口被占用**
```bash
# 修改config.toml中的端口号
port = 8051
```
3. **内存不足**
```bash
# 减少max_frames或resize_width参数
```
### 日志查看
服务启动时会显示详细的状态信息,包括:
- Rust二进制文件位置
- 配置加载状态
- 服务监听地址
## 集成支持
本API设计为独立服务可轻松集成到任何项目中
- **AI Bot项目**: 通过HTTP API调用
- **Web应用**: 直接前端调用或后端代理
- **移动应用**: REST API标准接口
- **批处理脚本**: Python/Shell脚本调用

View File

@@ -1,472 +0,0 @@
#!/usr/bin/env python3
"""
Rust Video Keyframe Extraction API Server
高性能视频关键帧提取API服务
功能:
- 视频上传和关键帧提取
- 异步多线程处理
- 性能监控和健康检查
- 自动资源清理
启动: python api_server.py
地址: http://localhost:8050
"""
import os
import json
import subprocess
import tempfile
import zipfile
import shutil
import asyncio
import time
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any
import uvicorn
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
# 导入配置管理
from config import config
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ============================================================================
# 内置视频处理器 (整合版)
# ============================================================================
class VideoKeyframeExtractor:
"""整合的视频关键帧提取器"""
def __init__(self, rust_binary_path: Optional[str] = None):
self.rust_binary_path = rust_binary_path or self._find_rust_binary()
if not self.rust_binary_path or not Path(self.rust_binary_path).exists():
raise FileNotFoundError(f"Rust binary not found: {self.rust_binary_path}")
def _find_rust_binary(self) -> str:
"""查找Rust二进制文件"""
possible_paths = [
"./target/debug/rust-video.exe",
"./target/release/rust-video.exe",
"./target/debug/rust-video",
"./target/release/rust-video"
]
for path in possible_paths:
if Path(path).exists():
return str(Path(path).absolute())
# 尝试构建
try:
subprocess.run(["cargo", "build"], check=True, capture_output=True)
for path in possible_paths:
if Path(path).exists():
return str(Path(path).absolute())
except subprocess.CalledProcessError:
pass
raise FileNotFoundError("Rust binary not found and build failed")
def process_video(
self,
video_path: str,
output_dir: str = "outputs",
scene_threshold: float = 0.3,
max_frames: int = 50,
resize_width: Optional[int] = None,
time_interval: Optional[float] = None
) -> Dict[str, Any]:
"""处理视频提取关键帧"""
video_path = Path(video_path)
if not video_path.exists():
raise FileNotFoundError(f"Video file not found: {video_path}")
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 构建命令
cmd = [self.rust_binary_path, str(video_path), str(output_dir)]
cmd.extend(["--scene-threshold", str(scene_threshold)])
cmd.extend(["--max-frames", str(max_frames)])
if resize_width:
cmd.extend(["--resize-width", str(resize_width)])
if time_interval:
cmd.extend(["--time-interval", str(time_interval)])
# 执行处理
start_time = time.time()
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
timeout=300 # 5分钟超时
)
processing_time = time.time() - start_time
# 解析输出
output_files = list(output_dir.glob("*.jpg"))
return {
"status": "success",
"processing_time": processing_time,
"output_directory": str(output_dir),
"keyframe_count": len(output_files),
"keyframes": [str(f) for f in output_files],
"rust_output": result.stdout,
"command": " ".join(cmd)
}
except subprocess.TimeoutExpired:
raise HTTPException(status_code=408, detail="Video processing timeout")
except subprocess.CalledProcessError as e:
raise HTTPException(
status_code=500,
detail=f"Video processing failed: {e.stderr}"
)
# ============================================================================
# 异步处理器 (整合版)
# ============================================================================
class AsyncVideoProcessor:
"""高性能异步视频处理器"""
def __init__(self):
self.extractor = VideoKeyframeExtractor()
async def process_video_async(
self,
upload_file: UploadFile,
processing_params: Dict[str, Any]
) -> Dict[str, Any]:
"""异步视频处理主流程"""
start_time = time.time()
# 1. 异步保存上传文件
upload_start = time.time()
temp_fd, temp_path_str = tempfile.mkstemp(suffix='.mp4')
temp_path = Path(temp_path_str)
try:
os.close(temp_fd)
# 异步读取并保存文件
content = await upload_file.read()
with open(temp_path, 'wb') as f:
f.write(content)
upload_time = time.time() - upload_start
file_size = len(content)
# 2. 多线程处理视频
process_start = time.time()
temp_output_dir = tempfile.mkdtemp()
output_path = Path(temp_output_dir)
try:
# 在线程池中异步处理
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
self._process_video_sync,
str(temp_path),
str(output_path),
processing_params
)
process_time = time.time() - process_start
total_time = time.time() - start_time
# 添加性能指标
result.update({
'performance': {
'file_size_mb': file_size / (1024 * 1024),
'upload_time': upload_time,
'processing_time': process_time,
'total_api_time': total_time,
'upload_speed_mbps': (file_size / (1024 * 1024)) / upload_time if upload_time > 0 else 0
}
})
return result
finally:
# 清理输出目录
try:
shutil.rmtree(temp_output_dir, ignore_errors=True)
except Exception as e:
logger.warning(f"Failed to cleanup output directory: {e}")
finally:
# 清理临时文件
try:
if temp_path.exists():
temp_path.unlink()
except Exception as e:
logger.warning(f"Failed to cleanup temp file: {e}")
def _process_video_sync(self, video_path: str, output_dir: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""在线程池中同步处理视频"""
return self.extractor.process_video(
video_path=video_path,
output_dir=output_dir,
**params
)
# ============================================================================
# FastAPI 应用初始化
# ============================================================================
app = FastAPI(
title="Rust Video Keyframe API",
description="高性能视频关键帧提取API服务",
version="2.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 全局处理器实例
video_processor = AsyncVideoProcessor()
# 简单的统计
stats = {
"total_requests": 0,
"processing_times": [],
"start_time": datetime.now()
}
# ============================================================================
# API 路由
# ============================================================================
@app.get("/", response_class=JSONResponse)
async def root():
"""API根路径"""
return {
"message": "Rust Video Keyframe Extraction API",
"version": "2.0.0",
"status": "ready",
"docs": "/docs",
"health": "/health",
"metrics": "/metrics"
}
@app.get("/health")
async def health_check():
"""健康检查端点"""
try:
# 检查Rust二进制
rust_binary = video_processor.extractor.rust_binary_path
rust_status = "ok" if Path(rust_binary).exists() else "missing"
return {
"status": rust_status,
"timestamp": datetime.now().isoformat(),
"version": "2.0.0",
"rust_binary": rust_binary
}
except Exception as e:
raise HTTPException(status_code=503, detail=f"Health check failed: {str(e)}")
@app.get("/metrics")
async def get_metrics():
"""获取性能指标"""
avg_time = sum(stats["processing_times"]) / len(stats["processing_times"]) if stats["processing_times"] else 0
uptime = (datetime.now() - stats["start_time"]).total_seconds()
return {
"total_requests": stats["total_requests"],
"average_processing_time": avg_time,
"last_24h_requests": stats["total_requests"], # 简化版本
"system_info": {
"uptime_seconds": uptime,
"memory_usage": "N/A", # 可以扩展
"cpu_usage": "N/A"
}
}
@app.post("/extract-keyframes")
async def extract_keyframes(
video: UploadFile = File(..., description="视频文件"),
scene_threshold: float = Form(0.3, description="场景变化阈值"),
max_frames: int = Form(50, description="最大关键帧数量"),
resize_width: Optional[int] = Form(None, description="调整宽度"),
time_interval: Optional[float] = Form(None, description="时间间隔")
):
"""提取视频关键帧 (主要API端点)"""
# 参数验证
if not video.filename.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
raise HTTPException(status_code=400, detail="不支持的视频格式")
# 更新统计
stats["total_requests"] += 1
try:
# 构建处理参数
params = {
"scene_threshold": scene_threshold,
"max_frames": max_frames
}
if resize_width:
params["resize_width"] = resize_width
if time_interval:
params["time_interval"] = time_interval
# 异步处理
start_time = time.time()
result = await video_processor.process_video_async(video, params)
processing_time = time.time() - start_time
# 更新统计
stats["processing_times"].append(processing_time)
if len(stats["processing_times"]) > 100: # 保持最近100次记录
stats["processing_times"] = stats["processing_times"][-100:]
return JSONResponse(content=result)
except Exception as e:
logger.error(f"Processing failed: {str(e)}")
raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")
@app.post("/extract-keyframes-zip")
async def extract_keyframes_zip(
video: UploadFile = File(...),
scene_threshold: float = Form(0.3),
max_frames: int = Form(50),
resize_width: Optional[int] = Form(None),
time_interval: Optional[float] = Form(None)
):
"""提取关键帧并返回ZIP文件"""
# 验证文件类型
if not video.filename.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
raise HTTPException(status_code=400, detail="不支持的视频格式")
# 创建临时目录
temp_input_fd, temp_input_path = tempfile.mkstemp(suffix='.mp4')
temp_output_dir = tempfile.mkdtemp()
try:
os.close(temp_input_fd)
# 保存上传的视频
content = await video.read()
with open(temp_input_path, 'wb') as f:
f.write(content)
# 处理参数
params = {
"scene_threshold": scene_threshold,
"max_frames": max_frames
}
if resize_width:
params["resize_width"] = resize_width
if time_interval:
params["time_interval"] = time_interval
# 处理视频
result = video_processor.extractor.process_video(
video_path=temp_input_path,
output_dir=temp_output_dir,
**params
)
# 创建ZIP文件
zip_fd, zip_path = tempfile.mkstemp(suffix='.zip')
os.close(zip_fd)
with zipfile.ZipFile(zip_path, 'w') as zip_file:
# 添加关键帧图片
for keyframe_path in result.get("keyframes", []):
if Path(keyframe_path).exists():
zip_file.write(keyframe_path, Path(keyframe_path).name)
# 添加处理信息
info_content = json.dumps(result, indent=2, ensure_ascii=False)
zip_file.writestr("processing_info.json", info_content)
# 返回ZIP文件
return FileResponse(
zip_path,
media_type='application/zip',
filename=f"keyframes_{video.filename}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")
finally:
# 清理临时文件
for path in [temp_input_path, temp_output_dir]:
try:
if Path(path).is_file():
Path(path).unlink()
elif Path(path).is_dir():
shutil.rmtree(path, ignore_errors=True)
except Exception:
pass
# ============================================================================
# 应用启动
# ============================================================================
def main():
"""启动API服务器"""
# 获取配置
server_config = config.get('server')
host = server_config.get('host', '0.0.0.0')
port = server_config.get('port', 8050)
print(f"""
Rust Video Keyframe Extraction API
=====================================
地址: http://{host}:{port}
文档: http://{host}:{port}/docs
健康检查: http://{host}:{port}/health
性能指标: http://{host}:{port}/metrics
=====================================
""")
# 检查Rust二进制
try:
rust_binary = video_processor.extractor.rust_binary_path
print(f"✓ Rust binary: {rust_binary}")
except Exception as e:
print(f"⚠️ Rust binary check failed: {e}")
# 启动服务器
uvicorn.run(
"api_server:app",
host=host,
port=port,
reload=False, # 生产环境关闭热重载
access_log=True
)
if __name__ == "__main__":
main()

View File

@@ -1,115 +0,0 @@
"""
配置管理模块
处理 config.toml 文件的读取和管理
"""
import os
from pathlib import Path
from typing import Dict, Any
try:
import toml
except ImportError:
print("⚠️ 需要安装 toml: pip install toml")
# 提供基础配置作为后备
toml = None
class ConfigManager:
"""配置管理器"""
def __init__(self, config_file: str = "config.toml"):
self.config_file = Path(config_file)
self._config = self._load_config()
def _load_config(self) -> Dict[str, Any]:
"""加载配置文件"""
if toml is None or not self.config_file.exists():
return self._get_default_config()
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
return toml.load(f)
except Exception as e:
print(f"⚠️ 配置文件读取失败: {e}")
return self._get_default_config()
def _get_default_config(self) -> Dict[str, Any]:
"""默认配置"""
return {
"server": {
"host": "0.0.0.0",
"port": 8000,
"workers": 1,
"reload": False,
"log_level": "info"
},
"api": {
"title": "Video Keyframe Extraction API",
"description": "高性能视频关键帧提取服务",
"version": "1.0.0",
"max_file_size": "100MB"
},
"processing": {
"default_threshold": 0.3,
"default_output_format": "png",
"max_frames": 10000,
"temp_dir": "temp",
"upload_dir": "uploads",
"output_dir": "outputs"
},
"rust": {
"executable_name": "video_keyframe_extractor",
"executable_path": "target/release"
},
"ffmpeg": {
"auto_detect": True,
"custom_path": "",
"timeout": 300
},
"storage": {
"cleanup_interval": 3600,
"max_storage_size": "10GB",
"result_retention_days": 7
},
"monitoring": {
"enable_metrics": True,
"enable_logging": True,
"log_file": "logs/api.log",
"max_log_size": "100MB"
},
"security": {
"allowed_origins": ["*"],
"max_concurrent_tasks": 10,
"rate_limit_per_minute": 60
},
"development": {
"debug": False,
"auto_reload": False,
"cors_enabled": True
}
}
def get(self, section: str, key: str = None, default=None):
"""获取配置值"""
if key is None:
return self._config.get(section, default)
return self._config.get(section, {}).get(key, default)
def get_server_config(self):
"""获取服务器配置"""
return self.get("server")
def get_api_config(self):
"""获取API配置"""
return self.get("api")
def get_processing_config(self):
"""获取处理配置"""
return self.get("processing")
def reload(self):
"""重新加载配置"""
self._config = self._load_config()
# 全局配置实例
config = ConfigManager()

View File

@@ -1,70 +0,0 @@
# 🔧 Video Keyframe Extraction API 配置文件
[server]
# 服务器配置
host = "0.0.0.0"
port = 8050
workers = 1
reload = false
log_level = "info"
[api]
# API 基础配置
title = "Video Keyframe Extraction API"
description = "视频关键帧提取服务"
version = "1.0.0"
max_file_size = "100MB" # 最大文件大小
[processing]
# 视频处理配置
default_threshold = 0.3
default_output_format = "png"
max_frames = 10000
temp_dir = "temp"
upload_dir = "uploads"
output_dir = "outputs"
[rust]
# Rust 程序配置
executable_name = "video_keyframe_extractor"
executable_path = "target/release" # 相对路径,自动检测
[ffmpeg]
# FFmpeg 配置
auto_detect = true
custom_path = "" # 留空则自动检测
timeout = 300 # 秒
[performance]
# 性能优化配置
async_workers = 4 # 异步文件处理工作线程数
upload_chunk_size = 8192 # 上传块大小 (字节)
max_concurrent_uploads = 10 # 最大并发上传数
compression_level = 1 # ZIP 压缩级别 (0-9, 1=快速)
stream_chunk_size = 8192 # 流式响应块大小
enable_performance_metrics = true # 启用性能监控
[storage]
# 存储配置
cleanup_interval = 3600 # 清理间隔(秒)
max_storage_size = "10GB"
result_retention_days = 7
[monitoring]
# 监控配置
enable_metrics = true
enable_logging = true
log_file = "logs/api.log"
max_log_size = "100MB"
[security]
# 安全配置
allowed_origins = ["*"]
max_concurrent_tasks = 10
rate_limit_per_minute = 60
[development]
# 开发环境配置
debug = false
auto_reload = false
cors_enabled = true

View File

@@ -1,710 +0,0 @@
//! # Rust Video Keyframe Extractor
//!
//! Ultra-fast video keyframe extraction tool with SIMD optimization.
//!
//! ## Features
//! - AVX2/SSE2 SIMD optimization for maximum performance
//! - Memory-efficient streaming processing with FFmpeg
//! - Multi-threaded parallel processing
//! - Release-optimized for production use
//!
//! ## Performance
//! - 150+ FPS processing speed
//! - Real-time video analysis capability
//! - Minimal memory footprint
//!
//! ## Usage
//! ```bash
//! # Single video processing
//! rust-video --input video.mp4 --output ./keyframes --threshold 2.0
//!
//! # Benchmark mode
//! rust-video --benchmark --input video.mp4 --output ./results
//! ```
use anyhow::{Context, Result};
use chrono::prelude::*;
use clap::Parser;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{BufReader, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::Instant;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
/// Ultra-fast video keyframe extraction tool
#[derive(Parser)]
#[command(name = "rust-video")]
#[command(version = "0.1.0")]
#[command(about = "Ultra-fast video keyframe extraction with SIMD optimization")]
#[command(long_about = None)]
struct Args {
/// Input video file path
#[arg(short, long, help = "Path to the input video file")]
input: Option<PathBuf>,
/// Output directory for keyframes and results
#[arg(short, long, default_value = "./output", help = "Output directory")]
output: PathBuf,
/// Change threshold for keyframe detection (higher = fewer keyframes)
#[arg(short, long, default_value = "2.0", help = "Keyframe detection threshold")]
threshold: f64,
/// Number of parallel threads (0 = auto-detect)
#[arg(short = 'j', long, default_value = "0", help = "Number of threads")]
threads: usize,
/// Maximum number of keyframes to save (0 = save all)
#[arg(short, long, default_value = "50", help = "Maximum keyframes to save")]
max_save: usize,
/// Run performance benchmark suite
#[arg(long, help = "Run comprehensive benchmark tests")]
benchmark: bool,
/// Maximum frames to process (0 = process all frames)
#[arg(long, default_value = "0", help = "Limit number of frames to process")]
max_frames: usize,
/// FFmpeg executable path
#[arg(long, default_value = "ffmpeg", help = "Path to FFmpeg executable")]
ffmpeg_path: PathBuf,
/// Enable SIMD optimizations (AVX2/SSE2)
#[arg(long, default_value = "true", help = "Enable SIMD optimizations")]
use_simd: bool,
/// Processing block size for cache optimization
#[arg(long, default_value = "8192", help = "Block size for processing")]
block_size: usize,
/// Verbose output
#[arg(short, long, help = "Enable verbose output")]
verbose: bool,
}
/// Video frame representation optimized for SIMD processing
#[derive(Debug, Clone)]
struct VideoFrame {
frame_number: usize,
width: usize,
height: usize,
data: Vec<u8>, // Grayscale data, aligned for SIMD
}
impl VideoFrame {
/// Create a new video frame with SIMD-aligned data
fn new(frame_number: usize, width: usize, height: usize, mut data: Vec<u8>) -> Self {
// Ensure data length is multiple of 32 for AVX2 processing
let remainder = data.len() % 32;
if remainder != 0 {
data.resize(data.len() + (32 - remainder), 0);
}
Self {
frame_number,
width,
height,
data,
}
}
/// Calculate frame difference using parallel SIMD processing
fn calculate_difference_parallel_simd(&self, other: &VideoFrame, block_size: usize, use_simd: bool) -> f64 {
if self.width != other.width || self.height != other.height {
return f64::MAX;
}
let total_pixels = self.width * self.height;
let num_blocks = (total_pixels + block_size - 1) / block_size;
let total_diff: u64 = (0..num_blocks)
.into_par_iter()
.map(|block_idx| {
let start = block_idx * block_size;
let end = ((block_idx + 1) * block_size).min(total_pixels);
let block_len = end - start;
if use_simd {
#[cfg(target_arch = "x86_64")]
{
unsafe {
if std::arch::is_x86_feature_detected!("avx2") {
return self.calculate_difference_avx2_block(&other.data, start, block_len);
} else if std::arch::is_x86_feature_detected!("sse2") {
return self.calculate_difference_sse2_block(&other.data, start, block_len);
}
}
}
}
// Fallback scalar implementation
self.data[start..end]
.iter()
.zip(other.data[start..end].iter())
.map(|(a, b)| (*a as i32 - *b as i32).abs() as u64)
.sum()
})
.sum();
total_diff as f64 / total_pixels as f64
}
/// Standard frame difference calculation (non-SIMD)
fn calculate_difference_standard(&self, other: &VideoFrame) -> f64 {
if self.width != other.width || self.height != other.height {
return f64::MAX;
}
let len = self.width * self.height;
let total_diff: u64 = self.data[..len]
.iter()
.zip(other.data[..len].iter())
.map(|(a, b)| (*a as i32 - *b as i32).abs() as u64)
.sum();
total_diff as f64 / len as f64
}
/// AVX2 optimized block processing
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn calculate_difference_avx2_block(&self, other_data: &[u8], start: usize, len: usize) -> u64 {
let mut total_diff = 0u64;
let chunks = len / 32;
for i in 0..chunks {
let offset = start + i * 32;
let a = _mm256_loadu_si256(self.data.as_ptr().add(offset) as *const __m256i);
let b = _mm256_loadu_si256(other_data.as_ptr().add(offset) as *const __m256i);
let diff = _mm256_sad_epu8(a, b);
let result = _mm256_extract_epi64(diff, 0) as u64 +
_mm256_extract_epi64(diff, 1) as u64 +
_mm256_extract_epi64(diff, 2) as u64 +
_mm256_extract_epi64(diff, 3) as u64;
total_diff += result;
}
// Process remaining bytes
for i in (start + chunks * 32)..(start + len) {
total_diff += (self.data[i] as i32 - other_data[i] as i32).abs() as u64;
}
total_diff
}
/// SSE2 optimized block processing
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "sse2")]
unsafe fn calculate_difference_sse2_block(&self, other_data: &[u8], start: usize, len: usize) -> u64 {
let mut total_diff = 0u64;
let chunks = len / 16;
for i in 0..chunks {
let offset = start + i * 16;
let a = _mm_loadu_si128(self.data.as_ptr().add(offset) as *const __m128i);
let b = _mm_loadu_si128(other_data.as_ptr().add(offset) as *const __m128i);
let diff = _mm_sad_epu8(a, b);
let result = _mm_extract_epi64(diff, 0) as u64 + _mm_extract_epi64(diff, 1) as u64;
total_diff += result;
}
// Process remaining bytes
for i in (start + chunks * 16)..(start + len) {
total_diff += (self.data[i] as i32 - other_data[i] as i32).abs() as u64;
}
total_diff
}
}
/// Performance measurement results
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PerformanceResult {
test_name: String,
video_file: String,
total_time_ms: f64,
frame_extraction_time_ms: f64,
keyframe_analysis_time_ms: f64,
total_frames: usize,
keyframes_extracted: usize,
keyframe_ratio: f64,
processing_fps: f64,
threshold: f64,
optimization_type: String,
simd_enabled: bool,
threads_used: usize,
timestamp: String,
}
/// Extract video frames using FFmpeg memory streaming
fn extract_frames_memory_stream(
video_path: &PathBuf,
ffmpeg_path: &PathBuf,
max_frames: usize,
verbose: bool,
) -> Result<(Vec<VideoFrame>, usize, usize)> {
if verbose {
println!("🎬 Extracting frames using FFmpeg memory streaming...");
println!("📁 Video: {}", video_path.display());
}
// Get video information
let probe_output = Command::new(ffmpeg_path)
.args(["-i", video_path.to_str().unwrap(), "-hide_banner"])
.output()
.context("Failed to probe video with FFmpeg")?;
let probe_info = String::from_utf8_lossy(&probe_output.stderr);
let (width, height) = parse_video_dimensions(&probe_info)
.ok_or_else(|| anyhow::anyhow!("Cannot parse video dimensions"))?;
if verbose {
println!("📐 Video dimensions: {}x{}", width, height);
}
// Build optimized FFmpeg command
let mut cmd = Command::new(ffmpeg_path);
cmd.args([
"-i", video_path.to_str().unwrap(),
"-f", "rawvideo",
"-pix_fmt", "gray",
"-an", // No audio
"-threads", "0", // Auto-detect threads
"-preset", "ultrafast", // Fastest preset
]);
if max_frames > 0 {
cmd.args(["-frames:v", &max_frames.to_string()]);
}
cmd.args(["-"]).stdout(Stdio::piped()).stderr(Stdio::null());
let start_time = Instant::now();
let mut child = cmd.spawn().context("Failed to spawn FFmpeg process")?;
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::with_capacity(1024 * 1024, stdout); // 1MB buffer
let frame_size = width * height;
let mut frames = Vec::new();
let mut frame_count = 0;
let mut frame_buffer = vec![0u8; frame_size];
if verbose {
println!("📦 Frame size: {} bytes", frame_size);
}
// Stream frame data directly into memory
loop {
match reader.read_exact(&mut frame_buffer) {
Ok(()) => {
frames.push(VideoFrame::new(
frame_count,
width,
height,
frame_buffer.clone(),
));
frame_count += 1;
if verbose && frame_count % 200 == 0 {
print!("\r⚡ Frames processed: {}", frame_count);
}
if max_frames > 0 && frame_count >= max_frames {
break;
}
}
Err(_) => break, // End of stream
}
}
let _ = child.wait();
if verbose {
println!("\r✅ Frame extraction complete: {} frames in {:.2}s",
frame_count, start_time.elapsed().as_secs_f64());
}
Ok((frames, width, height))
}
/// Parse video dimensions from FFmpeg probe output
fn parse_video_dimensions(probe_info: &str) -> Option<(usize, usize)> {
for line in probe_info.lines() {
if line.contains("Video:") && line.contains("x") {
for part in line.split_whitespace() {
if let Some(x_pos) = part.find('x') {
let width_str = &part[..x_pos];
let height_part = &part[x_pos + 1..];
let height_str = height_part.split(',').next().unwrap_or(height_part);
if let (Ok(width), Ok(height)) = (width_str.parse::<usize>(), height_str.parse::<usize>()) {
return Some((width, height));
}
}
}
}
}
None
}
/// Extract keyframes using optimized algorithms
fn extract_keyframes_optimized(
frames: &[VideoFrame],
threshold: f64,
use_simd: bool,
block_size: usize,
verbose: bool,
) -> Result<Vec<usize>> {
if frames.len() < 2 {
return Ok(Vec::new());
}
let optimization_name = if use_simd { "SIMD+Parallel" } else { "Standard Parallel" };
if verbose {
println!("🚀 Keyframe analysis (threshold: {}, optimization: {})", threshold, optimization_name);
}
let start_time = Instant::now();
// Parallel computation of frame differences
let differences: Vec<f64> = frames
.par_windows(2)
.map(|pair| {
if use_simd {
pair[0].calculate_difference_parallel_simd(&pair[1], block_size, true)
} else {
pair[0].calculate_difference_standard(&pair[1])
}
})
.collect();
// Find keyframes based on threshold
let keyframe_indices: Vec<usize> = differences
.par_iter()
.enumerate()
.filter_map(|(i, &diff)| {
if diff > threshold {
Some(i + 1)
} else {
None
}
})
.collect();
if verbose {
println!("⚡ Analysis complete in {:.2}s", start_time.elapsed().as_secs_f64());
println!("🎯 Found {} keyframes", keyframe_indices.len());
}
Ok(keyframe_indices)
}
/// Save keyframes as JPEG images using FFmpeg
fn save_keyframes_optimized(
video_path: &PathBuf,
keyframe_indices: &[usize],
output_dir: &PathBuf,
ffmpeg_path: &PathBuf,
max_save: usize,
verbose: bool,
) -> Result<usize> {
if keyframe_indices.is_empty() {
if verbose {
println!("⚠️ No keyframes to save");
}
return Ok(0);
}
if verbose {
println!("💾 Saving keyframes...");
}
fs::create_dir_all(output_dir).context("Failed to create output directory")?;
let save_count = keyframe_indices.len().min(max_save);
let mut saved = 0;
for (i, &frame_idx) in keyframe_indices.iter().take(save_count).enumerate() {
let output_path = output_dir.join(format!("keyframe_{:03}.jpg", i + 1));
let timestamp = frame_idx as f64 / 30.0; // Assume 30 FPS
let output = Command::new(ffmpeg_path)
.args([
"-i", video_path.to_str().unwrap(),
"-ss", &timestamp.to_string(),
"-vframes", "1",
"-q:v", "2", // High quality
"-y",
output_path.to_str().unwrap(),
])
.output()
.context("Failed to extract keyframe with FFmpeg")?;
if output.status.success() {
saved += 1;
if verbose && (saved % 10 == 0 || saved == save_count) {
print!("\r💾 Saved: {}/{} keyframes", saved, save_count);
}
} else if verbose {
eprintln!("⚠️ Failed to save keyframe {}", frame_idx);
}
}
if verbose {
println!("\r✅ Keyframe saving complete: {}/{}", saved, save_count);
}
Ok(saved)
}
/// Run performance test
fn run_performance_test(
video_path: &PathBuf,
threshold: f64,
test_name: &str,
ffmpeg_path: &PathBuf,
max_frames: usize,
use_simd: bool,
block_size: usize,
verbose: bool,
) -> Result<PerformanceResult> {
if verbose {
println!("\n{}", "=".repeat(60));
println!("⚡ Running test: {}", test_name);
println!("{}", "=".repeat(60));
}
let total_start = Instant::now();
// Frame extraction
let extraction_start = Instant::now();
let (frames, _width, _height) = extract_frames_memory_stream(video_path, ffmpeg_path, max_frames, verbose)?;
let extraction_time = extraction_start.elapsed().as_secs_f64() * 1000.0;
// Keyframe analysis
let analysis_start = Instant::now();
let keyframe_indices = extract_keyframes_optimized(&frames, threshold, use_simd, block_size, verbose)?;
let analysis_time = analysis_start.elapsed().as_secs_f64() * 1000.0;
let total_time = total_start.elapsed().as_secs_f64() * 1000.0;
let optimization_type = if use_simd {
format!("SIMD+Parallel(block:{})", block_size)
} else {
"Standard Parallel".to_string()
};
let result = PerformanceResult {
test_name: test_name.to_string(),
video_file: video_path.file_name().unwrap().to_string_lossy().to_string(),
total_time_ms: total_time,
frame_extraction_time_ms: extraction_time,
keyframe_analysis_time_ms: analysis_time,
total_frames: frames.len(),
keyframes_extracted: keyframe_indices.len(),
keyframe_ratio: keyframe_indices.len() as f64 / frames.len() as f64 * 100.0,
processing_fps: frames.len() as f64 / (total_time / 1000.0),
threshold,
optimization_type,
simd_enabled: use_simd,
threads_used: rayon::current_num_threads(),
timestamp: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
};
if verbose {
println!("\n⚡ Test Results:");
println!(" 🕐 Total time: {:.2}ms ({:.2}s)", result.total_time_ms, result.total_time_ms / 1000.0);
println!(" 📥 Extraction: {:.2}ms ({:.1}%)", result.frame_extraction_time_ms,
result.frame_extraction_time_ms / result.total_time_ms * 100.0);
println!(" 🧮 Analysis: {:.2}ms ({:.1}%)", result.keyframe_analysis_time_ms,
result.keyframe_analysis_time_ms / result.total_time_ms * 100.0);
println!(" 📊 Frames: {}", result.total_frames);
println!(" 🎯 Keyframes: {}", result.keyframes_extracted);
println!(" 🚀 Speed: {:.1} FPS", result.processing_fps);
println!(" ⚙️ Optimization: {}", result.optimization_type);
}
Ok(result)
}
/// Run comprehensive benchmark suite
fn run_benchmark_suite(video_path: &PathBuf, output_dir: &PathBuf, ffmpeg_path: &PathBuf, args: &Args) -> Result<()> {
println!("🚀 Rust Video Keyframe Extractor - Benchmark Suite");
println!("🕐 Time: {}", Local::now().format("%Y-%m-%d %H:%M:%S"));
println!("🎬 Video: {}", video_path.display());
println!("🧵 Threads: {}", rayon::current_num_threads());
// CPU feature detection
#[cfg(target_arch = "x86_64")]
{
println!("🔧 CPU Features:");
if std::arch::is_x86_feature_detected!("avx2") {
println!(" ✅ AVX2 supported");
} else if std::arch::is_x86_feature_detected!("sse2") {
println!(" ✅ SSE2 supported");
} else {
println!(" ⚠️ Scalar only");
}
}
let test_configs = vec![
("Standard Parallel", false, 8192),
("SIMD 8K blocks", true, 8192),
("SIMD 16K blocks", true, 16384),
("SIMD 32K blocks", true, 32768),
];
let mut results = Vec::new();
for (test_name, use_simd, block_size) in test_configs {
match run_performance_test(
video_path,
args.threshold,
test_name,
ffmpeg_path,
1000, // Test with 1000 frames
use_simd,
block_size,
args.verbose,
) {
Ok(result) => results.push(result),
Err(e) => println!("❌ Test failed {}: {:?}", test_name, e),
}
}
// Performance comparison table
println!("\n{}", "=".repeat(120));
println!("🏆 Benchmark Results");
println!("{}", "=".repeat(120));
println!("{:<20} {:<15} {:<12} {:<12} {:<12} {:<8} {:<8} {:<12} {:<20}",
"Test", "Total(ms)", "Extract(ms)", "Analyze(ms)", "Speed(FPS)", "Frames", "Keyframes", "Threads", "Optimization");
println!("{}", "-".repeat(120));
for result in &results {
println!("{:<20} {:<15.1} {:<12.1} {:<12.1} {:<12.1} {:<8} {:<8} {:<12} {:<20}",
result.test_name,
result.total_time_ms,
result.frame_extraction_time_ms,
result.keyframe_analysis_time_ms,
result.processing_fps,
result.total_frames,
result.keyframes_extracted,
result.threads_used,
result.optimization_type);
}
// Find best performance
if let Some(best_result) = results.iter().max_by(|a, b| a.processing_fps.partial_cmp(&b.processing_fps).unwrap()) {
println!("\n🏆 Best Performance: {}", best_result.test_name);
println!(" ⚡ Speed: {:.1} FPS", best_result.processing_fps);
println!(" 🕐 Time: {:.2}s", best_result.total_time_ms / 1000.0);
println!(" 🧮 Analysis: {:.2}s", best_result.keyframe_analysis_time_ms / 1000.0);
println!(" ⚙️ Tech: {}", best_result.optimization_type);
}
// Save detailed results
fs::create_dir_all(output_dir).context("Failed to create output directory")?;
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
let results_file = output_dir.join(format!("benchmark_results_{}.json", timestamp));
let json_results = serde_json::to_string_pretty(&results)?;
fs::write(&results_file, json_results)?;
println!("\n📄 Detailed results saved to: {}", results_file.display());
println!("{}", "=".repeat(120));
Ok(())
}
fn main() -> Result<()> {
let args = Args::parse();
// Setup thread pool
if args.threads > 0 {
rayon::ThreadPoolBuilder::new()
.num_threads(args.threads)
.build_global()
.context("Failed to set thread pool")?;
}
println!("🚀 Rust Video Keyframe Extractor v0.1.0");
println!("🧵 Threads: {}", rayon::current_num_threads());
// Verify FFmpeg availability
if !args.ffmpeg_path.exists() && args.ffmpeg_path.to_str() == Some("ffmpeg") {
// Try to find ffmpeg in PATH
if Command::new("ffmpeg").arg("-version").output().is_err() {
anyhow::bail!("FFmpeg not found. Please install FFmpeg or specify path with --ffmpeg-path");
}
} else if !args.ffmpeg_path.exists() {
anyhow::bail!("FFmpeg not found at: {}", args.ffmpeg_path.display());
}
if args.benchmark {
// Benchmark mode
let video_path = args.input.clone()
.ok_or_else(|| anyhow::anyhow!("Benchmark requires input video file --input <path>"))?;
if !video_path.exists() {
anyhow::bail!("Video file not found: {}", video_path.display());
}
run_benchmark_suite(&video_path, &args.output, &args.ffmpeg_path, &args)?;
} else {
// Single processing mode
let video_path = args.input
.ok_or_else(|| anyhow::anyhow!("Please specify input video file --input <path>"))?;
if !video_path.exists() {
anyhow::bail!("Video file not found: {}", video_path.display());
}
// Run single keyframe extraction
let result = run_performance_test(
&video_path,
args.threshold,
"Single Processing",
&args.ffmpeg_path,
args.max_frames,
args.use_simd,
args.block_size,
args.verbose,
)?;
// Extract and save keyframes
let (frames, _, _) = extract_frames_memory_stream(&video_path, &args.ffmpeg_path, args.max_frames, args.verbose)?;
let keyframe_indices = extract_keyframes_optimized(&frames, args.threshold, args.use_simd, args.block_size, args.verbose)?;
let saved_count = save_keyframes_optimized(&video_path, &keyframe_indices, &args.output, &args.ffmpeg_path, args.max_save, args.verbose)?;
println!("\n✅ Processing Complete!");
println!("🎯 Keyframes extracted: {}", result.keyframes_extracted);
println!("💾 Keyframes saved: {}", saved_count);
println!("⚡ Processing speed: {:.1} FPS", result.processing_fps);
println!("📁 Output directory: {}", args.output.display());
// Save processing report
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
let report_file = args.output.join(format!("processing_report_{}.json", timestamp));
let json_result = serde_json::to_string_pretty(&result)?;
fs::write(&report_file, json_result)?;
if args.verbose {
println!("📄 Processing report saved to: {}", report_file.display());
}
}
Ok(())
}

View File

@@ -1,219 +0,0 @@
#!/usr/bin/env python3
"""
启动脚本
支持开发模式和生产模式启动
"""
import os
import sys
import subprocess
import argparse
from pathlib import Path
from config import config
def check_rust_executable():
"""检查 Rust 可执行文件是否存在"""
rust_config = config.get("rust")
executable_name = rust_config.get("executable_name", "video_keyframe_extractor")
executable_path = rust_config.get("executable_path", "target/release")
possible_paths = [
f"./{executable_path}/{executable_name}.exe",
f"./{executable_path}/{executable_name}",
f"./{executable_name}.exe",
f"./{executable_name}"
]
for path in possible_paths:
if Path(path).exists():
print(f"✓ Found Rust executable: {path}")
return str(Path(path).absolute())
print("⚠ Warning: Rust executable not found")
print("Please compile first: cargo build --release")
return None
def check_dependencies():
"""检查 Python 依赖"""
try:
import fastapi
import uvicorn
print("✓ FastAPI dependencies available")
return True
except ImportError as e:
print(f"✗ Missing dependencies: {e}")
print("Please install: pip install -r requirements.txt")
return False
def install_dependencies():
"""安装依赖"""
print("Installing dependencies...")
try:
subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
check=True)
print("✓ Dependencies installed successfully")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to install dependencies: {e}")
return False
def start_development_server(host="127.0.0.1", port=8050, reload=True):
"""启动开发服务器"""
print(f" Starting development server on http://{host}:{port}")
print(f" API docs: http://{host}:{port}/docs")
print(f" Health check: http://{host}:{port}/health")
try:
import uvicorn
uvicorn.run(
"api_server:app",
host=host,
port=port,
reload=reload,
log_level="info"
)
except ImportError:
print("uvicorn not found, trying with subprocess...")
subprocess.run([
sys.executable, "-m", "uvicorn",
"api_server:app",
"--host", host,
"--port", str(port),
"--reload" if reload else ""
])
def start_production_server(host="0.0.0.0", port=8000, workers=4):
"""启动生产服务器"""
print(f"🚀 Starting production server on http://{host}:{port}")
print(f"Workers: {workers}")
subprocess.run([
sys.executable, "-m", "uvicorn",
"api_server:app",
"--host", host,
"--port", str(port),
"--workers", str(workers),
"--log-level", "warning"
])
def create_systemd_service():
"""创建 systemd 服务文件"""
current_dir = Path.cwd()
python_path = sys.executable
service_content = f"""[Unit]
Description=Video Keyframe Extraction API Server
After=network.target
[Service]
Type=exec
User=www-data
WorkingDirectory={current_dir}
Environment=PATH=/usr/bin:/usr/local/bin
ExecStart={python_path} -m uvicorn api_server:app --host 0.0.0.0 --port 8000 --workers 4
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
"""
service_file = Path("/etc/systemd/system/video-keyframe-api.service")
try:
with open(service_file, 'w') as f:
f.write(service_content)
print(f"✓ Systemd service created: {service_file}")
print("To enable and start:")
print(" sudo systemctl enable video-keyframe-api")
print(" sudo systemctl start video-keyframe-api")
except PermissionError:
print("✗ Permission denied. Please run with sudo for systemd service creation")
# 创建本地副本
local_service = Path("./video-keyframe-api.service")
with open(local_service, 'w') as f:
f.write(service_content)
print(f"✓ Service file created locally: {local_service}")
print(f"To install: sudo cp {local_service} /etc/systemd/system/")
def main():
parser = argparse.ArgumentParser(description="Video Keyframe Extraction API Server")
# 从配置文件获取默认值
server_config = config.get_server_config()
parser.add_argument("--mode", choices=["dev", "prod", "install"], default="dev",
help="运行模式: dev (开发), prod (生产), install (安装依赖)")
parser.add_argument("--host", default=server_config.get("host", "127.0.0.1"), help="绑定主机")
parser.add_argument("--port", type=int, default=server_config.get("port", 8000), help="端口号")
parser.add_argument("--workers", type=int, default=server_config.get("workers", 4), help="生产模式工作进程数")
parser.add_argument("--no-reload", action="store_true", help="禁用自动重载")
parser.add_argument("--check", action="store_true", help="仅检查环境")
parser.add_argument("--create-service", action="store_true", help="创建 systemd 服务")
args = parser.parse_args()
print("=== Video Keyframe Extraction API Server ===")
# 检查环境
rust_exe = check_rust_executable()
deps_ok = check_dependencies()
if args.check:
print("\n=== Environment Check ===")
print(f"Rust executable: {'' if rust_exe else ''}")
print(f"Python dependencies: {'' if deps_ok else ''}")
return
if args.create_service:
create_systemd_service()
return
# 安装模式
if args.mode == "install":
if not deps_ok:
install_dependencies()
else:
print("✓ Dependencies already installed")
return
# 检查必要条件
if not rust_exe:
print("✗ Cannot start without Rust executable")
print("Please run: cargo build --release")
sys.exit(1)
if not deps_ok:
print("Installing missing dependencies...")
if not install_dependencies():
sys.exit(1)
# 启动服务器
if args.mode == "dev":
start_development_server(
host=args.host,
port=args.port,
reload=not args.no_reload
)
elif args.mode == "prod":
start_production_server(
host=args.host,
port=args.port,
workers=args.workers
)
if __name__ == "__main__":
main()

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
视频分析器模块 - 优化版本
支持多种分析模式:批处理、逐帧、自动选择
视频分析器模块 - Rust优化版本
集成了Rust视频关键帧提取模块提供高性能的视频分析功能
支持SIMD优化、多线程处理和智能关键帧检测
"""
import os
import cv2
import tempfile
import asyncio
import base64
@@ -17,7 +17,6 @@ from PIL import Image
from pathlib import Path
from typing import List, Tuple, Optional, Dict
import io
from concurrent.futures import ThreadPoolExecutor
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config, model_config
@@ -26,6 +25,11 @@ from src.common.database.sqlalchemy_models import get_db_session, Videos
logger = get_logger("utils_video")
# 导入 Rust 视频处理模块
import rust_video
logger.info("✅ Rust 视频处理模块加载成功")
# 全局正在处理的视频哈希集合,用于防止重复处理
processing_videos = set()
processing_lock = asyncio.Lock()
@@ -35,110 +39,6 @@ video_events = {}
video_lock_manager = asyncio.Lock()
def _extract_frames_worker(video_path: str,
max_frames: int,
frame_quality: int,
max_image_size: int,
frame_extraction_mode: str,
frame_interval_seconds: Optional[float]) -> List[Tuple[str, float]]:
"""线程池中提取视频帧的工作函数"""
frames = []
try:
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps if fps > 0 else 0
if frame_extraction_mode == "time_interval":
# 新模式:按时间间隔抽帧
time_interval = frame_interval_seconds
next_frame_time = 0.0
extracted_count = 0 # 初始化提取帧计数器
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
current_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
if current_time >= next_frame_time:
# 转换为PIL图像并压缩
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(frame_rgb)
# 调整图像大小
if max(pil_image.size) > max_image_size:
ratio = max_image_size / max(pil_image.size)
new_size = tuple(int(dim * ratio) for dim in pil_image.size)
pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS)
# 转换为base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
frames.append((frame_base64, current_time))
extracted_count += 1
# 注意这里不能使用logger因为在线程池中
# logger.debug(f"提取第{extracted_count}帧 (时间: {current_time:.2f}s)")
next_frame_time += time_interval
else:
# 使用numpy优化帧间隔计算
if duration > 0:
frame_interval = max(1, int(duration / max_frames * fps))
else:
frame_interval = 30 # 默认间隔
# 使用numpy计算目标帧位置
target_frames = np.arange(0, min(max_frames, total_frames // frame_interval + 1)) * frame_interval
target_frames = target_frames[target_frames < total_frames].astype(int)
for target_frame in target_frames:
# 跳转到目标帧
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
ret, frame = cap.read()
if not ret:
continue
# 使用numpy优化图像处理
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 转换为PIL图像并使用numpy进行尺寸计算
height, width = frame_rgb.shape[:2]
max_dim = max(height, width)
if max_dim > max_image_size:
# 使用numpy计算缩放比例
ratio = max_image_size / max_dim
new_width = int(width * ratio)
new_height = int(height * ratio)
# 使用opencv进行高效缩放
frame_resized = cv2.resize(frame_rgb, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4)
pil_image = Image.fromarray(frame_resized)
else:
pil_image = Image.fromarray(frame_rgb)
# 转换为base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 计算时间戳
timestamp = target_frame / fps if fps > 0 else 0
frames.append((frame_base64, timestamp))
cap.release()
return frames
except Exception as e:
# 返回错误信息
return [("ERROR", str(e))]
class VideoAnalyzer:
"""优化的视频分析器类"""
@@ -168,6 +68,13 @@ class VideoAnalyzer:
self.max_image_size = getattr(config, 'max_image_size', 600)
self.enable_frame_timing = getattr(config, 'enable_frame_timing', True)
# Rust模块相关配置
self.rust_keyframe_threshold = getattr(config, 'rust_keyframe_threshold', 2.0)
self.rust_use_simd = getattr(config, 'rust_use_simd', True)
self.rust_block_size = getattr(config, 'rust_block_size', 8192)
self.rust_threads = getattr(config, 'rust_threads', 0)
self.ffmpeg_path = getattr(config, 'ffmpeg_path', 'ffmpeg')
# 从personality配置中获取人格信息
try:
personality_config = global_config.personality
@@ -225,6 +132,34 @@ class VideoAnalyzer:
self.system_prompt = "你是一个专业的视频内容分析助手。请仔细观察用户提供的视频关键帧,详细描述视频内容。"
logger.info(f"✅ 视频分析器初始化完成,分析模式: {self.analysis_mode}, 线程池: {self.use_multiprocessing}")
# 获取Rust模块系统信息
self._log_system_info()
def _log_system_info(self):
"""记录系统信息"""
try:
system_info = rust_video.get_system_info()
logger.info(f"🔧 系统信息: 线程数={system_info.get('threads', '未知')}")
# 记录CPU特性
features = []
if system_info.get('avx2_supported'):
features.append('AVX2')
if system_info.get('sse2_supported'):
features.append('SSE2')
if system_info.get('simd_supported'):
features.append('SIMD')
if features:
logger.info(f"🚀 CPU特性: {', '.join(features)}")
else:
logger.info("⚠️ 未检测到SIMD支持")
logger.info(f"📦 Rust模块版本: {system_info.get('version', '未知')}")
except Exception as e:
logger.warning(f"获取系统信息失败: {e}")
def _calculate_video_hash(self, video_data: bytes) -> str:
"""计算视频文件的hash值"""
@@ -245,6 +180,11 @@ class VideoAnalyzer:
def _store_video_result(self, video_hash: str, description: str, metadata: Optional[Dict] = None) -> Optional[Videos]:
"""存储视频分析结果到数据库"""
# 检查描述是否为错误信息,如果是则不保存
if description.startswith(""):
logger.warning(f"⚠️ 检测到错误信息,不保存到数据库: {description[:50]}...")
return None
try:
with get_db_session() as session:
# 只根据video_hash查找
@@ -299,171 +239,169 @@ class VideoAnalyzer:
logger.warning(f"无效的分析模式: {mode}")
async def extract_frames(self, video_path: str) -> List[Tuple[str, float]]:
"""提取视频帧 - 支持多进程和单线程模式"""
# 先获取视频信息
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps if fps > 0 else 0
cap.release()
logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}")
# 估算提取帧数
if duration > 0:
frame_interval = max(1, int(duration / self.max_frames * fps))
estimated_frames = min(self.max_frames, total_frames // frame_interval + 1)
else:
estimated_frames = self.max_frames
logger.info(f"计算得出帧间隔: {frame_interval} (将提取约{estimated_frames}帧)")
# 根据配置选择处理方式
if self.use_multiprocessing:
return await self._extract_frames_multiprocess(video_path)
else:
return await self._extract_frames_fallback(video_path)
async def _extract_frames_multiprocess(self, video_path: str) -> List[Tuple[str, float]]:
"""线程池版本的帧提取"""
loop = asyncio.get_event_loop()
"""提取视频帧 - 使用 Rust 实现"""
# 优先尝试高级接口,失败时回退到基础接口
try:
logger.info("🔄 启动线程池帧提取...")
# 使用线程池,避免进程间的导入问题
with ThreadPoolExecutor(max_workers=1) as executor:
frames = await loop.run_in_executor(
executor,
_extract_frames_worker,
video_path,
self.max_frames,
self.frame_quality,
self.max_image_size,
self.frame_extraction_mode,
self.frame_interval_seconds
)
return await self._extract_frames_rust_advanced(video_path)
except Exception as e:
logger.warning(f"高级接口失败: {e},使用基础接口")
return await self._extract_frames_rust(video_path)
async def _extract_frames_rust_advanced(self, video_path: str) -> List[Tuple[str, float]]:
"""使用 Rust 高级接口的帧提取"""
try:
logger.info("🔄 使用 Rust 高级接口提取关键帧...")
# 检查是否有错误
if frames and frames[0][0] == "ERROR":
logger.error(f"线程池帧提取失败: {frames[0][1]}")
# 降级到单线程模式
logger.info("🔄 降级到单线程模式...")
return await self._extract_frames_fallback(video_path)
# 创建 Rust 视频处理器,使用配置参数
extractor = rust_video.VideoKeyframeExtractor(
ffmpeg_path=self.ffmpeg_path,
threads=self.rust_threads,
verbose=False # 使用固定值,不需要配置
)
logger.info(f"✅ 成功提取{len(frames)}帧 (线程池模式)")
# 1. 提取所有帧
frames_data, width, height = extractor.extract_frames(
video_path=video_path,
max_frames=self.max_frames * 3 # 提取更多帧用于关键帧检测
)
logger.info(f"提取到 {len(frames_data)} 帧,视频尺寸: {width}x{height}")
# 2. 检测关键帧,使用配置参数
keyframe_indices = extractor.extract_keyframes(
frames=frames_data,
threshold=self.rust_keyframe_threshold,
use_simd=self.rust_use_simd,
block_size=self.rust_block_size
)
logger.info(f"检测到 {len(keyframe_indices)} 个关键帧")
# 3. 转换选定的关键帧为 base64
frames = []
frame_count = 0
for idx in keyframe_indices[:self.max_frames]:
if idx < len(frames_data):
try:
frame = frames_data[idx]
frame_data = frame.get_data()
# 将灰度数据转换为PIL图像
frame_array = np.frombuffer(frame_data, dtype=np.uint8).reshape((frame.height, frame.width))
pil_image = Image.fromarray(
frame_array,
mode='L' # 灰度模式
)
# 转换为RGB模式以便保存为JPEG
pil_image = pil_image.convert('RGB')
# 调整图像大小
if max(pil_image.size) > self.max_image_size:
ratio = self.max_image_size / max(pil_image.size)
new_size = tuple(int(dim * ratio) for dim in pil_image.size)
pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS)
# 转换为 base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=self.frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 估算时间戳
estimated_timestamp = frame.frame_number * (1.0 / 30.0) # 假设30fps
frames.append((frame_base64, estimated_timestamp))
frame_count += 1
logger.debug(f"处理关键帧 {frame_count}: 帧号 {frame.frame_number}, 时间 {estimated_timestamp:.2f}s")
except Exception as e:
logger.error(f"处理关键帧 {idx} 失败: {e}")
continue
logger.info(f"✅ Rust 高级提取完成: {len(frames)} 关键帧")
return frames
except Exception as e:
logger.error(f"线程池帧提取失败: {e}")
# 降级到原始方法
logger.info("🔄 降级到单线程模式...")
return await self._extract_frames_fallback(video_path)
logger.error(f"❌ Rust 高级帧提取失败: {e}")
# 回退到基础方法
logger.info("回退到基础 Rust 方法")
return await self._extract_frames_rust(video_path)
async def _extract_frames_fallback(self, video_path: str) -> List[Tuple[str, float]]:
"""帧提取的降级方法 - 原始异步版本"""
frames = []
extracted_count = 0
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps if fps > 0 else 0
logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}")
if self.frame_extraction_mode == "time_interval":
# 新模式:按时间间隔抽帧
time_interval = self.frame_interval_seconds
next_frame_time = 0.0
async def _extract_frames_rust(self, video_path: str) -> List[Tuple[str, float]]:
"""使用 Rust 实现的帧提取"""
try:
logger.info("🔄 使用 Rust 模块提取关键帧...")
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 创建临时输出目录
with tempfile.TemporaryDirectory() as temp_dir:
# 使用便捷函数进行关键帧提取,使用配置参数
result = rust_video.extract_keyframes_from_video(
video_path=video_path,
output_dir=temp_dir,
threshold=self.rust_keyframe_threshold,
max_frames=self.max_frames * 2, # 提取更多帧以便筛选
max_save=self.max_frames,
ffmpeg_path=self.ffmpeg_path,
use_simd=self.rust_use_simd,
threads=self.rust_threads,
verbose=False # 使用固定值,不需要配置
)
current_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
logger.info(f"Rust 处理完成: 总帧数 {result.total_frames}, 关键帧 {result.keyframes_extracted}, 处理速度 {result.processing_fps:.1f} FPS")
if current_time >= next_frame_time:
# 转换为PIL图像并压缩
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(frame_rgb)
# 调整图像大小
if max(pil_image.size) > self.max_image_size:
ratio = self.max_image_size / max(pil_image.size)
new_size = tuple(int(dim * ratio) for dim in pil_image.size)
pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS)
# 转换为base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=self.frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
frames.append((frame_base64, current_time))
extracted_count += 1
logger.debug(f"提取第{extracted_count}帧 (时间: {current_time:.2f}s)")
next_frame_time += time_interval
else:
# 使用numpy优化帧间隔计算
if duration > 0:
frame_interval = max(1, int(duration / self.max_frames * fps))
else:
frame_interval = 30 # 默认间隔
# 转换保存的关键帧为 base64 格式
frames = []
temp_dir_path = Path(temp_dir)
logger.info(f"计算得出帧间隔: {frame_interval} (将提取约{min(self.max_frames, total_frames // frame_interval + 1)}帧)")
# 使用numpy计算目标帧位置
target_frames = np.arange(0, min(self.max_frames, total_frames // frame_interval + 1)) * frame_interval
target_frames = target_frames[target_frames < total_frames].astype(int)
extracted_count = 0
for target_frame in target_frames:
# 跳转到目标帧
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
ret, frame = cap.read()
if not ret:
continue
# 使用numpy优化图像处理
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 获取所有保存的关键帧文件
keyframe_files = sorted(temp_dir_path.glob("keyframe_*.jpg"))
# 转换为PIL图像并使用numpy进行尺寸计算
height, width = frame_rgb.shape[:2]
max_dim = max(height, width)
for i, keyframe_file in enumerate(keyframe_files):
if len(frames) >= self.max_frames:
break
try:
# 读取关键帧文件
with open(keyframe_file, 'rb') as f:
image_data = f.read()
# 转换为 PIL 图像并压缩
pil_image = Image.open(io.BytesIO(image_data))
# 调整图像大小
if max(pil_image.size) > self.max_image_size:
ratio = self.max_image_size / max(pil_image.size)
new_size = tuple(int(dim * ratio) for dim in pil_image.size)
pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS)
# 转换为 base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=self.frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 估算时间戳(基于帧索引和总时长)
if result.total_frames > 0:
# 假设关键帧在时间上均匀分布
estimated_timestamp = (i * result.total_time_ms / 1000.0) / result.keyframes_extracted
else:
estimated_timestamp = i * 1.0 # 默认每秒一帧
frames.append((frame_base64, estimated_timestamp))
logger.debug(f"处理关键帧 {i+1}: 估算时间 {estimated_timestamp:.2f}s")
except Exception as e:
logger.error(f"处理关键帧 {keyframe_file.name} 失败: {e}")
continue
if max_dim > self.max_image_size:
# 使用numpy计算缩放比例
ratio = self.max_image_size / max_dim
new_width = int(width * ratio)
new_height = int(height * ratio)
# 使用opencv进行高效缩放
frame_resized = cv2.resize(frame_rgb, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4)
pil_image = Image.fromarray(frame_resized)
else:
pil_image = Image.fromarray(frame_rgb)
logger.info(f"✅ Rust 提取完成: {len(frames)} 关键帧")
return frames
# 转换为base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=self.frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 计算时间戳
timestamp = target_frame / fps if fps > 0 else 0
frames.append((frame_base64, timestamp))
extracted_count += 1
logger.debug(f"提取第{extracted_count}帧 (时间: {timestamp:.2f}s, 帧号: {target_frame})")
# 每提取一帧让步一次
await asyncio.sleep(0.001)
cap.release()
logger.info(f"✅ 成功提取{len(frames)}")
return frames
except Exception as e:
logger.error(f"❌ Rust 帧提取失败: {e}")
raise e
async def analyze_frames_batch(self, frames: List[Tuple[str, float]], user_question: str = None) -> str:
"""批量分析所有帧"""
@@ -493,29 +431,14 @@ class VideoAnalyzer:
prompt += "\n\n请基于所有提供的帧图像进行综合分析,关注并描述视频的完整内容和故事发展。"
try:
# 尝试使用多图片分析
# 使用多图片分析
response = await self._analyze_multiple_frames(frames, prompt)
logger.info("✅ 视频识别完成")
return response
except Exception as e:
logger.error(f"❌ 视频识别失败: {e}")
# 降级到单帧分析
logger.warning("降级到单帧分析模式")
try:
frame_base64, timestamp = frames[0]
fallback_prompt = prompt + f"\n\n注意由于技术限制当前仅显示第1帧 (时间: {timestamp:.2f}s),视频共有{len(frames)}帧。请基于这一帧进行分析。"
response, _ = await self.video_llm.generate_response_for_image(
prompt=fallback_prompt,
image_base64=frame_base64,
image_format="jpeg"
)
logger.info("✅ 降级的单帧分析完成")
return response
except Exception as fallback_e:
logger.error(f"❌ 降级分析也失败: {fallback_e}")
raise
raise e
async def _analyze_multiple_frames(self, frames: List[Tuple[str, float]], prompt: str) -> str:
"""使用多图片分析方法"""
@@ -616,15 +539,20 @@ class VideoAnalyzer:
# 如果汇总失败,返回各帧分析结果
return f"视频逐帧分析结果:\n\n{chr(10).join(frame_analyses)}"
async def analyze_video(self, video_path: str, user_question: str = None) -> str:
"""分析视频的主要方法"""
async def analyze_video(self, video_path: str, user_question: str = None) -> Tuple[bool, str]:
"""分析视频的主要方法
Returns:
Tuple[bool, str]: (是否成功, 分析结果或错误信息)
"""
try:
logger.info(f"开始分析视频: {os.path.basename(video_path)}")
# 提取帧
frames = await self.extract_frames(video_path)
if not frames:
return "❌ 无法从视频中提取有效帧"
error_msg = "❌ 无法从视频中提取有效帧"
return (False, error_msg)
# 根据模式选择分析方法
if self.analysis_mode == "auto":
@@ -641,12 +569,12 @@ class VideoAnalyzer:
result = await self.analyze_frames_sequential(frames, user_question)
logger.info("✅ 视频分析完成")
return result
return (True, result)
except Exception as e:
error_msg = f"❌ 视频分析失败: {str(e)}"
logger.error(error_msg)
return error_msg
return (False, error_msg)
async def analyze_video_from_bytes(self, video_bytes: bytes, filename: str = None, user_question: str = None, prompt: str = None) -> Dict[str, str]:
"""从字节数据分析视频
@@ -714,70 +642,60 @@ class VideoAnalyzer:
logger.info(f"✅ 获得锁后发现已有结果,直接返回 (id: {existing_video.id})")
video_event.set() # 通知其他等待者
return {"summary": existing_video.description}
# 未找到已存在记录,开始新的分析
logger.info("未找到已存在的视频记录,开始新的分析")
# 创建临时文件进行分析
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
temp_file.write(video_bytes)
temp_path = temp_file.name
try:
# 检查临时文件是否创建成功
if not os.path.exists(temp_path):
video_event.set() # 通知等待者
return {"summary": "❌ 临时文件创建失败"}
# 使用临时文件进行分析
result = await self.analyze_video(temp_path, question)
# 未找到已存在记录,开始新的分析
logger.info("未找到已存在的视频记录,开始新的分析")
finally:
# 清理临时文件
if os.path.exists(temp_path):
os.unlink(temp_path)
# 保存分析结果到数据库
metadata = {
"filename": filename,
"file_size": len(video_bytes),
"analysis_timestamp": time.time()
}
self._store_video_result(
video_hash=video_hash,
description=result,
metadata=metadata
)
# 处理完成,通知等待者并清理资源
video_event.set()
async with video_lock_manager:
# 清理资源
video_locks.pop(video_hash, None)
video_events.pop(video_hash, None)
return {"summary": result}
# 创建临时文件进行分析
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
temp_file.write(video_bytes)
temp_path = temp_file.name
try:
# 检查临时文件是否创建成功
if not os.path.exists(temp_path):
video_event.set() # 通知等待者
return {"summary": "❌ 临时文件创建失败"}
# 使用临时文件进行分析
success, result = await self.analyze_video(temp_path, question)
finally:
# 清理临时文件
if os.path.exists(temp_path):
os.unlink(temp_path)
# 保存分析结果到数据库(仅保存成功的结果)
if success:
metadata = {
"filename": filename,
"file_size": len(video_bytes),
"analysis_timestamp": time.time()
}
self._store_video_result(
video_hash=video_hash,
description=result,
metadata=metadata
)
logger.info("✅ 分析结果已保存到数据库")
else:
logger.warning("⚠️ 分析失败,不保存到数据库以便后续重试")
# 处理完成,通知等待者并清理资源
video_event.set()
async with video_lock_manager:
# 清理资源
video_locks.pop(video_hash, None)
video_events.pop(video_hash, None)
return {"summary": result}
except Exception as e:
error_msg = f"❌ 从字节数据分析视频失败: {str(e)}"
logger.error(error_msg)
# 即使失败也保存错误信息到数据库,避免重复处理
try:
metadata = {
"filename": filename,
"file_size": len(video_bytes),
"analysis_timestamp": time.time(),
"error": str(e)
}
self._store_video_result(
video_hash=video_hash,
description=error_msg,
metadata=metadata
)
logger.info("✅ 错误信息已保存到数据库")
except Exception as store_e:
logger.error(f"❌ 保存错误信息失败: {store_e}")
# 保存错误信息到数据库,允许后续重试
logger.info("💡 错误信息不保存到数据库,允许后续重试")
# 处理失败,通知等待者并清理资源
try:
@@ -797,6 +715,54 @@ class VideoAnalyzer:
supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v', '.3gp', '.webm'}
return Path(file_path).suffix.lower() in supported_formats
def get_processing_capabilities(self) -> Dict[str, any]:
"""获取处理能力信息"""
try:
system_info = rust_video.get_system_info()
# 创建一个临时的extractor来获取CPU特性
extractor = rust_video.VideoKeyframeExtractor(threads=0, verbose=False)
cpu_features = extractor.get_cpu_features()
capabilities = {
"system": {
"threads": system_info.get('threads', 0),
"rust_version": system_info.get('version', 'unknown'),
},
"cpu_features": cpu_features,
"recommended_settings": self._get_recommended_settings(cpu_features),
"analysis_modes": ["auto", "batch", "sequential"],
"supported_formats": ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v', '.3gp', '.webm']
}
return capabilities
except Exception as e:
logger.error(f"获取处理能力信息失败: {e}")
return {"error": str(e)}
def _get_recommended_settings(self, cpu_features: Dict[str, bool]) -> Dict[str, any]:
"""根据CPU特性推荐最佳设置"""
settings = {
"use_simd": any(cpu_features.values()),
"block_size": 8192,
"threads": 0 # 自动检测
}
# 根据CPU特性调整设置
if cpu_features.get('avx2', False):
settings["block_size"] = 16384 # AVX2支持更大的块
settings["optimization_level"] = "avx2"
elif cpu_features.get('sse2', False):
settings["block_size"] = 8192
settings["optimization_level"] = "sse2"
else:
settings["use_simd"] = False
settings["block_size"] = 4096
settings["optimization_level"] = "scalar"
return settings
# 全局实例
_video_analyzer = None

View File

@@ -561,13 +561,20 @@ class VideoAnalysisConfig(ValidatedConfigBase):
enable: bool = Field(default=True, description="启用")
analysis_mode: str = Field(default="batch_frames", description="分析模式")
frame_extraction_mode: str = Field(default="fixed_number", description="抽帧模式")
frame_extraction_mode: str = Field(default="keyframe", description="抽帧模式keyframe(关键帧), fixed_number(固定数量), time_interval(时间间隔)")
frame_interval_seconds: float = Field(default=2.0, description="抽帧时间间隔")
max_frames: int = Field(default=8, description="最大帧数")
frame_quality: int = Field(default=85, description="帧质量")
max_image_size: int = Field(default=800, description="最大图像大小")
enable_frame_timing: bool = Field(default=True, description="启用帧时间")
batch_analysis_prompt: str = Field(default="", description="批量分析提示")
# Rust模块相关配置
rust_keyframe_threshold: float = Field(default=2.0, description="关键帧检测阈值")
rust_use_simd: bool = Field(default=True, description="启用SIMD优化")
rust_block_size: int = Field(default=8192, description="Rust处理块大小")
rust_threads: int = Field(default=0, description="Rust线程数0表示自动检测")
ffmpeg_path: str = Field(default="ffmpeg", description="FFmpeg可执行文件路径")
class WebSearchConfig(ValidatedConfigBase):

View File

@@ -1,5 +1,5 @@
[inner]
version = "6.5.7"
version = "6.5.8"
#----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读----
#如果你想要修改配置文件请递增version的值
@@ -49,11 +49,11 @@ master_users = []# ["qq", "123456789"], # 示例QQ平台的Master用户
[bot]
platform = "qq"
qq_account = 1145141919810 # MoFox-Bot的QQ账号
nickname = "MoFox-Bot" # MoFox-Bot的昵称
alias_names = ["麦叠", "牢麦"] # MoFox-Bot的别名
nickname = "墨狐" # MoFox-Bot的昵称
alias_names = ["狐狐", "墨墨"] # MoFox-Bot的别名
[command]
command_prefixes = ['/', '!', '.', '#']
command_prefixes = ['/']
[personality]
# 建议50字以内描述人格的核心特质
@@ -395,15 +395,22 @@ pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问
[video_analysis] # 视频分析配置
enable = true # 是否启用视频分析功能
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择)
frame_extraction_mode = "fixed_number" # 抽帧模式: "fixed_number" (固定总帧数) 或 "time_interval" (按时间间隔)
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢)、"batch_frames"(批量分析,推荐)或 "auto"(自动选择)
frame_extraction_mode = "keyframe" # 抽帧模式: "keyframe" (智能关键帧,推荐)、"fixed_number" (固定总帧数) 或 "time_interval" (按时间间隔)
frame_interval_seconds = 2.0 # 按时间间隔抽帧的秒数(仅在 mode = "time_interval" 时生效)
max_frames = 16 # 最大分析帧数(仅在 mode = "fixed_number" 时生效)
max_frames = 16 # 最大分析帧数
frame_quality = 80 # 帧图像JPEG质量 (1-100)
max_image_size = 800 # 单帧最大图像尺寸(像素)
enable_frame_timing = true # 是否在分析中包含帧的时间信息
use_multiprocessing = true # 是否使用线程池处理视频帧提取(推荐开启,可防止卡死)
max_workers = 2 # 最大线程数建议1-2个避免过度消耗资源
# Rust模块相关配置
rust_keyframe_threshold = 2.0 # 关键帧检测阈值,值越大关键帧越少
rust_use_simd = true # 启用SIMD优化推荐
rust_block_size = 8192 # 处理块大小,较大值可能提高高分辨率视频性能
rust_threads = 0 # 线程数0表示自动检测
ffmpeg_path = "ffmpeg" # FFmpeg可执行文件路径
# 批量分析时使用的提示词
batch_analysis_prompt = """请以第一人称的视角来观看这一个视频,你看到的这些是从视频中按时间顺序提取的关键帧。