Skip to content

Commit 584a1dc

Browse files
committed
feat(v0.14): B/C/D 子项目实施(视觉系统/知识体系/采集质量)
1 parent 63f86ab commit 584a1dc

79 files changed

Lines changed: 6522 additions & 304 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎app/src-tauri/.cargo/config.toml‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@
1010
# 相对路径会解析到不存在的目录导致静默回退在线下载。
1111
# @ai-context: 预编译包下载命令(一次性,模型/库更新时重跑):
1212
# Invoke-WebRequest -Uri "https://github.com/k2-fsa/sherpa-onnx/releases/download/v1.13.5/sherpa-onnx-v1.13.5-win-x64-shared-MT-Release-lib.tar.bz2" -OutFile "sherpa-archive\sherpa-onnx-v1.13.5-win-x64-shared-MT-Release-lib.tar.bz2"
13-
# @ai-context: 以上本地预编译库路径均为 Windows 专用(sherpa-archive/ 与 ort/ 下的 win-x64 包),
14-
# CI(Linux/Windows runner)必须走在线下载,故限定 [target.x86_64-pc-windows-msvc.env]——
15-
# Linux 目标不注入这些 env,sherpa-onnx-sys/ort-sys 回退默认下载路径(CI 网络可达)。
16-
[target.x86_64-pc-windows-msvc.env]
17-
SHERPA_ONNX_ARCHIVE_DIR = "D:/Program own/aicode/work space/Entropydecrease/app/src-tauri/sherpa-archive"
13+
# @ai-context: 以上本地预编译库路径均为 Windows 专用(sherpa-archive/ 与 ort/ 下的 win-x64 包)。
14+
# 2026-08-28 定位:Cargo 1.96 下 [target.<triple>.env] / [target.'cfg()'.env] 均不再
15+
# 注入 build script(v0.12~v0.13 时代有效),改为全局 [env] + force=false——
16+
# CI 删本文件(在线下载)或 job env 覆盖(ORT_LIB_LOCATION 指向 ort/ci)均不受影响;
17+
# force=false 保证环境中已显式设置的变量优先(防误覆盖)。
18+
# 注:本项目 Windows 专用(CI 注释:Linux 编译 E0433 失败),全局注入无跨平台副作用。
19+
[env]
20+
SHERPA_ONNX_ARCHIVE_DIR = { value = "D:/Program own/aicode/work space/Entropydecrease/app/src-tauri/sherpa-archive", force = false }
21+
ORT_LIB_LOCATION = { value = "D:/Program own/aicode/work space/Entropydecrease/app/src-tauri/ort/onnxruntime-win-x64-gpu_cuda12-1.28.0/lib", force = false }
1822
# oar-ocr 底层同样使用 pyke ort:cdn.pyke.io 被墙,改用已下载的本地 ONNX Runtime(跳过在线下载)。
1923
# ADR-009(v0.4.0 M1):CUDA EP——ORT 1.28 官方 gpu_cuda12 包(含 CUDA 插件与运行时,
2024
# 一套运行时双后端:CPU EP 仍在其中)。下载命令(app/download-ort-cuda.ps1):
2125
# powershell -ExecutionPolicy Bypass -File app/download-ort-cuda.ps1
2226
# 回退到 CPU-only:改回 "ort/onnxruntime-win-x64-1.28.0/lib" 并关 cargo feature ocr-cuda。
23-
ORT_LIB_LOCATION = "D:/Program own/aicode/work space/Entropydecrease/app/src-tauri/ort/onnxruntime-win-x64-gpu_cuda12-1.28.0/lib"
2427
# ort-sys 2.0.0-rc.13:build script 下载 prebuilt 只受 ORT_SKIP_DOWNLOAD 门控
2528
# (ORT_LIB_LOCATION 仅影响链接阶段)——未设时仍会请求 cdn.pyke.io 被拦截,
2629
# 必须显式置 1 跳过在线下载,链接走下面 ORT_LIB_LOCATION 的本地库。

‎app/src-tauri/Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎app/src-tauri/build.rs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
fn main() {
1111
tauri_build::build();
1212
copy_onnxruntime_dll();
13+
copy_sherpa_dlls();
1314
declare_comctl32_v6_manifest();
1415
}
1516

@@ -59,6 +60,57 @@ fn declare_comctl32_v6_manifest() {
5960
#[cfg(not(all(target_os = "windows", target_env = "msvc")))]
6061
fn declare_comctl32_v6_manifest() {} // 非 Windows/MSVC 无 SxS manifest 机制
6162

63+
/// 复制 sherpa-onnx-c-api/cxx-api.dll 到 target 输出目录(dev/release 均生效)。
64+
///
65+
/// @ai-context: 集成测试 exe 位于 <target>/<profile>/deps/,静态导入 sherpa-onnx-c-api.dll
66+
/// (tauri dev 的主 exe 走 bundle.resources 侧;测试 exe 无此机制)——该 DLL 只在
67+
/// sherpa-onnx-sys 解压目录(target/sherpa-onnx-prebuilt/<pkg>/lib/),不在 exe
68+
/// 搜索路径时测试进程 0xC0000135。与 copy_onnxruntime_dll 同机制拷贝到 profile
69+
/// 与 deps(集成测试 exe 目录),免手工 PATH 注入。
70+
fn copy_sherpa_dlls() {
71+
let Ok(out_dir) = std::env::var("OUT_DIR") else {
72+
return;
73+
};
74+
let out_path = std::path::Path::new(&out_dir);
75+
let Some(profile_dir) = out_path
76+
.ancestors()
77+
.find(|p| p.file_name().is_some_and(|n| n == "build"))
78+
.and_then(|b| b.parent())
79+
else {
80+
return;
81+
};
82+
// sherpa-onnx-sys 解压目录:<target>/sherpa-onnx-prebuilt/<pkg>/lib/
83+
let target_dir = profile_dir.parent().unwrap_or(profile_dir);
84+
let Ok(entries) = std::fs::read_dir(target_dir.join("sherpa-onnx-prebuilt")) else {
85+
return;
86+
};
87+
let mut lib_dir = None;
88+
for entry in entries.flatten() {
89+
let candidate = entry.path().join("lib");
90+
if candidate.join("sherpa-onnx-c-api.dll").exists() {
91+
lib_dir = Some(candidate);
92+
break;
93+
}
94+
}
95+
let Some(lib_dir) = lib_dir else {
96+
return;
97+
};
98+
for name in ["sherpa-onnx-c-api.dll", "sherpa-onnx-cxx-api.dll"] {
99+
let src = lib_dir.join(name);
100+
if !src.exists() {
101+
continue;
102+
}
103+
println!("cargo:rerun-if-changed={}", src.display());
104+
for dest_dir in [profile_dir.to_path_buf(), profile_dir.join("deps")] {
105+
if dest_dir.is_dir() {
106+
if let Err(e) = std::fs::copy(&src, dest_dir.join(name)) {
107+
println!("cargo:warning=复制 {name} 失败: {e}");
108+
}
109+
}
110+
}
111+
}
112+
}
113+
62114
/// 复制 onnxruntime.dll 到 target 输出目录(dev/release 均生效)。
63115
fn copy_onnxruntime_dll() {
64116
println!("cargo:rerun-if-env-changed=ORT_LIB_LOCATION");
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"bilibili": {
3+
"videoRoi": [0.15, 0.08, 0.62, 0.72],
4+
"subtitleBand": { "y": 0.82, "h": 0.10 },
5+
"noiseMasks": ["danmaku_band", "right_recommend"]
6+
},
7+
"douyin": {
8+
"videoRoi": [0.0, 0.12, 1.0, 0.8],
9+
"subtitleBand": { "y": 0.6, "h": 0.15 },
10+
"noiseMasks": ["right_comments", "left_likes"]
11+
},
12+
"youtube": {
13+
"videoRoi": [0.0, 0.08, 1.0, 0.84],
14+
"subtitleBand": { "y": 0.88, "h": 0.08 },
15+
"noiseMasks": ["right_recommend"]
16+
},
17+
"edge-local": {
18+
"videoRoi": [0.0, 0.0, 1.0, 1.0],
19+
"noiseMasks": []
20+
}
21+
}

‎app/src-tauri/src/chapter_morph.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//! 章节形态决策(v0.14 D1 chapter_morph;纯函数,零依赖)。
2+
//!
3+
//! @ai-context: spec §4.1——用户纠正"不能做视频级二分法":每章独立决策。
4+
//! 双维信号:OCR 密度(章节窗口内 OCR 文本占比)× OCR 质量分。
5+
//! 密度高 + 质量高 → 图文(OCR 屏卡要点为主体);其余 → 口语
6+
//! (现状形态;低质量 OCR 弃用仅原料视图可查)。
7+
//! @ai-context: 边界(spec §4.1/§5)——无章节边界时全篇退化现状由编排层保证
8+
//! (本模块只做单章决策);空章节输入按口语处理(无内容不发明)。
9+
10+
/// 密度阈值(spec §4.1 DENSITY_TH:OCR 文本占章节文本三成以上视为"内容可视")。
11+
/// 初值经 golden 用例校准;可配置——上游统一读取本常量。
12+
pub const DENSITY_TH: f32 = 0.3;
13+
14+
/// 质量分阈值(spec §4.2 同源——直接别名 ocr_quality::QUALITY_TH,门控双出口共用,
15+
/// 避免两处常量漂移)。
16+
pub const QUALITY_TH: f32 = crate::ocr_quality::QUALITY_TH;
17+
18+
/// 章节形态:图文(OCR 屏卡要点为主体)/ 口语(现状形态)。
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20+
pub enum ChapterMorph {
21+
Graphic,
22+
Spoken,
23+
}
24+
25+
/// 章节形态决策输入(由编排层按章节窗口聚合)。
26+
pub struct MorphInput {
27+
/// 章节窗口内 OCR 文本字符数(屏卡要点量)
28+
pub ocr_chars: usize,
29+
/// 章节窗口内口语(转写)字符数
30+
pub transcript_chars: usize,
31+
/// 章节窗口 OCR 质量分(ocr_quality_score;空章节传 0.0)
32+
pub quality: f32,
33+
}
34+
35+
/// 章节形态决策(spec §4.1 三象限):
36+
/// 密度 ≥ DENSITY_TH && 质量 ≥ QUALITY_TH → 图文;
37+
/// 密度 < DENSITY_TH 或质量 < QUALITY_TH → 口语(OCR 弃用或现状)。
38+
pub fn decide_chapter_morph(input: &MorphInput) -> ChapterMorph {
39+
let total = input.ocr_chars + input.transcript_chars;
40+
if total == 0 {
41+
return ChapterMorph::Spoken; // 空章节:口语(无内容不发明)
42+
}
43+
let density = ocr_density(input.ocr_chars, input.transcript_chars);
44+
if density >= DENSITY_TH && input.quality >= QUALITY_TH {
45+
ChapterMorph::Graphic
46+
} else {
47+
ChapterMorph::Spoken
48+
}
49+
}
50+
51+
/// OCR 密度(章节窗口内 OCR 文本占比;0.0-1.0)。编排层信号下沉复用
52+
/// (spec 3.2:group_route.rs ocr_text_density 信号章节级复用)。
53+
pub fn ocr_density(ocr_chars: usize, transcript_chars: usize) -> f32 {
54+
let total = ocr_chars + transcript_chars;
55+
if total == 0 {
56+
return 0.0;
57+
}
58+
ocr_chars as f32 / total as f32
59+
}
60+
61+
#[cfg(test)]
62+
#[path = "chapter_morph_tests.rs"]
63+
mod tests;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! chapter_morph 单测(v0.14 D1 spec §6:四象限决策 + 边界;AAA 模式)。
2+
3+
use super::*;
4+
5+
/// 构造决策输入(密度由 ocr/transcript 字符数推得)。
6+
fn input(ocr: usize, transcript: usize, quality: f32) -> MorphInput {
7+
MorphInput { ocr_chars: ocr, transcript_chars: transcript, quality }
8+
}
9+
10+
#[test]
11+
fn four_quadrant_decisions() {
12+
// Arrange/Act/Assert:spec §4.1 三象限 + 边界
13+
// 象限 1:密度高 + 质量高 → 图文
14+
assert_eq!(decide_chapter_morph(&input(500, 200, 0.8)), ChapterMorph::Graphic);
15+
// 象限 2:密度高 + 质量低 → 口语(OCR 弃用)
16+
assert_eq!(decide_chapter_morph(&input(500, 200, 0.4)), ChapterMorph::Spoken);
17+
// 象限 3:密度低 + 质量高 → 口语(现状形态——无 PPT 不发明图文)
18+
assert_eq!(decide_chapter_morph(&input(100, 600, 0.9)), ChapterMorph::Spoken);
19+
// 象限 4:密度低 + 质量低 → 口语
20+
assert_eq!(decide_chapter_morph(&input(100, 600, 0.2)), ChapterMorph::Spoken);
21+
}
22+
23+
#[test]
24+
fn threshold_boundary_values() {
25+
// 恰好达标(密度 0.3、质量 0.6)→ 图文;差一点 → 口语
26+
assert_eq!(decide_chapter_morph(&input(300, 700, 0.6)), ChapterMorph::Graphic);
27+
assert_eq!(decide_chapter_morph(&input(299, 701, 0.6)), ChapterMorph::Spoken);
28+
assert_eq!(decide_chapter_morph(&input(300, 700, 0.599)), ChapterMorph::Spoken);
29+
}
30+
31+
#[test]
32+
fn empty_chapter_is_spoken() {
33+
// 空章节:不发明内容(spec §5 异常处理)
34+
assert_eq!(decide_chapter_morph(&input(0, 0, 0.0)), ChapterMorph::Spoken);
35+
}
36+
37+
#[test]
38+
fn pure_ocr_high_quality_is_graphic() {
39+
// 全高密度(无口语——PPT 讲解静止帧场景)→ 图文
40+
assert_eq!(decide_chapter_morph(&input(800, 0, 0.7)), ChapterMorph::Graphic);
41+
}
42+
43+
#[test]
44+
fn ocr_density_helper() {
45+
// 占比与零总量防御
46+
assert!((ocr_density(300, 700) - 0.3).abs() < 1e-4);
47+
assert_eq!(ocr_density(0, 0), 0.0);
48+
assert_eq!(ocr_density(0, 500), 0.0);
49+
}

0 commit comments

Comments
 (0)