- Overlapped Sliding Window
重叠滑动窗口 - Audio Chunking
音频分片 - VAD (Voice Activity Detection)
语音活动检测 - Linguistic Prosody Perception
语言韵律感知 - Text Deduplication & Merging
文本去重与合并 - Silence Detection
静音检测 - Humanized Rhythm Control
人性化节奏控制 -
// ====================== 可配置变量(你要的全部在这里) ======================const CHUNK_TIME = 0.8; // 每0.8秒上传一次分片const FINAL_WINDOW = 2.3; // 2.5秒窗口合并最终结果const SILENCE_TIMEOUT = 1.5; // 静音1.5秒自动收尾const OVERLAP_SEC = 0.3; // 音频重叠const SAMPLE_RATE = 16000; // 采样率 11776const HTTP_UPLOAD_URL = "http://127.0.0.1:20369/asr";const fais_asr_timemaxlen = 2.3; // 2.5秒窗口合并最终结果// =========================================================================const CHUNK = Math.floor(SAMPLE_RATE * CHUNK_TIME); // 0.8s = 12800const FINAL = Math.floor(7000 * FINAL_WINDOW); // 2.5s = 40000let isRecording = false;let audioContext, mediaStream, workletNode;let audioBuffer = [];let textBuffer = [];let finalText = "";let silenceTimer = null;const startRecord = document.getElementById("startRecord");const stopRecord = document.getElementById("stopRecord");const streamStatus = document.getElementById("streamStatus");const tempResult = document.getElementById("tempResult");const finalResult = document.getElementById("finalResult");const audioFile = document.getElementById("audioFile");const uploadBtn = document.getElementById("uploadBtn");const uploadStatus = document.getElementById("uploadStatus");const fileResult = document.getElementById("fileResult");const createAudioWorkletModule = () => {const code = `class AudioCaptureProcessor extends AudioWorkletProcessor {process(inputs) { const i=inputs[0];i.length&&this.port.postMessage(i[0]);return true; }}registerProcessor('audio-capture-processor', AudioCaptureProcessor);`;return URL.createObjectURL(new Blob([code], { type: 'application/javascript' }));};const float32ToInt16 = (float32Array) => {const int16Array = new Int16Array(float32Array.length);for (let i=0; i<float32Array.length; i++) {const v = Math.max(-1, Math.min(1, float32Array[i]));int16Array[i] = v < 0 ? v * 0x8000 : v * 0x7FFF;}return int16Array;};// 去重重叠文字function deduplicateOverlap(prev, curr) {if (!prev || !curr) return curr;const minLen = Math.min(prev.length, curr.length);for (let i = minLen; i >= 1; i--) {if (prev.slice(-i) === curr.slice(0, i)) {return curr.slice(i);}}return curr;}async function sendChunk(floatData) {const pcm = float32ToInt16(floatData);const blob = new Blob([pcm.buffer], { type: "audio/pcm" });const fd = new FormData();fd.append("audio", blob, "chunk.pcm");$cq.ajax({url: HTTP_UPLOAD_URL,type: "POST",dataType: "JSON",data: fd,success: (response) => {if (!isRecording) return;let text = response.message || "";const clean = deduplicateOverlap(textBuffer[textBuffer.length-1] || "", text);textBuffer.push(clean);// 实时显示(保留)tempResult.textContent = "【实时识别】\n" + textBuffer.join('');console.log("rt textBuffer",textBuffer);tempResult.textContent = "【实时识别】\n" + textBuffer.join('');console.log("diff audioBuffer.length"+audioBuffer.length+","+SAMPLE_RATE * FINAL_WINDOW);console.log("diff floatData.length"+floatData.length+","+7000 * FINAL_WINDOW);// 新的(正确:看发送的音频长度是否满 2.5s)const isFullWindow = (floatData.length >= FINAL);console.log("是否满2.5s窗口:", isFullWindow, floatData.length,"FINAL="+FINAL);// ====================== 【修复点1】判断当前发送音频是否满2.5秒 ======================// const currentAudioSec = floatData.length / SAMPLE_RATE;const currentAudioSec = floatData.length / 7000;if (currentAudioSec >= FINAL_WINDOW) {finalText += " " + clean;finalResult.textContent = "【最终定稿】\n" + finalText;textBuffer = [];}resetSilenceTimer();},error: (e) => console.error("上传失败", e)});}function resetSilenceTimer() {clearTimeout(silenceTimer);silenceTimer = setTimeout(() => {if (textBuffer.length > 0) {finalText += " " + textBuffer.join('');finalResult.textContent = "【最终定稿】\n" + finalText;textBuffer = [];}}, SILENCE_TIMEOUT * 1000);}// 开始录音startRecord.onclick = async () => {if (isRecording) return;isRecording = true;startRecord.disabled = true;stopRecord.disabled = false;streamStatus.textContent = "🎙️ 录音中(0.8秒分片)";audioBuffer = [];textBuffer = [];mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });await audioContext.audioWorklet.addModule(createAudioWorkletModule());workletNode = new AudioWorkletNode(audioContext, "audio-capture-processor");const source = audioContext.createMediaStreamSource(mediaStream);source.connect(workletNode);const chunkSamples = Math.floor(SAMPLE_RATE * CHUNK_TIME);const overlapSamples = Math.floor(SAMPLE_RATE * OVERLAP_SEC);const finalSamples = Math.floor(SAMPLE_RATE * FINAL_WINDOW);// ====================== 【修复点2】核心滑动窗口:累积音频,不被切碎 ======================/*workletNode.port.onmessage = (e) => {if (!isRecording) return;audioBuffer.push(...e.data);resetSilenceTimer();// 只保留最近 2.5 秒,防止内存爆炸if (audioBuffer.length > finalSamples) {audioBuffer = audioBuffer.slice(-finalSamples);}// 每 0.8s 发送一次【全部累积音频】while (audioBuffer.length >= chunkSamples) {sendChunk(audioBuffer); // 发送全部累积 = 越长识别越准audioBuffer = audioBuffer.slice(chunkSamples); // 滑动0.8s}};*//*workletNode.port.onmessage = (e) => {if (!isRecording) return;// 1. 持续累积音频(永不主动清空,只限制最大长度)audioBuffer.push(...e.data);resetSilenceTimer();const chunkSamples = Math.floor(SAMPLE_RATE * CHUNK_TIME); // 滑步:0.8sconst finalSamples = Math.floor(SAMPLE_RATE * FINAL_WINDOW); // 窗口:2.5s// 2. 限制最大长度 = 2.5s,避免无限增长if (audioBuffer.length > finalSamples) {audioBuffer = audioBuffer.slice(-finalSamples);}// 3. 每满 0.8s → 发送【完整2.5s窗口音频】→ 只滑0.8swhile (audioBuffer.length >= chunkSamples) {// ✅ 核心:每次发送 完整2.5s 音频(纯音频滑动窗口)const sendAudio = audioBuffer.slice(-finalSamples); // 取最后2.5ssendChunk(sendAudio);// ✅ 只滑动 0.8s(切掉头部 oldest 0.8s)audioBuffer = audioBuffer.slice(chunkSamples);}};*//*workletNode.port.onmessage = (e) => {if (!isRecording) return;// 1. 持续累积新音频audioBuffer.push(...e.data);resetSilenceTimer();const CHUNK内部 = Math.floor(SAMPLE_RATE * CHUNK_TIME); // 0.8s = 12800const FINAL内部 = Math.floor(7000 * FINAL_WINDOW); // 2.5s = 40000// ==============================================// ✅ 【真正的滑动窗口】// ==============================================while (audioBuffer.length >= CHUNK内部) {// 每次发送:取【最近 2.5s】音频let sendAudio = audioBuffer.slice(-FINAL内部);sendChunk(sendAudio);// 只滑动:移除【最早 0.8s】audioBuffer = audioBuffer.slice(CHUNK内部);}};*/workletNode.port.onmessage = (e) => {if (!isRecording) return;// 1. 不断追加新音频audioBuffer.push(...e.data);resetSilenceTimer();// ======================// 你自己的配置// ======================const chunkStep = Math.floor(SAMPLE_RATE * CHUNK_TIME); // 0.8s 触发一次const maxKeepLen = Math.floor(SAMPLE_RATE * fais_asr_timemaxlen); // 最大保留 2.3s// 2. 超过2.3s,只保留【最新2.3s】,前面的丢掉if (audioBuffer.length > maxKeepLen) {audioBuffer = audioBuffer.slice(-maxKeepLen); // 只截掉老的,保留最新一段完整}// 3. 每够0.8s,发送【当前完整缓冲区(≈2.3s)】if (audioBuffer.length >= chunkStep) {sendChunk(audioBuffer); // 发送的是一段完整长音频,不是只发0.8s}};resetSilenceTimer();};// 停止录音stopRecord.onclick = () => {isRecording = false;clearTimeout(silenceTimer);if (mediaStream) mediaStream.getTracks().forEach(t => t.stop());if (audioContext) audioContext.close();if (workletNode) workletNode.port.close();audioBuffer = [];startRecord.disabled = false;stopRecord.disabled = true;streamStatus.textContent = "✅ 已停止";};// 文件上传audioFile.onchange = () => {if (audioFile.files.length) {uploadStatus.textContent = "已选择:" + audioFile.files[0].name;}};uploadBtn.onclick = async () => {const file = audioFile.files[0];if (!file) {uploadStatus.textContent = "❌ 请选择文件";return;}uploadStatus.textContent = "⏳ 上传中...";const fd = new FormData();fd.append("audio", file);$cq.ajax({url: HTTP_UPLOAD_URL,type: "POST",dataType: "JSON",data: fd,success: (response) => {let text = response.message || response.text || "";fileResult.textContent = "文件识别结果:\n" + text;uploadStatus.textContent = "✅ 识别完成";},error: () => {uploadStatus.textContent = "❌ 上传失败";}});};
原创创意共创、永久收益分成,是东方仙盟始终坚守的核心理念。我们坚信,每一份原创智慧都值得被尊重与回馈,以永久分成锚定共创初心,让创意者长期享有价值红利,携手万千伙伴向着科技星辰大海笃定前行,拥抱硅基 生命与数字智能交融的未来,共筑跨越时代的数字文明共同体。
-
东方仙盟
东方仙盟:拥抱知识开源,共筑数字新生态
在全球化与数字化浪潮中,东方仙盟始终秉持开放协作、知识共享的理念,积极拥抱开源技术与开放标准。我们相信,唯有打破技术壁垒、汇聚全球智慧,才能真正推动行业的可持续发展。
开源赋能中小商户:通过将前端异常检测、跨系统数据互联等核心能力开源化,东方仙盟为全球中小商户提供了低成本、高可靠的技术解决方案,让更多商家能够平等享受数字转型的红利。
共建行业标准:我们积极参与国际技术社区,与全球开发者、合作伙伴共同制定开放协议与技术规范,推动跨境零售、文旅、餐饮等多业态的系统互联互通,构建更加公平、高效的数字生态。
知识普惠,共促发展:通过开源社区、技术文档与培训体系,东方仙盟致力于将前沿技术转化为可落地的行业实践,赋能全球合作伙伴,共同培育创新人才,推动数字经济 的普惠式增长阿雪技术观
在科技发展浪潮中,我们不妨积极投身技术共享。不满足于做受益者,更要主动担当贡献者。无论是分享代码、撰写技术博客,还是参与开源项目维护改进,每一个微小举动都可能蕴含推动技术进步的巨大能量。东方仙盟是汇聚力量的天地,我们携手在此探索硅基生命,为科技进步添砖加瓦。
Hey folks, in this wild tech - driven world, why not dive headfirst into the whole tech - sharing scene? Don't just be the one reaping all the benefits; step up and be a contributor too. Whether you're tossing out your code snippets, hammering out some tech blogs, or getting your hands dirty with maintaining and sprucing up open - source projects, every little thing you do might just end up being a massive force that pushes tech forward. And guess what? The Eastern FairyAlliance is this awesome place where we all come together. We're gonna team up and explore the whole silicon - based life thing, and in the process, we'll be fueling the growth of technology.
开通方法
关注我们
营销资讯:抖音运营,微信公众号运营,小红书运营
网站运营:安全、备案、申请网站,漏洞扫描,数字证书
开发:开发技巧、热点技术,人工智能,数据分析,数据优化,docker
数据服务:数据恢复,数据安全,数据融合,数据异地容灾,数据优化,数据自动备份、数据清洗
人工智能:智能物联网,智慧大屏幕,OCR,智慧刷脸,语音交互,智能机器人,数字生命,数字人,大模型,本地化,边缘化智能(手机模型)
支付:微信支付服务商,支付支付服务商,刷脸支付
安全服务:WAF安全,网安扫描,漏洞扫描,安全补丁,防火墙定制
智慧大屏:物资耗材大屏幕,单位用餐大数据,智慧场馆大屏,销售大屏幕,智慧社区大屏幕,大厅查询机,景区自助机
智慧酒店:酒店系统、酒店押金、酒店房价牌、酒店门锁、布草系统
行业软件:酒店、餐饮、便利店,美发、超市,批发,景区门票,道闸,堂食,配送系统,烘培系统,健身,美容系统,月子中心系统
物联网:智能衣柜,足浴店衣柜,售货柜,酒店自助入住机
国产化:uos系统答疑,国产软件开发,国产服务器配置,docker
一体化:酒店一体化(闸机,酒店系统,餐饮系统,售票,布草,无人酒店,在线订房),景区一体化(门票、餐饮、住宿、押金、超市、药店、商铺租赁,通车系统,售票大厅,大屏幕,无人景区,景区自助机)

