309 lines
12 KiB
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>
|