A real-time speech recognition optimization scheme based on overlapped sliding window and audio chunking technology.Combined with speech activity detection (VAD) and linguistic prosody perception principles, it uses adaptive silence detection and segment control to simulate natural human speaking rhythm.By adopting overlap chunk merging and text deduplication algorithm, it ensures low-latency response and continuous recognition output, achieving a humanized and smooth real-time interaction experience.

  • 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;   // 采样率 11776
    const 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 = 12800
    const FINAL = Math.floor(7000 * FINAL_WINDOW);   // 2.5s = 40000

    let 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.8s
            const 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.8s
            while (audioBuffer.length >= chunkSamples) {
                // ✅ 核心:每次发送 完整2.5s 音频(纯音频滑动窗口)
                const sendAudio = audioBuffer.slice(-finalSamples); // 取最后2.5s
                sendChunk(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 = 12800
            const 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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐