feat: 集成语音识别
This commit is contained in:
6
src/constant/speech.js
Normal file
6
src/constant/speech.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// App 端 yao-asdRealSpeech 使用的阿里云 DashScope 实时语音识别配置。
|
||||
// 将 apikey 填成实际的 DashScope API Key 后,App 端语音识别即可发起连接。
|
||||
export const appSpeechRecognitionOptions = {
|
||||
apikey: "SnoHqdtJ832riRg4",
|
||||
language_hints: ["zh"],
|
||||
};
|
||||
@@ -44,10 +44,16 @@
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
||||
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
"ios" : {},
|
||||
"ios" : {
|
||||
"privacyDescription" : {
|
||||
"NSMicrophoneUsageDescription" : "用于语音输入时录制并识别您的语音内容"
|
||||
}
|
||||
},
|
||||
"sdkConfigs" : {
|
||||
"oauth" : {}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,15 @@
|
||||
<!-- 录音按钮 -->
|
||||
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
|
||||
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<yao-asdRealSpeech
|
||||
ref="appSpeechRef"
|
||||
:options="appSpeechOptions"
|
||||
@result="handleAppSpeechResult"
|
||||
@change="handleAppSpeechChange"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
|
||||
<view class="color-99A0AE font-size-9 text-center text-gray-400">
|
||||
内容由AI大模型生成,请仔细鉴别
|
||||
</view>
|
||||
@@ -84,15 +93,25 @@
|
||||
import { ref, computed, watch, nextTick, onMounted, defineExpose, onUnmounted } from "vue";
|
||||
import RecordingWaveBtn from "@/components/Speech/RecordingWaveBtn.vue";
|
||||
import { getCurrentConfig } from "@/constant/base";
|
||||
import { appSpeechRecognitionOptions } from "@/constant/speech";
|
||||
|
||||
let manager = null;
|
||||
let speechProvider = "";
|
||||
const isSpeechRecognitionSupported = ref(false);
|
||||
const appSpeechOptions = appSpeechRecognitionOptions;
|
||||
|
||||
// WechatSI 是微信小程序插件,App 原生基座没有 requirePlugin。
|
||||
// #ifdef MP-WEIXIN
|
||||
const plugin = requirePlugin("WechatSI");
|
||||
manager = plugin.getRecordRecognitionManager();
|
||||
isSpeechRecognitionSupported.value = !!manager;
|
||||
speechProvider = manager ? "wechat" : "";
|
||||
// #endif
|
||||
|
||||
// App 端使用 yao-asdRealSpeech;apikey 请在 src/constant/speech.js 中配置。
|
||||
// #ifdef APP-PLUS
|
||||
isSpeechRecognitionSupported.value = true;
|
||||
speechProvider = "app";
|
||||
// #endif
|
||||
|
||||
const props = defineProps({
|
||||
@@ -112,6 +131,7 @@ const emit = defineEmits([
|
||||
|
||||
const textareaRef = ref(null);
|
||||
const recordingWaveBtnRef = ref(null);
|
||||
const appSpeechRef = ref(null);
|
||||
const placeholder = computed(() => {
|
||||
const config = getCurrentConfig();
|
||||
return `快告诉${config.name}您在想什么~`;
|
||||
@@ -122,7 +142,10 @@ const keyboardHeight = ref(0);
|
||||
const isVoiceMode = ref(false);
|
||||
const visibleWaveBtn = ref(false);
|
||||
const isRecording = ref(false);
|
||||
const appRecognizedText = ref("");
|
||||
let watchDogTimer = null;
|
||||
let appStopFallbackTimer = null;
|
||||
let hasSentAppRecognition = false;
|
||||
|
||||
const resetUI = () => {
|
||||
isRecording.value = false;
|
||||
@@ -148,6 +171,13 @@ const startWatchDog = (timeout = 10000) => {
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
const clearAppStopFallback = () => {
|
||||
if (appStopFallbackTimer) {
|
||||
clearTimeout(appStopFallbackTimer);
|
||||
appStopFallbackTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 保持和父组件同步
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -172,9 +202,38 @@ const toggleVoiceMode = () => {
|
||||
|
||||
// 处理语音按钮长按开始
|
||||
const handleVoiceTouchStart = () => {
|
||||
if (!manager) return;
|
||||
if (!isSpeechRecognitionSupported.value) return;
|
||||
|
||||
try {
|
||||
manager.start({ lang: "zh_CN" });
|
||||
if (speechProvider === "wechat") {
|
||||
if (!manager) return;
|
||||
manager.start({ lang: "zh_CN" });
|
||||
} else if (speechProvider === "app") {
|
||||
if (!appSpeechOptions.apikey) {
|
||||
uni.showToast({
|
||||
title: "请先配置语音识别API Key",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
appRecognizedText.value = "";
|
||||
hasSentAppRecognition = false;
|
||||
|
||||
const appSpeech = appSpeechRef.value;
|
||||
if (!appSpeech || typeof appSpeech.start !== "function") {
|
||||
uni.showToast({
|
||||
title: "语音组件未初始化",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
appSpeech.start();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
isRecording.value = true;
|
||||
visibleWaveBtn.value = true;
|
||||
|
||||
@@ -194,7 +253,7 @@ const handleVoiceTouchStart = () => {
|
||||
|
||||
// 处理语音按钮长按结束
|
||||
const handleVoiceTouchEnd = () => {
|
||||
if (!manager) {
|
||||
if (!isSpeechRecognitionSupported.value) {
|
||||
resetUI();
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +272,16 @@ const handleVoiceTouchEnd = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
manager.stop();
|
||||
if (speechProvider === "wechat") {
|
||||
manager?.stop?.();
|
||||
} else if (speechProvider === "app") {
|
||||
appSpeechRef.value?.stop?.();
|
||||
clearAppStopFallback();
|
||||
appStopFallbackTimer = setTimeout(() => {
|
||||
appStopFallbackTimer = null;
|
||||
sendAppRecognizedText();
|
||||
}, 600);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("record stop error:", err);
|
||||
} finally {
|
||||
@@ -255,6 +323,60 @@ const initRecord = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getAppSpeechText = (res) => {
|
||||
return (
|
||||
res?.sentence?.text ||
|
||||
res?.text ||
|
||||
res?.result ||
|
||||
res?.payload?.output?.sentence?.text ||
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
const sendAppRecognizedText = () => {
|
||||
if (hasSentAppRecognition) return;
|
||||
|
||||
const text = appRecognizedText.value.trim();
|
||||
if (!text) {
|
||||
console.log("没有说话");
|
||||
return;
|
||||
}
|
||||
|
||||
hasSentAppRecognition = true;
|
||||
clearAppStopFallback();
|
||||
inputMessage.value = text;
|
||||
emit("send", text);
|
||||
appRecognizedText.value = "";
|
||||
};
|
||||
|
||||
const handleAppSpeechResult = (res) => {
|
||||
const text = getAppSpeechText(res);
|
||||
if (!text) return;
|
||||
|
||||
appRecognizedText.value = text;
|
||||
inputMessage.value = text;
|
||||
};
|
||||
|
||||
const handleAppSpeechChange = (msg) => {
|
||||
if (!msg || !msg.status) return;
|
||||
|
||||
if (msg.status === "START") {
|
||||
isRecording.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "STOP") {
|
||||
resetUI();
|
||||
sendAppRecognizedText();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "ERROR") {
|
||||
console.error("app speech recognition error", msg.msg);
|
||||
resetUI();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听键盘高度变化
|
||||
onMounted(() => {
|
||||
// 监听键盘弹起
|
||||
@@ -281,6 +403,14 @@ onUnmounted(() => {
|
||||
manager.onStop = null;
|
||||
manager.onError = null;
|
||||
}
|
||||
if (appSpeechRef.value) {
|
||||
try {
|
||||
appSpeechRef.value.stop && appSpeechRef.value.stop();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
clearAppStopFallback();
|
||||
resetUI();
|
||||
});
|
||||
|
||||
|
||||
9
src/uni_modules/yao-asdRealSpeech/changelog.md
Normal file
9
src/uni_modules/yao-asdRealSpeech/changelog.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 1.0.3(2026-04-20)
|
||||
修复录音时间长合成音频文件失败问题
|
||||
## 1.0.2(2025-12-06)
|
||||
.
|
||||
## 1.0.1(2025-12-05)
|
||||
停止录音后保存音频
|
||||
## 1.0.0(2025-08-17)
|
||||
# yao-asdRealtimeSpeech
|
||||
阿里云Paraformer实时语音识别
|
||||
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<view
|
||||
:recorderStatus="recorderStatus"
|
||||
:change:recorderStatus="recorder.startRecord">
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
var ws;
|
||||
export default{
|
||||
data(){
|
||||
return{
|
||||
recorderStatus:null,
|
||||
task_id:'',
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
start(){
|
||||
this.webSocekt();
|
||||
},
|
||||
stop(){
|
||||
this.recorderStatus="stop";
|
||||
if(ws){
|
||||
ws.close();
|
||||
}
|
||||
},
|
||||
toShowToast(){
|
||||
uni.showToast({
|
||||
title:'发生错误,请检查是否有麦克风权限',
|
||||
icon:'none'
|
||||
});
|
||||
this.recorderStatus="stop";
|
||||
},
|
||||
startRecorder(){
|
||||
this.$emit('change',{status:"START",msg:"开始录音"});
|
||||
},
|
||||
webSocekt(){
|
||||
ws = uni.connectSocket({
|
||||
url: 'wss://dashscope.aliyuncs.com/api-ws/v1/inference',
|
||||
header: {
|
||||
"Authorization": "bearer "+this.options.apikey, // 将<your_dashscope_api_key>替换成您自己的API Key
|
||||
"X-DashScope-DataInspection": "enable"
|
||||
},
|
||||
method: 'GET',
|
||||
complete(){
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
ws.onOpen(()=>{
|
||||
var param={
|
||||
header: {
|
||||
action: "run-task",
|
||||
task_id: this.generateRandom32(), // 随机uuid
|
||||
streaming: "duplex"
|
||||
},
|
||||
payload:{
|
||||
task_group:"audio",
|
||||
task:"asr",
|
||||
function:"recognition",
|
||||
model:"paraformer-realtime-v2",
|
||||
parameters:{
|
||||
format:"pcm",
|
||||
sample_rate:16000,
|
||||
language_hints:this.options.language_hints
|
||||
},
|
||||
input:{}
|
||||
}
|
||||
};
|
||||
|
||||
ws.send({data:JSON.stringify(param)});
|
||||
});
|
||||
|
||||
ws.onMessage((data)=>{
|
||||
var msg=JSON.parse(data.data)
|
||||
if(msg.header.event=="task-started"){
|
||||
this.task_id=msg.header.task_id;
|
||||
this.recorderStatus="start";
|
||||
}
|
||||
|
||||
if(msg.header.event=="result-generated"){
|
||||
this.$emit('result',msg.payload.output);
|
||||
}
|
||||
});
|
||||
|
||||
ws.onError((err)=>{
|
||||
this.$emit('change',{status:"ERROR",msg:err});
|
||||
});
|
||||
|
||||
ws.onClose(()=>{
|
||||
this.$emit('change',{status:"CLOSE",msg:"连接已关闭"});
|
||||
})
|
||||
},
|
||||
generateRandom32() {
|
||||
let result = '';
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * chars.length);
|
||||
result += chars[randomIndex];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
frameRecorded({isLastFrame,frameBuffer}){
|
||||
ws.send({data:uni.base64ToArrayBuffer(frameBuffer)});
|
||||
},
|
||||
recordedChunks(base64){
|
||||
|
||||
var fileName = 'YP'+Date.now();
|
||||
this.base64ToFile(base64,fileName,(path)=>{
|
||||
this.$emit('change',{status:"STOP",msg:{
|
||||
path:path,
|
||||
base64:base64
|
||||
}})
|
||||
});
|
||||
//this.$emit('change',{status:"STOP",msg:base64});
|
||||
},
|
||||
//把base64编码转成文件
|
||||
base64ToFile (base64Str, fileName, callback) {
|
||||
|
||||
// 去除base64前缀
|
||||
var index=base64Str.indexOf(',')
|
||||
var base64Str=base64Str.slice(index+1,base64Str.length)
|
||||
|
||||
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function(fs){
|
||||
fs.root.getFile(fileName, {create: true}, function(entry){
|
||||
var fullPath = entry.fullPath;
|
||||
let platform = uni.getSystemInfoSync().platform
|
||||
|
||||
if(platform == 'android'){
|
||||
var FileOutputStream = plus.android.importClass("java.io.FileOutputStream");
|
||||
var Base64 = plus.android.importClass("android.util.Base64");
|
||||
|
||||
try {
|
||||
var out = new FileOutputStream(fullPath);
|
||||
|
||||
// 分块解码 - 每块300KB Base64(解码后约225KB)
|
||||
var CHUNK_SIZE = 300 * 1024; // 300KB
|
||||
var totalLen = base64Str.length;
|
||||
var processedLen = 0;
|
||||
|
||||
while(processedLen < totalLen) {
|
||||
var end = Math.min(processedLen + CHUNK_SIZE, totalLen);
|
||||
var chunk = base64Str.substring(processedLen, end);
|
||||
|
||||
// 确保chunk长度是4的倍数(Base64要求)
|
||||
if(chunk.length % 4 !== 0) {
|
||||
chunk += '='.repeat(4 - chunk.length % 4);
|
||||
}
|
||||
|
||||
var bytes = Base64.decode(chunk, Base64.DEFAULT);
|
||||
out.write(bytes);
|
||||
|
||||
processedLen = end;
|
||||
console.log('解码进度:', Math.floor(processedLen/totalLen*100) + '%');
|
||||
}
|
||||
|
||||
out.close();
|
||||
|
||||
callback && callback(entry.toLocalURL());
|
||||
|
||||
} catch(e) {
|
||||
console.log('分块写入失败:', e.message);
|
||||
// 失败时尝试使用文件写入API
|
||||
this.saveBase64AsFile(fullPath, base64Str, callback);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
saveBase64AsFile(fullPath, base64Str, callback) {
|
||||
// 将base64转为ArrayBuffer
|
||||
var binaryString = atob(base64Str);
|
||||
var bytes = new Uint8Array(binaryString.length);
|
||||
for (var i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// 使用 uni.writeFile
|
||||
uni.writeFile({
|
||||
filePath: fullPath,
|
||||
data: bytes.buffer,
|
||||
success: () => {
|
||||
console.log('uni.writeFile 成功');
|
||||
callback && callback(fullPath);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('uni.writeFile 失败:', err);
|
||||
// 最终方案:直接保存为临时文件
|
||||
this.saveAsTempFile(bytes.buffer, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
props:['options']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<script module="recorder" lang="renderjs">
|
||||
// 保存需要关闭的引用
|
||||
let mediaStream;
|
||||
let audioContext;
|
||||
let processor;
|
||||
// 全局变量存储录音数据
|
||||
let recordedChunks = [];
|
||||
export default {
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
async startRecord(val){
|
||||
|
||||
if(val==null) return;
|
||||
|
||||
if(val == "start"){
|
||||
try{
|
||||
recordedChunks = [];
|
||||
// 配置参数
|
||||
var a, i = 16000,
|
||||
s = 1280;
|
||||
|
||||
audioContext = new AudioContext();
|
||||
const devSampleRate = audioContext.sampleRate; // 例如 44100、48000 等
|
||||
// 加载并初始化AudioWorklet
|
||||
await audioContext.audioWorklet.addModule('static/dist/processor.worklet.js');
|
||||
|
||||
//const mediaStream = new MediaStream();
|
||||
mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
sampleRate: 16000,
|
||||
}
|
||||
});
|
||||
|
||||
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||
processor = new AudioWorkletNode(audioContext, 'processor-worklet');
|
||||
|
||||
// 初始化处理器
|
||||
processor.port.postMessage({
|
||||
type: 'init',
|
||||
data: {
|
||||
frameSize: 1280, // 样本数 (1280字节 / 2字节每样本)
|
||||
fromSampleRate: devSampleRate, // 输入采样率
|
||||
toSampleRate: 16000, // 输出采样率 (1/3)
|
||||
arrayBufferType: 'short16'
|
||||
}
|
||||
});
|
||||
|
||||
// 接收40ms间隔的音频数据
|
||||
processor.port.onmessage = (t) => {
|
||||
|
||||
var r = t.data,
|
||||
o = r.frameBuffer,
|
||||
n = r.isLastFrame;
|
||||
|
||||
if (null == o ? void 0 : o.byteLength)
|
||||
for (var a = 0; a < o.byteLength;) {
|
||||
const frameData = {
|
||||
isLastFrame: n && a + s >= o.byteLength,
|
||||
frameBuffer: t.data.frameBuffer.slice(a, a + s)
|
||||
};
|
||||
this.onFrameRecorded(frameData);
|
||||
|
||||
// 存储录音数据(仅在非暂停状态)
|
||||
recordedChunks.push(frameData.frameBuffer);
|
||||
|
||||
a += s;
|
||||
}
|
||||
else this.onFrameRecorded(t.data);
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
source.connect(processor);
|
||||
processor.connect(audioContext.destination);
|
||||
this.$ownerInstance.callMethod('startRecorder');
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
this.$ownerInstance.callMethod('toShowToast');
|
||||
|
||||
}
|
||||
}else if(val == "stop"){
|
||||
this.onStop();
|
||||
}
|
||||
|
||||
},
|
||||
onFrameRecorded({isLastFrame,frameBuffer}){
|
||||
this.$ownerInstance.callMethod('frameRecorded', {isLastFrame,frameBuffer:this.toBase64(frameBuffer)});
|
||||
},
|
||||
toBase64(buffer){
|
||||
let binary = "";
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
},
|
||||
async onRecordedChunks(chunks){
|
||||
|
||||
var mergedBuffer=this.mergeAudioBuffers(chunks);
|
||||
// 2. 将合并的二进制流转换为WAV格式(需要添加WAV文件头)
|
||||
const wavBlob = this.createWavBlob(mergedBuffer, 1, 16000); // 单声道,44100Hz
|
||||
// 3. 将WAV转为Base64(可选,若需传输)
|
||||
const base64 = await this.blobToBase64(wavBlob);
|
||||
this.$ownerInstance.callMethod('recordedChunks',base64)
|
||||
},
|
||||
onStop(){
|
||||
|
||||
this.onFrameRecorded({isLastFrame:true,frameBuffer:''});
|
||||
|
||||
this.onRecordedChunks(recordedChunks);
|
||||
recordedChunks = [];
|
||||
if (mediaStream) {
|
||||
// 停止所有媒体轨道
|
||||
mediaStream.getTracks().forEach(track => track.stop());
|
||||
mediaStream = null;
|
||||
}
|
||||
|
||||
if (processor) {
|
||||
// 断开音频节点连接
|
||||
processor.disconnect();
|
||||
processor = null;
|
||||
}
|
||||
|
||||
if (audioContext) {
|
||||
// 关闭音频上下文
|
||||
audioContext.close().then(() => {
|
||||
audioContext = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
//合并所有buffer
|
||||
mergeAudioBuffers(buffers) {
|
||||
let totalLength = buffers.reduce((acc, buf) => acc + buf.byteLength, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
buffers.forEach(buffer => {
|
||||
result.set(new Uint8Array(buffer), offset);
|
||||
offset += buffer.byteLength;
|
||||
});
|
||||
// 验证合并后的长度是否正确
|
||||
if (offset !== totalLength) console.error("合并后的长度不符!");
|
||||
return result.buffer;
|
||||
},
|
||||
// 新增WAV封装函数(基于前序回答的createWavBlob)
|
||||
createWavBlob(pcmData, numChannels, sampleRate) {
|
||||
const bytesPerSample = 2; // 16-bit PCM
|
||||
const blockAlign = numChannels * bytesPerSample;
|
||||
const byteRate = sampleRate * blockAlign;
|
||||
const bufferLength = pcmData.byteLength;
|
||||
const totalLength = 44 + bufferLength; // WAV头(44字节) + 音频数据
|
||||
|
||||
const buffer = new ArrayBuffer(totalLength);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// 写入WAV文件头(RIFF、fmt、data区块)
|
||||
this.writeString(view, 0, 'RIFF');
|
||||
view.setUint32(4, totalLength - 8, true);
|
||||
this.writeString(view, 8, 'WAVE');
|
||||
this.writeString(view, 12, 'fmt ');
|
||||
view.setUint32(16, 16, true); // fmt区块大小
|
||||
view.setUint16(20, 1, true); // PCM格式
|
||||
view.setUint16(22, numChannels, true);
|
||||
view.setUint32(24, sampleRate, true);
|
||||
view.setUint32(28, byteRate, true);
|
||||
view.setUint16(32, blockAlign, true);
|
||||
view.setUint16(34, 16, true); // 16位采样
|
||||
this.writeString(view, 36, 'data');
|
||||
view.setUint32(40, bufferLength, true);
|
||||
|
||||
// 写入音频数据(假设pcmData是Uint8Array,需转换为16位PCM)
|
||||
const pcm16 = new Int16Array(pcmData);
|
||||
for (let i = 0; i < pcm16.length; i++) {
|
||||
view.setInt16(44 + i * 2, pcm16[i], true);
|
||||
}
|
||||
|
||||
return new Blob([buffer], { type: 'audio/wav' });
|
||||
},
|
||||
// 辅助函数:Blob转Base64
|
||||
blobToBase64(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
},
|
||||
// 辅助函数:字符串写入DataView
|
||||
writeString(view, offset, string) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
97
src/uni_modules/yao-asdRealSpeech/package.json
Normal file
97
src/uni_modules/yao-asdRealSpeech/package.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"id": "yao-asdRealSpeech",
|
||||
"displayName": "阿里云实时语音识别",
|
||||
"version": "1.0.3",
|
||||
"description": "阿里云实时语音识别 支持:Android,ios",
|
||||
"keywords": [
|
||||
"实时语音识别,语音识别"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0",
|
||||
"uni-app": "^4.07",
|
||||
"uni-app-x": ""
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": "3371387322@qq.com"
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "需要麦克风权限"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "x",
|
||||
"chrome": "x"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "x",
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "x"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x",
|
||||
"alipay": "x",
|
||||
"toutiao": "x",
|
||||
"baidu": "x",
|
||||
"kuaishou": "x",
|
||||
"jd": "x",
|
||||
"harmony": "x",
|
||||
"qq": "x",
|
||||
"lark": "x"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "x",
|
||||
"union": "x"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/uni_modules/yao-asdRealSpeech/readme.md
Normal file
80
src/uni_modules/yao-asdRealSpeech/readme.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# yao-asdRealtimeSpeech
|
||||
|
||||
|
||||
# ##配置
|
||||
需要将模块下uni_modules/yao-asdRealSpeech的dist复制到static目录下面
|
||||
或者
|
||||
将uni_modules/yao-asdRealSpeech/dist目录的配置文件放到static/dist目录下面
|
||||
|
||||
|
||||
|
||||
# 代码示例
|
||||
|
||||
```javascript
|
||||
<template>
|
||||
<view class="content">
|
||||
<view class="btn-start" @click="onStart">开始说话</view>
|
||||
<view class="btn-stop" @click="onStop">停止说话</view>
|
||||
|
||||
<view style="width:90%;margin:0 auto;;">
|
||||
{{text}}
|
||||
</view>
|
||||
|
||||
<yao-asdRealSpeech
|
||||
ref="asdTimeSpeeh"
|
||||
:options="asdOptions"
|
||||
@result="onResult"
|
||||
@change="onChange"
|
||||
></yao-asdRealSpeech>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
text:"",
|
||||
asdOptions:{
|
||||
apikey:"", //自己申请的密钥
|
||||
language_hints:["zh","ja"], //识别语言代码 如果无法提前确定语种,可不设置 (当前设置了中文+日语)
|
||||
},
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
onStart(){
|
||||
//开始识别
|
||||
console.log('开始识别');
|
||||
this.$refs.asdTimeSpeeh.start();
|
||||
this.text="";
|
||||
},
|
||||
onStop(){
|
||||
//停止识别
|
||||
console.log('停止识别');
|
||||
this.$refs.asdTimeSpeeh.stop();
|
||||
},
|
||||
onResult(res){
|
||||
//识别结果
|
||||
console.log(res);
|
||||
this.text = res.sentence.text;
|
||||
},
|
||||
onChange(msg){
|
||||
console.log(msg)
|
||||
if(msg.status == "CLOSE"){
|
||||
//连接已关闭
|
||||
}else if(msg.status == "START"){
|
||||
//开始录音
|
||||
}else if(msg.status == "ERROR"){
|
||||
//连接发生错误
|
||||
}else if(msg.status == "STOP"){
|
||||
console.log("音频文件",msg.msg.path);
|
||||
console.log("音频编码base64",msg.msg.base64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
```
|
||||
Reference in New Issue
Block a user