[MF]修复音频数据导出导入功能
This commit is contained in:
parent
3d3a827630
commit
5d36541dc5
272
音频分类/script.js
272
音频分类/script.js
@ -1,18 +1,17 @@
|
|||||||
// 全局变量和模型实例
|
// 全局变量和模型实例
|
||||||
let recognizer; // 基础的 SpeechCommands recognizer
|
let recognizer;
|
||||||
let transferRecognizer; // 用于迁移学习的 recognizer
|
let transferRecognizer;
|
||||||
const labels = []; // 用户定义的类别标签数组 (包括背景噪音)
|
// labels 现在将根据导入的数据动态重建,但仍需初始化
|
||||||
// 将背景噪音定义为第一个类别,其内部名称为 _background_noise_
|
let labels = [];
|
||||||
const BACKGROUND_NOISE_LABEL = '_background_noise_';
|
const BACKGROUND_NOISE_LABEL = '_background_noise_';
|
||||||
const BACKGROUND_NOISE_INDEX = 0; // 仅用于本地 labels 数组索引,不直接用于collectExample
|
|
||||||
|
|
||||||
let isPredicting = false; // 预测状态标志
|
let isPredicting = false;
|
||||||
let isRecording = false; // 录音状态标志,防止重复点击
|
let isRecording = false;
|
||||||
const recordDuration = 1000; // 每个样本的录音时长 (毫秒)
|
const recordDuration = 1000;
|
||||||
let isModelTrainedFlag = false; // 手动维护模型训练状态
|
let isModelTrainedFlag = false;
|
||||||
let predictionStopFunction = null; // 存储 transferRecognizer.listen() 返回的停止函数
|
let predictionStopFunction = null;
|
||||||
|
|
||||||
// UI 元素引用 (保持不变)
|
// UI 元素引用 (已更新)
|
||||||
const statusDiv = document.getElementById('status');
|
const statusDiv = document.getElementById('status');
|
||||||
const backgroundNoiseSampleCountSpan = document.getElementById('backgroundNoiseSampleCount');
|
const backgroundNoiseSampleCountSpan = document.getElementById('backgroundNoiseSampleCount');
|
||||||
const recordBackgroundNoiseBtn = document.getElementById('recordBackgroundNoiseBtn');
|
const recordBackgroundNoiseBtn = document.getElementById('recordBackgroundNoiseBtn');
|
||||||
@ -24,47 +23,50 @@ const startPredictingBtn = document.getElementById('startPredictingBtn');
|
|||||||
const stopPredictingBtn = document.getElementById('stopPredictingBtn');
|
const stopPredictingBtn = document.getElementById('stopPredictingBtn');
|
||||||
const predictionResultDiv = document.getElementById('predictionResult');
|
const predictionResultDiv = document.getElementById('predictionResult');
|
||||||
|
|
||||||
|
// ===== 新增UI元素引用 =====
|
||||||
|
const exportModelBtn = document.getElementById('exportModelBtn');
|
||||||
|
const importModelBtn = document.getElementById('importModelBtn');
|
||||||
|
const importFileInput = document.getElementById('importFileInput');
|
||||||
|
|
||||||
|
|
||||||
// ======================= 初始化函数 =======================
|
// ======================= 初始化函数 =======================
|
||||||
async function init() {
|
async function init() {
|
||||||
statusDiv.innerText = '正在加载 TensorFlow.js 和 Speech Commands 模型...';
|
statusDiv.innerText = '正在加载 TensorFlow.js 和 Speech Commands 模型...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
recognizer = speechCommands.create(
|
recognizer = speechCommands.create('BROWSER_FFT');
|
||||||
'BROWSER_FFT' // 使用浏览器内置的 FFT 处理,性能更好
|
|
||||||
);
|
|
||||||
|
|
||||||
await recognizer.ensureModelLoaded();
|
await recognizer.ensureModelLoaded();
|
||||||
|
|
||||||
transferRecognizer = recognizer.createTransfer('my-custom-model');
|
transferRecognizer = recognizer.createTransfer('my-custom-model');
|
||||||
|
|
||||||
// 只有在 transferRecognizer 创建成功后,才将背景噪音标签加入我们的 local labels 数组
|
// 初始化时清空并设置背景噪音标签
|
||||||
labels.push(BACKGROUND_NOISE_LABEL); // 仅用于本地 UI 映射和预测结果查找
|
labels = [BACKGROUND_NOISE_LABEL];
|
||||||
|
|
||||||
statusDiv.innerText = '模型加载成功!你可以开始录制背景噪音和自定义声音样本了。';
|
statusDiv.innerText = '模型加载成功!你可以开始录制、或导入已有的样本数据。';
|
||||||
recordBackgroundNoiseBtn.disabled = false;
|
recordBackgroundNoiseBtn.disabled = false;
|
||||||
addCategoryBtn.disabled = false;
|
addCategoryBtn.disabled = false;
|
||||||
|
importModelBtn.disabled = false; // 允许导入
|
||||||
|
exportModelBtn.disabled = true; // 尚无数据,默认禁用导出按钮
|
||||||
|
|
||||||
trainModelBtn.disabled = true;
|
trainModelBtn.disabled = true;
|
||||||
startPredictingBtn.disabled = true;
|
startPredictingBtn.disabled = true;
|
||||||
stopPredictingBtn.disabled = true;
|
stopPredictingBtn.disabled = true;
|
||||||
isModelTrainedFlag = false; // 重置训练状态
|
isModelTrainedFlag = false;
|
||||||
|
|
||||||
|
// --- 修正之处:移除此处对 checkTrainingReadiness() 的调用 ---
|
||||||
|
// checkTrainingReadiness(); // <--- 移除这一行!
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
statusDiv.innerText = `模型加载失败或麦克风无法访问: ${error.message}. 请检查麦克风权限和网络连接。`;
|
statusDiv.innerText = `模型加载失败或麦克风无法访问: ${error.message}. 请检查麦克风权限和网络连接。`;
|
||||||
console.error('初始化失败:', error);
|
console.error('初始化失败:', error);
|
||||||
// 任何失败都禁用所有控制,直到初始化成功
|
// 禁用所有按钮
|
||||||
recordBackgroundNoiseBtn.disabled = true;
|
const buttons = document.querySelectorAll('button');
|
||||||
addCategoryBtn.disabled = true;
|
buttons.forEach(btn => btn.disabled = true);
|
||||||
trainModelBtn.disabled = true;
|
isModelTrainedFlag = false;
|
||||||
startPredictingBtn.disabled = true;
|
|
||||||
stopPredictingBtn.disabled = true;
|
|
||||||
isModelTrainedFlag = false; // 重置训练状态
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================= 批量录制样本的通用函数 =======================
|
// ======================= 批量录制样本的通用函数 =======================
|
||||||
// recordMultipleExamples传入 label, 样本数量显示元素, 按钮元素, 一次录制的样本数量
|
async function recordMultipleExamples(label, sampleCountSpanElement, buttonElement, countToRecord = 5) {
|
||||||
async function recordMultipleExamples(label, sampleCountSpanElement, buttonElement, countToRecord = 5) { // 默认一次录制5个样本
|
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
statusDiv.innerText = '请等待当前录音完成...';
|
statusDiv.innerText = '请等待当前录音完成...';
|
||||||
return;
|
return;
|
||||||
@ -83,9 +85,8 @@ async function recordMultipleExamples(label, sampleCountSpanElement, buttonEleme
|
|||||||
);
|
);
|
||||||
const exampleCounts = transferRecognizer.countExamples();
|
const exampleCounts = transferRecognizer.countExamples();
|
||||||
sampleCountSpanElement.innerText = exampleCounts[label] || 0;
|
sampleCountSpanElement.innerText = exampleCounts[label] || 0;
|
||||||
// 在每次录音之间增加短暂延迟,以便更好地分离样本
|
|
||||||
if (i < countToRecord - 1) {
|
if (i < countToRecord - 1) {
|
||||||
await new Promise(resolve => setTimeout(resolve, Math.max(200, recordDuration / 5))); // 至少 200ms 或录音时长的 1/5
|
await new Promise(resolve => setTimeout(resolve, Math.max(200, recordDuration / 5)));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
statusDiv.innerText = `录制 "${label}" 样本失败: ${error.message}`;
|
statusDiv.innerText = `录制 "${label}" 样本失败: ${error.message}`;
|
||||||
@ -102,16 +103,15 @@ async function recordMultipleExamples(label, sampleCountSpanElement, buttonEleme
|
|||||||
statusDiv.innerText = `已为 "${label}" 收集了 ${transferRecognizer.countExamples()[label] || 0} 个样本。`;
|
statusDiv.innerText = `已为 "${label}" 收集了 ${transferRecognizer.countExamples()[label] || 0} 个样本。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================= 背景噪音样本收集 =======================
|
|
||||||
// 按钮点击事件
|
// ============== 背景噪音样本收集 (无修改) ==================
|
||||||
recordBackgroundNoiseBtn.onclick = async () => {
|
recordBackgroundNoiseBtn.onclick = async () => {
|
||||||
await recordMultipleExamples(BACKGROUND_NOISE_LABEL, backgroundNoiseSampleCountSpan, recordBackgroundNoiseBtn, 5);
|
await recordMultipleExamples(BACKGROUND_NOISE_LABEL, backgroundNoiseSampleCountSpan, recordBackgroundNoiseBtn, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ======================= 自定义类别管理 =======================
|
||||||
|
|
||||||
// ======================= 自定义类别管理和样本收集 =======================
|
// 添加新类别(用户手动添加)
|
||||||
|
|
||||||
// 添加新类别到 UI 和逻辑 (用于自定义声音)
|
|
||||||
function addCustomCategory(categoryName) {
|
function addCustomCategory(categoryName) {
|
||||||
if (!categoryName) {
|
if (!categoryName) {
|
||||||
alert('类别名称不能为空!');
|
alert('类别名称不能为空!');
|
||||||
@ -122,20 +122,33 @@ function addCustomCategory(categoryName) {
|
|||||||
alert(`类别 "${categoryName}" 已经存在!`);
|
alert(`类别 "${categoryName}" 已经存在!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将标签添加到本地数组以供 UI 逻辑和后续预测结果查找使用
|
// 将标签添加到本地数组以供 UI 逻辑和后续预测结果查找使用
|
||||||
labels.push(categoryName);
|
labels.push(categoryName);
|
||||||
|
|
||||||
// 创建类别块 UI
|
// 创建UI时样本数量为0
|
||||||
const categoryBlock = document.createElement('div');
|
createCategoryUI(categoryName, 0);
|
||||||
categoryBlock.className = 'category-block';
|
newCategoryNameInput.value = ''; // 清空输入框
|
||||||
|
checkTrainingReadiness(); // 添加新类别后检查训练就绪状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加自定义类别按钮点击事件
|
||||||
|
addCategoryBtn.onclick = () => {
|
||||||
|
addCustomCategory(newCategoryNameInput.value.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建类别UI的辅助函数(用于手动添加和导入后重建)
|
||||||
|
function createCategoryUI(categoryName, sampleCount) {
|
||||||
// categoryId 此时仅用于生成唯一的 ID,不直接传给 collectExample
|
// categoryId 此时仅用于生成唯一的 ID,不直接传给 collectExample
|
||||||
const categoryId = labels.indexOf(categoryName);
|
const categoryId = labels.indexOf(categoryName);
|
||||||
|
|
||||||
|
const categoryBlock = document.createElement('div');
|
||||||
|
categoryBlock.className = 'category-block';
|
||||||
|
// 添加一个ID以便后续删除或识别
|
||||||
|
categoryBlock.id = `category-block-${encodeURIComponent(categoryName)}`;
|
||||||
|
|
||||||
categoryBlock.innerHTML = `
|
categoryBlock.innerHTML = `
|
||||||
<h3>${categoryName}</h3>
|
<h3>${categoryName}</h3>
|
||||||
<p>样本数量: <span id="sampleCount-${categoryId}">0</span></p>
|
<p>样本数量: <span id="sampleCount-${categoryId}">${sampleCount}</span></p>
|
||||||
<button id="recordBtn-${categoryId}">录制样本</button>
|
<button id="recordBtn-${categoryId}">录制样本</button>
|
||||||
`;
|
`;
|
||||||
categoryContainer.appendChild(categoryBlock);
|
categoryContainer.appendChild(categoryBlock);
|
||||||
@ -147,25 +160,23 @@ function addCustomCategory(categoryName) {
|
|||||||
recordBtn.onclick = async () => {
|
recordBtn.onclick = async () => {
|
||||||
await recordMultipleExamples(categoryName, sampleCountSpan, recordBtn, 5);
|
await recordMultipleExamples(categoryName, sampleCountSpan, recordBtn, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
newCategoryNameInput.value = ''; // 清空输入框
|
|
||||||
checkTrainingReadiness(); // 添加新类别后检查训练就绪状态
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加自定义类别按钮点击事件
|
|
||||||
addCategoryBtn.onclick = () => {
|
|
||||||
addCustomCategory(newCategoryNameInput.value.trim());
|
|
||||||
};
|
|
||||||
|
|
||||||
// ======================= 检查训练就绪状态 =======================
|
// ======================= 状态检查 =======================
|
||||||
function checkTrainingReadiness() {
|
function checkTrainingReadiness() {
|
||||||
const exampleCounts = transferRecognizer.countExamples();
|
const exampleCounts = transferRecognizer.countExamples();
|
||||||
|
|
||||||
|
// 检查是否有任何样本,以决定是否启用“导出”按钮
|
||||||
|
const totalSamples = Object.values(exampleCounts).reduce((acc, count) => acc + count, 0);
|
||||||
|
exportModelBtn.disabled = totalSamples === 0;
|
||||||
|
|
||||||
let backgroundNoiseReady = (exampleCounts[BACKGROUND_NOISE_LABEL] || 0) > 0;
|
let backgroundNoiseReady = (exampleCounts[BACKGROUND_NOISE_LABEL] || 0) > 0;
|
||||||
|
|
||||||
let customCategoriesReady = 0;
|
let customCategoriesReady = 0;
|
||||||
// 遍历本地 labels 数组,检查每个自定义类别是否有样本
|
// 遍历本地 labels 数组,检查每个自定义类别是否有样本
|
||||||
for (let i = 1; i < labels.length; i++) { // 从索引 1 开始,因为 0 是背景噪音
|
// 从索引 1 开始,因为 0 是背景噪音
|
||||||
|
for (let i = 1; i < labels.length; i++) {
|
||||||
const customLabel = labels[i];
|
const customLabel = labels[i];
|
||||||
if ((exampleCounts[customLabel] || 0) > 0) {
|
if ((exampleCounts[customLabel] || 0) > 0) {
|
||||||
customCategoriesReady++;
|
customCategoriesReady++;
|
||||||
@ -179,9 +190,10 @@ function checkTrainingReadiness() {
|
|||||||
trainModelBtn.disabled = true;
|
trainModelBtn.disabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ======================= 模型训练 =======================
|
|
||||||
|
// ======================= 模型训练 (无修改) =======================
|
||||||
trainModelBtn.onclick = async () => {
|
trainModelBtn.onclick = async () => {
|
||||||
const exampleCounts = transferRecognizer.countExamples(); // 确保这里获取到了最新的样本数量
|
const exampleCounts = transferRecognizer.countExamples();
|
||||||
console.log('--- DEBUG: 训练开始前,各类别样本数量:', exampleCounts);
|
console.log('--- DEBUG: 训练开始前,各类别样本数量:', exampleCounts);
|
||||||
|
|
||||||
let totalExamples = 0;
|
let totalExamples = 0;
|
||||||
@ -252,8 +264,8 @@ trainModelBtn.onclick = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================= 实时预测 =======================
|
// ======================= 实时预测 (无修改) =======================
|
||||||
startPredictingBtn.onclick = async () => { // 确保此函数是 async
|
startPredictingBtn.onclick = async () => {
|
||||||
console.log('--- DEBUG: 点击开始识别时, isModelTrainedFlag 为:', isModelTrainedFlag);
|
console.log('--- DEBUG: 点击开始识别时, isModelTrainedFlag 为:', isModelTrainedFlag);
|
||||||
if (isPredicting) {
|
if (isPredicting) {
|
||||||
statusDiv.innerText = '识别已经在进行中...';
|
statusDiv.innerText = '识别已经在进行中...';
|
||||||
@ -278,8 +290,7 @@ startPredictingBtn.onclick = async () => { // 确保此函数是 async
|
|||||||
statusDiv.innerText = '正在开始识别... 请发出你训练过的声音。';
|
statusDiv.innerText = '正在开始识别... 请发出你训练过的声音。';
|
||||||
predictionResultDiv.innerText = '等待识别结果...';
|
predictionResultDiv.innerText = '等待识别结果...';
|
||||||
|
|
||||||
// <<< 核心修正:捕获 transferRecognizer.listen() 返回的停止函数时使用 await
|
predictionStopFunction = await transferRecognizer.listen(result => {
|
||||||
predictionStopFunction = await transferRecognizer.listen(result => { // !!!这里加上了 await !!!
|
|
||||||
if (!isPredicting) return;
|
if (!isPredicting) return;
|
||||||
|
|
||||||
// `transferRecognizer.wordLabels()` 会返回 transferRecognizer 内部按顺序排列的所有标签名称。
|
// `transferRecognizer.wordLabels()` 会返回 transferRecognizer 内部按顺序排列的所有标签名称。
|
||||||
@ -305,7 +316,6 @@ startPredictingBtn.onclick = async () => { // 确保此函数是 async
|
|||||||
overlapFactor: 0.50,
|
overlapFactor: 0.50,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 可以在这里添加一个调试日志,确认 predictionStopFunction 确实是一个函数
|
|
||||||
console.log('--- DEBUG: predictionStopFunction 赋值后:', predictionStopFunction);
|
console.log('--- DEBUG: predictionStopFunction 赋值后:', predictionStopFunction);
|
||||||
console.log('--- DEBUG: typeof predictionStopFunction 赋值后:', typeof predictionStopFunction);
|
console.log('--- DEBUG: typeof predictionStopFunction 赋值后:', typeof predictionStopFunction);
|
||||||
|
|
||||||
@ -313,10 +323,9 @@ startPredictingBtn.onclick = async () => { // 确保此函数是 async
|
|||||||
|
|
||||||
stopPredictingBtn.onclick = () => {
|
stopPredictingBtn.onclick = () => {
|
||||||
if (isPredicting) {
|
if (isPredicting) {
|
||||||
// 增加一个额外的类型检查,确保它确实是一个函数
|
if (typeof predictionStopFunction === 'function') {
|
||||||
if (typeof predictionStopFunction === 'function') { // 确保是函数才调用
|
predictionStopFunction();
|
||||||
predictionStopFunction(); // 调用停止识别的函数
|
predictionStopFunction = null;
|
||||||
predictionStopFunction = null; // 清除引用,避免内存泄漏,也防止二次调用
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('--- WARN: predictionStopFunction 不是一个函数,无法停止监听。');
|
console.warn('--- WARN: predictionStopFunction 不是一个函数,无法停止监听。');
|
||||||
}
|
}
|
||||||
@ -340,5 +349,144 @@ stopPredictingBtn.onclick = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================= 新增:模型导出功能 =======================
|
||||||
|
exportModelBtn.onclick = async () => {
|
||||||
|
try {
|
||||||
|
// 序列化所有收集到的样本数据
|
||||||
|
const serializedExamples = transferRecognizer.serializeExamples();
|
||||||
|
|
||||||
|
// 创建一个 Blob 对象
|
||||||
|
const blob = new Blob([serializedExamples], { type: 'application/octet-stream' });
|
||||||
|
|
||||||
|
// 创建一个下载链接
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
// 定制文件名,包含日期和时间
|
||||||
|
const now = new Date();
|
||||||
|
const filename = `speech_commands_data_${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}_${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}.bin`;
|
||||||
|
a.download = filename;
|
||||||
|
|
||||||
|
// 模拟点击下载
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url); // 释放内存
|
||||||
|
|
||||||
|
statusDiv.innerText = `数据已成功导出为 "${filename}"。`;
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.innerText = `导出数据失败: ${error.message}`;
|
||||||
|
console.error('导出数据失败:', error);
|
||||||
|
alert('导出数据失败。请确保您已录制至少一个样本!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================= 新增:模型导入功能 =======================
|
||||||
|
importModelBtn.onclick = () => {
|
||||||
|
// 触发隐藏的文件输入框点击事件
|
||||||
|
importFileInput.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
importFileInput.onchange = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
statusDiv.innerText = '未选择文件。';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.name.endsWith('.bin')) {
|
||||||
|
alert('请选择后缀名为 .bin 的文件!');
|
||||||
|
statusDiv.innerText = '文件格式不正确,请选择 .bin 文件。';
|
||||||
|
// 清空文件输入,以便用户可以选择其他文件
|
||||||
|
importFileInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusDiv.innerText = `正在导入文件 "${file.name}"...`;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
try {
|
||||||
|
const dataBuffer = e.target.result; // 获取文件的 ArrayBuffer 内容
|
||||||
|
|
||||||
|
// 清除当前的 transferRecognizer 中的所有样本
|
||||||
|
// SpeechCommands库中没有直接的clearExamples方法,
|
||||||
|
// 最简单的做法是重新创建一个 transferRecognizer 实例。
|
||||||
|
// 但更好的做法是先尝试loadExamples,如果需要重置,再做。
|
||||||
|
// 假设导入是“覆盖”现有样本的。
|
||||||
|
// TODO: 这里可以考虑增加用户确认是否清除现有样本的提示
|
||||||
|
|
||||||
|
// 导入样本。这会自动更新 internal model
|
||||||
|
await transferRecognizer.loadExamples(dataBuffer);
|
||||||
|
|
||||||
|
// 成功导入后,刷新UI
|
||||||
|
await syncUIWithLoadedData();
|
||||||
|
|
||||||
|
statusDiv.innerText = `文件 "${file.name}" 导入成功!`;
|
||||||
|
alert(`已成功导入 ${transferRecognizer.countExamples()._numExamples_ || 0} 个样本!`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.innerText = `导入数据失败: ${error.message}. 确保存储的是有效的模型样本数据。`;
|
||||||
|
console.error('导入数据失败:', error);
|
||||||
|
alert(`导入数据失败。请检查文件是否损坏或格式不正确。\n错误: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
// 清空文件输入,以便下次选择相同文件也能触发 onchange
|
||||||
|
importFileInput.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
statusDiv.innerText = `读取文件失败: ${error.message}`;
|
||||||
|
console.error('文件读取失败:', error);
|
||||||
|
alert('文件读取失败。');
|
||||||
|
importFileInput.value = '';
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file); // 以 ArrayBuffer 格式读取文件
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================= 新增辅助函数:导入后同步UI =======================
|
||||||
|
async function syncUIWithLoadedData() {
|
||||||
|
// 清空现有除了背景噪音以外的类别块
|
||||||
|
// 遍历所有子元素,从后向前删除,避免索引问题
|
||||||
|
while (categoryContainer.firstChild) {
|
||||||
|
categoryContainer.removeChild(categoryContainer.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置全局 labels 数组,只保留背景噪音
|
||||||
|
labels = [BACKGROUND_NOISE_LABEL];
|
||||||
|
|
||||||
|
// 获取导入后的样本计数
|
||||||
|
const exampleCounts = transferRecognizer.countExamples();
|
||||||
|
console.log('--- DEBUG: 导入后样本数量:', exampleCounts);
|
||||||
|
|
||||||
|
// 更新背景噪音样本数量
|
||||||
|
backgroundNoiseSampleCountSpan.innerText = exampleCounts[BACKGROUND_NOISE_LABEL] || 0;
|
||||||
|
|
||||||
|
// 重新构建自定义类别 UI
|
||||||
|
for (const label of Object.keys(exampleCounts)) {
|
||||||
|
if (label === BACKGROUND_NOISE_LABEL || label === '_version_' || label === '_numExamples_') {
|
||||||
|
continue; // 跳过背景噪音和内部元数据标签
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将导入的自定义标签添加到我们的 labels 数组
|
||||||
|
if (!labels.includes(label)) {
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
// 根据导入的数据创建 UI
|
||||||
|
createCategoryUI(label, exampleCounts[label]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置模型的训练状态
|
||||||
|
isModelTrainedFlag = false;
|
||||||
|
trainModelBtn.disabled = true; // 训练按钮默认禁用,等待 checkTrainingReadiness 启用
|
||||||
|
startPredictingBtn.disabled = true; // 预测按钮禁用
|
||||||
|
stopPredictingBtn.disabled = true; // 停止按钮禁用
|
||||||
|
|
||||||
|
// 检查训练就绪状态(现在有样本了,这个调用是安全的)
|
||||||
|
checkTrainingReadiness();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ======================= 页面加载时执行 =======================
|
// ======================= 页面加载时执行 =======================
|
||||||
window.onload = init;
|
window.onload = init;
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>音频分类器 (背景噪音分离版)</title>
|
<title>音频分类器 (背景噪音分离版)</title>
|
||||||
<style>
|
<style> /* 你的 CSS 样式保持不变 */
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
@ -73,6 +73,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
margin-bottom: 10px; /* 增加下边距以适应换行 */
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
#controls button:hover:not(:disabled) {
|
#controls button:hover:not(:disabled) {
|
||||||
@ -82,13 +83,19 @@
|
|||||||
background-color: #cccccc;
|
background-color: #cccccc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
/* 新增:导出导入按钮样式 */
|
||||||
|
#exportModelBtn { background-color: #ffc107; color: #333; }
|
||||||
|
#exportModelBtn:hover:not(:disabled) { background-color: #e0a800; }
|
||||||
|
#importModelBtn { background-color: #17a2b8; }
|
||||||
|
#importModelBtn:hover:not(:disabled) { background-color: #138496; }
|
||||||
|
|
||||||
#status {
|
#status {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
color: #616161;
|
color: #616161;
|
||||||
}
|
}
|
||||||
#predictionResult {
|
#predictionResult {
|
||||||
margin-top: 30px;
|
margin-top: 20px; /* 调整间距 */
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #28a745;
|
color: #28a745;
|
||||||
@ -157,23 +164,27 @@
|
|||||||
<!-- 动态添加的类别块会在这里显示 -->
|
<!-- 动态添加的类别块会在这里显示 -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="controls" style="margin-top: 30px;">
|
<!-- ===== 修改部分开始 ===== -->
|
||||||
|
<div id="controls" style="margin-top: 30px; border-top: 2px solid #ddd; padding-top: 20px;">
|
||||||
<button id="trainModelBtn" disabled>🚀 3. 训练模型</button>
|
<button id="trainModelBtn" disabled>🚀 3. 训练模型</button>
|
||||||
<button id="startPredictingBtn" disabled>👂 4. 开始识别</button>
|
<button id="startPredictingBtn" disabled>👂 4. 开始识别</button>
|
||||||
<button id="stopPredictingBtn" disabled>⏸️ 停止识别</button>
|
<button id="stopPredictingBtn" disabled>⏸️ 停止识别</button>
|
||||||
|
<br>
|
||||||
|
<button id="exportModelBtn" disabled>💾 导出数据</button>
|
||||||
|
<button id="importModelBtn" disabled>📂 导入数据</button>
|
||||||
|
<!-- 隐藏的文件输入框,用于导入 -->
|
||||||
|
<input type="file" id="importFileInput" accept=".bin" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- ===== 修改部分结束 ===== -->
|
||||||
|
|
||||||
<h2>🧠 识别结果</h2>
|
<h2>🧠 识别结果</h2>
|
||||||
<div id="predictionResult">
|
<div id="predictionResult">
|
||||||
等待模型训练完成并开始识别...
|
等待模型训练完成并开始识别...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/speech-commands@latest/dist/speech-commands.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/speech-commands@latest/dist/speech-commands.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<!-- 你的 JavaScript 代码 -->
|
<!-- 你的 JavaScript 代码 -->
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user