From 0a647376f747aa2080b02824d453e1351aa9b89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=85=E8=AF=BA=E7=8B=90?= <212194964+foxcyber907@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:13:42 +0800 Subject: [PATCH] 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. --- src/chat/utils/rust-video/.gitignore | 1 - src/chat/utils/rust-video/Cargo.lock | 610 ------------------- src/chat/utils/rust-video/Cargo.toml | 24 - src/chat/utils/rust-video/README.md | 221 ------- src/chat/utils/rust-video/api_server.py | 472 -------------- src/chat/utils/rust-video/config.py | 115 ---- src/chat/utils/rust-video/config.toml | 70 --- src/chat/utils/rust-video/src/main.rs | 710 ---------------------- src/chat/utils/rust-video/start_server.py | 219 ------- src/chat/utils/utils_video.py | 642 +++++++++---------- src/config/official_configs.py | 9 +- template/bot_config_template.toml | 25 +- 12 files changed, 328 insertions(+), 2790 deletions(-) delete mode 100644 src/chat/utils/rust-video/.gitignore delete mode 100644 src/chat/utils/rust-video/Cargo.lock delete mode 100644 src/chat/utils/rust-video/Cargo.toml delete mode 100644 src/chat/utils/rust-video/README.md delete mode 100644 src/chat/utils/rust-video/api_server.py delete mode 100644 src/chat/utils/rust-video/config.py delete mode 100644 src/chat/utils/rust-video/config.toml delete mode 100644 src/chat/utils/rust-video/src/main.rs delete mode 100644 src/chat/utils/rust-video/start_server.py diff --git a/src/chat/utils/rust-video/.gitignore b/src/chat/utils/rust-video/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/src/chat/utils/rust-video/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/src/chat/utils/rust-video/Cargo.lock b/src/chat/utils/rust-video/Cargo.lock deleted file mode 100644 index 8041152b2..000000000 --- a/src/chat/utils/rust-video/Cargo.lock +++ /dev/null @@ -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" diff --git a/src/chat/utils/rust-video/Cargo.toml b/src/chat/utils/rust-video/Cargo.toml deleted file mode 100644 index 4120f6f2c..000000000 --- a/src/chat/utils/rust-video/Cargo.toml +++ /dev/null @@ -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 diff --git a/src/chat/utils/rust-video/README.md b/src/chat/utils/rust-video/README.md deleted file mode 100644 index e4c38b0ab..000000000 --- a/src/chat/utils/rust-video/README.md +++ /dev/null @@ -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脚本调用 diff --git a/src/chat/utils/rust-video/api_server.py b/src/chat/utils/rust-video/api_server.py deleted file mode 100644 index aeb3fa248..000000000 --- a/src/chat/utils/rust-video/api_server.py +++ /dev/null @@ -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() diff --git a/src/chat/utils/rust-video/config.py b/src/chat/utils/rust-video/config.py deleted file mode 100644 index c85b8f9ea..000000000 --- a/src/chat/utils/rust-video/config.py +++ /dev/null @@ -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() diff --git a/src/chat/utils/rust-video/config.toml b/src/chat/utils/rust-video/config.toml deleted file mode 100644 index 56e7799cc..000000000 --- a/src/chat/utils/rust-video/config.toml +++ /dev/null @@ -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 diff --git a/src/chat/utils/rust-video/src/main.rs b/src/chat/utils/rust-video/src/main.rs deleted file mode 100644 index 13fd98cbb..000000000 --- a/src/chat/utils/rust-video/src/main.rs +++ /dev/null @@ -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, - - /// 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, // 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) -> 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, 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::(), height_str.parse::()) { - 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> { - 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 = 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 = 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 { - 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 { - 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 "))?; - - 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 "))?; - - 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(()) -} diff --git a/src/chat/utils/rust-video/start_server.py b/src/chat/utils/rust-video/start_server.py deleted file mode 100644 index b1547d441..000000000 --- a/src/chat/utils/rust-video/start_server.py +++ /dev/null @@ -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() diff --git a/src/chat/utils/utils_video.py b/src/chat/utils/utils_video.py index 5e9f118da..003a75216 100644 --- a/src/chat/utils/utils_video.py +++ b/src/chat/utils/utils_video.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -视频分析器模块 - 优化版本 -支持多种分析模式:批处理、逐帧、自动选择 +视频分析器模块 - Rust优化版本 +集成了Rust视频关键帧提取模块,提供高性能的视频分析功能 +支持SIMD优化、多线程处理和智能关键帧检测 """ import os -import cv2 import tempfile import asyncio import base64 @@ -17,7 +17,6 @@ from PIL import Image from pathlib import Path from typing import List, Tuple, Optional, Dict import io -from concurrent.futures import ThreadPoolExecutor from src.llm_models.utils_model import LLMRequest from src.config.config import global_config, model_config @@ -26,6 +25,11 @@ from src.common.database.sqlalchemy_models import get_db_session, Videos logger = get_logger("utils_video") +# 导入 Rust 视频处理模块 +import rust_video + +logger.info("✅ Rust 视频处理模块加载成功") + # 全局正在处理的视频哈希集合,用于防止重复处理 processing_videos = set() processing_lock = asyncio.Lock() @@ -35,110 +39,6 @@ video_events = {} video_lock_manager = asyncio.Lock() -def _extract_frames_worker(video_path: str, - max_frames: int, - frame_quality: int, - max_image_size: int, - frame_extraction_mode: str, - frame_interval_seconds: Optional[float]) -> List[Tuple[str, float]]: - """线程池中提取视频帧的工作函数""" - frames = [] - try: - cap = cv2.VideoCapture(video_path) - fps = cap.get(cv2.CAP_PROP_FPS) - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - duration = total_frames / fps if fps > 0 else 0 - - if frame_extraction_mode == "time_interval": - # 新模式:按时间间隔抽帧 - time_interval = frame_interval_seconds - next_frame_time = 0.0 - extracted_count = 0 # 初始化提取帧计数器 - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - current_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0 - - if current_time >= next_frame_time: - # 转换为PIL图像并压缩 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - pil_image = Image.fromarray(frame_rgb) - - # 调整图像大小 - if max(pil_image.size) > max_image_size: - ratio = max_image_size / max(pil_image.size) - new_size = tuple(int(dim * ratio) for dim in pil_image.size) - pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS) - - # 转换为base64 - buffer = io.BytesIO() - pil_image.save(buffer, format='JPEG', quality=frame_quality) - frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') - - frames.append((frame_base64, current_time)) - extracted_count += 1 - - # 注意:这里不能使用logger,因为在线程池中 - # logger.debug(f"提取第{extracted_count}帧 (时间: {current_time:.2f}s)") - - next_frame_time += time_interval - else: - # 使用numpy优化帧间隔计算 - if duration > 0: - frame_interval = max(1, int(duration / max_frames * fps)) - else: - frame_interval = 30 # 默认间隔 - - # 使用numpy计算目标帧位置 - target_frames = np.arange(0, min(max_frames, total_frames // frame_interval + 1)) * frame_interval - target_frames = target_frames[target_frames < total_frames].astype(int) - - for target_frame in target_frames: - # 跳转到目标帧 - cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) - ret, frame = cap.read() - if not ret: - continue - - # 使用numpy优化图像处理 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # 转换为PIL图像并使用numpy进行尺寸计算 - height, width = frame_rgb.shape[:2] - max_dim = max(height, width) - - if max_dim > max_image_size: - # 使用numpy计算缩放比例 - ratio = max_image_size / max_dim - new_width = int(width * ratio) - new_height = int(height * ratio) - - # 使用opencv进行高效缩放 - frame_resized = cv2.resize(frame_rgb, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4) - pil_image = Image.fromarray(frame_resized) - else: - pil_image = Image.fromarray(frame_rgb) - - # 转换为base64 - buffer = io.BytesIO() - pil_image.save(buffer, format='JPEG', quality=frame_quality) - frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') - - # 计算时间戳 - timestamp = target_frame / fps if fps > 0 else 0 - frames.append((frame_base64, timestamp)) - - cap.release() - return frames - - except Exception as e: - # 返回错误信息 - return [("ERROR", str(e))] - - class VideoAnalyzer: """优化的视频分析器类""" @@ -168,6 +68,13 @@ class VideoAnalyzer: self.max_image_size = getattr(config, 'max_image_size', 600) self.enable_frame_timing = getattr(config, 'enable_frame_timing', True) + # Rust模块相关配置 + self.rust_keyframe_threshold = getattr(config, 'rust_keyframe_threshold', 2.0) + self.rust_use_simd = getattr(config, 'rust_use_simd', True) + self.rust_block_size = getattr(config, 'rust_block_size', 8192) + self.rust_threads = getattr(config, 'rust_threads', 0) + self.ffmpeg_path = getattr(config, 'ffmpeg_path', 'ffmpeg') + # 从personality配置中获取人格信息 try: personality_config = global_config.personality @@ -225,6 +132,34 @@ class VideoAnalyzer: self.system_prompt = "你是一个专业的视频内容分析助手。请仔细观察用户提供的视频关键帧,详细描述视频内容。" logger.info(f"✅ 视频分析器初始化完成,分析模式: {self.analysis_mode}, 线程池: {self.use_multiprocessing}") + + # 获取Rust模块系统信息 + self._log_system_info() + + def _log_system_info(self): + """记录系统信息""" + try: + system_info = rust_video.get_system_info() + logger.info(f"🔧 系统信息: 线程数={system_info.get('threads', '未知')}") + + # 记录CPU特性 + features = [] + if system_info.get('avx2_supported'): + features.append('AVX2') + if system_info.get('sse2_supported'): + features.append('SSE2') + if system_info.get('simd_supported'): + features.append('SIMD') + + if features: + logger.info(f"🚀 CPU特性: {', '.join(features)}") + else: + logger.info("⚠️ 未检测到SIMD支持") + + logger.info(f"📦 Rust模块版本: {system_info.get('version', '未知')}") + + except Exception as e: + logger.warning(f"获取系统信息失败: {e}") def _calculate_video_hash(self, video_data: bytes) -> str: """计算视频文件的hash值""" @@ -245,6 +180,11 @@ class VideoAnalyzer: def _store_video_result(self, video_hash: str, description: str, metadata: Optional[Dict] = None) -> Optional[Videos]: """存储视频分析结果到数据库""" + # 检查描述是否为错误信息,如果是则不保存 + if description.startswith("❌"): + logger.warning(f"⚠️ 检测到错误信息,不保存到数据库: {description[:50]}...") + return None + try: with get_db_session() as session: # 只根据video_hash查找 @@ -299,171 +239,169 @@ class VideoAnalyzer: logger.warning(f"无效的分析模式: {mode}") async def extract_frames(self, video_path: str) -> List[Tuple[str, float]]: - """提取视频帧 - 支持多进程和单线程模式""" - # 先获取视频信息 - cap = cv2.VideoCapture(video_path) - fps = cap.get(cv2.CAP_PROP_FPS) - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - duration = total_frames / fps if fps > 0 else 0 - cap.release() - - logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}秒") - - # 估算提取帧数 - if duration > 0: - frame_interval = max(1, int(duration / self.max_frames * fps)) - estimated_frames = min(self.max_frames, total_frames // frame_interval + 1) - else: - estimated_frames = self.max_frames - - logger.info(f"计算得出帧间隔: {frame_interval} (将提取约{estimated_frames}帧)") - - # 根据配置选择处理方式 - if self.use_multiprocessing: - return await self._extract_frames_multiprocess(video_path) - else: - return await self._extract_frames_fallback(video_path) - - async def _extract_frames_multiprocess(self, video_path: str) -> List[Tuple[str, float]]: - """线程池版本的帧提取""" - loop = asyncio.get_event_loop() - + """提取视频帧 - 使用 Rust 实现""" + # 优先尝试高级接口,失败时回退到基础接口 try: - logger.info("🔄 启动线程池帧提取...") - # 使用线程池,避免进程间的导入问题 - with ThreadPoolExecutor(max_workers=1) as executor: - frames = await loop.run_in_executor( - executor, - _extract_frames_worker, - video_path, - self.max_frames, - self.frame_quality, - self.max_image_size, - self.frame_extraction_mode, - self.frame_interval_seconds - ) + return await self._extract_frames_rust_advanced(video_path) + except Exception as e: + logger.warning(f"高级接口失败: {e},使用基础接口") + return await self._extract_frames_rust(video_path) + + async def _extract_frames_rust_advanced(self, video_path: str) -> List[Tuple[str, float]]: + """使用 Rust 高级接口的帧提取""" + try: + logger.info("🔄 使用 Rust 高级接口提取关键帧...") - # 检查是否有错误 - if frames and frames[0][0] == "ERROR": - logger.error(f"线程池帧提取失败: {frames[0][1]}") - # 降级到单线程模式 - logger.info("🔄 降级到单线程模式...") - return await self._extract_frames_fallback(video_path) + # 创建 Rust 视频处理器,使用配置参数 + extractor = rust_video.VideoKeyframeExtractor( + ffmpeg_path=self.ffmpeg_path, + threads=self.rust_threads, + verbose=False # 使用固定值,不需要配置 + ) - logger.info(f"✅ 成功提取{len(frames)}帧 (线程池模式)") + # 1. 提取所有帧 + frames_data, width, height = extractor.extract_frames( + video_path=video_path, + max_frames=self.max_frames * 3 # 提取更多帧用于关键帧检测 + ) + + logger.info(f"提取到 {len(frames_data)} 帧,视频尺寸: {width}x{height}") + + # 2. 检测关键帧,使用配置参数 + keyframe_indices = extractor.extract_keyframes( + frames=frames_data, + threshold=self.rust_keyframe_threshold, + use_simd=self.rust_use_simd, + block_size=self.rust_block_size + ) + + logger.info(f"检测到 {len(keyframe_indices)} 个关键帧") + + # 3. 转换选定的关键帧为 base64 + frames = [] + frame_count = 0 + + for idx in keyframe_indices[:self.max_frames]: + if idx < len(frames_data): + try: + frame = frames_data[idx] + frame_data = frame.get_data() + + # 将灰度数据转换为PIL图像 + frame_array = np.frombuffer(frame_data, dtype=np.uint8).reshape((frame.height, frame.width)) + pil_image = Image.fromarray( + frame_array, + mode='L' # 灰度模式 + ) + + # 转换为RGB模式以便保存为JPEG + pil_image = pil_image.convert('RGB') + + # 调整图像大小 + if max(pil_image.size) > self.max_image_size: + ratio = self.max_image_size / max(pil_image.size) + new_size = tuple(int(dim * ratio) for dim in pil_image.size) + pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS) + + # 转换为 base64 + buffer = io.BytesIO() + pil_image.save(buffer, format='JPEG', quality=self.frame_quality) + frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + # 估算时间戳 + estimated_timestamp = frame.frame_number * (1.0 / 30.0) # 假设30fps + + frames.append((frame_base64, estimated_timestamp)) + frame_count += 1 + + logger.debug(f"处理关键帧 {frame_count}: 帧号 {frame.frame_number}, 时间 {estimated_timestamp:.2f}s") + + except Exception as e: + logger.error(f"处理关键帧 {idx} 失败: {e}") + continue + + logger.info(f"✅ Rust 高级提取完成: {len(frames)} 关键帧") return frames except Exception as e: - logger.error(f"线程池帧提取失败: {e}") - # 降级到原始方法 - logger.info("🔄 降级到单线程模式...") - return await self._extract_frames_fallback(video_path) + logger.error(f"❌ Rust 高级帧提取失败: {e}") + # 回退到基础方法 + logger.info("回退到基础 Rust 方法") + return await self._extract_frames_rust(video_path) - async def _extract_frames_fallback(self, video_path: str) -> List[Tuple[str, float]]: - """帧提取的降级方法 - 原始异步版本""" - frames = [] - extracted_count = 0 - cap = cv2.VideoCapture(video_path) - fps = cap.get(cv2.CAP_PROP_FPS) - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - duration = total_frames / fps if fps > 0 else 0 - - logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}秒") - - - if self.frame_extraction_mode == "time_interval": - # 新模式:按时间间隔抽帧 - time_interval = self.frame_interval_seconds - next_frame_time = 0.0 + async def _extract_frames_rust(self, video_path: str) -> List[Tuple[str, float]]: + """使用 Rust 实现的帧提取""" + try: + logger.info("🔄 使用 Rust 模块提取关键帧...") - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break + # 创建临时输出目录 + with tempfile.TemporaryDirectory() as temp_dir: + # 使用便捷函数进行关键帧提取,使用配置参数 + result = rust_video.extract_keyframes_from_video( + video_path=video_path, + output_dir=temp_dir, + threshold=self.rust_keyframe_threshold, + max_frames=self.max_frames * 2, # 提取更多帧以便筛选 + max_save=self.max_frames, + ffmpeg_path=self.ffmpeg_path, + use_simd=self.rust_use_simd, + threads=self.rust_threads, + verbose=False # 使用固定值,不需要配置 + ) - current_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0 + logger.info(f"Rust 处理完成: 总帧数 {result.total_frames}, 关键帧 {result.keyframes_extracted}, 处理速度 {result.processing_fps:.1f} FPS") - if current_time >= next_frame_time: - # 转换为PIL图像并压缩 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - pil_image = Image.fromarray(frame_rgb) - - # 调整图像大小 - if max(pil_image.size) > self.max_image_size: - ratio = self.max_image_size / max(pil_image.size) - new_size = tuple(int(dim * ratio) for dim in pil_image.size) - pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS) - - # 转换为base64 - buffer = io.BytesIO() - pil_image.save(buffer, format='JPEG', quality=self.frame_quality) - frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') - - frames.append((frame_base64, current_time)) - extracted_count += 1 - - logger.debug(f"提取第{extracted_count}帧 (时间: {current_time:.2f}s)") - - next_frame_time += time_interval - else: - # 使用numpy优化帧间隔计算 - if duration > 0: - frame_interval = max(1, int(duration / self.max_frames * fps)) - else: - frame_interval = 30 # 默认间隔 + # 转换保存的关键帧为 base64 格式 + frames = [] + temp_dir_path = Path(temp_dir) - logger.info(f"计算得出帧间隔: {frame_interval} (将提取约{min(self.max_frames, total_frames // frame_interval + 1)}帧)") - - # 使用numpy计算目标帧位置 - target_frames = np.arange(0, min(self.max_frames, total_frames // frame_interval + 1)) * frame_interval - target_frames = target_frames[target_frames < total_frames].astype(int) - - extracted_count = 0 - - for target_frame in target_frames: - # 跳转到目标帧 - cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) - ret, frame = cap.read() - if not ret: - continue - - # 使用numpy优化图像处理 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + # 获取所有保存的关键帧文件 + keyframe_files = sorted(temp_dir_path.glob("keyframe_*.jpg")) - # 转换为PIL图像并使用numpy进行尺寸计算 - height, width = frame_rgb.shape[:2] - max_dim = max(height, width) + for i, keyframe_file in enumerate(keyframe_files): + if len(frames) >= self.max_frames: + break + + try: + # 读取关键帧文件 + with open(keyframe_file, 'rb') as f: + image_data = f.read() + + # 转换为 PIL 图像并压缩 + pil_image = Image.open(io.BytesIO(image_data)) + + # 调整图像大小 + if max(pil_image.size) > self.max_image_size: + ratio = self.max_image_size / max(pil_image.size) + new_size = tuple(int(dim * ratio) for dim in pil_image.size) + pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS) + + # 转换为 base64 + buffer = io.BytesIO() + pil_image.save(buffer, format='JPEG', quality=self.frame_quality) + frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + # 估算时间戳(基于帧索引和总时长) + if result.total_frames > 0: + # 假设关键帧在时间上均匀分布 + estimated_timestamp = (i * result.total_time_ms / 1000.0) / result.keyframes_extracted + else: + estimated_timestamp = i * 1.0 # 默认每秒一帧 + + frames.append((frame_base64, estimated_timestamp)) + + logger.debug(f"处理关键帧 {i+1}: 估算时间 {estimated_timestamp:.2f}s") + + except Exception as e: + logger.error(f"处理关键帧 {keyframe_file.name} 失败: {e}") + continue - if max_dim > self.max_image_size: - # 使用numpy计算缩放比例 - ratio = self.max_image_size / max_dim - new_width = int(width * ratio) - new_height = int(height * ratio) - - # 使用opencv进行高效缩放 - frame_resized = cv2.resize(frame_rgb, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4) - pil_image = Image.fromarray(frame_resized) - else: - pil_image = Image.fromarray(frame_rgb) + logger.info(f"✅ Rust 提取完成: {len(frames)} 关键帧") + return frames - # 转换为base64 - buffer = io.BytesIO() - pil_image.save(buffer, format='JPEG', quality=self.frame_quality) - frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') - - # 计算时间戳 - timestamp = target_frame / fps if fps > 0 else 0 - frames.append((frame_base64, timestamp)) - extracted_count += 1 - - logger.debug(f"提取第{extracted_count}帧 (时间: {timestamp:.2f}s, 帧号: {target_frame})") - - # 每提取一帧让步一次 - await asyncio.sleep(0.001) - - cap.release() - logger.info(f"✅ 成功提取{len(frames)}帧") - return frames + except Exception as e: + logger.error(f"❌ Rust 帧提取失败: {e}") + raise e async def analyze_frames_batch(self, frames: List[Tuple[str, float]], user_question: str = None) -> str: """批量分析所有帧""" @@ -493,29 +431,14 @@ class VideoAnalyzer: prompt += "\n\n请基于所有提供的帧图像进行综合分析,关注并描述视频的完整内容和故事发展。" try: - # 尝试使用多图片分析 + # 使用多图片分析 response = await self._analyze_multiple_frames(frames, prompt) logger.info("✅ 视频识别完成") return response except Exception as e: logger.error(f"❌ 视频识别失败: {e}") - # 降级到单帧分析 - logger.warning("降级到单帧分析模式") - try: - frame_base64, timestamp = frames[0] - fallback_prompt = prompt + f"\n\n注意:由于技术限制,当前仅显示第1帧 (时间: {timestamp:.2f}s),视频共有{len(frames)}帧。请基于这一帧进行分析。" - - response, _ = await self.video_llm.generate_response_for_image( - prompt=fallback_prompt, - image_base64=frame_base64, - image_format="jpeg" - ) - logger.info("✅ 降级的单帧分析完成") - return response - except Exception as fallback_e: - logger.error(f"❌ 降级分析也失败: {fallback_e}") - raise + raise e async def _analyze_multiple_frames(self, frames: List[Tuple[str, float]], prompt: str) -> str: """使用多图片分析方法""" @@ -616,15 +539,20 @@ class VideoAnalyzer: # 如果汇总失败,返回各帧分析结果 return f"视频逐帧分析结果:\n\n{chr(10).join(frame_analyses)}" - async def analyze_video(self, video_path: str, user_question: str = None) -> str: - """分析视频的主要方法""" + async def analyze_video(self, video_path: str, user_question: str = None) -> Tuple[bool, str]: + """分析视频的主要方法 + + Returns: + Tuple[bool, str]: (是否成功, 分析结果或错误信息) + """ try: logger.info(f"开始分析视频: {os.path.basename(video_path)}") # 提取帧 frames = await self.extract_frames(video_path) if not frames: - return "❌ 无法从视频中提取有效帧" + error_msg = "❌ 无法从视频中提取有效帧" + return (False, error_msg) # 根据模式选择分析方法 if self.analysis_mode == "auto": @@ -641,12 +569,12 @@ class VideoAnalyzer: result = await self.analyze_frames_sequential(frames, user_question) logger.info("✅ 视频分析完成") - return result + return (True, result) except Exception as e: error_msg = f"❌ 视频分析失败: {str(e)}" logger.error(error_msg) - return error_msg + return (False, error_msg) async def analyze_video_from_bytes(self, video_bytes: bytes, filename: str = None, user_question: str = None, prompt: str = None) -> Dict[str, str]: """从字节数据分析视频 @@ -714,70 +642,60 @@ class VideoAnalyzer: logger.info(f"✅ 获得锁后发现已有结果,直接返回 (id: {existing_video.id})") video_event.set() # 通知其他等待者 return {"summary": existing_video.description} - - # 未找到已存在记录,开始新的分析 - logger.info("未找到已存在的视频记录,开始新的分析") - - # 创建临时文件进行分析 - with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file: - temp_file.write(video_bytes) - temp_path = temp_file.name - - try: - # 检查临时文件是否创建成功 - if not os.path.exists(temp_path): - video_event.set() # 通知等待者 - return {"summary": "❌ 临时文件创建失败"} - # 使用临时文件进行分析 - result = await self.analyze_video(temp_path, question) + # 未找到已存在记录,开始新的分析 + logger.info("未找到已存在的视频记录,开始新的分析") - finally: - # 清理临时文件 - if os.path.exists(temp_path): - os.unlink(temp_path) - - # 保存分析结果到数据库 - metadata = { - "filename": filename, - "file_size": len(video_bytes), - "analysis_timestamp": time.time() - } - self._store_video_result( - video_hash=video_hash, - description=result, - metadata=metadata - ) - - # 处理完成,通知等待者并清理资源 - video_event.set() - async with video_lock_manager: - # 清理资源 - video_locks.pop(video_hash, None) - video_events.pop(video_hash, None) - - return {"summary": result} + # 创建临时文件进行分析 + with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file: + temp_file.write(video_bytes) + temp_path = temp_file.name + + try: + # 检查临时文件是否创建成功 + if not os.path.exists(temp_path): + video_event.set() # 通知等待者 + return {"summary": "❌ 临时文件创建失败"} + + # 使用临时文件进行分析 + success, result = await self.analyze_video(temp_path, question) + + finally: + # 清理临时文件 + if os.path.exists(temp_path): + os.unlink(temp_path) + + # 保存分析结果到数据库(仅保存成功的结果) + if success: + metadata = { + "filename": filename, + "file_size": len(video_bytes), + "analysis_timestamp": time.time() + } + self._store_video_result( + video_hash=video_hash, + description=result, + metadata=metadata + ) + logger.info("✅ 分析结果已保存到数据库") + else: + logger.warning("⚠️ 分析失败,不保存到数据库以便后续重试") + + # 处理完成,通知等待者并清理资源 + video_event.set() + async with video_lock_manager: + # 清理资源 + video_locks.pop(video_hash, None) + video_events.pop(video_hash, None) + + return {"summary": result} except Exception as e: error_msg = f"❌ 从字节数据分析视频失败: {str(e)}" logger.error(error_msg) - # 即使失败也保存错误信息到数据库,避免重复处理 - try: - metadata = { - "filename": filename, - "file_size": len(video_bytes), - "analysis_timestamp": time.time(), - "error": str(e) - } - self._store_video_result( - video_hash=video_hash, - description=error_msg, - metadata=metadata - ) - logger.info("✅ 错误信息已保存到数据库") - except Exception as store_e: - logger.error(f"❌ 保存错误信息失败: {store_e}") + # 不保存错误信息到数据库,允许后续重试 + logger.info("💡 错误信息不保存到数据库,允许后续重试") # 处理失败,通知等待者并清理资源 try: @@ -797,6 +715,54 @@ class VideoAnalyzer: supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v', '.3gp', '.webm'} return Path(file_path).suffix.lower() in supported_formats + def get_processing_capabilities(self) -> Dict[str, any]: + """获取处理能力信息""" + try: + system_info = rust_video.get_system_info() + + # 创建一个临时的extractor来获取CPU特性 + extractor = rust_video.VideoKeyframeExtractor(threads=0, verbose=False) + cpu_features = extractor.get_cpu_features() + + capabilities = { + "system": { + "threads": system_info.get('threads', 0), + "rust_version": system_info.get('version', 'unknown'), + }, + "cpu_features": cpu_features, + "recommended_settings": self._get_recommended_settings(cpu_features), + "analysis_modes": ["auto", "batch", "sequential"], + "supported_formats": ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v', '.3gp', '.webm'] + } + + return capabilities + + except Exception as e: + logger.error(f"获取处理能力信息失败: {e}") + return {"error": str(e)} + + def _get_recommended_settings(self, cpu_features: Dict[str, bool]) -> Dict[str, any]: + """根据CPU特性推荐最佳设置""" + settings = { + "use_simd": any(cpu_features.values()), + "block_size": 8192, + "threads": 0 # 自动检测 + } + + # 根据CPU特性调整设置 + if cpu_features.get('avx2', False): + settings["block_size"] = 16384 # AVX2支持更大的块 + settings["optimization_level"] = "avx2" + elif cpu_features.get('sse2', False): + settings["block_size"] = 8192 + settings["optimization_level"] = "sse2" + else: + settings["use_simd"] = False + settings["block_size"] = 4096 + settings["optimization_level"] = "scalar" + + return settings + # 全局实例 _video_analyzer = None diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 28e617474..32840e2d8 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -561,13 +561,20 @@ class VideoAnalysisConfig(ValidatedConfigBase): enable: bool = Field(default=True, description="启用") analysis_mode: str = Field(default="batch_frames", description="分析模式") - frame_extraction_mode: str = Field(default="fixed_number", description="抽帧模式") + frame_extraction_mode: str = Field(default="keyframe", description="抽帧模式:keyframe(关键帧), fixed_number(固定数量), time_interval(时间间隔)") frame_interval_seconds: float = Field(default=2.0, description="抽帧时间间隔") max_frames: int = Field(default=8, description="最大帧数") frame_quality: int = Field(default=85, description="帧质量") max_image_size: int = Field(default=800, description="最大图像大小") enable_frame_timing: bool = Field(default=True, description="启用帧时间") batch_analysis_prompt: str = Field(default="", description="批量分析提示") + + # Rust模块相关配置 + rust_keyframe_threshold: float = Field(default=2.0, description="关键帧检测阈值") + rust_use_simd: bool = Field(default=True, description="启用SIMD优化") + rust_block_size: int = Field(default=8192, description="Rust处理块大小") + rust_threads: int = Field(default=0, description="Rust线程数,0表示自动检测") + ffmpeg_path: str = Field(default="ffmpeg", description="FFmpeg可执行文件路径") class WebSearchConfig(ValidatedConfigBase): diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d3b997526..f996e0bea 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.5.7" +version = "6.5.8" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -49,11 +49,11 @@ master_users = []# ["qq", "123456789"], # 示例:QQ平台的Master用户 [bot] platform = "qq" qq_account = 1145141919810 # MoFox-Bot的QQ账号 -nickname = "MoFox-Bot" # MoFox-Bot的昵称 -alias_names = ["麦叠", "牢麦"] # MoFox-Bot的别名 +nickname = "墨狐" # MoFox-Bot的昵称 +alias_names = ["狐狐", "墨墨"] # MoFox-Bot的别名 [command] -command_prefixes = ['/', '!', '.', '#'] +command_prefixes = ['/'] [personality] # 建议50字以内,描述人格的核心特质 @@ -395,15 +395,22 @@ pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问 [video_analysis] # 视频分析配置 enable = true # 是否启用视频分析功能 -analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择) -frame_extraction_mode = "fixed_number" # 抽帧模式: "fixed_number" (固定总帧数) 或 "time_interval" (按时间间隔) +analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢)、"batch_frames"(批量分析,推荐)或 "auto"(自动选择) +frame_extraction_mode = "keyframe" # 抽帧模式: "keyframe" (智能关键帧,推荐)、"fixed_number" (固定总帧数) 或 "time_interval" (按时间间隔) frame_interval_seconds = 2.0 # 按时间间隔抽帧的秒数(仅在 mode = "time_interval" 时生效) -max_frames = 16 # 最大分析帧数(仅在 mode = "fixed_number" 时生效) +max_frames = 16 # 最大分析帧数 frame_quality = 80 # 帧图像JPEG质量 (1-100) max_image_size = 800 # 单帧最大图像尺寸(像素) enable_frame_timing = true # 是否在分析中包含帧的时间信息 -use_multiprocessing = true # 是否使用线程池处理视频帧提取(推荐开启,可防止卡死) -max_workers = 2 # 最大线程数(建议1-2个,避免过度消耗资源) + +# Rust模块相关配置 +rust_keyframe_threshold = 2.0 # 关键帧检测阈值,值越大关键帧越少 +rust_use_simd = true # 启用SIMD优化(推荐) +rust_block_size = 8192 # 处理块大小,较大值可能提高高分辨率视频性能 +rust_threads = 0 # 线程数,0表示自动检测 +ffmpeg_path = "ffmpeg" # FFmpeg可执行文件路径 + + # 批量分析时使用的提示词 batch_analysis_prompt = """请以第一人称的视角来观看这一个视频,你看到的这些是从视频中按时间顺序提取的关键帧。