SenseVoice-small WebUI使用:Chrome扩展程序集成语音输入功能

你是不是也遇到过这样的场景?在电脑上写文档、做笔记,或者需要快速记录一些想法时,打字速度跟不上思维,手忙脚乱。或者,在观看外语视频、参加线上会议时,希望能实时看到字幕,但又不想安装复杂的软件。

今天,我要分享一个超级实用的技巧:如何将SenseVoice-small这个强大的语音识别工具,通过Chrome扩展程序的方式,变成你浏览器里的“语音输入助手”。这不仅仅是简单的语音转文字,而是让你在任何网页输入框里,都能用说话代替打字,还能实时生成视频字幕,彻底解放你的双手。

1. 为什么需要浏览器语音输入?

在介绍具体方法之前,我们先聊聊为什么这个功能如此重要。

想象一下这些日常场景:

  • 写邮件或文档:对着麦克风说,文字自动出现在编辑框里
  • 在线搜索:直接说出你想找的内容,不用再费力打字
  • 填写在线表单:口述信息,自动填充到各个字段
  • 看视频学习:实时生成字幕,特别是外语内容
  • 会议记录:自动转录线上会议内容

传统的解决方案要么需要安装庞大的软件,要么必须联网使用,隐私和安全都成问题。而SenseVoice-small的本地部署特性,加上Chrome扩展的便捷性,正好解决了这些痛点。

2. SenseVoice-small:你的本地语音识别引擎

SenseVoice-small是一个轻量级的多任务语音模型,我使用的是它的ONNX量化版WebUI V1.0。简单来说,它有以下几个核心优势:

2.1 完全本地运行,保护隐私

所有语音处理都在你的设备上完成,音频数据不会上传到任何服务器。这对于处理敏感信息(如医疗记录、财务讨论、内部会议)来说至关重要。

2.2 支持50多种语言

不仅仅是中文和英文,还支持日语、韩语、粤语、俄语、西班牙语等全球主流语言。更厉害的是,它能自动检测语言类型,你不需要手动切换。

2.3 轻量高效,资源占用少

经过ONNX量化和优化后,这个模型可以在没有独立GPU的普通电脑上流畅运行,甚至在一些性能较好的手机和平板上也能使用。

2.4 多功能集成

除了基本的语音转文字,还能识别说话人的情感状态(开心、悲伤、愤怒等),并进行智能文本转换(比如把“一百二十”自动转成“120”)。

3. 准备工作:部署SenseVoice-small WebUI

在开始制作Chrome扩展之前,你需要先让SenseVoice-small在本地运行起来。

3.1 基础环境要求

  • 操作系统:Windows 10/11,macOS 10.15+,或Linux(Ubuntu 20.04+推荐)
  • 内存:至少8GB RAM(16GB更佳)
  • 存储空间:2GB可用空间用于模型文件
  • Python:3.8-3.11版本

3.2 快速部署步骤

如果你已经按照官方文档部署好了WebUI服务,可以跳过这一步。如果还没有,这里是最简化的部署流程:

# 1. 克隆项目代码
git clone https://github.com/your-repo/sensevoice-small-webui.git
cd sensevoice-small-webui

# 2. 创建Python虚拟环境(推荐)
python -m venv venv
source venv/bin/activate  # Linux/macOS
# 或 venv\Scripts\activate  # Windows

# 3. 安装依赖
pip install -r requirements.txt

# 4. 下载模型文件(如果还没有)
# 模型会自动下载,如果需要手动下载:
# 将模型文件放在 ./models/ 目录下

# 5. 启动WebUI服务
python webui.py

服务启动后,在浏览器中访问 http://localhost:7860,你应该能看到SenseVoice的Web界面。

3.3 验证服务正常运行

打开WebUI后,尝试上传一个音频文件或使用麦克风录音测试。如果一切正常,你会看到识别结果。记下这个本地服务的地址(通常是 http://localhost:7860),我们稍后会用到。

4. 创建Chrome扩展程序

现在进入核心部分:创建一个Chrome扩展,让它能够调用本地的SenseVoice服务。

4.1 扩展程序项目结构

首先创建一个新的文件夹,比如 sensevoice-extension,然后建立以下文件结构:

sensevoice-extension/
├── manifest.json          # 扩展配置文件
├── popup.html            # 弹出窗口界面
├── popup.js              # 弹出窗口逻辑
├── background.js         # 后台服务脚本
├── content.js           # 页面注入脚本
└── icons/               # 图标文件夹
    ├── icon16.png
    ├── icon48.png
    └── icon128.png

4.2 关键文件详解

manifest.json - 扩展的“身份证”
{
  "manifest_version": 3,
  "name": "SenseVoice 语音输入助手",
  "version": "1.0.0",
  "description": "使用本地SenseVoice服务实现语音输入和实时字幕",
  "permissions": [
    "activeTab",
    "storage",
    "scripting"
  ],
  "host_permissions": [
    "http://localhost:7860/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["content.css"]
    }
  ],
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}
popup.html - 扩展弹出界面
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body {
      width: 300px;
      padding: 15px;
      font-family: Arial, sans-serif;
    }
    .status {
      padding: 10px;
      margin: 10px 0;
      border-radius: 5px;
      text-align: center;
    }
    .connected { background: #d4edda; color: #155724; }
    .disconnected { background: #f8d7da; color: #721c24; }
    button {
      width: 100%;
      padding: 10px;
      margin: 5px 0;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    button:hover { background: #0056b3; }
    button:disabled { background: #cccccc; }
    .language-select {
      width: 100%;
      padding: 8px;
      margin: 10px 0;
    }
  </style>
</head>
<body>
  <h3>SenseVoice 语音输入</h3>
  
  <div id="status" class="status disconnected">
    正在连接服务...
  </div>
  
  <select id="language" class="language-select">
    <option value="auto">自动检测语言</option>
    <option value="zh">中文</option>
    <option value="en">英文</option>
    <option value="yue">粤语</option>
    <option value="ja">日语</option>
    <option value="ko">韩语</option>
  </select>
  
  <button id="startRecord">🎤 开始语音输入</button>
  <button id="stopRecord" disabled>⏹️ 停止录音</button>
  <button id="startSubtitle">📺 开启实时字幕</button>
  <button id="stopSubtitle" disabled>🛑 关闭字幕</button>
  
  <div style="margin-top: 15px; font-size: 12px; color: #666;">
    <label>
      <input type="checkbox" id="autoPunctuation" checked>
      自动添加标点
    </label>
  </div>
  
  <script src="popup.js"></script>
</body>
</html>
popup.js - 弹出窗口逻辑
// 连接状态检查
async function checkConnection() {
  try {
    const response = await fetch('http://localhost:7860/');
    if (response.ok) {
      document.getElementById('status').className = 'status connected';
      document.getElementById('status').textContent = '✅ 服务连接正常';
      return true;
    }
  } catch (error) {
    document.getElementById('status').className = 'status disconnected';
    document.getElementById('status').textContent = '❌ 服务未连接';
    return false;
  }
}

// 获取当前标签页
async function getCurrentTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  return tab;
}

// 开始语音输入
document.getElementById('startRecord').addEventListener('click', async () => {
  const tab = await getCurrentTab();
  const language = document.getElementById('language').value;
  const autoPunctuation = document.getElementById('autoPunctuation').checked;
  
  chrome.tabs.sendMessage(tab.id, {
    action: 'startRecording',
    language: language,
    autoPunctuation: autoPunctuation
  });
  
  document.getElementById('startRecord').disabled = true;
  document.getElementById('stopRecord').disabled = false;
});

// 停止录音
document.getElementById('stopRecord').addEventListener('click', async () => {
  const tab = await getCurrentTab();
  chrome.tabs.sendMessage(tab.id, { action: 'stopRecording' });
  
  document.getElementById('startRecord').disabled = false;
  document.getElementById('stopRecord').disabled = true;
});

// 开启实时字幕
document.getElementById('startSubtitle').addEventListener('click', async () => {
  const tab = await getCurrentTab();
  const language = document.getElementById('language').value;
  
  chrome.tabs.sendMessage(tab.id, {
    action: 'startSubtitle',
    language: language
  });
  
  document.getElementById('startSubtitle').disabled = true;
  document.getElementById('stopSubtitle').disabled = false;
});

// 关闭字幕
document.getElementById('stopSubtitle').addEventListener('click', async () => {
  const tab = await getCurrentTab();
  chrome.tabs.sendMessage(tab.id, { action: 'stopSubtitle' });
  
  document.getElementById('startSubtitle').disabled = false;
  document.getElementById('stopSubtitle').disabled = true;
});

// 初始化检查
checkConnection();
content.js - 页面内容脚本
// 语音识别状态
let isRecording = false;
let mediaRecorder = null;
let audioChunks = [];
let subtitleActive = false;
let subtitleElement = null;

// 创建字幕显示元素
function createSubtitleElement() {
  const subtitle = document.createElement('div');
  subtitle.id = 'sensevoice-subtitle';
  subtitle.style.cssText = `
    position: fixed;
    bottom: 100px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(0, 0, 0, 0.8);
    color: white;
    padding: 15px 25px;
    border-radius: 10px;
    font-size: 18px;
    max-width: 80%;
    text-align: center;
    z-index: 10000;
    backdrop-filter: blur(10px);
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  `;
  document.body.appendChild(subtitle);
  return subtitle;
}

// 发送音频到SenseVoice服务
async function sendAudioToSenseVoice(audioBlob, language, endpoint = 'transcribe') {
  const formData = new FormData();
  formData.append('audio', audioBlob, 'recording.webm');
  formData.append('language', language);
  
  try {
    const response = await fetch(`http://localhost:7860/${endpoint}`, {
      method: 'POST',
      body: formData
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const result = await response.json();
    return result;
  } catch (error) {
    console.error('识别失败:', error);
    return { text: '识别失败,请检查服务连接', error: error.message };
  }
}

// 开始录音
async function startRecording(language, autoPunctuation) {
  if (isRecording) return;
  
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    mediaRecorder = new MediaRecorder(stream, {
      mimeType: 'audio/webm;codecs=opus'
    });
    
    audioChunks = [];
    
    mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        audioChunks.push(event.data);
      }
    };
    
    mediaRecorder.onstop = async () => {
      const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
      
      // 获取当前聚焦的输入元素
      const activeElement = document.activeElement;
      const isInput = activeElement.tagName === 'INPUT' || 
                      activeElement.tagName === 'TEXTAREA' ||
                      activeElement.isContentEditable;
      
      if (isInput) {
        // 语音输入模式:将识别结果插入到输入框
        const result = await sendAudioToSenseVoice(audioBlob, language);
        if (result.text) {
          const textToInsert = autoPunctuation ? 
            addPunctuation(result.text) : result.text;
          
          if (activeElement.isContentEditable) {
            // 可编辑元素(如富文本编辑器)
            document.execCommand('insertText', false, textToInsert);
          } else {
            // 普通输入框或文本域
            const start = activeElement.selectionStart;
            const end = activeElement.selectionEnd;
            const text = activeElement.value;
            activeElement.value = text.substring(0, start) + 
                                 textToInsert + 
                                 text.substring(end);
            // 移动光标到插入文本后
            const newPos = start + textToInsert.length;
            activeElement.selectionStart = activeElement.selectionEnd = newPos;
          }
          
          // 触发输入事件,让页面知道内容已更改
          activeElement.dispatchEvent(new Event('input', { bubbles: true }));
          activeElement.dispatchEvent(new Event('change', { bubbles: true }));
        }
      } else if (subtitleActive && subtitleElement) {
        // 字幕模式:显示识别结果
        const result = await sendAudioToSenseVoice(audioBlob, language);
        if (result.text) {
          subtitleElement.textContent = result.text;
          // 3秒后自动清除字幕
          setTimeout(() => {
            if (subtitleElement.textContent === result.text) {
              subtitleElement.textContent = '';
            }
          }, 3000);
        }
      }
      
      // 清理资源
      stream.getTracks().forEach(track => track.stop());
    };
    
    mediaRecorder.start(1000); // 每1秒收集一次数据
    isRecording = true;
    
    // 如果正在输入框,显示录音提示
    if (document.activeElement.tagName === 'INPUT' || 
        document.activeElement.tagName === 'TEXTAREA') {
      showRecordingIndicator();
    }
    
  } catch (error) {
    console.error('录音失败:', error);
    alert('无法访问麦克风,请检查权限设置');
  }
}

// 停止录音
function stopRecording() {
  if (mediaRecorder && isRecording) {
    mediaRecorder.stop();
    isRecording = false;
    hideRecordingIndicator();
  }
}

// 显示录音指示器
function showRecordingIndicator() {
  let indicator = document.getElementById('recording-indicator');
  if (!indicator) {
    indicator = document.createElement('div');
    indicator.id = 'recording-indicator';
    indicator.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: #ff4757;
      color: white;
      padding: 8px 15px;
      border-radius: 20px;
      font-size: 14px;
      z-index: 10000;
      animation: pulse 1.5s infinite;
    `;
    document.body.appendChild(indicator);
    
    // 添加动画样式
    const style = document.createElement('style');
    style.textContent = `
      @keyframes pulse {
        0% { opacity: 1; }
        50% { opacity: 0.5; }
        100% { opacity: 1; }
      }
    `;
    document.head.appendChild(style);
  }
  indicator.textContent = '🎤 录音中...';
  indicator.style.display = 'block';
}

// 隐藏录音指示器
function hideRecordingIndicator() {
  const indicator = document.getElementById('recording-indicator');
  if (indicator) {
    indicator.style.display = 'none';
  }
}

// 智能添加标点
function addPunctuation(text) {
  // 简单的标点添加逻辑,可以根据需要扩展
  let result = text.trim();
  
  // 在疑问词后添加问号
  const questionWords = ['吗', '呢', '什么', '为什么', '怎么', '如何', '难道'];
  if (questionWords.some(word => result.includes(word)) && !result.endsWith('?')) {
    result += '?';
  } else if (!result.endsWith('。') && !result.endsWith('!') && !result.endsWith('?')) {
    // 默认添加句号
    result += '。';
  }
  
  return result;
}

// 监听来自popup的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  switch (request.action) {
    case 'startRecording':
      startRecording(request.language, request.autoPunctuation);
      break;
      
    case 'stopRecording':
      stopRecording();
      break;
      
    case 'startSubtitle':
      subtitleActive = true;
      if (!subtitleElement) {
        subtitleElement = createSubtitleElement();
      }
      subtitleElement.style.display = 'block';
      break;
      
    case 'stopSubtitle':
      subtitleActive = false;
      if (subtitleElement) {
        subtitleElement.style.display = 'none';
      }
      break;
  }
});

// 监听页面上的视频元素,为视频添加字幕支持
function setupVideoSubtitles() {
  const videos = document.querySelectorAll('video');
  videos.forEach(video => {
    // 如果视频有音频轨道,可以尝试为其生成字幕
    if (!video.dataset.sensevoiceProcessed) {
      video.dataset.sensevoiceProcessed = 'true';
      
      // 这里可以扩展为从视频提取音频并生成字幕
      // 由于涉及音频提取和实时处理,这里只做框架展示
    }
  });
}

// 初始设置
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', setupVideoSubtitles);
} else {
  setupVideoSubtitles();
}

// 监听动态加载的视频
const observer = new MutationObserver(setupVideoSubtitles);
observer.observe(document.body, { childList: true, subtree: true });
background.js - 后台服务脚本
// 后台服务,处理扩展的长期任务
chrome.runtime.onInstalled.addListener(() => {
  console.log('SenseVoice扩展已安装');
});

// 监听扩展图标点击
chrome.action.onClicked.addListener((tab) => {
  // 这里可以添加点击图标时的逻辑
  // 比如打开配置页面或显示状态信息
});

// 存储用户设置
chrome.storage.onChanged.addListener((changes, namespace) => {
  for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
    console.log(`设置变更: ${key} 从 ${oldValue} 变为 ${newValue}`);
  }
});

4.3 图标准备

你需要准备三个尺寸的图标(PNG格式):

  • 16x16像素(用于工具栏)
  • 48x48像素(用于扩展管理页面)
  • 128x128像素(用于应用商店)

可以使用简单的设计工具创建,或者用SenseVoice的logo。如果没有设计资源,可以先使用纯色图标占位。

5. 安装和测试扩展

5.1 加载扩展程序

  1. 打开Chrome浏览器,在地址栏输入:chrome://extensions/
  2. 开启右上角的“开发者模式”
  3. 点击“加载已解压的扩展程序”
  4. 选择你创建的 sensevoice-extension 文件夹
  5. 扩展程序应该会出现在列表中

5.2 配置本地服务地址

确保SenseVoice WebUI服务正在运行(http://localhost:7860)。如果服务运行在其他地址或端口,需要修改 content.js 中的API地址。

5.3 功能测试

测试1:语音输入
  1. 打开任何一个有输入框的网页(如Google搜索、Gmail、Notion等)
  2. 点击输入框,让光标闪烁
  3. 点击扩展图标,选择语言,点击“开始语音输入”
  4. 对着麦克风说话,观察文字是否自动输入
测试2:实时字幕
  1. 打开一个视频网站(如YouTube)
  2. 点击扩展图标,点击“开启实时字幕”
  3. 播放视频,观察底部是否出现字幕
  4. 注意:这个功能需要视频有音频轨道,并且浏览器能捕获到系统音频(可能需要额外配置)
测试3:多语言识别
  1. 用不同语言说话,测试自动检测功能
  2. 手动选择特定语言,测试识别准确率

6. 高级功能扩展

基本的语音输入和字幕功能已经实现了,但我们可以让它更强大。这里分享几个进阶功能的实现思路:

6.1 快捷键支持

让用户可以通过快捷键(如Ctrl+Shift+S)快速开始/停止录音,而不需要点击扩展图标。

// 在content.js中添加
document.addEventListener('keydown', (event) => {
  // Ctrl+Shift+S 开始/停止录音
  if (event.ctrlKey && event.shiftKey && event.key === 'S') {
    event.preventDefault();
    if (isRecording) {
      stopRecording();
    } else {
      const language = 'auto'; // 可以从存储中获取用户设置
      startRecording(language, true);
    }
  }
});

6.2 语音命令控制

实现简单的语音命令,比如“清空输入框”、“换行”、“保存”等。

// 语音命令识别
function processVoiceCommand(text) {
  const commands = {
    '清空': () => {
      const activeElement = document.activeElement;
      if (activeElement.value !== undefined) {
        activeElement.value = '';
        activeElement.dispatchEvent(new Event('input', { bubbles: true }));
      }
    },
    '换行': () => {
      document.execCommand('insertText', false, '\n');
    },
    '保存': () => {
      // 触发保存操作,具体取决于页面
      document.dispatchEvent(new KeyboardEvent('keydown', {
        key: 's',
        ctrlKey: true
      }));
    }
  };
  
  for (const [command, action] of Object.entries(commands)) {
    if (text.includes(command)) {
      action();
      return text.replace(command, '').trim();
    }
  }
  return text;
}

6.3 离线缓存支持

即使SenseVoice服务暂时不可用,也能提供基本的语音记录功能。

// 离线缓存实现
class AudioCache {
  constructor() {
    this.cache = new Map();
    this.maxSize = 10; // 最多缓存10条录音
  }
  
  addRecording(key, audioBlob, text) {
    this.cache.set(key, { audioBlob, text, timestamp: Date.now() });
    
    // 清理旧缓存
    if (this.cache.size > this.maxSize) {
      const oldestKey = [...this.cache.entries()]
        .reduce((oldest, current) => 
          current[1].timestamp < oldest[1].timestamp ? current : oldest)[0];
      this.cache.delete(oldestKey);
    }
  }
  
  getRecording(key) {
    return this.cache.get(key);
  }
  
  getAllRecordings() {
    return [...this.cache.entries()].map(([key, value]) => ({
      key,
      ...value
    }));
  }
}

6.4 自定义识别后处理

让用户可以添加自定义的文本处理规则,比如自动替换特定词汇、添加格式等。

// 用户自定义规则处理
const userRules = [
  { pattern: /github/gi, replacement: 'GitHub' },
  { pattern: /javascript/gi, replacement: 'JavaScript' },
  { pattern: /(\d+)元/g, replacement: '¥$1' }
];

function applyUserRules(text) {
  let result = text;
  userRules.forEach(rule => {
    result = result.replace(rule.pattern, rule.replacement);
  });
  return result;
}

7. 实际应用场景

这个扩展不仅仅是一个技术演示,它在实际工作中有很多实用场景:

7.1 内容创作者

  • 视频字幕生成:为自制视频快速添加字幕
  • 播客转录:将音频内容转为文字稿
  • 采访整理:录音采访自动转文字,提高整理效率

7.2 办公效率

  • 会议记录:线上会议自动记录,会后直接出纪要
  • 邮件撰写:口述邮件内容,自动生成草稿
  • 文档创作:语音输入代替打字,提高写作速度

7.3 学习辅助

  • 外语学习:实时生成视频字幕,辅助语言学习
  • 课程笔记:听课同时自动生成文字笔记
  • 资料整理:语音记录灵感,自动转为文字

7.4 无障碍支持

  • 听力辅助:为听力障碍者提供实时字幕
  • 语音控制:通过语音操作网页内容
  • 多语言沟通:实时翻译和转写外语内容

8. 性能优化建议

如果你发现扩展运行不够流畅,可以尝试以下优化:

8.1 音频处理优化

// 使用更高效的音频格式
const audioConfig = {
  mimeType: 'audio/webm;codecs=opus',
  audioBitsPerSecond: 16000, // 降低比特率
  sampleRate: 16000 // 降低采样率
};

// 分段发送音频,减少延迟
mediaRecorder.ondataavailable = async (event) => {
  if (event.data.size > 0) {
    // 立即发送这一小段音频
    const result = await sendAudioChunk(event.data);
    if (result.text) {
      // 实时更新,而不是等录音结束
      updateTextIncrementally(result.text);
    }
  }
};

8.2 网络请求优化

// 实现请求队列和重试机制
class RequestQueue {
  constructor(maxConcurrent = 2) {
    this.queue = [];
    this.activeCount = 0;
    this.maxConcurrent = maxConcurrent;
  }
  
  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject });
      this.processQueue();
    });
  }
  
  async processQueue() {
    if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) {
      return;
    }
    
    this.activeCount++;
    const { requestFn, resolve, reject } = this.queue.shift();
    
    try {
      const result = await this.retryRequest(requestFn, 3);
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.activeCount--;
      this.processQueue();
    }
  }
  
  async retryRequest(requestFn, maxRetries) {
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await requestFn();
      } catch (error) {
        if (i === maxRetries - 1) throw error;
        await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
      }
    }
  }
}

8.3 内存管理

// 定期清理资源
setInterval(() => {
  // 清理过期的音频数据
  const oneHourAgo = Date.now() - 60 * 60 * 1000;
  for (const [key, data] of audioCache.entries()) {
    if (data.timestamp < oneHourAgo) {
      audioCache.delete(key);
    }
  }
  
  // 清理DOM元素
  const indicators = document.querySelectorAll('.temp-indicator');
  indicators.forEach(indicator => {
    if (Date.now() - indicator.dataset.created > 5000) {
      indicator.remove();
    }
  });
}, 300000); // 每5分钟清理一次

9. 故障排除

9.1 常见问题及解决

问题1:扩展无法连接本地服务

可能原因

  • SenseVoice服务未启动
  • 防火墙阻止了连接
  • 服务运行在不同的端口

解决方法

// 在popup.js中添加多端口尝试
async function tryConnect(port) {
  try {
    const response = await fetch(`http://localhost:${port}/health`);
    return response.ok ? port : null;
  } catch {
    return null;
  }
}

async function findServicePort() {
  const ports = [7860, 7861, 7862, 8000, 8080];
  for (const port of ports) {
    const result = await tryConnect(port);
    if (result) return result;
  }
  return null;
}
问题2:麦克风权限被拒绝

解决方法

  • 检查浏览器麦克风权限设置
  • 确保没有其他应用占用麦克风
  • 尝试在浏览器设置中重置权限
问题3:识别速度慢

解决方法

  • 降低音频质量设置
  • 使用更短的录音分段
  • 检查本地服务性能
问题4:字幕显示位置不对

解决方法

// 动态调整字幕位置
function adjustSubtitlePosition() {
  if (!subtitleElement) return;
  
  const viewportHeight = window.innerHeight;
  const viewportWidth = window.innerWidth;
  
  // 避免被页面元素遮挡
  subtitleElement.style.bottom = '100px';
  subtitleElement.style.left = '50%';
  subtitleElement.style.transform = 'translateX(-50%)';
  subtitleElement.style.maxWidth = `${viewportWidth * 0.8}px`;
}

10. 总结

通过这个Chrome扩展,我们将本地的SenseVoice-small语音识别服务变成了一个随时可用的浏览器工具。这个方案有几个显著优势:

隐私安全:所有语音处理都在本地完成,敏感信息不会上传到云端。

多语言支持:自动检测50多种语言,满足国际化需求。

低资源占用:基于ONNX量化模型,即使在普通设备上也能流畅运行。

高度可定制:开源代码允许你根据具体需求进行调整和扩展。

离线可用:一旦部署完成,即使没有网络也能使用核心功能。

这个扩展只是一个起点,你可以在此基础上添加更多功能,比如:

  • 语音翻译功能
  • 自定义命令词
  • 批量处理历史录音
  • 与其他工具集成(如Notion、Google Docs等)
  • 团队协作功能

最重要的是,这个方案展示了如何将先进的AI能力以最便捷的方式集成到日常工具中。技术不应该只是实验室里的演示,而应该成为我们工作和生活中实实在在的生产力工具。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐