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:
1
src/chat/utils/rust-video/.gitignore
vendored
1
src/chat/utils/rust-video/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/target
|
|
||||||
610
src/chat/utils/rust-video/Cargo.lock
generated
610
src/chat/utils/rust-video/Cargo.lock
generated
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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脚本调用
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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", ×tamp.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(())
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
视频分析器模块 - 优化版本
|
视频分析器模块 - Rust优化版本
|
||||||
支持多种分析模式:批处理、逐帧、自动选择
|
集成了Rust视频关键帧提取模块,提供高性能的视频分析功能
|
||||||
|
支持SIMD优化、多线程处理和智能关键帧检测
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import cv2
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
@@ -17,7 +17,6 @@ from PIL import Image
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Tuple, Optional, Dict
|
from typing import List, Tuple, Optional, Dict
|
||||||
import io
|
import io
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from src.llm_models.utils_model import LLMRequest
|
from src.llm_models.utils_model import LLMRequest
|
||||||
from src.config.config import global_config, model_config
|
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")
|
logger = get_logger("utils_video")
|
||||||
|
|
||||||
|
# 导入 Rust 视频处理模块
|
||||||
|
import rust_video
|
||||||
|
|
||||||
|
logger.info("✅ Rust 视频处理模块加载成功")
|
||||||
|
|
||||||
# 全局正在处理的视频哈希集合,用于防止重复处理
|
# 全局正在处理的视频哈希集合,用于防止重复处理
|
||||||
processing_videos = set()
|
processing_videos = set()
|
||||||
processing_lock = asyncio.Lock()
|
processing_lock = asyncio.Lock()
|
||||||
@@ -35,110 +39,6 @@ video_events = {}
|
|||||||
video_lock_manager = asyncio.Lock()
|
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:
|
class VideoAnalyzer:
|
||||||
"""优化的视频分析器类"""
|
"""优化的视频分析器类"""
|
||||||
|
|
||||||
@@ -168,6 +68,13 @@ class VideoAnalyzer:
|
|||||||
self.max_image_size = getattr(config, 'max_image_size', 600)
|
self.max_image_size = getattr(config, 'max_image_size', 600)
|
||||||
self.enable_frame_timing = getattr(config, 'enable_frame_timing', True)
|
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配置中获取人格信息
|
# 从personality配置中获取人格信息
|
||||||
try:
|
try:
|
||||||
personality_config = global_config.personality
|
personality_config = global_config.personality
|
||||||
@@ -225,6 +132,34 @@ class VideoAnalyzer:
|
|||||||
self.system_prompt = "你是一个专业的视频内容分析助手。请仔细观察用户提供的视频关键帧,详细描述视频内容。"
|
self.system_prompt = "你是一个专业的视频内容分析助手。请仔细观察用户提供的视频关键帧,详细描述视频内容。"
|
||||||
|
|
||||||
logger.info(f"✅ 视频分析器初始化完成,分析模式: {self.analysis_mode}, 线程池: {self.use_multiprocessing}")
|
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:
|
def _calculate_video_hash(self, video_data: bytes) -> str:
|
||||||
"""计算视频文件的hash值"""
|
"""计算视频文件的hash值"""
|
||||||
@@ -245,6 +180,11 @@ class VideoAnalyzer:
|
|||||||
|
|
||||||
def _store_video_result(self, video_hash: str, description: str, metadata: Optional[Dict] = None) -> Optional[Videos]:
|
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:
|
try:
|
||||||
with get_db_session() as session:
|
with get_db_session() as session:
|
||||||
# 只根据video_hash查找
|
# 只根据video_hash查找
|
||||||
@@ -299,171 +239,169 @@ class VideoAnalyzer:
|
|||||||
logger.warning(f"无效的分析模式: {mode}")
|
logger.warning(f"无效的分析模式: {mode}")
|
||||||
|
|
||||||
async def extract_frames(self, video_path: str) -> List[Tuple[str, float]]:
|
async def extract_frames(self, video_path: str) -> List[Tuple[str, float]]:
|
||||||
"""提取视频帧 - 支持多进程和单线程模式"""
|
"""提取视频帧 - 使用 Rust 实现"""
|
||||||
# 先获取视频信息
|
# 优先尝试高级接口,失败时回退到基础接口
|
||||||
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()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("🔄 启动线程池帧提取...")
|
return await self._extract_frames_rust_advanced(video_path)
|
||||||
# 使用线程池,避免进程间的导入问题
|
except Exception as e:
|
||||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
logger.warning(f"高级接口失败: {e},使用基础接口")
|
||||||
frames = await loop.run_in_executor(
|
return await self._extract_frames_rust(video_path)
|
||||||
executor,
|
|
||||||
_extract_frames_worker,
|
async def _extract_frames_rust_advanced(self, video_path: str) -> List[Tuple[str, float]]:
|
||||||
video_path,
|
"""使用 Rust 高级接口的帧提取"""
|
||||||
self.max_frames,
|
try:
|
||||||
self.frame_quality,
|
logger.info("🔄 使用 Rust 高级接口提取关键帧...")
|
||||||
self.max_image_size,
|
|
||||||
self.frame_extraction_mode,
|
|
||||||
self.frame_interval_seconds
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查是否有错误
|
# 创建 Rust 视频处理器,使用配置参数
|
||||||
if frames and frames[0][0] == "ERROR":
|
extractor = rust_video.VideoKeyframeExtractor(
|
||||||
logger.error(f"线程池帧提取失败: {frames[0][1]}")
|
ffmpeg_path=self.ffmpeg_path,
|
||||||
# 降级到单线程模式
|
threads=self.rust_threads,
|
||||||
logger.info("🔄 降级到单线程模式...")
|
verbose=False # 使用固定值,不需要配置
|
||||||
return await self._extract_frames_fallback(video_path)
|
)
|
||||||
|
|
||||||
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
|
return frames
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"线程池帧提取失败: {e}")
|
logger.error(f"❌ Rust 高级帧提取失败: {e}")
|
||||||
# 降级到原始方法
|
# 回退到基础方法
|
||||||
logger.info("🔄 降级到单线程模式...")
|
logger.info("回退到基础 Rust 方法")
|
||||||
return await self._extract_frames_fallback(video_path)
|
return await self._extract_frames_rust(video_path)
|
||||||
|
|
||||||
async def _extract_frames_fallback(self, video_path: str) -> List[Tuple[str, float]]:
|
async def _extract_frames_rust(self, video_path: str) -> List[Tuple[str, float]]:
|
||||||
"""帧提取的降级方法 - 原始异步版本"""
|
"""使用 Rust 实现的帧提取"""
|
||||||
frames = []
|
try:
|
||||||
extracted_count = 0
|
logger.info("🔄 使用 Rust 模块提取关键帧...")
|
||||||
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
|
|
||||||
|
|
||||||
while cap.isOpened():
|
# 创建临时输出目录
|
||||||
ret, frame = cap.read()
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
if not ret:
|
# 使用便捷函数进行关键帧提取,使用配置参数
|
||||||
break
|
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:
|
# 转换保存的关键帧为 base64 格式
|
||||||
# 转换为PIL图像并压缩
|
frames = []
|
||||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
temp_dir_path = Path(temp_dir)
|
||||||
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 # 默认间隔
|
|
||||||
|
|
||||||
logger.info(f"计算得出帧间隔: {frame_interval} (将提取约{min(self.max_frames, total_frames // frame_interval + 1)}帧)")
|
# 获取所有保存的关键帧文件
|
||||||
|
keyframe_files = sorted(temp_dir_path.glob("keyframe_*.jpg"))
|
||||||
# 使用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)
|
|
||||||
|
|
||||||
# 转换为PIL图像并使用numpy进行尺寸计算
|
for i, keyframe_file in enumerate(keyframe_files):
|
||||||
height, width = frame_rgb.shape[:2]
|
if len(frames) >= self.max_frames:
|
||||||
max_dim = max(height, width)
|
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:
|
logger.info(f"✅ Rust 提取完成: {len(frames)} 关键帧")
|
||||||
# 使用numpy计算缩放比例
|
return frames
|
||||||
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)
|
|
||||||
|
|
||||||
# 转换为base64
|
except Exception as e:
|
||||||
buffer = io.BytesIO()
|
logger.error(f"❌ Rust 帧提取失败: {e}")
|
||||||
pil_image.save(buffer, format='JPEG', quality=self.frame_quality)
|
raise e
|
||||||
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
|
|
||||||
|
|
||||||
async def analyze_frames_batch(self, frames: List[Tuple[str, float]], user_question: str = None) -> str:
|
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请基于所有提供的帧图像进行综合分析,关注并描述视频的完整内容和故事发展。"
|
prompt += "\n\n请基于所有提供的帧图像进行综合分析,关注并描述视频的完整内容和故事发展。"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 尝试使用多图片分析
|
# 使用多图片分析
|
||||||
response = await self._analyze_multiple_frames(frames, prompt)
|
response = await self._analyze_multiple_frames(frames, prompt)
|
||||||
logger.info("✅ 视频识别完成")
|
logger.info("✅ 视频识别完成")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ 视频识别失败: {e}")
|
logger.error(f"❌ 视频识别失败: {e}")
|
||||||
# 降级到单帧分析
|
raise 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
|
|
||||||
|
|
||||||
async def _analyze_multiple_frames(self, frames: List[Tuple[str, float]], prompt: str) -> str:
|
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)}"
|
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:
|
try:
|
||||||
logger.info(f"开始分析视频: {os.path.basename(video_path)}")
|
logger.info(f"开始分析视频: {os.path.basename(video_path)}")
|
||||||
|
|
||||||
# 提取帧
|
# 提取帧
|
||||||
frames = await self.extract_frames(video_path)
|
frames = await self.extract_frames(video_path)
|
||||||
if not frames:
|
if not frames:
|
||||||
return "❌ 无法从视频中提取有效帧"
|
error_msg = "❌ 无法从视频中提取有效帧"
|
||||||
|
return (False, error_msg)
|
||||||
|
|
||||||
# 根据模式选择分析方法
|
# 根据模式选择分析方法
|
||||||
if self.analysis_mode == "auto":
|
if self.analysis_mode == "auto":
|
||||||
@@ -641,12 +569,12 @@ class VideoAnalyzer:
|
|||||||
result = await self.analyze_frames_sequential(frames, user_question)
|
result = await self.analyze_frames_sequential(frames, user_question)
|
||||||
|
|
||||||
logger.info("✅ 视频分析完成")
|
logger.info("✅ 视频分析完成")
|
||||||
return result
|
return (True, result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"❌ 视频分析失败: {str(e)}"
|
error_msg = f"❌ 视频分析失败: {str(e)}"
|
||||||
logger.error(error_msg)
|
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]:
|
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})")
|
logger.info(f"✅ 获得锁后发现已有结果,直接返回 (id: {existing_video.id})")
|
||||||
video_event.set() # 通知其他等待者
|
video_event.set() # 通知其他等待者
|
||||||
return {"summary": existing_video.description}
|
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:
|
# 创建临时文件进行分析
|
||||||
# 清理临时文件
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
|
||||||
if os.path.exists(temp_path):
|
temp_file.write(video_bytes)
|
||||||
os.unlink(temp_path)
|
temp_path = temp_file.name
|
||||||
|
|
||||||
# 保存分析结果到数据库
|
try:
|
||||||
metadata = {
|
# 检查临时文件是否创建成功
|
||||||
"filename": filename,
|
if not os.path.exists(temp_path):
|
||||||
"file_size": len(video_bytes),
|
video_event.set() # 通知等待者
|
||||||
"analysis_timestamp": time.time()
|
return {"summary": "❌ 临时文件创建失败"}
|
||||||
}
|
|
||||||
self._store_video_result(
|
# 使用临时文件进行分析
|
||||||
video_hash=video_hash,
|
success, result = await self.analyze_video(temp_path, question)
|
||||||
description=result,
|
|
||||||
metadata=metadata
|
finally:
|
||||||
)
|
# 清理临时文件
|
||||||
|
if os.path.exists(temp_path):
|
||||||
# 处理完成,通知等待者并清理资源
|
os.unlink(temp_path)
|
||||||
video_event.set()
|
|
||||||
async with video_lock_manager:
|
# 保存分析结果到数据库(仅保存成功的结果)
|
||||||
# 清理资源
|
if success:
|
||||||
video_locks.pop(video_hash, None)
|
metadata = {
|
||||||
video_events.pop(video_hash, None)
|
"filename": filename,
|
||||||
|
"file_size": len(video_bytes),
|
||||||
return {"summary": result}
|
"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:
|
except Exception as e:
|
||||||
error_msg = f"❌ 从字节数据分析视频失败: {str(e)}"
|
error_msg = f"❌ 从字节数据分析视频失败: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|
||||||
# 即使失败也保存错误信息到数据库,避免重复处理
|
# 不保存错误信息到数据库,允许后续重试
|
||||||
try:
|
logger.info("💡 错误信息不保存到数据库,允许后续重试")
|
||||||
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}")
|
|
||||||
|
|
||||||
# 处理失败,通知等待者并清理资源
|
# 处理失败,通知等待者并清理资源
|
||||||
try:
|
try:
|
||||||
@@ -797,6 +715,54 @@ class VideoAnalyzer:
|
|||||||
supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v', '.3gp', '.webm'}
|
supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v', '.3gp', '.webm'}
|
||||||
return Path(file_path).suffix.lower() in supported_formats
|
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
|
_video_analyzer = None
|
||||||
|
|||||||
@@ -561,13 +561,20 @@ class VideoAnalysisConfig(ValidatedConfigBase):
|
|||||||
|
|
||||||
enable: bool = Field(default=True, description="启用")
|
enable: bool = Field(default=True, description="启用")
|
||||||
analysis_mode: str = Field(default="batch_frames", 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="抽帧时间间隔")
|
frame_interval_seconds: float = Field(default=2.0, description="抽帧时间间隔")
|
||||||
max_frames: int = Field(default=8, description="最大帧数")
|
max_frames: int = Field(default=8, description="最大帧数")
|
||||||
frame_quality: int = Field(default=85, description="帧质量")
|
frame_quality: int = Field(default=85, description="帧质量")
|
||||||
max_image_size: int = Field(default=800, description="最大图像大小")
|
max_image_size: int = Field(default=800, description="最大图像大小")
|
||||||
enable_frame_timing: bool = Field(default=True, description="启用帧时间")
|
enable_frame_timing: bool = Field(default=True, description="启用帧时间")
|
||||||
batch_analysis_prompt: str = Field(default="", 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):
|
class WebSearchConfig(ValidatedConfigBase):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[inner]
|
[inner]
|
||||||
version = "6.5.7"
|
version = "6.5.8"
|
||||||
|
|
||||||
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
||||||
#如果你想要修改配置文件,请递增version的值
|
#如果你想要修改配置文件,请递增version的值
|
||||||
@@ -49,11 +49,11 @@ master_users = []# ["qq", "123456789"], # 示例:QQ平台的Master用户
|
|||||||
[bot]
|
[bot]
|
||||||
platform = "qq"
|
platform = "qq"
|
||||||
qq_account = 1145141919810 # MoFox-Bot的QQ账号
|
qq_account = 1145141919810 # MoFox-Bot的QQ账号
|
||||||
nickname = "MoFox-Bot" # MoFox-Bot的昵称
|
nickname = "墨狐" # MoFox-Bot的昵称
|
||||||
alias_names = ["麦叠", "牢麦"] # MoFox-Bot的别名
|
alias_names = ["狐狐", "墨墨"] # MoFox-Bot的别名
|
||||||
|
|
||||||
[command]
|
[command]
|
||||||
command_prefixes = ['/', '!', '.', '#']
|
command_prefixes = ['/']
|
||||||
|
|
||||||
[personality]
|
[personality]
|
||||||
# 建议50字以内,描述人格的核心特质
|
# 建议50字以内,描述人格的核心特质
|
||||||
@@ -395,15 +395,22 @@ pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问
|
|||||||
|
|
||||||
[video_analysis] # 视频分析配置
|
[video_analysis] # 视频分析配置
|
||||||
enable = true # 是否启用视频分析功能
|
enable = true # 是否启用视频分析功能
|
||||||
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择)
|
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢)、"batch_frames"(批量分析,推荐)或 "auto"(自动选择)
|
||||||
frame_extraction_mode = "fixed_number" # 抽帧模式: "fixed_number" (固定总帧数) 或 "time_interval" (按时间间隔)
|
frame_extraction_mode = "keyframe" # 抽帧模式: "keyframe" (智能关键帧,推荐)、"fixed_number" (固定总帧数) 或 "time_interval" (按时间间隔)
|
||||||
frame_interval_seconds = 2.0 # 按时间间隔抽帧的秒数(仅在 mode = "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)
|
frame_quality = 80 # 帧图像JPEG质量 (1-100)
|
||||||
max_image_size = 800 # 单帧最大图像尺寸(像素)
|
max_image_size = 800 # 单帧最大图像尺寸(像素)
|
||||||
enable_frame_timing = true # 是否在分析中包含帧的时间信息
|
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 = """请以第一人称的视角来观看这一个视频,你看到的这些是从视频中按时间顺序提取的关键帧。
|
batch_analysis_prompt = """请以第一人称的视角来观看这一个视频,你看到的这些是从视频中按时间顺序提取的关键帧。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user