7f25c8f6bb
- main.py:uv run main.py 一键启动——读取 backend/.env、TORCH_HOME/NLTK_DATA 指向项目内缓存、certifi SSL + NO_PROXY 网络直连、默认开启 s2s,启动横幅 - vendor/:补丁后 s2s 云化栈移入项目(backend/vendor/s2s-cloud),pyproject sources 改相对路径 - .gitignore:忽略 .torch-cache / nltk_data 缓存 - AGENTS.md:进程管理铁律(AI 不启动/重启服务,由用户操作)
76 lines
3.2 KiB
HTML
76 lines
3.2 KiB
HTML
<!doctype html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>麦克风诊断</title>
|
||
<style>
|
||
body { font-family: system-ui, -apple-system, "PingFang SC", sans-serif; max-width: 560px;
|
||
margin: 0 auto; padding: 16px; background: #14161a; color: #e6e8eb; }
|
||
h1 { font-size: 18px; }
|
||
button { padding: 10px 18px; border-radius: 8px; border: none; background: #1f6feb;
|
||
color: #fff; font-size: 14px; cursor: pointer; }
|
||
pre { background: #1c1f24; padding: 12px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
|
||
.ok { color: #3fb950; } .bad { color: #f85149; }
|
||
.meter { height: 18px; width: 100%; background: #1c1f24; border-radius: 4px; margin: 6px 0; overflow: hidden; }
|
||
.meter > div { height: 100%; width: 0%; background: #3fb950; transition: width 60ms linear; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🎤 麦克风诊断页(只请求麦克风)</h1>
|
||
<p>在 <b>你刚才打开主页面的那个浏览器窗口</b> 打开本页,点下面按钮。</p>
|
||
<button id="go">请求麦克风并测试</button>
|
||
<pre id="out">等待操作…</pre>
|
||
<div class="meter"><div id="bar"></div></div>
|
||
|
||
<script>
|
||
const out = document.getElementById("out");
|
||
const bar = document.getElementById("bar");
|
||
const log = (s, cls = "") => { out.innerHTML += (cls ? `<span class="${cls}">` : "") + s.replace(/</g, "<") + (cls ? "</span>" : "") + "\n"; };
|
||
|
||
document.getElementById("go").onclick = async () => {
|
||
out.innerHTML = "";
|
||
log("1) 环境检查…");
|
||
log(" secureContext = " + window.isSecureContext + " (必须是 true)");
|
||
log(" navigator.mediaDevices = " + (navigator.mediaDevices ? "存在" : "不存在"));
|
||
|
||
log("2) 枚举设备…");
|
||
try {
|
||
const devs = await navigator.mediaDevices.enumerateDevices();
|
||
for (const d of devs) log(" [" + d.kind + "] " + (d.label || "(无标签=权限未授予)"));
|
||
if (!devs.some(d => d.kind === "audioinput"))
|
||
log(" ⚠️ 系统里没发现任何麦克风设备(驱动/虚拟声卡问题)", "bad");
|
||
} catch (e) { log(" 枚举失败: " + e.name + ": " + e.message, "bad"); }
|
||
|
||
log("3) getUserMedia…");
|
||
let stream;
|
||
try {
|
||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||
log(" ✅ 麦克风授权成功", "ok");
|
||
} catch (e) {
|
||
log(" ❌ " + e.name + ": " + e.message, "bad");
|
||
log(" (NotAllowedError=权限被拒;NotFoundError=无设备;NotReadableError=被其他应用占用)");
|
||
return;
|
||
}
|
||
|
||
log("4) 电平检测(说话时绿条应跳动)…");
|
||
const ctx = new AudioContext();
|
||
const src = ctx.createMediaStreamSource(stream);
|
||
const an = ctx.createAnalyser(); an.fftSize = 256;
|
||
src.connect(an);
|
||
const buf = new Uint8Array(an.frequencyBinCount);
|
||
let peak = 0;
|
||
setInterval(() => {
|
||
an.getByteTimeDomainData(buf);
|
||
let sum = 0;
|
||
for (let i = 0; i < buf.length; i++) sum += Math.abs(buf[i] - 128);
|
||
const rms = sum / buf.length;
|
||
if (rms > peak) peak = rms;
|
||
bar.style.width = Math.min(100, (rms / 40) * 100) + "%";
|
||
}, 80);
|
||
setTimeout(() => log(" 请对着麦克风说话,看上面的绿条。当前峰值=" + peak.toFixed(1) + "/127(≥10 说明有声音进)"), 2000);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|