mobileNet/音频分类/voice.html

309 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音频分类器 (背景噪音分离版)</title>
<style> /* 你的 CSS 样式保持不变 */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f4f7f6;
color: #333;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
.category-block {
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
width: 280px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.category-block h3 {
margin-top: 0;
color: #007bff;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 15px;
}
/* 特殊样式给背景噪音 */
#backgroundNoiseBlock h3 {
color: #dc3545; /* 红色 */
}
#backgroundNoiseBlock button {
background-color: #dc3545; /* 红色 */
}
#backgroundNoiseBlock button:hover:not(:disabled) {
background-color: #c82333;
}
.category-block button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.2s ease;
margin-top: 10px;
}
.category-block button:hover:not(:disabled) {
background-color: #0056b3;
}
.category-block button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#controls button {
background-color: #28a745;
color: white;
border: none;
padding: 12px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 1.1em;
margin-right: 15px;
margin-bottom: 10px; /* 增加下边距以适应换行 */
transition: background-color 0.2s ease;
}
#controls button:hover:not(:disabled) {
background-color: #218838;
}
#controls button:disabled {
background-color: #cccccc;
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 {
margin-top: 15px;
font-size: 1.1em;
color: #616161;
}
#predictionResult {
margin-top: 20px; /* 调整间距 */
font-size: 1.8em;
font-weight: bold;
color: #28a745;
padding: 15px;
border: 2px dashed #28a745;
background-color: #e6ffed;
border-radius: 8px;
}
.add-category-section {
background-color: #e9f5ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.add-category-section input[type="text"] {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
width: 250px;
margin-right: 10px;
}
.add-category-section button {
background-color: #17a2b8;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
}
.add-category-section button:hover {
background-color: #138496;
}
.sample-count {
font-size: 0.9em;
color: #6a6a6a;
margin-top: 5px;
}
</style>
</head>
<body>
<h1>浏览器音频分类器 (背景噪音分离增强版)</h1>
<p>这个工具通过分离背景噪音和目标声音的录制,来提高分类准确性。</p>
<div id="status">正在初始化模型和音频设备... 请稍候。</div>
<h2>🤫 1. 录制背景噪音</h2>
<div id="backgroundNoiseBlock" class="category-block">
<h3>背景噪音 (Background Noise)</h3>
<p>样本数量: <span id="backgroundNoiseSampleCount">0</span></p>
<button id="recordBackgroundNoiseBtn">录制样本</button>
<p style="font-size: 0.85em; color: #6a6a6a; margin-top: 10px;">
请录制您所处环境的<b>无特定声音</b>的噪音,帮助模型区分目标声音与环境杂音。建议多录制一些。
</p>
</div>
<h2>🗣️ 2. 录制您要分类的声音</h2>
<div class="add-category-section">
<h3>🎉 添加新类别</h3>
<input type="text" id="newCategoryName" placeholder="输入类别名称 (例如: 拍手, 响指, 警告音)">
<button id="addCategoryBtn">添加类别</button>
</div>
<div id="categoryContainer" class="container">
<!-- 动态添加的类别块会在这里显示 -->
</div>
<!-- ===== 修改部分开始 ===== -->
<div id="controls" style="margin-top: 30px; border-top: 2px solid #ddd; padding-top: 20px;">
<button id="trainModelBtn" disabled>🚀 3. 训练模型</button>
<button id="startPredictingBtn" disabled>👂 4. 开始识别</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>
<!-- ===== 修改部分结束 ===== -->
<h2>🧠 识别结果</h2>
<div id="predictionResult">
等待模型训练完成并开始识别...
</div>
<!-- !!!!!! 核心劫持代码:确保在任何 TF.js 库之前加载 !!!!!! -->
<!-- 如果你有其他模型的劫持代码,请确保它们都先于 TF.js 库 -->
<script>
(function() {
// 定义你的镜像服务器的公共前缀,用于存放 Speech Commands 模型文件
// 根据你提供的最新路径进行更新。
const SPEECH_COMMANDS_MIRROR_BASE_URL = 'https://goood-space-assets.oss-cn-beijing.aliyuncs.com/public/fetch/speech-commands/';
// 定义需要被劫持的原始 URL 的域名模式
// 明确指出是 tfjs-models 下的 speech-commands 模型
const INTERCEPT_DOMAINS = [
// 'https://storage.googleapis.com/tfjs-models/tfjs/speech-commands/',
'https://storage.googleapis.com/tfjs-models/tfjs/speech-commands/v0.5/browser_fft/18w/',
];
// 备份原始的 fetch 函数
const originalFetch = window.fetch;
window.fetch = function(input, init) {
let url = input;
if (input instanceof Request) {
url = input.url;
}
let newUrl = url;
let isIntercepted = false;
// 检查 URL 是否以我们关注的域名开头
for (const domain of INTERCEPT_DOMAINS) {
if (url.startsWith(domain)) {
// 尝试从 URL 中提取文件名 (不包含查询参数)
// 匹配 metadata.json, model.json, group1-shardXofY (注意这里没有.bin后缀)
const fileNameMatch = url.match(/(metadata\.json|model\.json|group1-shard\dof2)/);
if (fileNameMatch) {
const fileName = fileNameMatch[0]; // 获取匹配到的文件名
newUrl = SPEECH_COMMANDS_MIRROR_BASE_URL + fileName; // 拼接新的镜像 URL
isIntercepted = true;
break; // 找到匹配的域名和文件,停止循环
}
}
}
if (isIntercepted) {
console.warn(`[TFJS Fetch Intercepted] Speech Commands - Original: ${url}`);
console.warn(`[TFJS Fetch Intercepted] Speech Commands - Redirecting to: ${newUrl}`);
if (input instanceof Request) {
try {
input = new Request(newUrl, {
method: input.method,
headers: input.headers,
body: input.body,
referrer: input.referrer,
referrerPolicy: input.referrerPolicy,
mode: 'cors',
credentials: input.credentials,
cache: 'default',
redirect: 'follow',
integrity: undefined, // 移除 integrity 属性以避免校验失败
signal: input.signal,
});
} catch (e) {
console.error(`[TFJS Fetch Intercepted Error] Speech Commands - Failed to create new Request object: ${e.message}. Falling back to URL string.`, input);
input = newUrl;
}
} else {
input = newUrl;
}
}
return originalFetch(input, init).catch(error => {
console.error(`[TFJS Fetch Intercepted Error] Speech Commands - Failed to load ${url} (redirected to ${newUrl || url || input}):`, error);
throw error;
});
};
// -------------------- 劫持 XMLHttpRequest API (备用安全网) --------------------
const originalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
xhr.open = function(method, url, async = true, user = null, password = null) {
let newUrl = url;
let isIntercepted = false;
for (const domain of INTERCEPT_DOMAINS) {
if (url.startsWith(domain)) {
const fileNameMatch = url.match(/(metadata\.json|model\.json|group1-shard\dof\d)/);
if (fileNameMatch) {
const fileName = fileNameMatch[0];
newUrl = SPEECH_COMMANDS_MIRROR_BASE_URL + fileName;
isIntercepted = true;
break;
}
}
}
if (isIntercepted) {
console.warn(`[TFJS XHR Intercepted] Speech Commands - Original: ${url}`);
console.warn(`[TFJS XHR Intercepted] Speech Commands - Redirecting to: ${newUrl}`);
url = newUrl;
}
return originalOpen.apply(this, arguments);
};
for (const key in originalXHR) {
if (typeof originalXHR[key] !== 'function' && originalXHR.hasOwnProperty(key)) {
window.XMLHttpRequest[key] = originalXHR[key];
}
}
return xhr;
};
})();
</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>
<!-- 你的 JavaScript 代码 -->
<script src="script.js"></script>
</body>
</html>