stram代码

完整代码

<!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 testa</h2>

        <!-- HTTP Stream 实时录音识别(AudioWorklet实现) -->
        <div class="panel">
            <h3>\u1F399\uFE0F 实时麦克风录音(HTTP Stream)</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>

        <!-- HTTP 文件上传(独立使用) -->
        <div class="panel">
            <h3>\u1F4E2 音频文件上传(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 STREAM_URL = "http://127.0.0.1:20369/stream";
        const HTTP_UPLOAD_URL = "http://127.0.0.1:20369/asr";
        // ====================================================

        // 全局状态
        let isRecording = false;
        let audioContext = null;
        let mediaStream = null;
        let workletNode = null;
        let streamReader = null;
        let streamController = null;

        // 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处理器(无需单独js文件)
        const createAudioWorkletModule = () => {
            const processorCode = `
                class AudioCaptureProcessor extends AudioWorkletProcessor {
                    constructor() {
                        super();
                        // 监听主线程消息,无额外逻辑仅做采集
                    }
                    process(inputs, outputs, parameters) {
                        const input = inputs[0];
                        if (input.length === 0) return true;
                        const channelData = input[0];
                        // 将Float32音频数据发送到主线程
                        this.port.postMessage(channelData);
                        return true;
                    }
                }
                registerProcessor('audio-capture-processor', AudioCaptureProcessor);
            `;
            // 转成Blob URL,模拟模块加载
            const blob = new Blob([processorCode], { type: 'application/javascript' });
            return URL.createObjectURL(blob);
        };

        // Float32转Int16 PCM(适配C++服务的extract_pcm)
        const float32ToInt16 = (float32Array) => {
            const int16Array = new Int16Array(float32Array.length);
            for (let i = 0; i < float32Array.length; i++) {
                const val = Math.max(-1, Math.min(1, float32Array[i]));
                int16Array[i] = val < 0 ? val * 0x8000 : val * 0x7FFF;
            }
            return int16Array;
        };

        // 第二步:开始录音-核心逻辑(AudioWorklet+HTTP Stream)
        startRecord.onclick = async () => {
            if (isRecording) return;
            isRecording = true;
            startRecord.disabled = true;
            stopRecord.disabled = false;

            try {
                // 1. 获取麦克风媒体流(16000采样率,单声道)
                mediaStream = await navigator.mediaDevices.getUserMedia({
                    audio: {
                        sampleRate: 16000,
                        channelCount: 1,
                        echoCancellation: false,
                        noiseSuppression: false
                    }
                });
                streamStatus.textContent = "\u23F1\uFE0F 录音中...建立流连接";

                // 2. 初始化音频上下文(强制16000采样率)
                audioContext = new AudioContext({ sampleRate: 16000 });
                // 加载内联的AudioWorklet模块
                await audioContext.audioWorklet.addModule(createAudioWorkletModule());

                // 3. 创建AudioWorkletNode,连接麦克风流
                workletNode = new AudioWorkletNode(audioContext, 'audio-capture-processor');
                const source = audioContext.createMediaStreamSource(mediaStream);
                source.connect(workletNode);
                workletNode.connect(audioContext.destination);

                // 4. 建立HTTP Stream长连接(POST /stream)
                const fetchResponse = await fetch(STREAM_URL, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/octet-stream",
                        "Connection": "keep-alive",
                        "Cache-Control": "no-cache"
                    },
                    body: new ReadableStream({
                        start(controller) {
                            streamController = controller;
                            // 监听AudioWorklet的音频数据,推送到C++服务
                            workletNode.port.onmessage = (e) => {
                                if (!isRecording) return;
                                const int16Data = float32ToInt16(e.data);
                                streamController.enqueue(int16Data);
                            };
                        },
                        cancel() {
                            streamController = null;
                        }
                    })
                });

                // 5. 实时读取C++服务的识别结果
                if (!fetchResponse.ok) throw new Error(`HTTP错误:${fetchResponse.status}`);
                streamStatus.textContent = "\u23F1\uFE0F 录音中...识别中";
                streamReader = fetchResponse.body.getReader();
                const decoder = new TextDecoder();
                // 循环读取结果
                const readRecognizeResult = async () => {
                    while (isRecording) {
                        const { done, value } = await streamReader.read();
                        if (done) break;
                        const result = decoder.decode(value, { stream: true });
                        realResult.textContent = "实时识别结果:\n" + result;
                    }
                };
                readRecognizeResult();

            } catch (err) {
                // 精准判断错误类型,不再统一提示
                isRecording = false;
                startRecord.disabled = false;
                stopRecord.disabled = true;
                if (err.name === 'NotAllowedError') {
                    streamStatus.textContent = "\u274C 麦克风权限被拒绝,请在浏览器设置中开启";
                } else if (err.message.includes('HTTP')) {
                    streamStatus.textContent = `\u274C 连接C++服务失败:${err.message}`;
                } else {
                    streamStatus.textContent = `\u274C 初始化失败:${err.message}`;
                }
                console.error("录音启动失败:", err);
            }
        };

        // 第三步:停止录音-释放所有资源
        stopRecord.onclick = () => {
            if (!isRecording) return;
            isRecording = false;

            // 1. 停止麦克风流
            if (mediaStream) {
                mediaStream.getTracks().forEach(track => track.stop());
                mediaStream = null;
            }
            // 2. 关闭音频上下文和Worklet
            if (audioContext) audioContext.close();
            if (workletNode) {
                workletNode.port.close();
                workletNode.disconnect();
                workletNode = null;
            }
            // 3. 关闭流读取和推送
            if (streamReader) streamReader.cancel();
            if (streamController) streamController.close();
            // 4. 重置UI和状态
            startRecord.disabled = false;
            stopRecord.disabled = true;
            streamStatus.textContent = "\u2705 录音已停止";
        };

        // ========== 音频文件上传(原逻辑保留,适配/asr) ==========
        audioFile.onchange = () => {
            if (audioFile.files.length > 0) {
                uploadStatus.textContent = "已选择:" + audioFile.files[0].name;
            }
        };

        uploadBtn.onclick = async () => {
            const file = audioFile.files[0];
            if (!file) {
                uploadStatus.textContent = "\u274C 请先选择音频文件";
                return;
            }

            uploadStatus.textContent = "\u23F3 上传识别中...";
            fileResult.textContent = "文件识别结果:处理中...";

            try {
                const formData = new FormData();
                formData.append("audio", file);

                const res = await fetch(HTTP_UPLOAD_URL, {
                    method: "POST",
                    body: formData
                });

                const text = await res.text();
                fileResult.textContent = "文件识别结果:\n" + text;
                uploadStatus.textContent = "\u2705 识别完成";
            } catch (e) {
                uploadStatus.textContent = "\u274C 上传失败,请检查C++服务是否启动";
                fileResult.textContent = "文件识别结果:上传失败";
                console.error("文件上传失败:", e);
            }
        };
    </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 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐