阿里云百炼 fun-asr-realtime uniapp vue3 调用

复制代码
<template>
	<view class="container">
		<view class="status-bar">
			<text class="status-text">{{ statusText }}</text>
		</view>

		<scroll-view class="transcript-area" scroll-y>
			<text class="transcript-text">{{ transcript || '识别结果将显示在这里...' }}</text>
		</scroll-view>

		<view class="button-group">
			<button class="action-btn start-btn" :disabled="isRecording" @click="startRecording">
				开始录音
			</button>
			<button class="action-btn stop-btn" :disabled="!isRecording" @click="stopRecording">
				停止录音
			</button>
		</view>
	</view>
</template>

<script setup>
	import {
		ref,
		onUnmounted
	} from 'vue'
	// ==================== 配置区域 ====================
	// ⚠️ 安全提醒:生产环境请勿将 API Key 硬编码在前端。
	// 建议由后端签发临时 Token(60秒有效),或通过 WebSocket 中转服务代理。
	const CONFIG = {
		// 替换为你的百炼 Workspace ID
		workspaceId: 'llm-kivup',
		// 替换为你的 API Key(仅用于测试,生产环境务必替换)
		apiKey: 'sk-24d81b9',
		// 模型名称
		model: 'fun-asr-realtime'//-2026-02-28
	}
	// =================================================

	// 响应式状态
	const statusText = ref('准备就绪')
	const transcript = ref('')
	const isRecording = ref(false)

	// 内部状态(非响应式)
	let ws = null
	let mediaStream = null
	let audioContext = null
	let sourceNode = null
	let workletNode = null
	let taskId = null
	let isTaskStarted = false
	
	let socketTask = null

	// ==================== 工具函数 ====================
	const generateTaskId = () => {
		return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
			const r = Math.random() * 16 | 0
			const v = c === 'x' ? r : (r & 0x3 | 0x8)
			return v.toString(16)
		})
	}

	// 将 Float32 音频数据转换为 16-bit PCM 的 ArrayBuffer
	const floatTo16BitPCM = (float32Array) => {
		const buffer = new ArrayBuffer(float32Array.length * 2)
		const view = new DataView(buffer)
		for (let i = 0; i < float32Array.length; i++) {
			const s = Math.max(-1, Math.min(1, float32Array[i]))
			view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
		}
		return buffer
	}

	// ==================== WebSocket 管理 ====================
	const connectWebSocket = () => {
		return new Promise((resolve, reject) => {
			//?token=${CONFIG.apiKey}
			const url = `wss://${CONFIG.workspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference`

			// 使用 uni.connectSocket,明确传入 Authorization 头
			socketTask = uni.connectSocket({
				url: url,
				header: {
					// 关键:百炼要求 Bearer <api_key> 格式,注意大小写和空格
					'Authorization': `Bearer ${CONFIG.apiKey}`
				},
				success: () => {
					console.log('WebSocket 连接发起成功')
				},
				fail: (err) => {
					console.error('WebSocket 连接失败', err)
					statusText.value = '连接失败,请检查 API Key 和网络'
					reject(err)
				}
			})

			// 监听连接打开事件
			uni.onSocketOpen(() => {
				console.log('WebSocket 已连接')
				resolve()
			})

			uni.onSocketError((err) => {
				console.error('WebSocket 错误', err)
				statusText.value = '连接错误,请检查鉴权信息'
				console.log('ccccccccccccccccc',isTaskStarted)
				reject(err)
			})

			uni.onSocketClose(() => {
				console.log('WebSocket 已关闭')
				isTaskStarted = false
			})

			uni.onSocketMessage((res) => {
				try {
					
					//console.log('bbbbbbbbbbbb',res.data)
					const data = JSON.parse(res.data)
					handleServerEvent(data)
				} catch (e) {
					// 忽略非 JSON 消息
				}
			})
		})
	}

	// ==================== 服务端事件处理 ====================
	const handleServerEvent = (data) => {
		const header = data.header || {}
		const payload = data.payload || {}

		switch (header.event) {
			case 'task-started':
				console.log('任务已启动')
				isTaskStarted = true
				statusText.value = '正在识别...'
				console.log('aaaaaaaaaa',isTaskStarted)
				break

			case 'result-generated':
				// 处理识别结果
				if (payload.output) {
					const sentence = payload.output.sentence
					if (sentence && sentence.text) {
						// 如果是最终结果,追加到 transcript
						
						console.log('ttttttttttt',sentence.text)
						if (sentence.sentence_end) {
							transcript.value += sentence.text
						} else {
							// 中间结果,可以实时显示(这里简单追加,实际可根据需求优化 UI)
							// 为简化,这里仅在最终结果时追加
						}
					}
				}
				break

			case 'task-finished':
				console.log('任务已结束')
				statusText.value = '识别完成'
				isTaskStarted = false
				break

			case 'task-failed':
				console.error('任务失败', payload)
				statusText.value = `任务失败: ${payload.message || '未知错误'}`
				isTaskStarted = false
				break

			default:
				console.log('收到事件:', header.event, data)
		}
	}

	// ==================== 发送指令 ====================
	const sendRunTask = () => {
		taskId = generateTaskId()
		const message = {
			header: {
				action: 'run-task',
				task_id: taskId,
				streaming: 'duplex'
			},
			payload: {
				task_group: 'audio',
				task: 'asr',
				function: 'recognition',
				model: CONFIG.model,
				parameters: {
					format: 'pcm',
					sample_rate: 16000
				},
				input: {}
			}
		}
		//uni.sendSocketMessage(JSON.stringify(message))
		
		uni.sendSocketMessage({
		  data: JSON.stringify(message),
		  fail(err) { console.error('send 失败', err) }
		})
		console.log('已发送 run-task:', message)
	}

	const sendFinishTask = () => {
		if (ws && ws.readyState === WebSocket.OPEN && taskId) {
			const message = {
				header: {
					action: 'finish-task',
					task_id: taskId,
					streaming: 'duplex'
				},
				payload: {}
			}
			//uni.sendSocketMessage(JSON.stringify(message))
			uni.sendSocketMessage({
			  data: JSON.stringify(message),
			  fail(err) { console.error('send 失败', err) }
			})
			console.log('已发送 finish-task')
		}
	}

	// ==================== 音频采集 ====================
	let ar = null
	
	const startAudioCapture = async () => {
	    try {
	        // 1. 引入原生插件(建议在 script 顶部统一声明,这里为聚焦方法内部逻辑)

	        ar = uni.requireNativePlugin("YL-AudioRecorder");

	        // 2. 配置参数:16kHz, 单声道, 16bit
	        // 这与你的 H5 版本参数一致,也是 STT 标准输入格式
	        ar.setConfig(16000, 1, 16);
	
	        // 3. 开始录音,并在回调中直接发送 PCM 数据
	        // 注意:App 端没有 isTaskStarted/ws 的浏览器对象,需用你自己的状态变量
	        /* ar.start((res) => {
	            // res.buffer 是 Uint8Array 类型的原始 PCM 帧
	            // 只有当 socket 连接就绪且任务已启动时才发送
				
				//console.log('aaa',res.buffer.buffer)
				console.log('aaab',isTaskStarted)
				//console.log('aaab',socketTask)

				
	            if (!res.buffer || !isTaskStarted || !socketTask) return;
	
	            // 关键:发送底层的 ArrayBuffer,而不是 Uint8Array 本身
	            // socketTask.send 在 App 端支持 ArrayBuffer
				console.log('aaa',res.buffer.buffer)
	            socketTask.send({
	                data: res.buffer.buffer
	            });
	        }); */
			
			ar.start((res) => {
				console.log('res.buffer',!res.buffer);
				console.log('isTaskStarted',!isTaskStarted);
				
			    if (!res.buffer || !isTaskStarted) return;
			
			    let data = res.buffer;
			    if (data instanceof Uint8Array) {
			        // TypedArray.slice() 会拷贝出一段独立的内存,避免和插件的内存池竞争
			        data = data.slice().buffer;
			    }
			    socketTask.send({
			        data,
			        fail(err) { console.error('PCM 发送失败:', err); }
			    });
			});
	
	        console.log('音频采集已启动 (YL-AudioRecorder)');
	    } catch (err) {
	        console.error('获取麦克风权限失败:', err);
	        statusText.value = '无法访问麦克风,请检查权限';
	        throw err;
	    }
	};

	const stopAudioCapture = () => {
		ar.stop();          // 停止录音
		console.log('音频采集已停止')
	}

	// ==================== 对外方法 ====================
	const startRecording = async () => {
		if (isRecording.value) return

		try {
			statusText.value = '正在连接...'
			transcript.value = ''

			// 1. 建立 WebSocket 连接
			await connectWebSocket()

			// 2. 发送 run-task 指令
			sendRunTask()

			// 3. 等待 task-started 事件后开始采集音频
			// 这里简单等待一下,实际应通过事件回调控制
			await new Promise(resolve => setTimeout(resolve, 1000))

			// 4. 启动音频采集
			await startAudioCapture()

			isRecording.value = true
			statusText.value = '正在录音...'
		} catch (err) {
			console.error('启动失败:', err)
			statusText.value = '启动失败,请重试'
			cleanup()
		}
	}

	const stopRecording = () => {
		if (!isRecording.value) return

		isRecording.value = false
		statusText.value = '正在完成...'

		// 1. 停止音频采集
		stopAudioCapture()

		// 2. 发送 finish-task
		sendFinishTask()

		cleanup()
	}

	const cleanup = () => {
		stopAudioCapture()
		
		// 正常关闭
		socketTask.close({
		    code: 1000,              // 1000 表示正常关闭,默认值
		    reason: '用户停止录音',   // 可选,关闭原因
		    success: () => {
		        console.log('WebSocket 已关闭')
		    },
		    fail: (err) => {
		        console.error('关闭失败', err)
		    }
		})
		
		socketTask = null;
		
		isRecording.value = false
		isTaskStarted = false
	}

	// 组件卸载时清理资源
	onUnmounted(() => {
		cleanup()
	})
</script>

<style scoped>
	.container {
		display: flex;
		flex-direction: column;
		height: 100vh;
		padding: 20rpx;
		box-sizing: border-box;
		background-color: #f5f5f5;
	}

	.status-bar {
		padding: 20rpx;
		background-color: #fff;
		border-radius: 12rpx;
		margin-bottom: 20rpx;
	}

	.status-text {
		font-size: 28rpx;
		color: #666;
	}

	.transcript-area {
		flex: 1;
		background-color: #fff;
		border-radius: 12rpx;
		padding: 24rpx;
		margin-bottom: 20rpx;
		box-sizing: border-box;
	}

	.transcript-text {
		font-size: 32rpx;
		line-height: 1.6;
		color: #333;
		word-break: break-all;
	}

	.button-group {
		display: flex;
		gap: 20rpx;
	}

	.action-btn {
		flex: 1;
		height: 88rpx;
		line-height: 88rpx;
		border-radius: 12rpx;
		font-size: 32rpx;
		border: none;
	}

	.start-btn {
		background-color: #07c160;
		color: #fff;
	}

	.start-btn:disabled {
		background-color: #9edba9;
		color: #fff;
	}

	.stop-btn {
		background-color: #ee0a24;
		color: #fff;
	}

	.stop-btn:disabled {
		background-color: #f5a3a8;
		color: #fff;
	}
</style>

识别错别字比较多

相关推荐
李剑一2 小时前
AntDesign老版本没有月份和年份区间选择组件?我封装了一个完美版,附源码!
前端·vue.js
默_笙2 小时前
🚊 别再让 AI 的记忆写两遍了:PostgreSQL 把关系查询和语义检索装进一张表
前端·javascript
lzhdim2 小时前
C#不为人知的10个魔法特性:资深开发者也会震惊的底层奥秘
java·前端·javascript·算法·c#
计算机毕设定制辅导-无忧学长2 小时前
《基于Spring Boot的减脂轻食购物网站》
java·vue.js·spring boot·后端·减脂轻食购物网站
只睡四小时3 小时前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
一 乐4 小时前
博客管理系统|基于springboot + vue博客管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕设
喵个咪5 小时前
GoWind Admin|风行 — 开箱即用的企业级全栈中后台框架:脚本系统实战
javascript·后端·go
樱花落木兰5 小时前
SpringBoot + ECharts 后台数据统计报表模块实战
java·javascript·spring boot·ai·log4j·github·echarts