Skip to content

Commit 25fa747

Browse files
committed
feat(profiles): 档案新增未知选项且无法识别时选中
1 parent 0ab8af8 commit 25fa747

7 files changed

Lines changed: 59 additions & 22 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ pub fn build_artifact(
507507
let analysis = analyze_session(detail, profile);
508508
let blocks = match profile {
509509
ProfileKind::Lecture => lecture_blocks(detail, &analysis, keyframes),
510+
// v0.7.1:未知档案无专属模板——回退网课讲义(与 profile_by_kind 默认档同口径)
511+
ProfileKind::Unknown => lecture_blocks(detail, &analysis, keyframes),
510512
ProfileKind::HandsOn => hands_on_blocks(detail, keyframes, &analysis),
511513
ProfileKind::TalkingHead => talking_head_blocks(detail, &analysis),
512514
ProfileKind::Interview => interview_blocks(detail, &analysis),

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,17 @@ fn empty_session_produces_minimal_artifact() {
245245
ProfileKind::Exercise,
246246
ProfileKind::FollowAlong,
247247
ProfileKind::Coding,
248+
ProfileKind::Unknown,
248249
] {
249250
let artifact = build_artifact(kind, &d, &[]);
250-
// Assert:讲义式模板(网课/白板/题目/编程)产标题块;其余空产物——均不崩溃
251+
// Assert:讲义式模板(网课/白板/题目/编程/未知)产标题块;其余空产物——均不崩溃
251252
let title_only = matches!(
252253
kind,
253254
ProfileKind::Lecture
254255
| ProfileKind::Whiteboard
255256
| ProfileKind::Exercise
256257
| ProfileKind::Coding
258+
| ProfileKind::Unknown
257259
);
258260
if title_only {
259261
assert_eq!(artifact.blocks.len(), 1, "{:?} 讲义式空会话应只有标题块", kind);

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
use serde::{Deserialize, Serialize};
1313

14-
/// 十二类档案标识(全栈统一业务术语)。
14+
/// 十二类档案标识(全栈统一业务术语;v0.7.1 起含 Unknown 共十三值)。
1515
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1616
#[serde(rename_all = "kebab-case")]
1717
pub enum ProfileKind {
@@ -39,6 +39,9 @@ pub enum ProfileKind {
3939
FollowAlong,
4040
/// v0.7.0 REQ-121(T3):编程实战——OCR+ASR 双通道(示例代码提取)
4141
Coding,
42+
/// v0.7.1 用户需求:未知——自动检测无法识别时如实标注(不假装猜中);
43+
/// 无内置档案配置,管线参数回退默认(Lecture)档(零回归),产物模板同网课讲义。
44+
Unknown,
4245
}
4346

4447
impl ProfileKind {
@@ -58,12 +61,14 @@ impl ProfileKind {
5861
ProfileKind::Exercise => "题目讲解",
5962
ProfileKind::FollowAlong => "跟练",
6063
ProfileKind::Coding => "编程实战",
64+
ProfileKind::Unknown => "未知",
6165
}
6266
}
6367

6468
/// 解析前端传入的档案标识(kebab-case);非法值回退 Lecture(默认档案不阻断)。
6569
pub fn parse(s: &str) -> ProfileKind {
6670
match s {
71+
"unknown" => ProfileKind::Unknown,
6772
"hands-on" => ProfileKind::HandsOn,
6873
"talking-head" => ProfileKind::TalkingHead,
6974
"interview" => ProfileKind::Interview,
@@ -94,6 +99,7 @@ impl ProfileKind {
9499
ProfileKind::Exercise => "exercise",
95100
ProfileKind::FollowAlong => "follow-along",
96101
ProfileKind::Coding => "coding",
102+
ProfileKind::Unknown => "unknown",
97103
}
98104
}
99105
}
@@ -213,7 +219,7 @@ pub struct VideoProfile {
213219
/// 五档案内置常量(默认值;JSON 导出后可人工校准覆盖)。
214220
pub use crate::video_profile_data::builtin_profiles;
215221

216-
/// 按档案标识查内置档案(未知标识回退 Lecture——默认档案不阻断)。
222+
/// 按档案标识查内置档案(Unknown/未知标识回退 Lecture——默认档案不阻断)。
217223
pub fn profile_by_kind(kind: ProfileKind) -> VideoProfile {
218224
let mut profiles = builtin_profiles();
219225
let idx = profiles.iter().position(|p| p.kind == kind).unwrap_or(0);
@@ -246,7 +252,7 @@ pub struct ProfileCandidate {
246252
/// 检测结果。
247253
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248254
pub struct DetectResult {
249-
/// 候选档案(降序;全部 0 分时为默认 Lecture 单候选)
255+
/// 候选档案(降序;全部 0 分时为 Unknown 单候选——诚实未知,不猜默认)
250256
pub candidates: Vec<ProfileCandidate>,
251257
/// 是否需用户确认(信号冲突/得分不足——置信度低才问,高则静默生效可改)
252258
pub needs_confirmation: bool,
@@ -273,9 +279,10 @@ pub fn vote_detect(signals: &ObservedSignals) -> DetectResult {
273279
.collect();
274280
let max = scored.iter().map(|(_, s)| *s).fold(0.0f32, f32::max);
275281
if max <= 0.0 {
276-
// 无任何信号命中:默认 Lecture 且需确认(不静默假设)
282+
// 无任何信号命中:Unknown 单候选 + 需确认(诚实未知,不假装猜中网课;
283+
// v0.7.1 用户需求:无法自动识别时选中"未知")
277284
return DetectResult {
278-
candidates: vec![ProfileCandidate { kind: ProfileKind::Lecture, score: 1.0 }],
285+
candidates: vec![ProfileCandidate { kind: ProfileKind::Unknown, score: 1.0 }],
279286
needs_confirmation: true,
280287
memory_hit: None,
281288
};

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ fn builtin_has_all_profiles_with_distinct_kinds() {
4545

4646
#[test]
4747
fn profile_by_kind_returns_matching_and_defaults() {
48-
// Act:查各档案 + 非法标识回退
48+
// Act:查各档案 + 未知标识回退
4949
assert_eq!(profile_by_kind(ProfileKind::Meeting).kind, ProfileKind::Meeting);
5050
assert_eq!(profile_by_kind(ProfileKind::HandsOn).artifact_template, ArtifactTemplate::StepCards);
51+
// Assert:Unknown 无内置档案 → 回退 Lecture 默认配置(管线不阻断、零回归)
52+
assert_eq!(profile_by_kind(ProfileKind::Unknown).kind, ProfileKind::Lecture);
53+
assert_eq!(profile_by_kind(ProfileKind::Unknown).artifact_template, ArtifactTemplate::LectureNotes);
5154
// Assert:未知标识回退 Lecture(默认档案不阻断)
5255
assert_eq!(profile_by_kind(ProfileKind::Lecture).kind, ProfileKind::Lecture);
5356
}
@@ -66,18 +69,23 @@ fn profile_kind_parse_and_label() {
6669
assert_eq!(ProfileKind::parse("exercise"), ProfileKind::Exercise);
6770
assert_eq!(ProfileKind::parse("follow-along"), ProfileKind::FollowAlong);
6871
assert_eq!(ProfileKind::parse("coding"), ProfileKind::Coding);
72+
// v0.7.1:未知标识(前端「未知」选项)解析为 Unknown
73+
assert_eq!(ProfileKind::parse("unknown"), ProfileKind::Unknown);
6974
// Assert:非法值回退 Lecture(默认档案不阻断)
70-
assert_eq!(ProfileKind::parse("unknown"), ProfileKind::Lecture);
7175
assert_eq!(ProfileKind::parse(""), ProfileKind::Lecture);
72-
// 展示名非空(全部十二档案)
76+
assert_eq!(ProfileKind::parse("no-such-kind"), ProfileKind::Lecture);
77+
// 展示名非空(全部十三档案)
7378
for k in [
7479
ProfileKind::Lecture, ProfileKind::HandsOn, ProfileKind::TalkingHead,
7580
ProfileKind::Interview, ProfileKind::Meeting, ProfileKind::Podcast,
7681
ProfileKind::Live, ProfileKind::Whiteboard, ProfileKind::GameTutorial,
7782
ProfileKind::Exercise, ProfileKind::FollowAlong, ProfileKind::Coding,
83+
ProfileKind::Unknown,
7884
] {
7985
assert!(!k.label().is_empty(), "{:?} 展示名非空", k);
8086
}
87+
// Unknown 的标识串往返一致(会话 profile 列落库/读取同口径)
88+
assert_eq!(ProfileKind::parse(ProfileKind::Unknown.as_str()), ProfileKind::Unknown);
8189
}
8290

8391
#[test]
@@ -126,14 +134,14 @@ fn detect_hands_on_by_high_frame_switch() {
126134
}
127135

128136
#[test]
129-
fn detect_no_signal_returns_default_lecture_needs_confirmation() {
137+
fn detect_no_signal_returns_unknown_needs_confirmation() {
130138
// Arrange:全空信号
131139
let s = signals();
132140
// Act
133141
let result = vote_detect(&s);
134-
// Assert:默认 Lecture 单候选 + 必须确认(不静默假设)
142+
// Assert:v0.7.1 无法自动识别 → Unknown 单候选 + 必须确认(诚实未知,不猜默认)
135143
assert_eq!(result.candidates.len(), 1);
136-
assert_eq!(result.candidates[0].kind, ProfileKind::Lecture);
144+
assert_eq!(result.candidates[0].kind, ProfileKind::Unknown);
137145
assert!(result.needs_confirmation);
138146
}
139147

‎app/src/components/ProfileDetector.tsx‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const KIND_LABELS: Record<ProfileKind, string> = {
2222
exercise: "题目讲解",
2323
"follow-along": "跟练",
2424
coding: "编程实战",
25+
unknown: "未知",
2526
};
2627

2728
const ALL_KINDS: ProfileKind[] = [
@@ -37,6 +38,8 @@ const ALL_KINDS: ProfileKind[] = [
3738
"exercise",
3839
"follow-along",
3940
"coding",
41+
// v0.7.1:未知选项(自动检测无法识别时选中;无内置档案配置,参数走默认档)
42+
"unknown",
4043
];
4144

4245
export default function ProfileDetector({
@@ -78,6 +81,12 @@ export default function ProfileDetector({
7881
const top = r.candidates[0]?.kind ?? "lecture";
7982
setSelected(top);
8083
onProfileChange?.(top);
84+
} else if (r.candidates[0]?.kind === "unknown") {
85+
// v0.7.1 例外(用户需求):无法自动识别(零信号命中 → 候选=未知)时
86+
// 选中「未知」并生效——"未知"不是猜测的档案而是诚实标注,其参数=
87+
// 默认档(Lecture 配置零回归),自动选中无误导风险;用户仍可改选
88+
setSelected("unknown");
89+
onProfileChange?.("unknown");
8190
}
8291
})
8392
.catch((e) => {
@@ -118,7 +127,7 @@ export default function ProfileDetector({
118127

119128
return (
120129
<div style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: 12 }}>
121-
<div style={{ fontSize: 12, color: "#6b7280", marginBottom: 6 }}>视频类型档案(12 类,v0.7.0)</div>
130+
<div style={{ fontSize: 12, color: "#6b7280", marginBottom: 6 }}>视频类型档案(13 类,v0.7.1)</div>
122131
{detecting ? (
123132
<div style={{ fontSize: 11, color: "#9ca3af" }}>检测中…</div>
124133
) : (
@@ -155,14 +164,20 @@ export default function ProfileDetector({
155164
候选:{result.candidates.map((c) => `${KIND_LABELS[c.kind]}(${(c.score * 100) | 0}%)`).join(" / ")}
156165
</div>
157166
)}
158-
{profiles.length > 0 && (
167+
{selected === "unknown" ? (
159168
<div style={{ fontSize: 10, color: "#9ca3af", marginTop: 4 }}>
160-
{profiles.find((p) => p.kind === selected)?.artifact_template ?? ""} 模板 · 采样档位{" "}
161-
{(() => {
162-
const b = profiles.find((p) => p.kind === selected)?.sampling_budget;
163-
return b ? `${b.subtitle_every}s/字幕 · ${b.full_every}s/全帧` : "";
164-
})()}
169+
未知:管线参数用默认档(网课采样,零回归)· 产物模板同网课讲义
165170
</div>
171+
) : (
172+
profiles.length > 0 && (
173+
<div style={{ fontSize: 10, color: "#9ca3af", marginTop: 4 }}>
174+
{profiles.find((p) => p.kind === selected)?.artifact_template ?? ""} 模板 · 采样档位{" "}
175+
{(() => {
176+
const b = profiles.find((p) => p.kind === selected)?.sampling_budget;
177+
return b ? `${b.subtitle_every}s/字幕 · ${b.full_every}s/全帧` : "";
178+
})()}
179+
</div>
180+
)
166181
)}
167182
{error && <div style={{ fontSize: 11, color: "#dc2626", marginTop: 4 }}>{error}</div>}
168183
</div>

‎app/src/pages/ClassroomPage.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
232232
};
233233

234234
// ── 视频类型档案(v0.5.0 M1,REQ-043:混合检测用户确认结果)──
235-
const [profileKind, setProfileKind] = useState<ProfileKind>("lecture");
235+
// v0.7.1:初始「未知」——未检测/无法自动识别时如实标注(参数走默认档零回归)
236+
const [profileKind, setProfileKind] = useState<ProfileKind>("unknown");
236237

237238
/** 开始实时捕获(REQ-007~012):窗口可选(未选=全屏);携带档案(REQ-043) */
238239
const startLive = async () => {

‎app/src/types.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export interface OcrDeviceStatus {
190190
// 视频类型档案领域类型(v0.5.0 M1,REQ-043,与 Rust serde 契约对齐)
191191
// ────────────────────────────────────────────────────────────
192192

193-
/** 十二类档案标识(Rust ProfileKind,kebab-case 序列化;v0.7.0 M2 扩至十二) */
193+
/** 十三类档案标识(Rust ProfileKind,kebab-case 序列化;v0.7.1 增 Unknown) */
194194
export type ProfileKind =
195195
| "lecture"
196196
| "hands-on"
@@ -203,7 +203,9 @@ export type ProfileKind =
203203
| "game-tutorial"
204204
| "exercise"
205205
| "follow-along"
206-
| "coding";
206+
| "coding"
207+
/** v0.7.1:未知——自动检测无法识别时的如实标注(管线参数回退默认档) */
208+
| "unknown";
207209

208210
/** 检测信号配置(Rust DetectSignals) */
209211
export interface DetectSignals {

0 commit comments

Comments
 (0)