基于AI会话来探索微信小程序实时语音功能实现以及过程中碰到得小问题分享

1,024 阅读6分钟

1-实现实时语音需要准备哪些东西

1-1 注册腾讯云并开通实时语音服务并创建秘钥拿到appid和SecretId和SecretKey(新用户免费送4个小时)

image.png

image.png

image.png

1-2 前往微信公众号插件市场开通腾讯云智能语音

腾讯云智能语音 | 小程序

image.png

1-3 前往隐私协议设置获取语音权限(切记,隐私权限更新后不会立马生效,需要大概1个小时左右)

2-实现方法(Taro版-vue写法)

2-1 新建一个index文件

app.json
plugins: {
    QCloudAIVoice: {
      version: '2.3.4', 插件版本号
      provider: '' 插件appid
    }
  },


//写在顶部
let plugin = Taro.requirePlugin('QCloudAIVoice')
let speechRecognizerManager = plugin.speechRecognizerManager()

//进入页面需要初始化得方法 一般放在onload里面或者onMonted

const speechRecognizerManagerFn = () => {
  // 开始识别
  speechRecognizerManager.OnRecognitionStart = res => {
    // 表示链接建立成功,收到此回调才能调用stop方法关闭录音和识别链接
    console.log('开始识别', res)
  }
  // 一句话开始
  speechRecognizerManager.OnSentenceBegin = res => {
    console.log('一句话开始', res)
  }
  // 识别变化时
  speechRecognizerManager.OnRecognitionResultChange = res => {
    noSuccess(res)
    if (res.code === 0) {
      speechGesturePrompt.value = res.result.voice_text_str
    } else {
      countdownSendMessage()
      noSuccess(res)
    }
  }
  // 一句话结束
  speechRecognizerManager.OnSentenceEnd = res => {
    speechGesturePrompt.value = res.result.voice_text_str
    console.log('一句话结束', res)
  }
  // 识别结束
  speechRecognizerManager.OnRecognitionComplete = res => {
    console.log('识别结束', res)
  }
  // 识别错误
  speechRecognizerManager.OnError = res => {
    // code为6001时,国内站用户请检查是否使用境外代理,如果使用请关闭。境外调用需开通国际站服务
    console.log('识别失败', res)
    noSuccess(res)
  }
  // 录音结束(最长10分钟)时回调
  speechRecognizerManager.OnRecorderStop = res => {
    console.log('录音结束', res)
  }
}


//配置参数
const speechParameter = {
  secretkey: '', 腾讯云获取
  secretid: '', 腾讯云获取
  appid: '', 腾讯云获取
  engine_model_type: '16k_zh-PY', 语言种类
  word_info: 2, 是否显示词级别时间戳
  vad_silence_time: 240, 语音断句检测阈值
  voice_format: 1 voice_format
}



//调用语音
speechRecognizerManager.start(speechParameter)

//结束调用
speechRecognizerManager.stop()

3-实现效果

6beux-7vc4b.gif

4-如何在程序实现sse

const requestTask = Taro.request({
      url: `xxx`, // 需要请求的接口地址
      enableChunked: true, // enableChunked必须为true 分段响应
      method: "GET",
      timeout: '120000',
      success(res) {
        console.log(res.data)
      },
      fail: function (error) {
        // 请求失败的操作
        console.error(error);
      },
      complete: function () {
        // 请求完成的操作,无论成功或失败都会执行
        console.log('请求完成', str);
      }
    })
    // 监听服务端返回的数据
    requestTask.onChunkReceived(res => {
      console.log( res, res.data);
    })
    
    
原理:

在微信小程序中实现并利用`enableChunked`选项,可以通过`wx.request`方法来实现。`enableChunked`设置为`true`允许微信小程序通过流式传输接收服务器发送的数据,这对于需要实时更新数据的应用场景非常有用。

首先,需要在发起请求时设置`enableChunked``true`,这样微信小程序就会在响应头中开启`transfer-encoding: chunked`,从而支持流式传输。通过调用`onChunkReceived`方法,可以监听到每个数据块(chunk)的接收。这个方法会在接收到新的数据块时被触发,允许开发者对每个数据块进行处理。

此外,为了监听SSE连接的结束,可以在`wx.request`的配置中添加一个`complete`回调函数。这个回调函数会在请求完成(无论是成功还是失败)时被调用,可以用来检测连接是否结束。如果需要在连接结束时执行特定的操作,可以在这个回调函数中添加相应的逻辑。

需要注意的是,由于微信小程序的API并不直接支持SSE协议,因此需要通过`wx.request`接口模拟实现SSE的功能。这意味着接收到的数据可能是以`Uint8Array`的形式,需要将其转换为文本以便进一步处理。这可以通过使用`TextDecoder`来实现,将接收到的数据解码为UTF-8格式的字符串。

综上所述,微信小程序通过`wx.request`方法并设置`enableChunked``true`来实现SSE的流式传输,并通过监听`onChunkReceived`方法来处理每个接收到的数据块。同时,通过在`wx.request`配置中添加`complete`回调函数来检测连接是否结束,并在需要时执行相应的操作‌

5-拿到后端返回的流式数据如何进行处理并实现打字机效果

     let self = this
     
      function setIntervalAnimateResponseText () {
        self.animationFrameId = setInterval(() => {
          animateResponseText()
        }, 20)
      }
      
      setIntervalAnimateResponseText()

      async function animateResponseText () {
        if (self.isStop) {
          return
        }
        if (self.finished && responseQueue.length === 0) {
          self.stopMessageRequestTask(assistantId)
          if (!self.topicId) {
            await self.brainyTopics()
          }
          if (!fileUrls?.length) {
            await self.chatgptQuestions({
              id: assistantId,
              prompt: !message ? fileUrls : message,
              answer: self?.messagesList?.find((x: any) => x.id === assistantId)?.content
            })
          }

          return
        }

       //控制输入多少个字 每次
        if (responseQueue.length > 0) {
          // const fetchCount = Math.max(1, Math.round(responseQueue.length / 30))
          const fetchCount = 3
          for (let i = 0; i < fetchCount; i++) {
            const item = responseQueue[i]
            if (item) {
              content += item
            }
          }

          responseQueue = responseQueue.slice(fetchCount)
        }
        //更新到页面
        self.uploadMessage({
          id: assistantId,
          content: content,
          role: 'assistant'
        })
        self.setScrollTopsState(1)
      }

      //拿到流式数据
      requestTask.onChunkReceived(res => {
        animationFrameIdIndex += 1
        //对二进制进行转义
        const text = utf8Array2Str(new Uint8Array(res.data))
        self.chatLoading = true
        if (animationFrameIdIndex > 5) {
          self.clearIntervalAnimationFrameId()
          animationFrameIdIndex = 0
          setIntervalAnimateResponseText()
        }
        //拼接数据
        text.split('data:').map((res1: any) => {
          if (res1.replaceAll(/\s+/g, '') !== '') {
            try {
              const reText = res1
              const newRes = JSON.parse(reText)
              const input = ![ 'DONE', '[DONE]' ].includes(newRes) ? JSON.parse(reText) : ''
              if (input.length > 0) {
                const text = JSON.parse(input).choices[0].message.content
                for (const item of text.replace(/\\n/g, '\n\n')) {
                  responseQueue.push(item)
                }
              }
              self.finished = [ 'DONE', '[DONE]' ].includes(newRes)
            } catch (error) {
              self.clearIntervalAnimationFrameId()
              console.log(error)
              content = ''
            }
          }
          return null
        })
      })

5-1 注意

let decoder = new TextDecoder('utf-8');
let text = decoder.decode(res.data)
在真机是失效得 需要换成  npm install utf8Array2Str
const text = utf8Array2Str(new Uint8Array(res.data))

6-其他问题

6-1 scroll-view动画过渡效果频繁调用一直滑到底部会偶尔回弹

1-原因

因为 scroll-view得动画效果有300ms得过渡时间,当开始往底部滚动得时候,调用次数过于频繁,比如100ms一次得时候,此时scroll-view动画效果还没有结束,此时在执行新的动画效果旧得没有结束就会出现错乱,这个是官方得老bug了,

2-解决方法
写入一个定时器 每一次执行动画效果后再执行新一轮动画效果
  nextTick(() => {
    if (timer.value) return
    timer.value = setTimeout(() => {
      // viewId.value = 'id-1'
      Taro.createSelectorQuery()
        .select('#scrollView')
        .boundingClientRect(res => {
          let scrollTops = 0
          scrollTops += res?.height + 6666
          if (scrollTops > 0) {
            if (messageStore.scrollTop === scrollTops) return
            messageStore.scrollTop = scrollTops
          }
        })
        .exec()
      clearTimeout(timer.value)
      timer.value = null
    }, time)
  })

6-2 输入框出现光标调用微信获取键盘高度api 键盘和输入框有一定距离

分情况
1-当有两个输入框都调用的时候 且配置不一样 则最终出现得高度按最高算 需要保持一致
2-可能用的是高度height,建议用定位进行处处理

6-3 如何实现微信小程序上传本机pdf

新建外部得h5文件并部署在单独服务器上,然后用<web-view>嵌入并进行通信就可以实现

6-4 调用微信上传api拿到得文件名是官方定义得文件名不是自己得文件名

在进行上传得时候 携带一个文件名给后端 然后上传成功后 后端返回带有正确文件名得地址回来

6-5 textarea对于数字英文无法进行换行

给一个class类并加入line-break: anywhere; 该问题是官方老bug

6-6 如何对markdown字符进行标识

如果你是taro用户 直接去uniapp插件市场下载源码 并写入到代码中