之前 evaluate() 写死 0.8、decide() 直接透传,history 参数没人用, "自学习"只有骨架。这次补上: 置信度(MetadataNormalizer.evaluate) - 字段来源可追溯:NormalizedMetadata 新增 titleFromTag / artistFromTag / albumFromTag - 基础分 0.25;标题 +0.20、标题来自标签再 +0.10(文件名当标题则 -0.10) - 艺术家 +0.15、来自标签 +0.10;专辑 +0.10;年份 +0.03;封面 +0.02 - 历史一致性:同艺人 >=2 条 +0.05,同专辑 >=2 条 +0.03 - 结果 clamp 到 0.05~0.98,且每个加分项写入 evidence,便于日志解释 状态机(MetadataService) - 只有 confidence>=0.6 且 status=verified 的记录才直接命中 - pending 记录下次播放会重新读取并合并(自修复) - validation_count 累加;>=2 次且置信度达标才升级为 verified - 新结果置信度低于旧结果时保留旧字段,只累加验证次数 - 日志输出 status / confidence / validation_count / evidence 测试 - 新增 test/metadata_normalizer_test.dart,9 个用例覆盖来源标记、分级、 历史加权、证据、阈值;flutter test 全绿 真机验证 - 起播看门狗在真实弱网下触发:23:44:50 打开失败 → 15s 后自动重连 → 1.4s 出声(open 仅 100ms)
153 lines
3.4 KiB
Dart
153 lines
3.4 KiB
Dart
// lib/metadata/metadata_model.dart
|
|
import 'dart:typed_data';
|
|
|
|
/// 从音频文件读取的原始元数据
|
|
class RawMetadata {
|
|
final String title;
|
|
final String artist;
|
|
final String album;
|
|
final List<String> genres;
|
|
final List<String> performers;
|
|
final int year;
|
|
final int trackNumber;
|
|
final int trackTotal;
|
|
final int discNumber;
|
|
final int totalDisc;
|
|
final Duration? duration;
|
|
final Uint8List? artwork;
|
|
|
|
const RawMetadata({
|
|
this.title = '',
|
|
this.artist = '',
|
|
this.album = '',
|
|
this.genres = const [],
|
|
this.performers = const [],
|
|
this.year = 0,
|
|
this.trackNumber = 0,
|
|
this.trackTotal = 0,
|
|
this.discNumber = 0,
|
|
this.totalDisc = 0,
|
|
this.duration,
|
|
this.artwork,
|
|
});
|
|
|
|
bool get isEmpty =>
|
|
title.isEmpty &&
|
|
artist.isEmpty &&
|
|
album.isEmpty &&
|
|
genres.isEmpty &&
|
|
performers.isEmpty &&
|
|
year == 0;
|
|
|
|
bool get isNotEmpty => !isEmpty;
|
|
}
|
|
|
|
/// 标准化后的元数据
|
|
class NormalizedMetadata {
|
|
final String title;
|
|
final String artist;
|
|
final String album;
|
|
final List<String> genres;
|
|
final List<String> performers;
|
|
final int year;
|
|
final int trackNumber;
|
|
final int trackTotal;
|
|
final int discNumber;
|
|
final int totalDisc;
|
|
final Duration? duration;
|
|
final Uint8List? artwork;
|
|
|
|
/// 字段来源:true 表示来自标签,false 表示从文件名/路径推断。
|
|
/// 置信度模型需要区分这两者——"标签里的标题"和"文件名当标题"价值完全不同。
|
|
final bool titleFromTag;
|
|
final bool artistFromTag;
|
|
final bool albumFromTag;
|
|
|
|
const NormalizedMetadata({
|
|
this.title = '',
|
|
this.artist = '',
|
|
this.album = '',
|
|
this.genres = const [],
|
|
this.performers = const [],
|
|
this.year = 0,
|
|
this.trackNumber = 0,
|
|
this.trackTotal = 0,
|
|
this.discNumber = 0,
|
|
this.totalDisc = 0,
|
|
this.duration,
|
|
this.artwork,
|
|
this.titleFromTag = false,
|
|
this.artistFromTag = false,
|
|
this.albumFromTag = false,
|
|
});
|
|
|
|
bool get isEmpty =>
|
|
title.isEmpty &&
|
|
artist.isEmpty &&
|
|
album.isEmpty &&
|
|
genres.isEmpty &&
|
|
performers.isEmpty &&
|
|
year == 0;
|
|
|
|
bool get isNotEmpty => !isEmpty;
|
|
}
|
|
|
|
/// 候选元数据
|
|
class CandidateMetadata {
|
|
final NormalizedMetadata metadata;
|
|
final double confidence;
|
|
final Map<String, double> evidence;
|
|
|
|
const CandidateMetadata({
|
|
required this.metadata,
|
|
required this.confidence,
|
|
this.evidence = const {},
|
|
});
|
|
}
|
|
|
|
/// 最终确认的元数据
|
|
class FinalMetadata {
|
|
final String title;
|
|
final String artist;
|
|
final String album;
|
|
final String genre;
|
|
final List<String> genres;
|
|
final List<String> performers;
|
|
final int year;
|
|
final int trackNumber;
|
|
final int trackTotal;
|
|
final int discNumber;
|
|
final int totalDisc;
|
|
final Duration? duration;
|
|
final Uint8List? artwork;
|
|
final String source;
|
|
|
|
const FinalMetadata({
|
|
this.title = '',
|
|
this.artist = '',
|
|
this.album = '',
|
|
this.genre = '',
|
|
this.genres = const [],
|
|
this.performers = const [],
|
|
this.year = 0,
|
|
this.trackNumber = 0,
|
|
this.trackTotal = 0,
|
|
this.discNumber = 0,
|
|
this.totalDisc = 0,
|
|
this.duration,
|
|
this.artwork,
|
|
this.source = 'unknown',
|
|
});
|
|
|
|
bool get isEmpty =>
|
|
title.isEmpty &&
|
|
artist.isEmpty &&
|
|
album.isEmpty &&
|
|
genre.isEmpty &&
|
|
genres.isEmpty &&
|
|
performers.isEmpty &&
|
|
year == 0;
|
|
|
|
bool get isNotEmpty => !isEmpty;
|
|
}
|