微信小程序语音转文字

要在微信中实现语音转文字,可通过官方提供的「微信同声传译插件」或第三方工具完成。以下是具体方案

1. 接入流程

添加插件:登录微信公众平台(https://mp.weixin.qq.com),进入「设置 → 第三方服务 → 插件管理」,搜索「微信同声传译」并添加,获取其 AppID(如wx069ba97219f66d99)和最新版本号
配置小程序:在app.json中配置插件信息,示例代码如下
    {
      "plugins": {
        "WechatSI": {
          "version": "0.3.4", // 填写实际版本号
          "provider": "wx069ba97219f66d99"
        }
      }
    }
  • 需确保版本号与插件详情页一致,否则可能导致功能异常

2. 代码实现

引入插件:在 JS 文件中引入插件并获取语音识别管理器
  const plugin = requirePlugin('WechatSI');
  const manager = plugin.getRecordRecognitionManager();
录音控制:通过按钮绑定录音事件,示例代码
    // 开始录音
    touchStart() {
      this.setData({ recordState: true });
      manager.start({ lang: 'zh_CN' }); // 支持zh_CN、en_US、zh_HK等语言
    }

    // 结束录音
    touchEnd() {
      this.setData({ recordState: false });
      manager.stop();
    }
监听识别结果:通过回调函数获取实时或最终识别文本
    manager.onRecognize = (res) => {
      this.setData({ content: res.result }); // 实时更新文本
    };

    manager.onStop = (res) => {
      console.log('最终识别结果:', res.result); // 结束后完整文本
    };
  • 需注意,onRecognize可能包含重复内容,需结合onStop结果进行去重或合并

3. 注意事项

权限申请:需在小程序后台的「用户隐私保护指引」中声明使用录音功能,否则无法获取麦克风权限
以下完整代码
  • app.json

      {
        "pages": [
          "pages/voiceToText/voiceToText"
        ],
        "window": {
          "backgroundTextStyle": "light",
          "navigationBarBackgroundColor": "#fff",
          "navigationBarTitleText": "微信同声传译示例",
          "navigationBarTextStyle": "black"
        },
        "plugins": {
          "WechatSI": {
            "version": "0.3.4",
            "provider": "wx069ba97219f66d99"
          }
        },
        "sitemapLocation": "sitemap.json"
      }
    
  • pages/voiceToText/voiceToText.json

      {
        "navigationBarTitleText": "语音转文字",
        "usingComponents": {}
      }
    
  • pages/voiceToText/voiceToText.ts

      // 引入微信同声传译插件
      const plugin = requirePlugin('WechatSI')
      // 获取全局唯一的语音识别管理器
      const manager = plugin.getRecordRecognitionManager()
    
      Page({
        /**
        * 页面的初始数据
        */
        data: {
          // 录音状态:false-未录音,true-正在录音
          isRecording: false,
          // 识别结果文本
          resultText: '',
          // 历史记录列表
          historyList: [] as string[],
          // 录音时长(秒)
          recordDuration: 0,
          // 定时器ID
          timer: 0 as number,
          // 支持的语言列表
          languages: [
            { code: 'zh_CN', name: '中文' },
            { code: 'en_US', name: '英语' },
            { code: 'zh_HK', name: '粤语' }
          ],
          // 当前选中的语言
          currentLang: 'zh_CN'
        },
    
        /**
        * 生命周期函数--监听页面加载
        */
        onLoad() {
          this.initRecordManager()
          // 从本地存储加载历史记录
          const history = wx.getStorageSync('voiceToTextHistory')
          if (history && Array.isArray(history)) {
            this.setData({ historyList: history })
          }
        },
    
        /**
        * 初始化录音管理器
        */
        initRecordManager() {
          // 识别中事件
          manager.onRecognize = (res: { result: string }) => {
            console.log('实时识别结果:', res.result)
            this.setData({
              resultText: res.result
            })
          }
    
          // 识别结束事件
          manager.onStop = (res: { result: string }) => {
            console.log('识别结束:', res)
            let text = res.result
            
            // 处理可能的空结果
            if (!text) {
              wx.showToast({
                title: '未识别到内容',
                icon: 'none',
                duration: 2000
              })
              text = ''
            } else {
              // 保存到历史记录
              this.addToHistory(text)
            }
            
            this.setData({
              resultText: text,
              isRecording: false,
              recordDuration: 0
            })
            
            // 清除定时器
            if (this.data.timer) {
              clearInterval(this.data.timer)
            }
          }
    
          // 识别错误事件
          manager.onError = (err: { errMsg: string }) => {
            console.error('识别错误:', err)
            wx.showToast({
              title: '识别失败: ' + err.errMsg,
              icon: 'none',
              duration: 3000
            })
            this.setData({
              isRecording: false,
              recordDuration: 0
            })
            
            if (this.data.timer) {
              clearInterval(this.data.timer)
            }
          }
        },
    
        /**
        * 开始录音
        */
        startRecord() {
          // 检查录音权限
          wx.getSetting({
            success: (res) => {
              if (!res.authSetting['scope.record']) {
                // 申请录音权限
                wx.authorize({
                  scope: 'scope.record',
                  success: () => {
                    this.startRecording()
                  },
                  fail: () => {
                    wx.showModal({
                      title: '权限不足',
                      content: '需要录音权限才能使用语音识别功能,请在设置中开启',
                      confirmText: '去设置',
                      success: (modalRes) => {
                        if (modalRes.confirm) {
                          wx.openSetting()
                        }
                      }
                    })
                  }
                })
              } else {
                this.startRecording()
              }
            }
          })
        },
    
        /**
        * 实际开始录音的逻辑
        */
        startRecording() {
          // 重置结果
          this.setData({
            resultText: '',
            isRecording: true,
            recordDuration: 0
          })
    
          // 开始录音
          manager.start({
            lang: this.data.currentLang,
            duration: 60000 // 最长录音时间,单位ms
          })
    
          // 开始计时
          const timer = setInterval(() => {
            this.setData({
              recordDuration: this.data.recordDuration + 1
            })
          }, 1000)
    
          this.setData({ timer })
        },
    
        /**
        * 停止录音
        */
        stopRecord() {
          if (this.data.isRecording) {
            manager.stop()
          }
        },
    
        /**
        * 添加到历史记录
        */
        addToHistory(text: string) {
          const newHistory = [text, ...this.data.historyList]
          // 限制历史记录数量为20条
          if (newHistory.length > 20) {
            newHistory.pop()
          }
          
          this.setData({ historyList: newHistory })
          // 保存到本地存储
          wx.setStorageSync('voiceToTextHistory', newHistory)
        },
    
        /**
        * 清空当前结果
        */
        clearResult() {
          this.setData({ resultText: '' })
        },
    
        /**
        * 复制结果到剪贴板
        */
        copyResult() {
          if (!this.data.resultText) {
            wx.showToast({
              title: '没有可复制的内容',
              icon: 'none'
            })
            return
          }
    
          wx.setClipboardData({
            data: this.data.resultText,
            success: () => {
              wx.showToast({
                title: '复制成功',
                icon: 'success'
              })
            }
          })
        },
    
        /**
        * 清空历史记录
        */
        clearHistory() {
          if (this.data.historyList.length === 0) return
    
          wx.showModal({
            title: '确认',
            content: '确定要清空所有历史记录吗?',
            success: (res) => {
              if (res.confirm) {
                this.setData({ historyList: [] })
                wx.removeStorageSync('voiceToTextHistory')
              }
            }
          })
        },
    
        /**
        * 选择语言
        */
        selectLanguage(e: { currentTarget: { dataset: { code: string } } }) {
          const langCode = e.currentTarget.dataset.code
          this.setData({ currentLang: langCode })
        },
    
        /**
        * 生命周期函数--监听页面卸载
        */
        onUnload() {
          // 页面卸载时停止录音并清除定时器
          if (this.data.isRecording) {
            manager.stop()
          }
          if (this.data.timer) {
            clearInterval(this.data.timer)
          }
        }
      })
    
  • pages/voiceToText/voiceToText.wxml

        <view class="container">
          <view class="title">语音转文字</view>
          
          <!-- 语言选择 -->
          <view class="language-selector">
            <view class="lang-item" 
                  wx:for="{{languages}}" 
                  wx:key="code"
                  data-code="{{item.code}}"
                  bindtap="selectLanguage"
                  class="{{currentLang === item.code ? 'lang-selected' : ''}}">
              {{item.name}}
            </view>
          </view>
          
          <!-- 结果显示区域 -->
          <view class="result-container">
            <textarea 
              class="result-text" 
              value="{{resultText}}" 
              placeholder="点击下方按钮开始说话..."
              disabled
            ></textarea>
            
            <!-- 操作按钮 -->
            <view class="action-buttons">
              <button 
                class="btn clear-btn" 
                bindtap="clearResult"
                disabled="{{!resultText}}"
              >
                清空
              </button>
              <button 
                class="btn copy-btn" 
                bindtap="copyResult"
                disabled="{{!resultText}}"
              >
                复制
              </button>
            </view>
          </view>
          
          <!-- 录音控制 -->
          <view class="record-control">
            <view 
              class="record-button" 
              bindtouchstart="startRecord" 
              bindtouchend="stopRecord"
              class="{{isRecording ? 'recording' : ''}}"
            >
              <text class="record-icon">{{isRecording ? '⏹️' : '🎤'}}</text>
              <text class="record-text">{{isRecording ? '松开停止' : '按住说话'}}</text>
              <text class="record-time" wx:if="{{isRecording}}">{{recordDuration}}s</text>
            </view>
          </view>
          
          <!-- 历史记录 -->
          <view class="history-section" wx:if="{{historyList.length > 0}}">
            <view class="history-header">
              <text>历史记录</text>
              <button class="clear-history-btn" bindtap="clearHistory">清空</button>
            </view>
            
            <view class="history-list">
              <view class="history-item" wx:for="{{historyList}}" wx:key="index">
                <text>{{item}}</text>
              </view>
            </view>
          </view>
        </view>
    
  • pages/voiceToText/voiceToText.wxss

      .container {
        display: flex;
        flex-direction: column;
        min-height: 100vh;
        background-color: #f5f5f5;
        padding: 20rpx;
      }
    
      .title {
        font-size: 36rpx;
        font-weight: bold;
        text-align: center;
        margin: 30rpx 0;
        color: #333;
      }
    
      .language-selector {
        display: flex;
        justify-content: center;
        margin-bottom: 20rpx;
        flex-wrap: wrap;
      }
    
      .lang-item {
        padding: 10rpx 20rpx;
        margin: 0 10rpx 10rpx;
        background-color: #fff;
        border-radius: 20rpx;
        font-size: 28rpx;
        border: 1px solid #eee;
      }
    
      .lang-selected {
        background-color: #07c160;
        color: white;
        border-color: #07c160;
      }
    
      .result-container {
        background-color: #fff;
        border-radius: 16rpx;
        padding: 20rpx;
        margin-bottom: 30rpx;
        box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
      }
    
      .result-text {
        width: 100%;
        min-height: 200rpx;
        font-size: 30rpx;
        line-height: 1.6;
        padding: 10rpx;
        border: none;
        resize: none;
        color: #333;
      }
    
      .result-text::placeholder {
        color: #ccc;
      }
    
      .action-buttons {
        display: flex;
        justify-content: flex-end;
        margin-top: 10rpx;
      }
    
      .btn {
        padding: 10rpx 20rpx;
        margin-left: 15rpx;
        font-size: 26rpx;
        border-radius: 8rpx;
      }
    
      .clear-btn {
        background-color: #f5f5f5;
        color: #666;
      }
    
      .copy-btn {
        background-color: #07c160;
        color: white;
      }
    
      .record-control {
        display: flex;
        justify-content: center;
        margin: 40rpx 0 60rpx;
      }
    
      .record-button {
        width: 160rpx;
        height: 160rpx;
        border-radius: 50%;
        background-color: #ff4d4f;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        color: white;
        box-shadow: 0 5rpx 15rpx rgba(255,77,79,0.4);
        touch-action: manipulation;
      }
    
      .record-button.recording {
        background-color: #faad14;
        box-shadow: 0 5rpx 15rpx rgba(250,173,20,0.4);
      }
    
      .record-icon {
        font-size: 60rpx;
        margin-bottom: 5rpx;
      }
    
      .record-text {
        font-size: 24rpx;
      }
    
      .record-time {
        font-size: 22rpx;
        margin-top: 5rpx;
      }
    
      .history-section {
        margin-top: auto;
        background-color: #fff;
        border-radius: 16rpx;
        padding: 20rpx;
        box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
      }
    
      .history-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 15rpx;
        padding-bottom: 10rpx;
        border-bottom: 1px solid #f5f5f5;
      }
    
      .history-header text {
        font-size: 30rpx;
        font-weight: 500;
        color: #666;
      }
    
      .clear-history-btn {
        font-size: 24rpx;
        color: #07c160;
        background: none;
        padding: 0;
      }
    
      .history-list {
        max-height: 300rpx;
        overflow-y: auto;
      }
    
      .history-item {
        padding: 15rpx 0;
        border-bottom: 1px solid #f5f5f5;
        font-size: 26rpx;
        color: #333;
        line-height: 1.5;
      }
    
      .history-item:last-child {
        border-bottom: none;
      }
    
posted @ 2025-09-11 10:49  不完美的完美  阅读(147)  评论(0)    收藏  举报