阿里云百炼 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>

识别错别字比较多

相关推荐
ShineWinsu4 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
lemon_yyds7 小时前
Vue 项目嵌入 React H5 技术方案
vue.js·react.js
顽疲7 小时前
java springboot vue 养老系统源码详解:请假 care_leave 销假与床位停费日扣实践
java·vue.js·spring boot
szp20057 小时前
Cache API 会丢掉 URL 里的 #:我的 9 个模型分块一直写在同一条缓存上
javascript·浏览器
keyipatience8 小时前
TypeScript学习(1)
前端·javascript·学习·typescript·html·css3
xifangge20258 小时前
基于 JavaScript 实现 CNC 数控切削参数与螺纹公差纯前端核算引擎
开发语言·前端·javascript
honkun69 小时前
vue 表格组件 vxe-table 修改滚动条样式,修改滚动条样式,统一宽度、颜色与自动隐藏
前端·javascript·vue.js
FRing0310 小时前
JavaScript入门到精通二十四(正则表达式、元字符、修饰符)
开发语言·前端·javascript·正则表达式
171320330计算机毕设编程10 小时前
计算机毕业设计 基于springboot+vue的在线观鸟视频播放网站 LW PPT
java·大数据·vue.js·spring boot·算法·课程设计
大明二代10 小时前
从 RAG 到有记忆的 Agent:Mastra Memory 与 PostgreSQL 实战
前端·javascript·人工智能