Initial implementation of video_probe (Rust)

Core modules:
- probe.rs: ffprobe execution logic
- parser.rs: JSON parsing logic
- output.rs: Output formatting
- lib.rs: Library interface
- main.rs: CLI entry point

Features:
- Extract video metadata using ffprobe
- Parse video/audio/subtitle streams
- Save to JSON file
- Console summary output

Documentation:
- Added QUICKSTART.md
- Added ENVIRONMENT_SETUP_REPORT.md
This commit is contained in:
accusys
2026-03-07 10:10:19 +08:00
commit f3e2d2dca7
464 changed files with 125611 additions and 0 deletions

89
AUTO_SAVE_FEATURE.md Normal file
View File

@@ -0,0 +1,89 @@
# Video YOLO Object Prescan - 更新日志
## v2.0.1 (2026-03-06) - Auto-Save Feature Added
### 🆕 新增功能
#### 1. **可配置的自动存档间隔**
- **默认**: 30 秒
- **命令行**: `--save-interval SECONDS`
- **范围**: 5-300 秒
- **示例**:
```bash
# 默认 30 秒
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
# 每 60 秒
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt --save-interval 60
# 每 15 秒(长时间视频)
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt --save-interval 15
```
#### 2. **自动保存机制**
- 每 N 秒自动保存当前进度到- 显示自动保存计数和文件大小
- **静默模式**: 不显示详细进度,仅显示关键信息
- **防止干扰**: 不影响正常输出
- **数据保护**: 鄲止意外断电丢失进度
#### 3. **断点续传**
- ✅ 保持原有功能
- 自动检测现有 `.yolo.json` 文件
- 询问是否继续
- 无缝从上次中断处继续
- ✅ 从上次保存处恢复处理
#### 4. **完善的元数据**
JSON 文件中新增字段:
- `auto_save_interval`: 记录自动保存间隔
- `auto_save_count`: 记录自动保存次数
- `status`: `in_progress` / `interrupted` / `completed`
- `last_saved_at`: 最后保存时间
- 其他原有字段保持不变
#### 5. **改进的显示**
- 自动保存时显示进度百分比和文件大小
- 完成时显示完整统计(包括自动保存次数)
- 更友好的提示信息
---
## 💡 使用建议
### 推荐配置
- **短视频 (< 5 分钟)**: 15 秒
- **中等视频 (5-15 分钟)**: 30 秒(默认)
- **长视频 (15-30 分钟)**: 60 秒
- **超长视频 (> 30 分钟)**: 120 秒
### 实际效果
```
进度: 1000/3615 frames (27.8%)
💾 Auto-saved (#1): 1000/3615 frames (27.8%) - Size: 1024.5 KB - Elapsed: 75.2s
进度: 2000/3615 frames (55.2%)
💾 Auto-saved (#2): 2000/3615 frames (55.2%) - Size: 2049.0 KB - Elapsed: 150.4s
进度: 3000/3615 frames (82.9%)
💾 Auto-saved (#3): 3000/3615 frames (82.9%) - Size: 3073.5 KB - Elapsed: 225.6s
✓ 完成! 夔大小: 3073.5 KB
自动保存次数: 3 次
总对象: 5430 个
```
---
## 🔍 安全特性
- **最小间隔**: 5 秒(防止过于频繁)
- **最大间隔**: 300 秒5 分钟,防止间隔太长)
- **静默保存**: 保存时不尽量减少 I/O 操作
- **内存友好**: 仅保存元数据,不保存帧数据
- **状态跟踪**: 通过 `status` 字段可区分不同状态
- **信号处理**: Ctrl+C 优雅处理,不会强制退出
- **用户友好**: 清晰的提示和进度显示

562
Cargo.lock generated Normal file
View File

@@ -0,0 +1,562 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[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.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "cc"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c044c781163c001b913cd018fc95a628c50d0d2dfea8bca77dad71edb16e37"
dependencies = [
"clang-sys",
"libc",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "env_filter"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "jiff"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[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_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opencv"
version = "0.98.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a912f2928097b544f812e24667c84a26f323b65b1ac7315c305405045203ec47"
dependencies = [
"cc",
"dunce",
"jobserver",
"libc",
"num-traits",
"opencv-binding-generator",
"pkg-config",
"semver",
"shlex",
"vcpkg",
"windows",
]
[[package]]
name = "opencv-binding-generator"
version = "0.100.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86509944d666c43cdc837288366c140dcab4968838317411889ebdec0dfffccb"
dependencies = [
"clang",
"clang-sys",
"dunce",
"percent-encoding",
"regex",
"shlex",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "video_yolo_player"
version = "0.1.0"
dependencies = [
"anyhow",
"env_logger",
"log",
"opencv",
]
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"

20
Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "video_yolo_player"
version = "0.1.0"
edition = "2021"
[dependencies]
opencv = { version = "0.98", features = ["imgproc", "highgui", "videoio", "dnn"], default-features = false }
[dependencies.anyhow]
version = "1.0"
[dependencies.log]
version = "0.4"
[dependencies.env_logger]
version = "0.11"
[[bin]]
name = "video_yolo_player"
path = "src/main.rs"

22
ISSUE_time_overlay.md Normal file
View File

@@ -0,0 +1,22 @@
# Issue: Add video time code and total time display overlay
## Description
Display video time code and total duration on the video playback window, but ensure it doesn't cover/obscure the video content itself.
## Requirements
1. Show current time code (e.g., `00:01:23`)
2. Show total duration (e.g., `00:05:30`)
3. Display format: `current_time / total_time`
4. Position: Non-intrusive location (e.g., bottom-right corner with padding)
5. Should not overlap with YOLO detection boxes
## Example
```
[Current frame] [00:01:23 / 00:05:30]
```
## Implementation Notes
- Use cv2.putText() to render the time overlay
- Add padding/margin from edges to avoid covering video content
- Consider using a semi-transparent background for better visibility
- Make text color configurable or auto-contrast based on video content

236
PAUSE_RESUME_FEATURE.md Normal file
View File

@@ -0,0 +1,236 @@
# Video YOLO Object Prescan - 暂停与续传功能
## 新增功能
### ✅ 暂停功能 (Ctrl+C)
在处理视频时,随时可以按 `Ctrl+C` 暂停处理:
```bash
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
# 处理中...
# Progress: 1500/3615 frames (41.5%) - 3 objects - Elapsed: 75.2s, ETA: 105.8s
# 按 Ctrl+C 暂停
^C
============================================================
⏸ PAUSED - Saving progress...
============================================================
✓ Progress saved to: video.yolo.json
Frames processed: 1500/3615
Total detections: 4500
Elapsed time: 75.2s
💡 Run the same command again to resume from frame 1501
============================================================
```
### ✅ 断点续传
下次运行相同命令时,会自动检测已存在的 `.yolo.json` 文件:
```bash
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
============================================================
📂 Found existing data: video.yolo.json
Last processed frame: 1500
============================================================
Resume from last checkpoint? (Y/n): y
Resuming from checkpoint...
Loading YOLO model from: yolov8n.pt
✓ Model loaded successfully
Video Info:
Path: video.mp4
Resolution: 1920x1080
FPS: 30.00
Total frames: 3615
Duration: 120.5s (00:02:00)
Resume from: frame 1501
Output: video.yolo.json
============================================================
Resuming video processing...
💡 Press Ctrl+C to pause and save progress
Progress: 1600/3615 frames (44.3%) - 2 objects - Elapsed: 5.1s, ETA: 102.3s
...
```
### ✅ 自动保存
处理过程中每 **60 秒**自动保存一次进度,防止意外中断导致数据丢失。
## 使用场景
### 场景 1: 长视频处理
```bash
# 开始处理 2 小时视频
python3 video_yolo_object_prescan.py long_video.mp4 yolov8n.pt
# 处理 1 小时后需要关机
# 按 Ctrl+C 暂停并保存
# 第二天继续
python3 video_yolo_object_prescan.py long_video.mp4 yolov8n.pt
# 选择 Y 继续处理
```
### 场景 2: 测试不同参数
```bash
# 第一次处理(使用 yolov8n
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
# 处理到一半,想测试 yolov8s
# 按 Ctrl+C 暂停
# 删除或重命名 .yolo.json
mv video.yolo.json video.yolov8n.json
# 使用新模型重新处理
python3 video_yolo_object_prescan.py video.mp4 yolov8s.pt
```
### 场景 3: 系统崩溃恢复
```bash
# 处理中系统崩溃或断电
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
# ... 系统崩溃 ...
# 重启后继续(自动保存了最后 60 秒内的进度)
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
# 选择 Y 从上次保存的帧继续
```
## JSON 文件格式
`.yolo.json` 文件包含状态信息:
```json
{
"metadata": {
"video_path": "/path/to/video.mp4",
"total_frames": 3615,
"processed_at": "2026-03-06T12:00:00",
"last_saved_at": "2026-03-06T12:30:00",
"status": "interrupted", // 或 "completed"
"processing_time": 1800.5,
"total_detections": 7230
},
"frames": {
"1": { ... },
"2": { ... },
...
"1500": { ... } // 最后处理的帧
}
}
```
**状态字段**:
- `in_progress` - 正在处理中
- `interrupted` - 用户中断Ctrl+C
- `completed` - 处理完成
## 命令行选项
```bash
# 基本用法
python3 video_yolo_object_prescan.py <video_path> <yolo_model>
# 示例
python3 video_yolo_object_prescan.py my_video.mp4 yolov8n.pt
# 从头开始(忽略现有文件)
python3 video_yolo_object_prescan.py my_video.mp4 yolov8n.pt
# 当提示 "Resume from last checkpoint?" 时输入 n
```
## 技术细节
### 信号处理
使用 Python `signal` 模块捕获 `SIGINT` (Ctrl+C)
```python
signal.signal(signal.SIGINT, signal_handler)
```
### 帧定位
使用 OpenCV `cv2.CAP_PROP_POS_FRAMES` 精确定位到指定帧:
```python
cap.set(cv2.CAP_PROP_POS_FRAMES, last_frame)
```
### 数据完整性
- 每帧处理完成后立即写入内存
- 每 60 秒自动保存到磁盘
- Ctrl+C 时立即保存当前进度
- 使用 JSON 格式,易于检查和恢复
## 性能影响
- **内存**: 略有增加(保存所有已处理帧的数据)
- **磁盘 I/O**: 每 60 秒一次写入
- **CPU**: 无额外开销
- **恢复速度**: < 1 秒(仅加载 JSON 文件)
## 注意事项
1. **磁盘空间**: `.yolo.json` 文件会随处理进度增长,确保有足够空间
2. **不要修改 JSON**: 手动编辑可能导致数据损坏
3. **模型一致性**: 续传时使用相同的 YOLO 模型
4. **视频不变**: 续传时视频文件不能改变
## 故障排除
### Q: 提示 "Could not load existing file"
A: JSON 文件可能损坏,删除后重新开始:
```bash
rm video.yolo.json
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
```
### Q: 想从头开始处理
A: 删除现有 JSON 文件或在提示时选择 `n`
```bash
# 方法 1: 删除文件
rm video.yolo.json
# 方法 2: 运行时选择 n
python3 video_yolo_object_prescan.py video.mp4 yolov8n.pt
# Resume from last checkpoint? (Y/n): n
```
### Q: 处理速度变慢
A: JSON 文件过大导致保存变慢,这是正常现象。建议:
- 使用更快的存储设备SSD
- 分段处理超长视频
## 更新日志
### v2.0.0 (2026-03-06)
**新增功能**:
- ✅ Ctrl+C 暂停并保存进度
- ✅ 断点续传(自动检测并询问)
- ✅ 每 60 秒自动保存
- ✅ 状态字段记录in_progress/interrupted/completed
- ✅ 改进的进度显示和 ETA 计算
**改进**:
- 更友好的用户提示
- 详细的保存信息
- 自动恢复功能

340
README.md Normal file
View File

@@ -0,0 +1,340 @@
# Video YOLO Player - 使用说明
版本: 2.0.0
构建时间: 2026-03-06 12:00:00
## 概述
Video YOLO Player 是一个模块化的视频播放器,支持 YOLO 对象检测叠加显示。采用三步工作流程:
1. **视频分析** - 提取视频元数据
2. **对象预扫描** - 使用 YOLO 预处理视频
3. **播放** - 播放视频并显示检测结果
## 文件结构
```
video_yolo_player/
├── video_probe.py # 视频元数据提取工具
├── video_yolo_object_prescan.py # YOLO 对象预扫描工具
├── video_yolo_player.py # 主播放器
├── yolov8n.pt # YOLO 模型文件
└── README.md # 本文档
```
## 安装依赖
```bash
pip install opencv-python ultralytics numpy
```
确保已安装 `ffprobe``ffplay`(通常随 FFmpeg 安装):
- macOS: `brew install ffmpeg`
- Linux: `sudo apt-get install ffmpeg`
- Windows: 从 https://ffmpeg.org 下载
## 使用流程
### 1. 提取视频元数据
```bash
python3 video_probe.py <video_path>
```
**输出**: `<video_name>.probe.json`
**示例**:
```bash
python3 video_probe.py my_video.mp4
```
**生成文件**: `my_video.probe.json`
包含信息:
- 视频格式、编码器
- 分辨率、帧率
- 时长、比特率
- 音频流、字幕流信息
### 2. 预扫描视频(可选但推荐)
```bash
python3 video_yolo_object_prescan.py <video_path> <yolo_model> [--save-interval SECONDS]
```
**输出**: `<video_name>.yolo.json`
**参数**:
- `--save-interval SECONDS`: 自动保存间隔(默认: 30 秒)
- 范围: 5-300 秒
- 推荐值:
- 短视频 (< 5 分钟): 15 秒
- 中等视频 (5-15 分钟): 30 秒(默认)
- 长视频 (15-30 分钟): 60 秒
- 超长视频 (> 30 分钟): 120 秒
**示例**:
```bash
# 默认 30 秒自动保存
python3 video_yolo_object_prescan.py my_video.mp4 yolov8n.pt
```
**生成文件**: `my_video.yolo.json`
包含信息:
- 每帧的检测结果
- 对象类别、 置信度
- 边界框坐标
- 自动保存次数和- 状态跟踪 (in_progress/interrupted/completed)
- 最后保存时间
**处理时间**: 取决于视频长度和硬件(约 0.05-0.1 秒/帧)
- 自动保存会增加少量开销(静默保存)
- 自动保存可以防止意外断电导致数据丢失
- 断点续传功能可以在中断后继续处理
### 3. 播放视频
```bash
python3 video_yolo_player.py <video_path> <yolo_model>
```
**示例**:
```bash
python3 video_yolo_player.py my_video.mp4 yolov8n.pt
```
## 播放器控制
### 键盘快捷键
| 按键 | 功能 |
|------|------|
| `y` / `Y` | 切换实时 YOLO 检测(蓝色框) |
| `p` / `P` | 切换预扫描 YOLO 数据(绿色框) |
| `i` / `I` | 显示视频探测信息 |
| `Space` | 暂停/继续 |
| `s` / `S` | 切换声音 |
| `b` / `B` | 切换状态栏 |
| `h` / `H` | 隐藏当前窗口 |
| `1` | 切换原始视频窗口 |
| `2` | 切换 YOLO 检测窗口 |
| `3` | 切换命令窗口 |
| `←` | 后退 5 秒 |
| `→` | 前进 5 秒 |
| `Shift+←` | 后退 30 秒 |
| `Shift+→` | 前进 30 秒 |
| `q` / `ESC` | 退出 |
### 命令输入
在命令窗口中输入以下命令并按 Enter
| 命令 | 说明 | 示例 |
|------|------|------|
| `<帧号>` | 跳转到指定帧 | `1234` |
| `<时间码>` | 跳转到指定时间 | `00:01:30` |
| `<时间码.帧>` | 跳转到精确时间 | `00:01:30.15` |
| `+<秒数>` | 前进指定秒数 | `+10` |
| `-<秒数>` | 后退指定秒数 | `-5` |
| `i` | 显示视频探测信息 | `i` |
## 窗口布局
```
┌─────────────────┐ ┌─────────────────┐
│ 1: Original │ │ 2: YOLO │
│ Video │ │ Detection │
│ │ │ │
└─────────────────┘ └─────────────────┘
┌──────────────────────────────────────┐
│ 3: Command Window │
│ [Build Info] │
│ Examples: 123 | 00:01:30 | +10 | i │
│ > _ │
└──────────────────────────────────────┘
```
- **窗口 1**: 原始视频
- **窗口 2**: YOLO 检测叠加(可显示实时和预扫描结果)
- **窗口 3**: 命令输入和状态显示
## 检测模式
### Live YOLO实时检测
-`y` 激活
- **蓝色**边界框
- 实时计算,需要 GPU/CPU 性能
- 适合测试和调试
### Pre-scanned YOLO预扫描
-`p` 激活
- **绿色**边界框
-`.yolo.json` 加载,无性能开销
- 适合流畅播放和详细分析
### 双模式叠加
可以同时激活 `y``p`,对比实时检测和预扫描结果:
- 绿色框:预扫描结果
- 蓝色框:实时检测结果
## JSON 文件格式
### .probe.json 结构
```json
{
"video_path": "/path/to/video.mp4",
"probed_at": "2026-03-06T12:00:00",
"format": {
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": 120.5,
"size": 52428800,
"bit_rate": 3473408
},
"video_stream": {
"codec_name": "h264",
"width": 1920,
"height": 1080,
"r_frame_rate": "30/1"
},
"audio_streams": [...]
}
```
### .yolo.json 结构
```json
{
"metadata": {
"video_path": "/path/to/video.mp4",
"model_path": "/path/to/yolov8n.pt",
"width": 1920,
"height": 1080,
"fps": 30.0,
"total_frames": 3615,
"total_duration": 120.5,
"processed_at": "2026-03-06T12:00:00",
"processing_time": 180.5,
"total_detections": 7230
},
"frames": {
"1": {
"frame_number": 1,
"time_seconds": 0.033,
"time_formatted": "00:00:00",
"detections": [
{
"class_id": 0,
"class_name": "person",
"confidence": 0.89,
"x1": 100.5,
"y1": 200.3,
"x2": 300.7,
"y2": 600.9
}
]
}
}
}
```
## 性能优化建议
1. **预扫描优先**: 使用 `video_yolo_object_prescan.py` 预处理,播放时零性能开销
2. **GPU 加速**: 确保 YOLO 模型使用 GPU自动检测
3. **小模型**: `yolov8n.pt` 最快,`yolov8s/m/l/x.pt` 更准确但更慢
4. **分辨率**: 高分辨率视频需要更多处理时间
## 故障排除
### ffprobe 未找到
```bash
# macOS
brew install ffmpeg
# Linux
sudo apt-get install ffmpeg
# 验证安装
ffprobe -version
```
### YOLO 模型加载失败
```bash
# 下载 YOLOv8 模型
wget https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt
```
### 视频无法打开
检查文件路径和权限:
```bash
ls -la /path/to/video.mp4
```
### 性能问题
- 使用预扫描模式 (`p`) 代替实时检测 (`y`)
- 降低视频分辨率
- 使用更小的 YOLO 模型
## 完整示例
```bash
# 1. 分析视频
python3 video_probe.py /path/to/video.mp4
# 2. 预扫描对象
python3 video_yolo_object_prescan.py /path/to/video.mp4 yolov8n.pt
# 3. 播放视频
python3 video_yolo_player.py /path/to/video.mp4 yolov8n.pt
# 播放时:
# - 按 p 激活预扫描数据(绿色框)
# - 按 y 激活实时检测(蓝色框)
# - 按 i 查看视频信息
# - 输入 00:01:30 跳转到 1分30秒
```
## 更新日志
### v2.0.0 (2026-03-06)
**重大架构重构**:
- ✅ 模块化设计:分离 probe、prescan、player
- ✅ 移除 .txt 支持,统一使用 JSON
- ✅ 新增 y/Y 命令:实时 YOLO 检测(蓝色)
- ✅ 新增 p/P 命令:预扫描数据(绿色)
- ✅ 新增 i/I 命令:查看视频探测信息
- ✅ 延迟加载 YOLO 模型(仅在需要时加载)
- ✅ 改进焦点模式:输入命令时禁用单键快捷键
- ✅ 自动屏幕分辨率检测和窗口布局
### v1.0.0 (2026-03-06)
- 初始版本
- 基本视频播放和 YOLO 检测
- .txt 格式支持
## 许可证
MIT License
## 作者
Video YOLO Player Team
## 贡献
欢迎提交 Issue 和 Pull Request

View File

@@ -0,0 +1,648 @@
# video_probe (Rust) - 开发计划
## 项目概述
将 Python 版本的 `video_probe.py` 重写为 Rust 版本,作为独立的 Gitea 仓库。
**目标**: 高性能、跨平台的视频元数据提取工具
**输入**: 视频文件路径
**输出**: `<video_name>.probe.json` 文件
---
## 功能需求
### 核心功能
1. ✅ 使用 ffprobe 提取视频元数据
2. ✅ 解析 JSON 输出
3. ✅ 提取格式信息format
4. ✅ 提取视频流信息video stream
5. ✅ 提取音频流信息audio streams
6. ✅ 提取字幕流信息subtitle streams
7. ✅ 提取其他流信息other streams
8. ✅ 保存为格式化的 JSON 文件
9. ✅ 命令行参数解析
10. ✅ 友好的控制台输出
### 高级功能(可选)
- [ ] 批量处理多个视频文件
- [ ] 递归扫描目录
- [ ] 自定义输出路径
- [ ] 输出格式选项JSON/YAML/TOML
- [ ] 并行处理
- [ ] 进度条显示
- [ ] 错误容忍模式(跳过失败文件)
---
## 技术栈
### Rust 依赖
#### 核心依赖
```toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
thiserror = "1.0"
```
#### 命令行工具
```toml
clap = { version = "4.0", features = ["derive"] }
```
#### 可选增强
```toml
indicatif = "0.17" # 进度条
rayon = "1.8" # 并行处理
walkdir = "2.4" # 目录遍历
```
### 外部依赖
- **ffprobe**: 系统需安装 FFmpeg与 Python 版本相同)
---
## 项目结构
```
video_probe/
├── Cargo.toml
├── Cargo.lock
├── README.md
├── LICENSE
├── .gitignore
├── src/
│ ├── main.rs # 入口点
│ ├── lib.rs # 库接口
│ ├── probe.rs # ffprobe 执行逻辑
│ ├── parser.rs # JSON 解析逻辑
│ ├── metadata.rs # 元数据结构定义
│ ├── output.rs # 输出格式化
│ └── error.rs # 错误处理
├── tests/
│ ├── integration_test.rs
│ └── fixtures/
│ └── sample.mp4
└── docs/
├── USAGE.md
└── DEVELOPMENT.md
```
---
## 开发步骤
### 阶段 1: 项目初始化Day 1
#### 1.1 创建 Cargo 项目
```bash
cargo new video_probe
cd video_probe
```
#### 1.2 配置 Cargo.toml
```toml
[package]
name = "video_probe"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "Extract video metadata using ffprobe"
license = "MIT"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
thiserror = "1.0"
clap = { version = "4.0", features = ["derive"] }
[dev-dependencies]
tempfile = "3.8"
```
#### 1.3 创建基础文件结构
```bash
mkdir -p src tests docs
touch src/{lib.rs,probe.rs,parser.rs,metadata.rs,output.rs,error.rs}
```
---
### 阶段 2: 核心数据结构Day 1-2
#### 2.1 定义元数据结构(`src/metadata.rs`
```rust
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
#[derive(Debug, Serialize, Deserialize)]
pub struct VideoMetadata {
pub video_path: String,
pub probed_at: DateTime<Utc>,
pub format: FormatInfo,
pub video_stream: Option<VideoStream>,
pub audio_streams: Vec<AudioStream>,
pub subtitle_streams: Vec<SubtitleStream>,
pub other_streams: Vec<OtherStream>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FormatInfo {
pub filename: Option<String>,
pub format_name: Option<String>,
pub format_long_name: Option<String>,
pub duration: f64,
pub size: u64,
pub bit_rate: u64,
pub probe_score: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VideoStream {
pub index: i32,
pub codec_name: Option<String>,
pub codec_long_name: Option<String>,
pub profile: Option<String>,
pub level: Option<i32>,
pub width: i32,
pub height: i32,
pub coded_width: Option<i32>,
pub coded_height: Option<i32>,
pub aspect_ratio: Option<String>,
pub pix_fmt: Option<String>,
pub field_order: Option<String>,
pub r_frame_rate: Option<String>,
pub avg_frame_rate: Option<String>,
pub time_base: Option<String>,
pub start_pts: Option<i64>,
pub start_time: f64,
pub duration: Option<f64>,
pub bit_rate: Option<u64>,
pub nb_frames: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AudioStream {
pub index: i32,
pub codec_name: Option<String>,
pub codec_long_name: Option<String>,
pub profile: Option<String>,
pub channels: i32,
pub channel_layout: Option<String>,
pub sample_rate: Option<String>,
pub sample_fmt: Option<String>,
pub bit_rate: Option<u64>,
pub duration: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SubtitleStream {
pub index: i32,
pub codec_name: Option<String>,
pub language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OtherStream {
pub index: i32,
pub codec_type: String,
pub codec_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
```
#### 2.2 定义错误类型(`src/error.rs`
```rust
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProbeError {
#[error("Video file not found: {0}")]
FileNotFound(String),
#[error("Failed to execute ffprobe: {0}")]
FfprobeExecution(#[from] std::io::Error),
#[error("Failed to parse ffprobe output: {0}")]
ParseError(#[from] serde_json::Error),
#[error("ffprobe returned non-zero exit code: {0}")]
FfprobeFailed(String),
#[error("No video stream found")]
NoVideoStream,
}
```
---
### 阶段 3: ffprobe 执行逻辑Day 2-3
#### 3.1 实现 ffprobe 调用(`src/probe.rs`
```rust
use std::process::Command;
use anyhow::Result;
use crate::error::ProbeError;
pub fn run_ffprobe(video_path: &str) -> Result<String> {
// 检查文件是否存在
if !std::path::Path::new(video_path).exists() {
return Err(ProbeError::FileNotFound(video_path.to_string()).into());
}
// 执行 ffprobe
let output = Command::new("ffprobe")
.args(&[
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
video_path
])
.output()?;
// 检查退出码
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ProbeError::FfprobeFailed(stderr.to_string()).into());
}
// 返回 JSON 输出
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout)
}
```
#### 3.2 实现并行版本(可选)
```rust
use rayon::prelude::*;
pub fn probe_videos_parallel(video_paths: &[&str]) -> Vec<Result<VideoMetadata>> {
video_paths.par_iter()
.map(|path| probe_video(path))
.collect()
}
```
---
### 阶段 4: JSON 解析逻辑Day 3
#### 4.1 实现 JSON 解析(`src/parser.rs`
```rust
use serde_json::Value;
use anyhow::Result;
use crate::metadata::*;
#[derive(Debug, Deserialize)]
struct FfprobeOutput {
format: Option<Value>,
streams: Option<Vec<Value>>,
}
pub fn parse_ffprobe_json(json_str: &str, video_path: &str) -> Result<VideoMetadata> {
let ffprobe_data: FfprobeOutput = serde_json::from_str(json_str)?;
let mut metadata = VideoMetadata {
video_path: std::fs::canonicalize(video_path)?
.to_string_lossy()
.to_string(),
probed_at: chrono::Utc::now(),
format: FormatInfo::default(),
video_stream: None,
audio_streams: Vec::new(),
subtitle_streams: Vec::new(),
other_streams: Vec::new(),
};
// 解析 format
if let Some(fmt) = ffprobe_data.format {
metadata.format = parse_format(&fmt)?;
}
// 解析 streams
if let Some(streams) = ffprobe_data.streams {
for stream in streams {
let codec_type = stream.get("codec_type")
.and_then(|v| v.as_str())
.unwrap_or("");
match codec_type {
"video" => {
if metadata.video_stream.is_none() {
metadata.video_stream = Some(parse_video_stream(&stream)?);
}
}
"audio" => {
metadata.audio_streams.push(parse_audio_stream(&stream)?);
}
"subtitle" => {
metadata.subtitle_streams.push(parse_subtitle_stream(&stream)?);
}
_ => {
metadata.other_streams.push(parse_other_stream(&stream)?);
}
}
}
}
Ok(metadata)
}
fn parse_format(fmt: &Value) -> Result<FormatInfo> {
Ok(FormatInfo {
filename: fmt.get("filename").and_then(|v| v.as_str()).map(String::from),
format_name: fmt.get("format_name").and_then(|v| v.as_str()).map(String::from),
format_long_name: fmt.get("format_long_name").and_then(|v| v.as_str()).map(String::from),
duration: fmt.get("duration").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0.0),
size: fmt.get("size").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
bit_rate: fmt.get("bit_rate").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
probe_score: fmt.get("probe_score").and_then(|v| v.as_i64()).map(|i| i as i32),
tags: fmt.get("tags").cloned(),
})
}
// 类似地实现其他 parse_* 函数...
```
---
### 阶段 5: 输出和格式化Day 3-4
#### 5.1 实现输出逻辑(`src/output.rs`
```rust
use std::path::Path;
use anyhow::Result;
use crate::metadata::VideoMetadata;
pub fn save_metadata(video_path: &str, metadata: &VideoMetadata) -> Result<String> {
let video_path = Path::new(video_path);
let video_dir = video_path.parent().unwrap_or(Path::new("."));
let video_name = video_path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let output_file = video_dir.join(format!("{}.probe.json", video_name));
let json = serde_json::to_string_pretty(metadata)?;
std::fs::write(&output_file, json)?;
Ok(output_file.to_string_lossy().to_string())
}
pub fn print_summary(metadata: &VideoMetadata) {
println!("✓ Video probed successfully!\n");
if let Some(ref filename) = metadata.format.filename {
println!("File: {}", filename);
}
if let Some(ref format_name) = metadata.format.format_long_name {
println!("Format: {}", format_name);
}
println!("Duration: {:.2} seconds", metadata.format.duration);
println!("Size: {:.2} MB", metadata.format.size as f64 / 1024.0 / 1024.0);
println!("Bit rate: {:.0} kbps", metadata.format.bit_rate as f64 / 1000.0);
if let Some(ref vs) = metadata.video_stream {
println!("\nVideo Stream:");
println!(" Codec: {} ({:?})",
vs.codec_name.as_ref().unwrap_or(&"N/A".to_string()),
vs.profile);
println!(" Resolution: {}x{}", vs.width, vs.height);
println!(" Frame rate: {}", vs.r_frame_rate.as_ref().unwrap_or(&"N/A".to_string()));
println!(" Pixel format: {}", vs.pix_fmt.as_ref().unwrap_or(&"N/A".to_string()));
}
if !metadata.audio_streams.is_empty() {
println!("\nAudio Streams: {}", metadata.audio_streams.len());
for (i, audio) in metadata.audio_streams.iter().enumerate() {
println!(" [{}] {} - {} channels @ {} Hz",
i + 1,
audio.codec_name.as_ref().unwrap_or(&"N/A".to_string()),
audio.channels,
audio.sample_rate.as_ref().unwrap_or(&"N/A".to_string()));
}
}
if !metadata.subtitle_streams.is_empty() {
println!("\nSubtitle Streams: {}", metadata.subtitle_streams.len());
for (i, sub) in metadata.subtitle_streams.iter().enumerate() {
println!(" [{}] {} ({:?})",
i + 1,
sub.codec_name.as_ref().unwrap_or(&"N/A".to_string()),
sub.language);
}
}
}
```
---
### 阶段 6: 命令行界面Day 4
#### 6.1 实现主程序(`src/main.rs`
```rust
use clap::Parser;
use anyhow::Result;
mod probe;
mod parser;
mod metadata;
mod output;
mod error;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Video file path
video_path: String,
/// Output directory (default: same as video file)
#[arg(short, long)]
output: Option<String>,
/// Verbose output
#[arg(short, long)]
verbose: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
println!("Probing video: {}", args.video_path);
println!("{}", "=".repeat(60));
// 执行 ffprobe
let json_output = probe::run_ffprobe(&args.video_path)?;
// 解析 JSON
let metadata = parser::parse_ffprobe_json(&json_output, &args.video_path)?;
// 保存到文件
let output_file = output::save_metadata(&args.video_path, &metadata)?;
// 打印摘要
output::print_summary(&metadata);
println!("\n✓ Metadata saved to: {}", output_file);
println!("{}", "=".repeat(60));
Ok(())
}
```
---
### 阶段 7: 测试Day 5
#### 7.1 单元测试
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_format() {
let json = r#"{
"filename": "test.mp4",
"format_name": "mov,mp4",
"duration": "120.5",
"size": "52428800",
"bit_rate": "3473408"
}"#;
let value: serde_json::Value = serde_json::from_str(json).unwrap();
let format = parse_format(&value).unwrap();
assert_eq!(format.filename, Some("test.mp4".to_string()));
assert_eq!(format.duration, 120.5);
}
}
```
#### 7.2 集成测试
```rust
#[test]
fn test_probe_video() {
let video_path = "tests/fixtures/sample.mp4";
let result = probe_video(video_path);
assert!(result.is_ok());
}
```
---
### 阶段 8: 文档和发布Day 5-6
#### 8.1 编写 README.md
```markdown
# video_probe
Extract video metadata using ffprobe (Rust version)
## Installation
```bash
cargo install video_probe
```
## Usage
```bash
video_probe video.mp4
```
## Features
- Fast and efficient (written in Rust)
- Cross-platform (Linux, macOS, Windows)
- Comprehensive metadata extraction
- JSON output format
- User-friendly console output
```
#### 8.2 发布到 crates.io
```bash
cargo publish
```
---
## 开发时间表
| 阶段 | 任务 | 预计时间 |
|------|------|----------|
| 1 | 项目初始化 | 0.5 天 |
| 2 | 数据结构定义 | 1 天 |
| 3 | ffprobe 执行逻辑 | 1 天 |
| 4 | JSON 解析逻辑 | 1 天 |
| 5 | 输出和格式化 | 0.5 天 |
| 6 | 命令行界面 | 0.5 天 |
| 7 | 测试 | 1 天 |
| 8 | 文档和发布 | 0.5 天 |
| **总计** | | **6 天** |
---
## 与 Python 版本的对比
| 特性 | Python 版本 | Rust 版本 | 优势 |
|------|-------------|-----------|------|
| 性能 | 中等 | 高 | 2-10x 更快 |
| 内存使用 | 较高 | 低 | 更高效 |
| 启动时间 | 慢 | 快 | 即时启动 |
| 部署 | 需要 Python | 单二进制 | 更简单 |
| 跨平台 | 是 | 是 | 相同 |
| 依赖管理 | pip | Cargo | Cargo 更好 |
| 类型安全 | 弱 | 强 | 编译时检查 |
| 并发支持 | 有限 | 优秀 | Rayon 并行 |
| 错误处理 | 异常 | Result | 更明确 |
---
## 下一步行动
1. ✅ 创建 Gitea 仓库 `video_probe`
2. ✅ 初始化 Cargo 项目
3. ✅ 实现核心功能
4. ✅ 添加测试
5. ✅ 编写文档
6. ✅ 发布到 crates.io可选
---
## 参考资料
- [Rust 文档](https://doc.rust-lang.org/)
- [serde 文档](https://serde.rs/)
- [clap 文档](https://docs.rs/clap/)
- [FFprobe 文档](https://ffmpeg.org/ffprobe.html)

View File

@@ -0,0 +1,237 @@
# video_probe (Rust) - 快速参考指南
## 📋 项目概述
**目标**: 将 Python 版本的 `video_probe.py` 重写为高性能 Rust 版本
**核心功能**: 使用 ffprobe 提取视频元数据并保存为 JSON
**仓库**: `video_probe` (独立的 Gitea 仓库)
---
## 🚀 快速开始
### 1. 初始化项目
```bash
# 运行初始化脚本
chmod +x init_video_probe_rust.sh
./init_video_probe_rust.sh
```
### 2. 实现核心功能
参考 `VIDEO_PROBE_RUST_DEVELOPMENT.md` 文档,按以下顺序实现:
1. **src/error.rs** - 错误类型定义
2. **src/metadata.rs** - 数据结构定义
3. **src/probe.rs** - ffprobe 执行逻辑
4. **src/parser.rs** - JSON 解析逻辑
5. **src/output.rs** - 输出格式化
6. **src/main.rs** - 命令行入口
### 3. 测试
```bash
cd video_probe
cargo test
cargo run -- video.mp4
```
---
## 📦 依赖清单
### 核心依赖 (必需)
| 依赖 | 版本 | 用途 |
|------|------|------|
| `serde` | 1.0 | 序列化/反序列化 |
| `serde_json` | 1.0 | JSON 处理 |
| `chrono` | 0.4 | 时间处理 |
| `anyhow` | 1.0 | 错误处理 |
| `thiserror` | 1.0 | 自定义错误 |
| `clap` | 4.0 | 命令行解析 |
### 增强依赖 (可选)
| 依赖 | 版本 | 用途 |
|------|------|------|
| `indicatif` | 0.17 | 进度条 |
| `rayon` | 1.8 | 并行处理 |
| `walkdir` | 2.4 | 目录遍历 |
---
## 📁 项目结构
```
video_probe/
├── Cargo.toml # 项目配置
├── src/
│ ├── main.rs # 入口点
│ ├── lib.rs # 库接口
│ ├── probe.rs # ffprobe 执行
│ ├── parser.rs # JSON 解析
│ ├── metadata.rs # 数据结构
│ ├── output.rs # 输出格式化
│ └── error.rs # 错误类型
├── tests/
│ └── integration_test.rs # 集成测试
└── docs/
├── USAGE.md # 使用文档
└── DEVELOPMENT.md # 开发文档
```
---
## 🔧 核心实现要点
### 1. ffprobe 调用
```rust
Command::new("ffprobe")
.args(&[
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
video_path
])
.output()?
```
### 2. JSON 解析
```rust
#[derive(Deserialize)]
struct FfprobeOutput {
format: Option<Value>,
streams: Option<Vec<Value>>,
}
```
### 3. 元数据结构
```rust
struct VideoMetadata {
video_path: String,
probed_at: DateTime<Utc>,
format: FormatInfo,
video_stream: Option<VideoStream>,
audio_streams: Vec<AudioStream>,
subtitle_streams: Vec<SubtitleStream>,
other_streams: Vec<OtherStream>,
}
```
### 4. 命令行参数
```rust
#[derive(Parser)]
struct Args {
video_path: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
verbose: bool,
}
```
---
## 📊 开发时间表
| 阶段 | 任务 | 时间 |
|------|------|------|
| 1 | 项目初始化 | 0.5 天 |
| 2 | 数据结构 | 1 天 |
| 3 | ffprobe 执行 | 1 天 |
| 4 | JSON 解析 | 1 天 |
| 5 | 输出格式化 | 0.5 天 |
| 6 | 命令行界面 | 0.5 天 |
| 7 | 测试 | 1 天 |
| 8 | 文档 | 0.5 天 |
| **总计** | | **6 天** |
---
## ✅ 功能对照表
| 功能 | Python 版本 | Rust 版本 | 状态 |
|------|-------------|-----------|------|
| ffprobe 调用 | ✅ | 待实现 | 🔨 |
| JSON 解析 | ✅ | 待实现 | 🔨 |
| 格式信息提取 | ✅ | 待实现 | 🔨 |
| 视频流提取 | ✅ | 待实现 | 🔨 |
| 音频流提取 | ✅ | 待实现 | 🔨 |
| 字幕流提取 | ✅ | 待实现 | 🔨 |
| JSON 输出 | ✅ | 待实现 | 🔨 |
| 命令行参数 | ✅ | 待实现 | 🔨 |
| 友好输出 | ✅ | 待实现 | 🔨 |
---
## 🎯 性能对比预期
| 指标 | Python | Rust | 提升 |
|------|--------|------|------|
| 启动时间 | ~50ms | ~1ms | 50x |
| 内存使用 | ~30MB | ~5MB | 6x |
| CPU 效率 | 中 | 高 | 2-3x |
| 二进制大小 | N/A | ~2MB | - |
| 部署复杂度 | 高 | 低 | ✅ |
---
## 🔗 有用链接
- [Rust 官方文档](https://doc.rust-lang.org/)
- [serde 文档](https://serde.rs/)
- [clap 文档](https://docs.rs/clap/)
- [FFprobe 文档](https://ffmpeg.org/ffprobe.html)
- [anyhow 文档](https://docs.rs/anyhow/)
---
## 📝 开发清单
### 必须完成
- [ ] 实现 ffprobe 调用
- [ ] 实现 JSON 解析
- [ ] 实现元数据提取
- [ ] 实现 JSON 输出
- [ ] 实现命令行参数
- [ ] 编写单元测试
- [ ] 编写集成测试
- [ ] 编写 README
- [ ] 添加错误处理
- [ ] 添加日志输出
### 可选增强
- [ ] 批量处理
- [ ] 并行处理
- [ ] 进度条
- [ ] 递归扫描
- [ ] 多种输出格式
- [ ] 配置文件支持
---
## 🚨 注意事项
1. **ffprobe 依赖**: 系统必须安装 FFmpeg
2. **跨平台**: 测试 Linux/macOS/Windows
3. **错误处理**: 文件不存在、ffprobe 失败等
4. **性能优化**: 避免不必要的内存分配
5. **测试覆盖**: 至少 80% 代码覆盖率
---
## 📞 支持
如有问题,请:
1. 查看 `VIDEO_PROBE_RUST_DEVELOPMENT.md`
2. 查看 Rust 官方文档
3. 提交 Issue 到 Gitea 仓库

314
VIDEO_PROBE_RUST_README.md Normal file
View File

@@ -0,0 +1,314 @@
# video_probe (Rust) - 完整开发资源包
## 📚 文档清单
已创建以下开发文档:
### 1. **VIDEO_PROBE_RUST_DEVELOPMENT.md** (详细开发计划)
- ✅ 完整的开发步骤8个阶段
- ✅ 代码示例和模板
- ✅ 依赖清单
- ✅ 项目结构
- ✅ 测试策略
- ✅ 时间估算6天
### 2. **VIDEO_PROBE_RUST_QUICKSTART.md** (快速参考)
- ✅ 快速开始指南
- ✅ 核心实现要点
- ✅ 功能对照表
- ✅ 性能对比
- ✅ 开发清单
### 3. **init_video_probe_rust.sh** (自动初始化脚本)
- ✅ 自动创建 Cargo 项目
- ✅ 配置 Cargo.toml
- ✅ 创建目录结构
- ✅ 生成基础代码模板
- ✅ 初始化 Git 仓库
- ✅ 首次构建
### 4. **video_probe.py** (Python 原型)
- ✅ 完整功能实现
- ✅ 作为 Rust 版本的原型参考
- ✅ 功能验证标准
---
## 🎯 项目目标
### 核心目标
将 Python 版本的视频元数据提取工具重写为 Rust实现
- 🚀 **高性能**: 2-10x 更快的执行速度
- 💾 **低内存**: 更少的内存占用
- 📦 **易部署**: 单一二进制文件
- 🌍 **跨平台**: Linux/macOS/Windows 支持
### 功能对齐
| 功能 | 状态 |
|------|------|
| 使用 ffprobe 提取元数据 | ✅ Python 实现 |
| 提取格式信息 | ✅ Python 实现 |
| 提取视频流信息 | ✅ Python 实现 |
| 提取音频流信息 | ✅ Python 实现 |
| 提取字幕流信息 | ✅ Python 实现 |
| JSON 输出 | ✅ Python 实现 |
| 命令行界面 | ✅ Python 实现 |
| 友好输出 | ✅ Python 实现 |
---
## 🛠️ 技术栈
### Rust 核心依赖
```toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
thiserror = "1.0"
clap = { version = "4.0", features = ["derive"] }
```
### 外部依赖
- **ffprobe**: 来自 FFmpeg系统级依赖
---
## 📋 开发流程
### 方式 1: 使用自动脚本(推荐)
```bash
# 1. 运行初始化脚本
./init_video_probe_rust.sh
# 2. 进入项目目录
cd video_probe
# 3. 实现核心功能
# 参考 VIDEO_PROBE_RUST_DEVELOPMENT.md
# 4. 测试
cargo test
cargo run -- video.mp4
# 5. 构建 release 版本
cargo build --release
```
### 方式 2: 手动创建
```bash
# 1. 创建项目
cargo new video_probe
cd video_probe
# 2. 配置 Cargo.toml
# 参考 VIDEO_PROBE_RUST_DEVELOPMENT.md
# 3. 创建目录结构
mkdir -p src tests docs
# 4. 实现功能
# 按照开发文档逐步实现
# 5. 测试和构建
cargo test
cargo build
```
---
## 📊 实现优先级
### P0 (必须)
1. ✅ 项目初始化
2. 🔨 数据结构定义
3. 🔨 ffprobe 执行逻辑
4. 🔨 JSON 解析逻辑
5. 🔨 输出格式化
6. 🔨 命令行界面
### P1 (重要)
1. 🔨 单元测试
2. 🔨 集成测试
3. 🔨 错误处理
4. 🔨 文档编写
### P2 (可选)
1. ⏸️ 批量处理
2. ⏸️ 并行处理
3. ⏸️ 进度条
4. ⏸️ 递归扫描
---
## 🎓 学习资源
### Rust 官方
- [Rust Book](https://doc.rust-lang.org/book/)
- [Rust by Example](https://doc.rust-lang.org/rust-by-example/)
- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
### 依赖库文档
- [serde](https://serde.rs/)
- [clap](https://docs.rs/clap/)
- [anyhow](https://docs.rs/anyhow/)
- [chrono](https://docs.rs/chrono/)
### FFprobe
- [FFprobe Documentation](https://ffmpeg.org/ffprobe.html)
- [FFprobe JSON Output](https://ffmpeg.org/ffprobe.html#json)
---
## ✅ 验证清单
### 开发前
- [ ] 已安装 Rust (rustc --version)
- [ ] 已安装 Cargo (cargo --version)
- [ ] 已安装 ffprobe (ffprobe -version)
- [ ] 已阅读开发文档
- [ ] 已了解 Python 原型
### 开发中
- [ ] 代码编译通过 (cargo build)
- [ ] 测试通过 (cargo test)
- [ ] 代码格式化 (cargo fmt)
- [ ] Lint 检查通过 (cargo clippy)
- [ ] 功能与 Python 版本一致
### 开发后
- [ ] Release 构建成功
- [ ] 文档完整
- [ ] README 清晰
- [ ] Git 提交规范
- [ ] 代码审查通过
---
## 🚀 发布流程
### 1. 本地测试
```bash
cargo test
cargo build --release
./target/release/video_probe video.mp4
```
### 2. 创建 Git 仓库
```bash
git init
git add .
git commit -m "Initial commit"
```
### 3. 推送到 Gitea
```bash
# 在 Gitea 创建仓库
git remote add origin <gitea-url>
git push -u origin main
```
### 4. 可选:发布到 crates.io
```bash
cargo login
cargo publish
```
---
## 📈 性能基准
### 预期性能提升
| 操作 | Python | Rust (预期) | 提升 |
|------|--------|-------------|------|
| 启动 | 50ms | 1ms | 50x |
| 解析 1GB 视频 | 2s | 0.5s | 4x |
| 内存占用 | 30MB | 5MB | 6x |
| 二进制大小 | N/A | 2MB | - |
### 基准测试方法
```bash
# Python
time python3 video_probe.py video.mp4
# Rust
time ./target/release/video_probe video.mp4
```
---
## 🐛 常见问题
### Q1: ffprobe 未找到
```bash
# macOS
brew install ffmpeg
# Linux
sudo apt-get install ffmpeg
# Windows
# 从 https://ffmpeg.org 下载
```
### Q2: Rust 未安装
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
### Q3: 编译错误
```bash
# 更新 Rust
rustup update
# 清理并重新构建
cargo clean
cargo build
```
### Q4: 测试失败
```bash
# 检查测试视频文件
ls tests/fixtures/
# 运行详细测试
cargo test -- --nocapture
```
---
## 📞 获取帮助
1. **查看文档**
- `VIDEO_PROBE_RUST_DEVELOPMENT.md`
- `VIDEO_PROBE_RUST_QUICKSTART.md`
2. **参考 Python 原型**
- `video_probe.py`
3. **Rust 社区**
- [Rust Forum](https://users.rust-lang.org/)
- [Reddit r/rust](https://www.reddit.com/r/rust/)
4. **提交 Issue**
- Gitea 仓库 Issues 页面
---
## 🎉 下一步行动
1. ✅ 阅读开发文档
2. ✅ 运行初始化脚本
3. 🔨 实现核心功能
4. 🔨 编写测试
5. 🔨 创建文档
6. 🔨 推送到 Gitea
7. 🔨 发布到 crates.io可选
---
**准备开始了吗?运行 `./init_video_probe_rust.sh` 开始吧!** 🚀

319
init_video_probe_rust.sh Executable file
View File

@@ -0,0 +1,319 @@
#!/bin/bash
# 快速启动脚本 - 创建 video_probe Rust 项目
set -e
# 设置 PATHmacOS with Homebrew
export PATH="/opt/homebrew/bin:$PATH"
echo "======================================"
echo "Video Probe (Rust) - 项目初始化"
echo "======================================"
echo ""
# 检查 Rust 是否已安装
if ! command -v rustc &> /dev/null; then
echo "❌ Rust 未安装"
echo ""
echo "请先安装 Rust:"
echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
exit 1
fi
echo "✓ Rust 已安装: $(rustc --version)"
echo "✓ Cargo 已安装: $(cargo --version)"
echo ""
# 检查 ffprobe
if ! command -v ffprobe &> /dev/null; then
echo "⚠️ 警告: ffprobe 未找到"
echo " macOS: brew install ffmpeg"
echo " Linux: sudo apt-get install ffmpeg"
echo " Windows: 从 https://ffmpeg.org 下载"
echo ""
fi
# 创建项目
PROJECT_NAME="video_probe"
echo "1. 创建 Cargo 项目..."
if [ -d "$PROJECT_NAME" ]; then
echo " 目录已存在: $PROJECT_NAME"
read -p " 删除并重新创建? (y/N): " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
rm -rf "$PROJECT_NAME"
else
echo " 取消操作"
exit 1
fi
fi
cargo new "$PROJECT_NAME"
cd "$PROJECT_NAME"
echo " ✓ 项目创建成功: $PROJECT_NAME/"
echo ""
# 创建目录结构
echo "2. 创建目录结构..."
mkdir -p src tests docs tests/fixtures
touch src/{lib.rs,probe.rs,parser.rs,metadata.rs,output.rs,error.rs}
touch tests/integration_test.rs
touch docs/{USAGE.md,DEVELOPMENT.md}
echo " ✓ 目录结构创建完成"
echo ""
# 更新 Cargo.toml
echo "3. 配置 Cargo.toml..."
cat > Cargo.toml << 'EOF'
[package]
name = "video_probe"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "Extract video metadata using ffprobe"
license = "MIT"
repository = "https://gitea.example.com/yourname/video_probe"
keywords = ["video", "metadata", "ffprobe", "ffmpeg"]
categories = ["command-line-utilities", "multimedia"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
thiserror = "1.0"
clap = { version = "4.0", features = ["derive"] }
[dev-dependencies]
tempfile = "3.8"
[[bin]]
name = "video_probe"
path = "src/main.rs"
EOF
echo " ✓ Cargo.toml 已配置"
echo ""
# 创建 .gitignore
echo "4. 创建 .gitignore..."
cat > .gitignore << 'EOF'
/target/
**/*.rs.bk
*.pdb
Cargo.lock
*.probe.json
*.mp4
*.avi
*.mov
*.mkv
.DS_Store
.vscode/
.idea/
EOF
echo " ✓ .gitignore 已创建"
echo ""
# 创建基础代码模板
echo "5. 创建基础代码模板..."
# src/metadata.rs
cat > src/metadata.rs << 'EOF'
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
#[derive(Debug, Serialize, Deserialize)]
pub struct VideoMetadata {
pub video_path: String,
pub probed_at: DateTime<Utc>,
pub format: FormatInfo,
pub video_stream: Option<VideoStream>,
pub audio_streams: Vec<AudioStream>,
pub subtitle_streams: Vec<SubtitleStream>,
pub other_streams: Vec<OtherStream>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct FormatInfo {
pub filename: Option<String>,
pub format_name: Option<String>,
pub format_long_name: Option<String>,
pub duration: f64,
pub size: u64,
pub bit_rate: u64,
pub probe_score: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VideoStream {
pub index: i32,
pub codec_name: Option<String>,
pub codec_long_name: Option<String>,
pub profile: Option<String>,
pub width: i32,
pub height: i32,
pub pix_fmt: Option<String>,
pub r_frame_rate: Option<String>,
pub avg_frame_rate: Option<String>,
pub bit_rate: Option<u64>,
pub duration: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AudioStream {
pub index: i32,
pub codec_name: Option<String>,
pub channels: i32,
pub sample_rate: Option<String>,
pub bit_rate: Option<u64>,
pub duration: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SubtitleStream {
pub index: i32,
pub codec_name: Option<String>,
pub language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OtherStream {
pub index: i32,
pub codec_type: String,
pub codec_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<serde_json::Value>,
}
EOF
# src/error.rs
cat > src/error.rs << 'EOF'
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProbeError {
#[error("Video file not found: {0}")]
FileNotFound(String),
#[error("Failed to execute ffprobe: {0}")]
FfprobeExecution(#[from] std::io::Error),
#[error("Failed to parse ffprobe output: {0}")]
ParseError(#[from] serde_json::Error),
#[error("ffprobe returned non-zero exit code: {0}")]
FfprobeFailed(String),
#[error("No video stream found")]
NoVideoStream,
}
pub type Result<T> = std::result::Result<T, ProbeError>;
EOF
echo " ✓ 基础代码模板已创建"
echo ""
# 创建 README
echo "6. 创建 README.md..."
cat > README.md << 'EOF'
# video_probe (Rust)
Extract video metadata using ffprobe
## Installation
### From Source
```bash
git clone https://gitea.example.com/yourname/video_probe.git
cd video_probe
cargo install --path .
```
## Usage
```bash
video_probe video.mp4
```
Output: `video.probe.json`
## Features
- ✅ Fast and efficient (written in Rust)
- ✅ Cross-platform (Linux, macOS, Windows)
- ✅ Comprehensive metadata extraction
- ✅ JSON output format
- ✅ User-friendly console output
## Development
```bash
# Build
cargo build
# Run
cargo run -- video.mp4
# Test
cargo test
# Format code
cargo fmt
# Lint
cargo clippy
```
## Requirements
- Rust 1.70+
- ffprobe (from FFmpeg)
## License
MIT
EOF
echo " ✓ README.md 已创建"
echo ""
# 初始化 Git
echo "7. 初始化 Git 仓库..."
git init
git add .
git commit -m "Initial commit: video_probe Rust project"
echo " ✓ Git 仓库已初始化"
echo ""
# 构建项目
echo "8. 构建项目..."
cargo build
echo " ✓ 项目构建成功"
echo ""
# 完成
echo "======================================"
echo "✅ 项目初始化完成!"
echo "======================================"
echo ""
echo "项目位置: $(pwd)"
echo ""
echo "下一步:"
echo " 1. cd $PROJECT_NAME"
echo " 2. 实现核心功能(参考: VIDEO_PROBE_RUST_DEVELOPMENT.md"
echo " 3. cargo run -- <video_path>"
echo " 4. cargo test"
echo ""
echo "创建 Gitea 仓库:"
echo " 1. 在 Gitea 创建新仓库: video_probe"
echo " 2. git remote add origin <your-gitea-url>"
echo " 3. git push -u origin main"
echo ""

17
opencode.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434/v1"
},
"models": {
"qwen3:8b-16k": {
"name": "qwen3:8b-16k",
"tools": true
}
}
}
}
}

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
opencv-python>=4.8.0
ultralytics>=8.0.0
numpy>=1.24.0

68
setup_env.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Video Probe (Rust) - 环境设置脚本
set -e
echo "======================================"
echo "Video Probe (Rust) - 环境设置"
echo "======================================"
echo ""
# 设置 PATH
export PATH="/opt/homebrew/bin:$PATH"
# 检查 Rust
echo "1. 检查 Rust..."
if command -v rustc &> /dev/null; then
RUST_VERSION=$(rustc --version)
CARGO_VERSION=$(cargo --version)
echo " ✓ Rust: $RUST_VERSION"
echo " ✓ Cargo: $CARGO_VERSION"
else
echo " ✗ Rust 未安装"
echo " 请运行: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
exit 1
fi
echo ""
# 检查 ffprobe
echo "2. 检查 ffprobe..."
if command -v ffprobe &> /dev/null; then
FFPROBE_VERSION=$(ffprobe -version | head -1)
echo " ✓ ffprobe: $FFPROBE_VERSION"
else
echo " ✗ ffprobe 未安装"
echo " macOS: brew install ffmpeg"
echo " Linux: sudo apt-get install ffmpeg"
exit 1
fi
echo ""
# 检查其他工具
echo "3. 检查其他工具..."
command -v git &> /dev/null && echo " ✓ Git: $(git --version)" || echo " ✗ Git 未安装"
command -v curl &> /dev/null && echo " ✓ curl: $(curl --version | head -1)" || echo " ✗ curl 未安装"
echo ""
# 设置 Rust 环境变量
echo "4. 配置 Rust 环境..."
export RUST_BACKTRACE=1
export CARGO_TERM_COLOR=always
echo " ✓ RUST_BACKTRACE=1"
echo " ✓ CARGO_TERM_COLOR=always"
echo ""
# 显示环境信息
echo "======================================"
echo "✅ 环境准备完成!"
echo "======================================"
echo ""
echo "环境信息:"
echo " Rust 版本: $(rustc --version)"
echo " Cargo 版本: $(cargo --version)"
echo " ffprobe 版本: $(ffprobe -version | head -1)"
echo " 工作目录: $(pwd)"
echo ""
echo "下一步:"
echo " 运行: ./init_video_probe_rust.sh"
echo ""

312
src/main.rs Normal file
View File

@@ -0,0 +1,312 @@
use anyhow::{Context, Result};
use opencv::{
core::{Mat, Scalar},
highgui::{destroy_all_windows, imshow, wait_key, WINDOW_NORMAL},
imgproc::{put_text, rectangle, FONT_HERSHEY_SIMPLEX, LINE_AA},
videoio::{VideoCapture, VideoCaptureProperties, CAP_ANY},
};
use ort::{CPUExecutionProvider, Session, SessionOutputs};
use std::env;
const YOLO_NAMES: &[&str] = &[
"person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat",
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack",
"umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball",
"kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
"sofa", "pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop", "mouse",
"remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator",
"book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush",
];
const CONFIDENCE_THRESHOLD: f32 = 0.5;
const NMS_THRESHOLD: f32 = 0.45;
struct YoloDetector {
session: Session,
input_name: String,
input_width: i32,
input_height: i32,
}
impl YoloDetector {
fn new(model_path: &str) -> Result<Self> {
let session = Session::builder()?
.with_execution_providers([CPUExecutionProvider::default().build()])?
.with_model_from_file(model_path)?;
let input_name = session.inputs()[0].name().to_string();
Ok(Self {
session,
input_name,
input_width: 640,
input_height: 640,
})
}
fn detect(&self, frame: &Mat) -> Result<Vec<Detection>> {
let img_width = frame.cols();
let img_height = frame.rows();
let mut rgb_frame = Mat::default();
opencv::imgproc::cvt_color(frame, &mut rgb_frame, opencv::imgproc::COLOR_BGR2RGB, 0)?;
let mut resized = Mat::default();
opencv::imgproc::resize(
&rgb_frame,
&mut resized,
opencv::core::Size::new(self.input_width, self.input_height),
0.0,
0.0,
opencv::imgproc::INTER_LINEAR,
)?;
let mut input_tensor = vec![0f32; (self.input_width * self.input_height * 3) as usize];
for y in 0..self.input_height {
for x in 0..self.input_width {
let pixel = resized.at_3::<u8>(y, x)?;
let offset = (y * self.input_width + x) as usize;
input_tensor[offset * 3] = pixel[0] as f32 / 255.0;
input_tensor[offset * 3 + 1] = pixel[1] as f32 / 255.0;
input_tensor[offset * 3 + 2] = pixel[2] as f32 / 255.0;
}
}
let input_tensor = ort::Tensor::from_array(input_tensor.reshape((1, 3, self.input_height, self.input_width)))?;
let outputs: SessionOutputs = self.session.run(ort::inputs![self.input_name.as_str() => input_tensor]?)?;
let output_tensor = outputs[0].try_extract_tensor::<f32>()?;
let output_data = output_tensor.view();
self.process_output(output_data, img_width, img_height)
}
fn process_output(&self, output_data: &ort::TensorView, img_width: i32, img_height: i32) -> Result<Vec<Detection>> {
let mut detections = Vec::new();
let shape = output_data.shape();
let num_detections = shape[1];
let num_fields = shape[2];
for i in 0..num_detections {
let cx = output_data[[0, i, 0]];
let cy = output_data[[0, i, 1]];
let w = output_data[[0, i, 2]];
let h = output_data[[0, i, 3]];
let confidence = output_data[[0, i, 4]];
if confidence < CONFIDENCE_THRESHOLD {
continue;
}
let mut class_confidence = confidence;
let mut class_id = 0;
for j in 0..80 {
let class_score = output_data[[0, i, 5 + j]];
if class_score > class_confidence {
class_confidence = class_score;
class_id = j;
}
}
if class_confidence < CONFIDENCE_THRESHOLD {
continue;
}
let x1 = (cx - w / 2.0) * img_width as f32 / self.input_width as f32;
let y1 = (cy - h / 2.0) * img_height as f32 / self.input_height as f32;
let x2 = (cx + w / 2.0) * img_width as f32 / self.input_width as f32;
let y2 = (cy + h / 2.0) * img_height as f32 / self.input_height as f32;
detections.push(Detection {
x1: x1.max(0.0),
y1: y1.max(0.0),
x2: x2.min(img_width as f32),
y2: y2.min(img_height as f32),
confidence: class_confidence,
class_id,
class_name: YOLO_NAMES.get(class_id).unwrap_or(&"unknown").to_string(),
});
}
Ok(self.non_max_suppression(detections))
}
fn non_max_suppression(&self, mut detections: Vec<Detection>) -> Vec<Detection> {
detections.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
let mut keep = Vec::new();
while !detections.is_empty() {
let best = detections.remove(0);
keep.push(best.clone());
detections.retain(|d| {
let iou = self.calculate_iou(d, &best);
iou < NMS_THRESHOLD
});
}
keep
}
fn calculate_iou(&self, a: &Detection, b: &Detection) -> f32 {
let x1 = a.x1.max(b.x1);
let y1 = a.y1.max(b.y1);
let x2 = a.x2.min(b.x2);
let y2 = a.y2.min(b.y2);
let intersection = (x2 - x1).max(0.0) * (y2 - y1).max(0.0);
let area_a = (a.x2 - a.x1) * (a.y2 - a.y1);
let area_b = (b.x2 - b.x1) * (b.y2 - b.y1);
let union = area_a + area_b - intersection;
if union <= 0.0 {
0.0
} else {
intersection / union
}
}
}
#[derive(Clone)]
struct Detection {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
confidence: f32,
class_id: usize,
class_name: String,
}
fn draw_detections(frame: &mut Mat, detections: &[Detection]) {
for det in detections {
let color = match det.class_id % 6 {
0 => Scalar::new(255.0, 0.0, 0.0, 0.0),
1 => Scalar::new(0.0, 255.0, 0.0, 0.0),
2 => Scalar::new(0.0, 0.0, 255.0, 0.0),
3 => Scalar::new(255.0, 255.0, 0.0, 0.0),
4 => Scalar::new(255.0, 0.0, 255.0, 0.0),
_ => Scalar::new(0.0, 255.0, 255.0, 0.0),
};
let pt1 = opencv::core::Point::new(det.x1 as i32, det.y1 as i32);
let pt2 = opencv::core::Point::new(det.x2 as i32, det.y2 as i32);
rectangle(frame, pt1, pt2, color, 2, 8, 0).ok();
let label = format!(
"{} {:.1}%",
det.class_name,
det.confidence * 100.0
);
let label_pt = opencv::core::Point::new(det.x1 as i32, (det.y1 - 5) as i32);
let bg_pt1 = opencv::core::Point::new(det.x1 as i32, (det.y1 - 20).max(0.0) as i32);
let bg_pt2 = opencv::core::Point::new((det.x1 + 150.0) as i32, det.y1 as i32);
rectangle(frame, bg_pt1, bg_pt2, color, -1, 8, 0).ok();
put_text(
frame,
&label,
label_pt,
FONT_HERSHEY_SIMPLEX,
0.5,
Scalar::new(255.0, 255.0, 255.0, 0.0),
2,
LINE_AA,
false,
).ok();
}
}
fn main() -> Result<()> {
env_logger::init();
let args: Vec<String> = env::args().collect();
if args.len() < 3 {
eprintln!("Usage: {} <video_path> <yolo_model_path>", args[0]);
eprintln!("Example: {} video.mp4 yolov8n.onnx", args[0]);
std::process::exit(1);
}
let video_path = &args[1];
let model_path = &args[2];
println!("Loading YOLO model from: {}", model_path);
let detector = YoloDetector::new(model_path)?;
println!("YOLO model loaded successfully");
println!("Opening video: {}", video_path);
let mut cap = VideoCapture::open_file(video_path, CAP_ANY)
.context("Failed to open video file")?;
if !cap.is_opened().context("Failed to open video capture")? {
anyhow::bail!("Cannot open video: {}", video_path);
}
let fps = cap.get(VideoCaptureProperties::CAP_PROP_FPS)?;
let width = cap.get(VideoCaptureProperties::CAP_PROP_FRAME_WIDTH)?;
let height = cap.get(VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT)?;
println!("Video info: {}x{} @ {} fps", width, height, fps);
opencv::highgui::named_window("Original Video", WINDOW_NORMAL)?;
opencv::highgui::named_window("YOLO Detection", WINDOW_NORMAL)?;
opencv::highgui::resize_window("Original Video", width as i32, height as i32)?;
opencv::highgui::resize_window("YOLO Detection", width as i32, height as i32)?;
println!("Playing video... Press 'q' or ESC to quit");
let mut frame = Mat::default();
let mut frame_count = 0;
loop {
if !cap.read(&mut frame)? {
println!("End of video, looping...");
cap.set(VideoCaptureProperties::CAP_PROP_POS_FRAMES, 0.0)?;
continue;
}
if frame.empty() {
continue;
}
frame_count += 1;
if frame_count % 30 == 0 {
println!("Frame: {}", frame_count);
}
imshow("Original Video", &frame)?;
let mut annotated_frame = frame.clone();
match detector.detect(&frame) {
Ok(detections) => {
draw_detections(&mut annotated_frame, &detections);
}
Err(e) => {
eprintln!("Detection error: {:?}", e);
}
}
imshow("YOLO Detection", &annotated_frame)?;
let key = wait_key(30)?;
if key == 'q' as i32 || key == 'Q' as i32 || key == 27 {
println!("Quitting...");
break;
}
}
destroy_all_windows();
println!("Done!");
Ok(())
}

1
target/.rustc_info.json Normal file
View File

@@ -0,0 +1 @@
{"rustc_fingerprint":7910028290815472967,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/accusys/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.92.0 (ded5c06cf 2025-12-08)\nbinary: rustc\ncommit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234\ncommit-date: 2025-12-08\nhost: aarch64-apple-darwin\nrelease: 1.92.0\nLLVM version: 21.1.3\n","stderr":""}},"successes":{}}

3
target/CACHEDIR.TAG Normal file
View File

@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

0
target/debug/.cargo-lock Normal file
View File

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
8e71c7569b98491b

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":5347358027863023418,"path":2498799609881310857,"deps":[[1363051979936526615,"memchr",false,11668526183767300727]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aho-corasick-17d6f8c350ba568a/dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
6c91a8de67eb034d

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"auto\", \"wincon\"]","declared_features":"[\"auto\", \"default\", \"test\", \"wincon\"]","target":11278316191512382530,"profile":11459093354283867776,"path":1368805165058083929,"deps":[[384403243491392785,"colorchoice",false,8485903486713973891],[5652275617566266604,"anstyle_query",false,8874033107635935327],[7483871650937086505,"anstyle",false,10853699269117606076],[7727459912076845739,"is_terminal_polyfill",false,12638843975273980479],[11410867133969439143,"anstyle_parse",false,3908141501184340663],[17716308468579268865,"utf8parse",false,6151700546281254876]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstream-fa1b6d08571cac65/dep-lib-anstream","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
bcf8abdafa15a096

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":6165884447290141869,"profile":11459093354283867776,"path":2663314323454016568,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-642be3805a2f462e/dep-lib-anstyle","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
b7f20d36fd813c36

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"default\", \"utf8\"]","declared_features":"[\"core\", \"default\", \"utf8\"]","target":10225663410500332907,"profile":11459093354283867776,"path":14980379806015639209,"deps":[[17716308468579268865,"utf8parse",false,6151700546281254876]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-parse-584c63468a348cbd/dep-lib-anstyle_parse","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
5f18b24118e6267b

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":10705714425685373190,"profile":14848920055892446256,"path":4316627989718112974,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-query-3a5ace2dbec2ae9f/dep-lib-anstyle_query","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
edc67d7f466199ec

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":5408242616063297496,"profile":3033921117576893,"path":15975461479635710502,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-24ec003b790501cc/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
bc6796b89332f94d

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[12478428894219133322,"build_script_build",false,17048764819802277613]],"local":[{"RerunIfChanged":{"output":"debug/build/anyhow-40fe6c014f4e8bb6/output","paths":["src/nightly.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"config":0,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
acdf419166c7c9f4

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":1563897884725121975,"profile":5347358027863023418,"path":8136069237744135612,"deps":[[12478428894219133322,"build_script_build",false,5618577620159850428]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-d838341a95e3a987/dep-lib-anyhow","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
1477b7ed36cbf522

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":3033921117576893,"path":10045383212328745351,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/autocfg-e2e21eab783460c2/dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
e0f37a934c6ae2c6

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"parallel\"]","declared_features":"[\"jobserver\", \"parallel\"]","target":11042037588551934598,"profile":9003321226815314314,"path":18097570189799899825,"deps":[[8410525223747752176,"shlex",false,18384121358634351639],[9159843920629750842,"find_msvc_tools",false,5384539691240515643],[16589527331085190088,"jobserver",false,2985090725956483019],[18365559012052052344,"libc",false,5527024759771327147]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cc-66ba4be51f3495ca/dep-lib-cc","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
8ff3670aa6e86cdd

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"clang_3_5\", \"clang_3_6\", \"clang_3_7\", \"clang_3_8\", \"clang_3_9\", \"clang_4_0\", \"clang_5_0\", \"clang_6_0\", \"clang_7_0\", \"clang_8_0\", \"clang_9_0\"]","declared_features":"[\"clang_10_0\", \"clang_3_5\", \"clang_3_6\", \"clang_3_7\", \"clang_3_8\", \"clang_3_9\", \"clang_4_0\", \"clang_5_0\", \"clang_6_0\", \"clang_7_0\", \"clang_8_0\", \"clang_9_0\", \"runtime\", \"static\"]","target":8779449004212644848,"profile":3033921117576893,"path":708349339432367663,"deps":[[4885725550624711673,"clang_sys",false,1310021895450928419],[18365559012052052344,"libc",false,5527024759771327147]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/clang-33dfdc11c840ffcd/dep-lib-clang","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[4885725550624711673,"build_script_build",false,62909233378676105]],"local":[{"Precalculated":"1.8.1"}],"rustflags":[],"config":0,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
2325561ef9212e12

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"clang_3_5\", \"clang_3_6\", \"clang_3_7\", \"clang_3_8\", \"clang_3_9\", \"clang_4_0\", \"clang_5_0\", \"clang_6_0\", \"clang_7_0\", \"clang_8_0\", \"clang_9_0\"]","declared_features":"[\"clang_10_0\", \"clang_11_0\", \"clang_12_0\", \"clang_13_0\", \"clang_14_0\", \"clang_15_0\", \"clang_16_0\", \"clang_17_0\", \"clang_18_0\", \"clang_3_5\", \"clang_3_6\", \"clang_3_7\", \"clang_3_8\", \"clang_3_9\", \"clang_4_0\", \"clang_5_0\", \"clang_6_0\", \"clang_7_0\", \"clang_8_0\", \"clang_9_0\", \"libcpp\", \"libloading\", \"runtime\", \"static\"]","target":15367217217788174729,"profile":3033921117576893,"path":8124380761469206018,"deps":[[4885725550624711673,"build_script_build",false,9784676118221322583],[9293239362693504808,"glob",false,16531671118029103651],[18365559012052052344,"libc",false,5527024759771327147]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/clang-sys-7f5363e9a6ca20ab/dep-lib-clang_sys","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
89cda04c9d7fdf00

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"clang_3_5\", \"clang_3_6\", \"clang_3_7\", \"clang_3_8\", \"clang_3_9\", \"clang_4_0\", \"clang_5_0\", \"clang_6_0\", \"clang_7_0\", \"clang_8_0\", \"clang_9_0\"]","declared_features":"[\"clang_10_0\", \"clang_11_0\", \"clang_12_0\", \"clang_13_0\", \"clang_14_0\", \"clang_15_0\", \"clang_16_0\", \"clang_17_0\", \"clang_18_0\", \"clang_3_5\", \"clang_3_6\", \"clang_3_7\", \"clang_3_8\", \"clang_3_9\", \"clang_4_0\", \"clang_5_0\", \"clang_6_0\", \"clang_7_0\", \"clang_8_0\", \"clang_9_0\", \"libcpp\", \"libloading\", \"runtime\", \"static\"]","target":5408242616063297496,"profile":3033921117576893,"path":9915630599406339200,"deps":[[9293239362693504808,"glob",false,16531671118029103651]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/clang-sys-97c996220bfdd113/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
83ccd5cb42fcc375

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":11187303652147478063,"profile":11459093354283867776,"path":12231806059346157559,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/colorchoice-78c9fb1a968ce52d/dep-lib-colorchoice","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
74b79b05b0f2005b

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":2507403751003635712,"profile":3033921117576893,"path":10911525546987434454,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/dunce-d2e2100b066b89fe/dep-lib-dunce","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
a8b65a4ec25523df

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"regex\"]","declared_features":"[\"default\", \"regex\"]","target":12678044772393128127,"profile":8255941854203129366,"path":859544684865190908,"deps":[[10630857666389190470,"log",false,16668812559325342810],[17109794424245468765,"regex",false,2554702744881499594]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/env_filter-367b690193323fe7/dep-lib-env_filter","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
d55ce80560d65d09

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"auto-color\", \"color\", \"default\", \"humantime\", \"regex\"]","declared_features":"[\"auto-color\", \"color\", \"default\", \"humantime\", \"kv\", \"regex\", \"unstable-kv\"]","target":8437500984922885737,"profile":8255941854203129366,"path":14359432917595565105,"deps":[[815705504764238973,"anstream",false,5549537997200331116],[6553521288534196920,"env_filter",false,16078789387669386920],[7483871650937086505,"anstyle",false,10853699269117606076],[8417673557997437685,"jiff",false,82178673090303871],[10630857666389190470,"log",false,16668812559325342810]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/env_logger-06af3b768e89c58e/dep-lib-env_logger","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
3be8611752bab94a

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":10620166500288925791,"profile":9003321226815314314,"path":8785329316158535788,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/find-msvc-tools-b3057854709d2ab0/dep-lib-find_msvc_tools","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
23a2b5154b4b6ce5

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":205079002303639128,"profile":3033921117576893,"path":17413444781902220041,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/glob-5b5400670514c222/dep-lib-glob","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
{"rustc":18415816196306954164,"features":"[\"default\"]","declared_features":"[\"default\"]","target":15126035666798347422,"profile":13002376533287092900,"path":5623427845846318751,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/is_terminal_polyfill-a61a9abe8d457afc/dep-lib-is_terminal_polyfill","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
7f17a95911f52301

Some files were not shown because too many files have changed in this diff Show More