要在微信中实现语音转文字,可通过官方提供的「微信同声传译插件」或第三方工具完成。以下是具体方案
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,// 定时器IDtimer: 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) returnwx.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.codethis.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;}