- BLE: 修复 BlueZ LocalName 与 Includes 冲突,串行注册与退避 - HTTP: 语音对话补 Live2D talking 标志与 session;Web 补 X-Session-Id - Flutter: 角色/对话/模型页 + API,Gradle/依赖升级,麦克风权限 - 文档: 新增 docs/STATUS.md,校准 CLAUDE/PROGRESS/README - 清理: 移除根目录截图与临时模型,运维脚本迁入 scripts/device
124 lines
3.4 KiB
Dart
124 lines
3.4 KiB
Dart
class AiModelInfo {
|
|
const AiModelInfo({
|
|
required this.id,
|
|
required this.kind,
|
|
required this.name,
|
|
required this.version,
|
|
required this.size,
|
|
required this.memoryRequired,
|
|
required this.recommendedTier,
|
|
required this.downloaded,
|
|
required this.downloadProgress,
|
|
});
|
|
|
|
final String id;
|
|
final String kind;
|
|
final String name;
|
|
final String version;
|
|
final int size;
|
|
final int memoryRequired;
|
|
final String recommendedTier;
|
|
final bool downloaded;
|
|
final int downloadProgress;
|
|
|
|
String get kindLabel {
|
|
switch (kind.toLowerCase()) {
|
|
case 'llm':
|
|
return 'LLM';
|
|
case 'asr':
|
|
return 'ASR';
|
|
case 'tts':
|
|
return 'TTS';
|
|
default:
|
|
return kind.toUpperCase();
|
|
}
|
|
}
|
|
|
|
String get sizeLabel => _formatBytes(size);
|
|
String get memoryLabel => _formatBytes(memoryRequired);
|
|
|
|
factory AiModelInfo.fromJson(Map<String, dynamic> json) {
|
|
return AiModelInfo(
|
|
id: json['id']?.toString() ?? '',
|
|
kind: json['kind']?.toString() ?? '',
|
|
name: json['name']?.toString() ?? json['id']?.toString() ?? '',
|
|
version: json['version']?.toString() ?? '',
|
|
size: _asInt(json['size']),
|
|
memoryRequired: _asInt(json['memory_required']),
|
|
recommendedTier: json['recommended_tier']?.toString() ?? '',
|
|
downloaded: json['downloaded'] == true,
|
|
downloadProgress: _asInt(json['download_progress']),
|
|
);
|
|
}
|
|
|
|
static int _asInt(dynamic value) {
|
|
if (value is int) return value;
|
|
if (value is num) return value.toInt();
|
|
return int.tryParse(value?.toString() ?? '') ?? 0;
|
|
}
|
|
|
|
static String _formatBytes(int bytes) {
|
|
if (bytes <= 0) return '0 B';
|
|
if (bytes < 1024) return '$bytes B';
|
|
if (bytes < 1024 * 1024) {
|
|
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
}
|
|
if (bytes < 1024 * 1024 * 1024) {
|
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
}
|
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
|
}
|
|
}
|
|
|
|
class ModelsSnapshot {
|
|
const ModelsSnapshot({
|
|
required this.models,
|
|
required this.active,
|
|
required this.usedSpace,
|
|
required this.quota,
|
|
});
|
|
|
|
final List<AiModelInfo> models;
|
|
final Map<String, String> active;
|
|
final int usedSpace;
|
|
final int quota;
|
|
|
|
String get usedLabel => AiModelInfo._formatBytes(usedSpace);
|
|
String get quotaLabel => AiModelInfo._formatBytes(quota);
|
|
double get usageRatio => quota <= 0 ? 0 : (usedSpace / quota).clamp(0.0, 1.0);
|
|
|
|
bool isActive(AiModelInfo model) {
|
|
final current = active[model.kind.toLowerCase()];
|
|
return current != null && current == model.id;
|
|
}
|
|
|
|
factory ModelsSnapshot.fromJson(Map<String, dynamic> json) {
|
|
final rawModels = json['models'];
|
|
final models = <AiModelInfo>[];
|
|
if (rawModels is List) {
|
|
for (final item in rawModels) {
|
|
if (item is Map<String, dynamic>) {
|
|
models.add(AiModelInfo.fromJson(item));
|
|
} else if (item is Map) {
|
|
models.add(AiModelInfo.fromJson(Map<String, dynamic>.from(item)));
|
|
}
|
|
}
|
|
}
|
|
|
|
final active = <String, String>{};
|
|
final rawActive = json['active'];
|
|
if (rawActive is Map) {
|
|
rawActive.forEach((key, value) {
|
|
active[key.toString().toLowerCase()] = value.toString();
|
|
});
|
|
}
|
|
|
|
return ModelsSnapshot(
|
|
models: models,
|
|
active: active,
|
|
usedSpace: AiModelInfo._asInt(json['used_space']),
|
|
quota: AiModelInfo._asInt(json['quota']),
|
|
);
|
|
}
|
|
}
|