核心代码

完整代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>未来之窗-SenseVoice-CPP 8 语音client testa</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: "Microsoft YaHei", sans-serif;
        }
        body {
            max-width: 800px;
            margin: 30px auto;
            padding: 0 20px;
            background: #f5f7fa;
        }
        .container {
            background: white;
            padding: 25px;
            border-radius: 12px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h2 {
            text-align: center;
            color: #333;
            margin-bottom: 25px;
        }
        .panel {
            margin: 20px 0;
            padding: 15px;
            border: 1px solid #eee;
            border-radius: 8px;
        }
        .panel h3 {
            color: #409eff;
            margin-bottom: 12px;
            font-size: 16px;
        }
        .btn {
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            margin-right: 10px;
            background: #409eff;
            color: white;
        }
        .btn:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        .red { background: #f56c6c; }
        .green { background: #67c23a; }
        .status {
            margin: 10px 0;
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 13px;
            background: #f0f2f5;
        }
        .result-box {
            margin-top: 15px;
            padding: 15px;
            min-height: 80px;
            border: 1px dashed #999;
            border-radius: 6px;
            white-space: pre-wrap;
            color: #333;
        }
        input[type="file"] {
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>未来之窗-SenseVoice-CPP 8 分片技术 语音client </h2>

        <div class="panel">
            <h3> 实时麦克风录音(HTTP短连接 0.7s/次)</h3>
            <button class="btn green" id="startRecord">开始录音</button>
            <button class="btn red" id="stopRecord" disabled>停止录音</button>
            <div class="status" id="streamStatus">等待录音</div>
            <div class="result-box" id="realResult">实时识别结果:</div>
        </div>

        <div class="panel">
            <h3>音频文件上传(HTTP POST)</h3>
            <input type="file" id="audioFile" accept="audio/*">
            <button class="btn" id="uploadBtn">上传并识别</button>
            <div class="status" id="uploadStatus">等待选择文件</div>
            <div class="result-box" id="fileResult">文件识别结果:</div>
        </div>
    </div>

    <script>
        // ====================== 可配置变量(你要的时长都在这里)======================
        const UPLOAD_INTERVAL_SEC = 0.7;     // 每0.7秒发送一次
        const OVERLAP_SEC = 0.3;            // 保留上一段0.3秒
        const TOTAL_CHUNK_SEC = 1.0;        // 每次发送总时长1秒
        const SAMPLE_RATE = 16000;         // 采样率
        const CHANNEL_COUNT = 1;           // 单声道
        const SHORT_URL = "http://127.0.0.1:20369/asr"; // 短连接接口
        const HTTP_UPLOAD_URL = "http://127.0.0.1:20369/asr";
        // =========================================================================

        let isRecording = false;
        let audioContext = null;
        let mediaStream = null;
        let workletNode = null;
        let sendTimer = null;

        // 音频缓存
        let audioBuffer = [];
        const overlapSampleCount = Math.floor(SAMPLE_RATE * OVERLAP_SEC);
        const chunkSampleCount = Math.floor(SAMPLE_RATE * TOTAL_CHUNK_SEC);

        // DOM
        const startRecord = document.getElementById("startRecord");
        const stopRecord = document.getElementById("stopRecord");
        const streamStatus = document.getElementById("streamStatus");
        const realResult = document.getElementById("realResult");
        const audioFile = document.getElementById("audioFile");
        const uploadBtn = document.getElementById("uploadBtn");
        const uploadStatus = document.getElementById("uploadStatus");
        const fileResult = document.getElementById("fileResult");

        // 动态创建AudioWorklet
        const createAudioWorkletModule = () => {
            const processorCode = `
                class AudioCaptureProcessor extends AudioWorkletProcessor {
                    process(inputs) {
                        const input = inputs[0];
                        if (input.length > 0) this.port.postMessage(input[0]);
                        return true;
                    }
                }
                registerProcessor('audio-capture-processor', AudioCaptureProcessor);
            `;
            return URL.createObjectURL(new Blob([processorCode], { type: 'application/javascript' }));
        };

        // 浮点数转16位PCM
        const float32ToInt16 = (float32Array) => {
            const int16 = new Int16Array(float32Array.length);
            for (let i = 0; i < float32Array.length; i++) {
                const v = Math.max(-1, Math.min(1, float32Array[i]));
                int16[i] = v < 0 ? v * 0x8000 : v * 0x7FFF;
            }
            return int16;
        };

        // 发送音频片段(短连接POST)
        const sendAudioChunk = async (pcmData) => {
            try {
                const res = await fetch(SHORT_URL, {
                    method: "POST",
                    headers: { "Content-Type": "application/octet-stream" },
                    body: pcmData.buffer
                });
                const text = await res.text();
                realResult.textContent = `实时识别结果:\n` + text;
            } catch (e) {
                console.error("发送失败", e);
            }
        };

        // 开始录音
        startRecord.onclick = async () => {
            if (isRecording) return;
            isRecording = true;
            startRecord.disabled = true;
            stopRecord.disabled = false;
            streamStatus.textContent = " 录音中(0.7s发送一次)";
            audioBuffer = [];

            try {
                mediaStream = await navigator.mediaDevices.getUserMedia({
                    audio: { sampleRate: SAMPLE_RATE, channelCount: CHANNEL_COUNT }
                });

                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);

                // 收集音频
                workletNode.port.onmessage = (e) => {
                    if (!isRecording) return;
                    audioBuffer.push(...e.data);
                };

                // 定时器:每0.7秒发一次
                sendTimer = setInterval(() => {
                    if (audioBuffer.length >= chunkSampleCount) {
                        // 取最后1秒数据
                        const chunk = audioBuffer.slice(-chunkSampleCount);
                        const pcm = float32ToInt16(new Float32Array(chunk));
                        sendAudioChunk(pcm);

                        // 保留最后0.3秒用于下一次重叠
                        audioBuffer = audioBuffer.slice(-overlapSampleCount);
                    }
                }, UPLOAD_INTERVAL_SEC * 1000);

            } catch (err) {
                console.error("录音失败", err);
                streamStatus.textContent = "❌ 启动失败:" + err.message;
                isRecording = false;
                startRecord.disabled = false;
                stopRecord.disabled = true;
            }
        };

        // 停止录音
        stopRecord.onclick = () => {
            if (!isRecording) return;
            isRecording = false;

            clearInterval(sendTimer);
            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);

            try {
                const res = await fetch(HTTP_UPLOAD_URL, { method: "POST", body: fd });
                const text = await res.text();
                fileResult.textContent = "文件识别结果:\n" + text;
                uploadStatus.textContent = "✅ 识别完成";
            } catch (e) {
                uploadStatus.textContent = "❌ 上传失败";
            }
        };
    </script>
</body>
</html>

人人皆为创造者,共创方能共成长


每个人都是使用者,也是创造者;是数字世界的消费者,更是价值的生产者与分享者。在智能时代的浪潮里,单打独斗的发展模式早已落幕,唯有开放连接、创意共创、利益共享,才能让个体价值汇聚成生态合力,让技术与创意双向奔赴,实现平台与伙伴的快速成长、共赢致远。

原创永久分成,共赴星辰大海

原创创意共创、永久收益分成,是东方仙盟始终坚守的核心理念。我们坚信,每一份原创智慧都值得被尊重与回馈,以永久分成锚定共创初心,让创意者长期享有价值红利,携手万千伙伴向着科技星辰大海笃定前行,拥抱硅基  生命与数字智能交融的未来,共筑跨越时代的数字文明共同体。

东方仙盟:拥抱知识开源,共筑数字新生态

在全球化与数字化浪潮中,东方仙盟始终秉持开放协作、知识共享的理念,积极拥抱开源技术与开放标准。我们相信,唯有打破技术壁垒、汇聚全球智慧,才能真正推动行业的可持续发展。

开源赋能中小商户:通过将前端异常检测、跨系统数据互联等核心能力开源化,东方仙盟为全球中小商户提供了低成本、高可靠的技术解决方案,让更多商家能够平等享受数字转型的红利。
共建行业标准:我们积极参与国际技术社区,与全球开发者、合作伙伴共同制定开放协议    与技术规范,推动跨境零售、文旅、餐饮等多业态的系统互联互通,构建更加公平、高效的数字生态。
知识普惠,共促发展:通过开源社区    、技术文档与培训体系,东方仙盟致力于将前沿技术转化为可落地的行业实践,赋能全球合作伙伴,共同培育创新人才,推动数字经济  的普惠式增长


阿雪技术观


在科技发展浪潮中,我们不妨积极投身技术共享。不满足于做受益者,更要主动担当贡献者。无论是分享代码、撰写技术博客,还是参与开源项目    维护改进,每一个微小举动都可能蕴含推动技术进步的巨大能量。东方仙盟是汇聚力量的天地,我们携手在此探索硅基 生命,为科技进步添砖加瓦。

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

更多推荐