guoyu/fronted_uniapp/uni_modules/xwq-speech-to-text/utssdk/app-android/index.uts
2025-12-03 18:58:36 +08:00

411 lines
12 KiB
Plaintext

import Context from "android.content.Context"
import File from "java.io.File"
import FileOutputStream from "java.io.FileOutputStream"
import FileInputStream from "java.io.FileInputStream"
import IOException from "java.io.IOException"
import InputStream from "java.io.InputStream"
import AudioFormat from "android.media.AudioFormat"
import AudioRecord from "android.media.AudioRecord"
import MediaRecorder from "android.media.MediaRecorder"
import ZipEntry from "java.util.zip.ZipEntry"// ZIP条目处理
import ZipInputStream from "java.util.zip.ZipInputStream"// ZIP解压流
import Activity from 'android.app.Activity';
import { UTSAndroid } from "io.dcloud.uts";
import ByteArray from "kotlin.ByteArray";
import Environment from 'android.os.Environment'
import Model from "org.vosk.Model"
import Recognizer from "org.vosk.Recognizer"
import RecognitionListener from "org.vosk.android.RecognitionListener"
import SpeechService from "org.vosk.android.SpeechService"
import SpeechStreamService from "org.vosk.android.SpeechStreamService"
import StorageService from "org.vosk.android.StorageService"
import { initPerssion } from './getPermission.uts';
import { CallbackType, InitModelData, SpeechResultCallback, RedultType, DownloadCallback } from '../interface.uts';
const sampleRate : Float = (16000.0).toFloat() // 必须与模型匹配
// 语音监听器类
@UTSJS.keepAlive
class MyRecognitionListener implements RecognitionListener {
private TAG = "SpeechListener";
private activity : Activity | null = null;
private onSpeekResultCallback : SpeechResultCallback | null = null;
constructor(activity : Activity) {
super();
this.activity = activity;
}
setSpeekResultCallback(cb : SpeechResultCallback) {
this.onSpeekResultCallback = cb
}
override onResult(hypothesis : string) : void {
// console.log('hypothesis==', hypothesis)
const result = (JSON.parse(hypothesis) as UTSJSONObject).text ?? '';
this.onSpeekResultCallback?.({
code: 0,
data: {
text: (result as string).replace(/\s/g, '')
}
})
}
override onPartialResult(hypothesis : string) : void {
// console.log("临时结果: " + hypothesis);
}
override onFinalResult(hypothesis : string) : void {
// console.log("最终确认结果: " + hypothesis);
}
override onError(e : Exception) : void {
throw IOException(`识别错误: ${e.message}`, e);
}
override onTimeout() : void {
throw IOException("识别超时")
}
}
class ZipExtractor {
private zis : ZipInputStream | null = null;
private fos : FileOutputStream | null = null;
private recognizer : Recognizer | null = null;
private speechService : SpeechService | null = null;
private unzipPath : string = '';
private voskType : string = "1";
private extractModel(modelZipFile : File, outputDir : File, cb : () => void) : void {
UTSAndroid.getDispatcher("io").async(function (_) {
if (Thread.currentThread().name.contains("DefaultDispatcher")) {
try {
// 确保输出目录为空
if (outputDir.exists()) {
let files = outputDir.list()
if (outputDir.isDirectory && files != null && files.isNotEmpty()) {
// 删除目录及其内容
outputDir.deleteRecursively()
}
} else if (!outputDir.mkdirs()) {
throw IOException("Directory creation failed: " + outputDir.getAbsolutePath())
}
this.zis = new ZipInputStream(new FileInputStream(modelZipFile));
let entry = this.zis!.getNextEntry();
while (entry != null) {
let outputFile : File = new File(outputDir, entry.getName());
//处理目录
if (entry.isDirectory()) {
if (!outputFile.exists() && !outputFile.mkdirs()) {
throw new IOException("Directory creation failed: " + outputFile.getAbsolutePath());
}
}
//处理文件
else {
// 确保父目录存在
let parent = outputFile.getParentFile();
if (parent != null && !parent.exists()) {
if (!parent.mkdirs()) {
throw IOException("Failed to create the parent directory: " + parent);
}
}
// 如果文件已存在,先删除
if (outputFile.exists() && !outputFile.delete()) {
throw IOException("Cannot delete the existing files: " + outputFile.getAbsolutePath())
}
// 创建一个空文件
if (!outputFile.createNewFile()) {
throw IOException("Unable to create the file: " + outputFile.getAbsolutePath())
}
try {
if (!outputFile.isDirectory) {
this.fos = new FileOutputStream(outputFile)
let lens : Int = 1024;
const buffer : ByteArray = new ByteArray(lens);
let len : Int = this.zis!.read(buffer);
while (len > 0) {
this.fos!.write(buffer, 0, len);
len = this.zis!.read(buffer)
}
this.fos!.fd.sync()
}
} catch (e : Error) {
// console.log('e====', e)
}
finally {
if (this.fos != null) {
this.fos!.close();
}
}
}
// 关闭当前条目,准备读取下一个条目
this.zis?.closeEntry()
// 获取下一个条目
entry = this.zis!.getNextEntry()
}
//关闭ZipInputStream
this.zis!.close()
cb();
} catch (e : Error) {
throw IOException(`解压失败: ${e.message}`, e);
} finally {
if (this.zis != null) {
this.zis!.close();
}
}
}
}, null)
}
//初始化模型
initModel(option : InitModelData, cb : (result : CallbackType) => void) {
if (option.voskType != null && option.modelPath != '') {
this.voskType = option.voskType!
}
//已解压过模型,不需要重复解压
if (option.modelPath != null && option.modelPath != '') {
this.unzipPath = option.modelPath!
cb({
code: 0,
data: {
modelPath: this.unzipPath
}
} as CallbackType)
return
}
const suorceSplit = option.zipModelPath.split('/');
const zipName = suorceSplit.pop()?.replace('.zip', '');
// #ifdef UNI-APP-X
const userFilePath = UTSAndroid.convert2AbsFullPath(uni.env.USER_DATA_PATH);
let cpFilePath = userFilePath + suorceSplit[suorceSplit.length - 1];
const fileManager = uni.getFileSystemManager();
//项目目录地址才复制,网络下载直接就是应用缓存地址
if (option.zipModelPath.startsWith('/static')) {
fileManager.copyFileSync(option.zipModelPath, cpFilePath);
} else {
cpFilePath = option.zipModelPath;
}
const outZipPath = UTSAndroid.convert2AbsFullPath(uni.env.USER_DATA_PATH + '/vosk-model');
const modelZip : File = new File(cpFilePath);
const outputDir : File = new File(outZipPath);
this.extractModel(modelZip, outputDir, () => {
const uuidFile : File = new File(outputDir, `/${zipName}/uuid`);
uuidFile.createNewFile();
this.unzipPath = new File(outputDir, `/${zipName}`).getAbsolutePath();
cb({
code: 0,
data: {
modelPath: this.unzipPath
}
} as CallbackType)
});
// #endif
// #ifdef APP-PLUS
const status = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
if (!status) {
throw IOException("External storage is unavailable")
}
const ExternalPath = this.getExternalPath();
const modelZipTemp : File = new File(option.zipModelPath);
const outputDirTemp : File = new File(`${ExternalPath}/${zipName}`);
this.extractModel(modelZipTemp, outputDirTemp, () => {
const uuidFile : File = new File(outputDirTemp, `/${zipName}/uuid`);
uuidFile.createNewFile();
this.unzipPath = new File(outputDirTemp, `/${zipName}`).getAbsolutePath();
cb({
code: 0,
data: {
modelPath: this.unzipPath
}
} as CallbackType)
});
// #endif
}
/**
* plus项目获取外置储存file目录
*/
getExternalPath() : string {
const externalDir = UTSAndroid.getAppContext()!.getExternalFilesDir(null)
const ExternalPath : string = externalDir!.getAbsolutePath() ?? ""
return ExternalPath;
}
//开始识别
startSpeechVoice(cb : SpeechResultCallback) {
UTSAndroid.getDispatcher("io").async(function (_) {
let model = new Model(this.unzipPath) // 加载模型
this.recognizer = new Recognizer(model, sampleRate) // 采样率16kHz
//实时语音识别
if (this.voskType === '1') {
this.speechService = new SpeechService(this.recognizer!, sampleRate);
const recognitionListenerClass:MyRecognitionListener=new MyRecognitionListener(UTSAndroid.getUniActivity()!);
recognitionListenerClass.setSpeekResultCallback(cb);
this.speechService!.startListening(recognitionListenerClass);
} else {
// 读取音频文件(需先转换为 PCM 格式)
const audioFile = new File(UTSAndroid.convert2AbsFullPath('/static/test01.wav'))
const inputStream = new FileInputStream(audioFile)
const buffer = new ByteArray(4096)
while (inputStream.read(buffer) != -1) {
if (this.recognizer!.acceptWaveForm(buffer, buffer.size)) {
// console.log(this.recognizer!.result)
}
}
}
}, null)
}
//停止识别
stopSpeechVoice() {
this.speechService?.stop()
this.speechService?.reset()
}
}
const voskTool : ZipExtractor = ZipExtractor();
/**
* 初始化模型
*/
export function initVoskModel(option : InitModelData, cb : (result : CallbackType) => void) {
initPerssion(['android.permission.RECORD_AUDIO', 'android.permission.WRITE_EXTERNAL_STORAGE', 'android.permission.READ_EXTERNAL_STORAGE']).then(res => {
if (res.isPass) {
voskTool.initModel(option, cb);
} else {
initVoskModel(option, cb)
}
})
}
/**
* 开始识别
*/
@UTSJS.keepAlive
export function startSpeechVoice(cb : SpeechResultCallback) {
voskTool.startSpeechVoice(cb)
}
/**
* 停止识别
*/
export function stopSpeechVoice() {
voskTool.stopSpeechVoice()
}
/**
* 下载模型
*/
export function downloadModel(url : string, cb : DownloadCallback) {
// #ifdef UNI-APP-X
const userFilePath = UTSAndroid.convert2AbsFullPath(uni.env.USER_DATA_PATH);
const suorceSplit = url.split('/');
const cpFilePath = userFilePath + suorceSplit.pop();
const isfileExist = new File(cpFilePath);
if (isfileExist.exists()) {
cb({
code: 0,
filePath: cpFilePath
})
return
}
UTSAndroid.getDispatcher("io").async(function (_) {
try {
const loadTask = uni.downloadFile({
url: url,
filePath: cpFilePath,
success: (result : DownloadFileSuccess) => {
cb({
code: 0,
filePath: result.tempFilePath
})
},
fail: (result : DownloadFileFail) => {
cb({
code: -1,
filePath: ''
})
throw IOException(`Model download failed: ${result.errCode}`, result.cause);
}
});
loadTask?.onProgressUpdate((update : OnProgressDownloadResult) => {
console.log("模型下载进度: ", update.progress + '%');
})
} catch (e : Error) {
console.log('模型下载失败===', e)
}
}, null)
// #endif
// #ifdef APP-PLUS
const userFilePathTemp = voskTool.getExternalPath();
const suorceSplitTemp = url.split('/');
const cpFilePathTemp = `${userFilePathTemp}/${suorceSplitTemp.pop()}`;
UTSAndroid.getDispatcher("io").async(function (_) {
try {
const loadTask = uni.downloadFile({
url: url,
filePath: cpFilePathTemp,
success: (result : DownloadFileSuccess) => {
cb({
code: 0,
filePath: result.tempFilePath
})
},
fail: (result : DownloadFileFail) => {
cb({
code: -1,
filePath: ''
})
throw IOException(`Model download failed: ${result.errCode}`, result.cause);
}
});
loadTask?.onProgressUpdate((update : OnProgressDownloadResult) => {
console.log("模型下载进度: ", update.progress + '%');
})
} catch (e : Error) {
console.log('模型下载失败===', e)
}
}, null)
// #endif
}