chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:10 +08:00
commit c397331b1e
3684 changed files with 990993 additions and 0 deletions
View File
@@ -0,0 +1,71 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.yeyupiaoling.androidclient'
compileSdk 33
defaultConfig {
applicationId "com.yeyupiaoling.androidclient"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.4.3'
}
packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.activity:activity-compose:1.7.0'
implementation platform('androidx.compose:compose-bom:2023.03.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.yeyupiaoling.androidclient
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.yeyupiaoling.androidclient", appContext.packageName)
}
}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/logo"
android:label="@string/app_name"
android:roundIcon="@drawable/logo"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidClient"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,216 @@
package com.yeyupiaoling.androidclient;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class AudioView extends View {
// 频谱数量
private static final int LUMP_COUNT = 128;
private static final int LUMP_WIDTH = 6;
private static final int LUMP_SPACE = 2;
private static final int LUMP_MIN_HEIGHT = LUMP_WIDTH;
private static final int LUMP_MAX_HEIGHT = 200;//TODO: HEIGHT
private static final int LUMP_SIZE = LUMP_WIDTH + LUMP_SPACE;
private static final int LUMP_COLOR = Color.parseColor("#6de8fd");
private static final int WAVE_SAMPLING_INTERVAL = 3;
private static final float SCALE = LUMP_MAX_HEIGHT / LUMP_COUNT;
private ShowStyle upShowStyle = ShowStyle.STYLE_HOLLOW_LUMP;
private ShowStyle downShowStyle = ShowStyle.STYLE_WAVE;
private byte[] waveData;
List<Point> pointList;
private Paint lumpPaint;
Path wavePath = new Path();
public AudioView(Context context) {
super(context);
init();
}
public AudioView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public AudioView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
lumpPaint = new Paint();
lumpPaint.setAntiAlias(true);
lumpPaint.setColor(LUMP_COLOR);
lumpPaint.setStrokeWidth(2);
lumpPaint.setStyle(Paint.Style.STROKE);
}
public void setWaveData(byte[] data) {
this.waveData = readyData(data);
genSamplingPoint(data);
invalidate();
}
public void setStyle(ShowStyle upShowStyle, ShowStyle downShowStyle) {
this.upShowStyle = upShowStyle;
this.downShowStyle = downShowStyle;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
wavePath.reset();
for (int i = 0; i < LUMP_COUNT; i++) {
if (waveData == null) {
canvas.drawRect((LUMP_WIDTH + LUMP_SPACE) * i,
LUMP_MAX_HEIGHT - LUMP_MIN_HEIGHT,
(LUMP_WIDTH + LUMP_SPACE) * i + LUMP_WIDTH,
LUMP_MAX_HEIGHT,
lumpPaint);
continue;
}
switch (upShowStyle) {
case STYLE_HOLLOW_LUMP:
drawLump(canvas, i, false);
break;
case STYLE_WAVE:
drawWave(canvas, i, false);
break;
default:
break;
}
switch (downShowStyle) {
case STYLE_HOLLOW_LUMP:
drawLump(canvas, i, true);
break;
case STYLE_WAVE:
drawWave(canvas, i, true);
break;
default:
break;
}
}
}
/**
* 预处理数据
*
* @return
*/
private static byte[] readyData(byte[] fft) {
byte[] newData = new byte[LUMP_COUNT];
byte abs;
for (int i = 0; i < LUMP_COUNT; i++) {
abs = (byte) Math.abs(fft[i]);
//描述:Math.abs -128时越界
newData[i] = abs < 0 ? 127 : abs;
}
return newData;
}
/**
* 绘制曲线
*
* @param canvas
* @param i
* @param reversal
*/
private void drawWave(Canvas canvas, int i, boolean reversal) {
if (pointList == null || pointList.size() < 2) {
return;
}
float ratio = SCALE * (reversal ? -1 : 1);
if (i < pointList.size() - 2) {
Point point = pointList.get(i);
Point nextPoint = pointList.get(i + 1);
int midX = (point.x + nextPoint.x) >> 1;
if (i == 0) {
wavePath.moveTo(point.x, LUMP_MAX_HEIGHT - point.y * ratio);
}
wavePath.cubicTo(midX, LUMP_MAX_HEIGHT - point.y * ratio,
midX, LUMP_MAX_HEIGHT - nextPoint.y * ratio,
nextPoint.x, LUMP_MAX_HEIGHT - nextPoint.y * ratio);
canvas.drawPath(wavePath, lumpPaint);
}
}
/**
* 绘制矩形条
*/
private void drawLump(Canvas canvas, int i, boolean reversal) {
int minus = reversal ? -1 : 1;
float top = (LUMP_MAX_HEIGHT - (LUMP_MIN_HEIGHT + waveData[i] * SCALE) * minus);
canvas.drawRect(LUMP_SIZE * i,
top,
LUMP_SIZE * i + LUMP_WIDTH,
LUMP_MAX_HEIGHT,
lumpPaint);
}
/**
* 生成波形图的采样数据,减少计算量
*
* @param data
*/
private void genSamplingPoint(byte[] data) {
if (upShowStyle != ShowStyle.STYLE_WAVE && downShowStyle != ShowStyle.STYLE_WAVE) {
return;
}
if (pointList == null) {
pointList = new ArrayList<>();
} else {
pointList.clear();
}
pointList.add(new Point(0, 0));
for (int i = WAVE_SAMPLING_INTERVAL; i < LUMP_COUNT; i += WAVE_SAMPLING_INTERVAL) {
pointList.add(new Point(LUMP_SIZE * i, waveData[i]));
}
pointList.add(new Point(LUMP_SIZE * LUMP_COUNT, 0));
}
/**
* 可视化样式
*/
public enum ShowStyle {
/**
* 空心的矩形小块
*/
STYLE_HOLLOW_LUMP,
/**
* 曲线
*/
STYLE_WAVE,
/**
* 不显示
*/
STYLE_NOTHING
}
}
@@ -0,0 +1,361 @@
package com.yeyupiaoling.androidclient;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class MainActivity extends AppCompatActivity {
public static final String TAG = MainActivity.class.getSimpleName();
// WebSocket地址
public String ASR_HOST = "";
// 官方WebSocket地址
public static final String DEFAULT_HOST = "wss://101.37.77.25:10088";
// 发送的JSON数据
public static final String MODE = "2pass";
public static final String CHUNK_SIZE = "5, 10, 5";
public static final int CHUNK_INTERVAL = 10;
public static final int SEND_SIZE = 1920;
// 热词
private String hotWords = "阿里巴巴 20\n达摩院 20\n夜雨飘零 20\n";
// 采样率
public static final int SAMPLE_RATE = 16000;
// 声道数
public static final int CHANNEL = AudioFormat.CHANNEL_IN_MONO;
// 返回的音频数据的格式
public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private AudioRecord audioRecord;
private boolean isRecording = false;
private AudioView audioView;
private String allAsrText = "";
private String asrText = "";
private SharedPreferences sharedPreferences;
// 控件
private Button recordBtn;
private TextView resultText;
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 请求权限
if (!hasPermission()) {
requestPermission();
}
// 显示识别结果控件
resultText = findViewById(R.id.result_text);
// 显示录音状态控件
audioView = findViewById(R.id.audioView);
audioView.setStyle(AudioView.ShowStyle.STYLE_HOLLOW_LUMP, AudioView.ShowStyle.STYLE_NOTHING);
// 按下识别按钮
recordBtn = findViewById(R.id.record_button);
recordBtn.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (!ASR_HOST.equals("")) {
isRecording = false;
stopRecording();
recordBtn.setText("按下录音");
}
} else if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (!ASR_HOST.equals("")) {
allAsrText = "";
asrText = "";
isRecording = true;
startRecording();
recordBtn.setText("录音中...");
}
}
return true;
});
// 读取WebSocket地址
sharedPreferences = getSharedPreferences("FunASR", MODE_PRIVATE);
String uri = sharedPreferences.getString("uri", "");
if (uri.equals("")) {
showUriInput();
} else {
ASR_HOST = uri;
}
// 读取热词
String hotWords = sharedPreferences.getString("hotwords", null);
if (hotWords != null) {
this.hotWords = hotWords;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if (id == R.id.change_uri) {
showUriInput();
return true;
} else if (id == R.id.change_hotwords) {
showHotWordsInput();
return true;
}
return super.onOptionsItemSelected(item);
}
// 显示WebSocket地址输入框
private void showUriInput() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("请输入WebSocket地址:");
View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.dialog_input_uri, null);
final EditText input = view.findViewById(R.id.uri_edit_text);
if (!ASR_HOST.equals("")) {
input.setText(ASR_HOST);
}
builder.setView(view);
builder.setPositiveButton("确定", (dialog, id) -> {
ASR_HOST = input.getText().toString();
if (!ASR_HOST.equals("")) {
Toast.makeText(MainActivity.this, "WebSocket地址:" + ASR_HOST, Toast.LENGTH_SHORT).show();
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("uri", ASR_HOST);
editor.apply();
}
});
builder.setNeutralButton("使用官方服务", (dialog, id) -> {
ASR_HOST = DEFAULT_HOST;
input.setText(DEFAULT_HOST);
Toast.makeText(MainActivity.this, "WebSocket地址:" + ASR_HOST, Toast.LENGTH_SHORT).show();
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("uri", ASR_HOST);
editor.apply();
});
AlertDialog dialog = builder.create();
dialog.show();
}
// 显示热词输入框
private void showHotWordsInput() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("请输入热词:");
View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.dialog_input_hotwords, null);
final EditText input = view.findViewById(R.id.hotwords_edit_text);
if (!this.hotWords.equals("")) {
input.setText(this.hotWords);
}
builder.setView(view);
builder.setPositiveButton("确定", (dialog, id) -> {
String hotwords = input.getText().toString();
this.hotWords = hotwords;
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("hotwords", hotwords);
editor.apply();
});
AlertDialog dialog = builder.create();
dialog.show();
}
// 开始录音
private void startRecording() {
// 准备录音器
try {
// 确保有权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
requestPermission();
return;
}
// 创建录音器
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL, AUDIO_FORMAT, SEND_SIZE);
} catch (IllegalStateException e) {
e.printStackTrace();
}
// 开启一个线程将录音数据写入文件
Thread recordingAudioThread = new Thread(() -> {
try {
setAudioData();
} catch (Exception e) {
e.printStackTrace();
}
});
recordingAudioThread.start();
// 启动录音器
audioRecord.startRecording();
audioView.setVisibility(View.VISIBLE);
}
// 停止录音器
private void stopRecording() {
audioRecord.stop();
audioRecord.release();
audioRecord = null;
audioView.setVisibility(View.GONE);
}
// 读取录音数据
private void setAudioData() throws Exception {
// 建立WebSocket连接
OkHttpClient client = new OkHttpClient.Builder()
// 忽略验证证书
.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager())
// 不验证域名
.hostnameVerifier(SSLSocketClient.getHostnameVerifier())
.build();
Request request = new Request.Builder()
.url(ASR_HOST)
.build();
WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
// 连接成功时的处理
Log.d(TAG, "WebSocket连接成功");
runOnUiThread(() -> Toast.makeText(MainActivity.this, "WebSocket连接成功", Toast.LENGTH_SHORT).show());
}
@Override
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
// 接收到消息时的处理
Log.d(TAG, "WebSocket接收到消息: " + text);
try {
JSONObject jsonObject = new JSONObject(text);
String t = jsonObject.getString("text");
boolean isFinal = jsonObject.getBoolean("is_final");
if (!t.equals("")) {
// 拼接识别结果
String mode = jsonObject.getString("mode");
if (mode.equals("2pass-offline")) {
asrText = "";
allAsrText = allAsrText + t;
// 这里可以做一些自动停止录音识别的程序
} else {
asrText = asrText + t;
}
}
// 显示语音识别结果消息
if (!(allAsrText + asrText).equals("")) {
runOnUiThread(() -> resultText.setText(allAsrText + asrText));
}
// 如果检测的录音停止就关闭WebSocket连接
if (isFinal) {
webSocket.close(1000, "关闭WebSocket连接");
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
@Override
public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
// 关闭连接时的处理
Log.d(TAG, "WebSocket关闭连接: " + reason);
}
@Override
public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, Response response) {
// 连接失败时的处理
Log.d(TAG, "WebSocket连接失败: " + t + ": " + response);
runOnUiThread(() -> Toast.makeText(MainActivity.this, "WebSocket连接失败:" + t, Toast.LENGTH_SHORT).show());
}
});
String message = getMessage(true);
Log.d(TAG, "WebSocket发送消息: " + message);
webSocket.send(message);
audioRecord.startRecording();
byte[] bytes = new byte[SEND_SIZE];
while (isRecording) {
int readSize = audioRecord.read(bytes, 0, SEND_SIZE);
if (readSize > 0) {
ByteString byteString = ByteString.of(bytes);
webSocket.send(byteString);
audioView.post(() -> audioView.setWaveData(bytes));
}
}
JSONObject obj = new JSONObject();
obj.put("is_speaking", false);
webSocket.send(obj.toString());
// webSocket.close(1000, "关闭WebSocket连接");
}
// 发送第一步的JSON数据
public String getMessage(boolean isSpeaking) {
try {
JSONObject obj = new JSONObject();
obj.put("mode", MODE);
JSONArray array = new JSONArray();
String[] chunkList = CHUNK_SIZE.split(",");
for (String s : chunkList) {
array.put(Integer.valueOf(s.trim()));
}
obj.put("chunk_size", array);
obj.put("chunk_interval", CHUNK_INTERVAL);
obj.put("wav_name", "default");
if (!hotWords.equals("")) {
JSONObject hotwordsJSON = new JSONObject();
// 分割每一行字符串
String[] hotWordsList = hotWords.split("\n");
for (String s : hotWordsList) {
if (s.equals("")) {
Log.w(TAG, "hotWords为空");
continue;
}
// 按照空格分割字符串
String[] hotWordsArray = s.split(" ");
if (hotWordsArray.length != 2) {
Log.w(TAG, "hotWords格式不正确");
continue;
}
hotwordsJSON.put(hotWordsArray[0], Integer.valueOf(hotWordsArray[1]));
}
obj.put("hotwords", hotwordsJSON.toString());
}
obj.put("wav_format", "pcm");
obj.put("is_speaking", isSpeaking);
return obj.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
// 检查权限
private boolean hasPermission() {
return checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED &&
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
// 请求权限
private void requestPermission() {
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
}
@@ -0,0 +1,71 @@
package com.yeyupiaoling.androidclient;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class SSLSocketClient {
//获取SSLSocketFactory
public static SSLSocketFactory getSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, getTrustManager(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//获取X509TrustManager
public static X509TrustManager getX509TrustManager() {
X509TrustManager x509TrustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
return x509TrustManager;
}
//获取TrustManager
private static TrustManager[] getTrustManager() {
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
return trustAllCerts;
}
//获取HostnameVerifier
public static HostnameVerifier getHostnameVerifier() {
HostnameVerifier hostnameVerifier = (s, sslSession) -> true;
return hostnameVerifier;
}
}
@@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="#000000" />
<padding android:left="5dp" android:top="5dp" android:right="5dp" android:bottom="5dp" />
</shape>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/record_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
android:text="按下录音" />
<com.yeyupiaoling.androidclient.AudioView
android:id="@+id/audioView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_above="@id/record_button"
android:layout_marginStart="10dp"
android:visibility="gone" />
<TextView
android:id="@+id/result_text"
android:layout_above="@id/record_button"
android:layout_width="match_parent"
android:hint="显示识别结果"
android:textSize="22sp"
android:layout_height="match_parent"/>
</RelativeLayout>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/hotwords_edit_text"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_margin="10dp"
android:inputType="textMultiLine"
android:background="@drawable/edittext_border"
android:minLines="3"
android:gravity="top"
android:hint="每一行为:热词 权重" />
</LinearLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/uri_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/edittext_border"
android:hint="wss://" />
</LinearLayout>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/change_uri"
android:title="服务地址" />
<item
android:id="@+id/change_hotwords"
android:title="热词" />
</menu>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">FunASR</string>
</resources>
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AndroidClient" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!--
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.yeyupiaoling.androidclient
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.1.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
}
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
Binary file not shown.
@@ -0,0 +1,6 @@
#Fri Oct 13 14:55:29 CST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "AndroidClient"
include ':app'
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

+20
View File
@@ -0,0 +1,20 @@
# AndroidClient
先说明,本项目是使用WebSocket连接服务器的语音识别服务,并不是将FunASR部署到Android里,服务启动方式请查看文档[SDK_advanced_guide_online_zh.md](https://github.com/modelscope/FunASR/blob/main/funasr/runtime/docs/SDK_advanced_guide_online_zh.md)。
使用最新的 Android Studio 打开`AndroidClient`项目,运行即可。也可以直接下载[APK安装包](https://yeyupiaoling.cn/AndroidClient.apk)安装使用,或者使用手机扫码下载。
<div align="center">
<img src="./images/QRcode.png" alt="APK安装包" width="300">
</div>
应用只有一个功能,按钮下开始识别,松开按钮结束识别。第一次打开应用需要设置WebSocket的地址,也可以在菜单栏修改,同时也可以在菜单栏修改热词。
应用效果图:
<div align="center">
<img src="./images/image1.png" alt="应用效果图" width="300">
<img src="./images/image2.png" alt="应用效果图" width="300">
<img src="./images/image3.png" alt="应用效果图" width="300">
</div>
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="NAudio" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AliFsmnVadSharp\AliFsmnVadSharp.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,107 @@
using AliFsmnVadSharp;
using AliFsmnVadSharp.Model;
using CommandLine;
using NAudio.Wave;
internal static class Program
{
public class ProgramParams
{
[Option('i', "input", Required = true, HelpText = "Input wav file/folder path.")]
public string WavFilePath { get; set; }
[Option('m', "model", Default = "speech_fsmn_vad_zh-cn-16k-common-onnx", HelpText = "Model path.")]
public string Model { get; set; }
}
[STAThread]
private static void Main(string[] args)
{
var argParams = Parser.Default.ParseArguments<ProgramParams>(args).Value;
string modelPath = argParams.Model;
if (!Directory.Exists(argParams.Model))
{
modelPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, modelPath);
if (!Directory.Exists(modelPath))
{
throw new DirectoryNotFoundException($"Model not found: {argParams.Model}");
}
}
string modelFilePath = Path.Combine(modelPath, "model_quant.onnx");
string configFilePath = Path.Combine(modelPath, "config.yaml");
string mvnFilePath = Path.Combine(modelPath, "am.mvn");
int batchSize = 1;
AliFsmnVad aliFsmnVad = new AliFsmnVad(modelFilePath, configFilePath, mvnFilePath, batchSize);
List<string> wavFiles = new List<string>();
if (File.Exists(argParams.WavFilePath))
{
wavFiles.Add(argParams.WavFilePath);
}
else if (Directory.Exists(argParams.WavFilePath))
{
foreach (var wavFilePath in Directory.GetFiles(argParams.WavFilePath, "*.wav"))
{
wavFiles.Add(wavFilePath);
}
}
else
{
throw new Exception($"Invalid wav input path. {argParams.WavFilePath}");
}
var start_time = DateTime.Now;
TimeSpan total_duration = new TimeSpan(0L);
for (int i = 0; i < wavFiles.Count; i += batchSize)
{
List<float[]> samples = new List<float[]>();
foreach(var wavFile in wavFiles.Skip(i).Take(batchSize))
{
(var sample, var duration) = LoadWavFile(wavFile);
samples.Add(sample);
total_duration += duration;
}
SegmentEntity[] segments_duration = aliFsmnVad.GetSegments(samples);
Console.WriteLine("vad infer result:");
foreach (SegmentEntity segment in segments_duration)
{
Console.Write("[");
foreach (var x in segment.Segment)
{
Console.Write("[" + string.Join(",", x.ToArray()) + "]");
}
Console.Write("]\r\n");
}
}
var end_time = DateTime.Now;
double elapsed_milliseconds = (end_time - start_time).TotalMilliseconds;
double rtf = elapsed_milliseconds / total_duration.TotalMilliseconds;
Console.WriteLine("elapsed_milliseconds:{0}", elapsed_milliseconds.ToString());
Console.WriteLine("total_duration:{0}", total_duration.TotalMilliseconds.ToString());
Console.WriteLine("rtf:{1}", "0".ToString(), rtf.ToString());
Console.WriteLine("------------------------");
}
private static (float[] sample, TimeSpan duration) LoadWavFile(string wavFilePath)
{
AudioFileReader _audioFileReader = new AudioFileReader(wavFilePath);
byte[] datas = new byte[_audioFileReader.Length];
_audioFileReader.Read(datas, 0, datas.Length);
var duration = _audioFileReader.TotalTime;
float[] wavdata = new float[datas.Length / 4];
Buffer.BlockCopy(datas, 0, wavdata, 0, datas.Length);
var sample = wavdata.Select((float x) => x * 32768f).ToArray();
return (sample, duration);
}
}
@@ -0,0 +1,37 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32210.238
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliFsmnVadSharp", "AliFsmnVadSharp\AliFsmnVadSharp.csproj", "{BFB82F2E-AD5B-405C-AAFF-3CE33C548748}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliFsmnVadSharp.Examples", "AliFsmnVadSharp.Examples\AliFsmnVadSharp.Examples.csproj", "{2FFA4D03-A62B-435B-B57B-7E49209810E1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{212561CC-9836-4F45-A31B-298EF576F519}"
ProjectSection(SolutionItems) = preProject
license = license
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BFB82F2E-AD5B-405C-AAFF-3CE33C548748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFB82F2E-AD5B-405C-AAFF-3CE33C548748}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFB82F2E-AD5B-405C-AAFF-3CE33C548748}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFB82F2E-AD5B-405C-AAFF-3CE33C548748}.Release|Any CPU.Build.0 = Release|Any CPU
{2FFA4D03-A62B-435B-B57B-7E49209810E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2FFA4D03-A62B-435B-B57B-7E49209810E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FFA4D03-A62B-435B-B57B-7E49209810E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FFA4D03-A62B-435B-B57B-7E49209810E1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FCC1BBCC-91A3-4223-B368-D272FB5108B6}
EndGlobalSection
EndGlobal
@@ -0,0 +1,410 @@
using AliFsmnVadSharp.Model;
using AliFsmnVadSharp.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
// 模型文件下载地址:https://modelscope.cn/models/iic/speech_fsmn_vad_zh-cn-16k-common-onnx/
namespace AliFsmnVadSharp
{
public class AliFsmnVad : IDisposable
{
private bool _disposed;
private InferenceSession _onnxSession;
private readonly ILogger _logger;
private string _frontend;
private WavFrontend _wavFrontend;
private int _batchSize = 1;
private int _max_end_sil = int.MinValue;
private EncoderConfEntity _encoderConfEntity;
private VadPostConfEntity _vad_post_conf;
public AliFsmnVad(string modelFilePath, string configFilePath, string mvnFilePath, int batchSize = 1)
{
SessionOptions options = new SessionOptions();
options.AppendExecutionProvider_CPU(0);
options.InterOpNumThreads = 1;
_onnxSession = new InferenceSession(modelFilePath, options);
VadYamlEntity vadYamlEntity = YamlHelper.ReadYaml<VadYamlEntity>(configFilePath);
_wavFrontend = new WavFrontend(mvnFilePath, vadYamlEntity.frontend_conf);
_frontend = vadYamlEntity.frontend;
_vad_post_conf = vadYamlEntity.model_conf;
_batchSize = batchSize;
_max_end_sil = _max_end_sil != int.MinValue ? _max_end_sil : vadYamlEntity.model_conf.max_end_silence_time;
_encoderConfEntity = vadYamlEntity.encoder_conf;
ILoggerFactory loggerFactory = new LoggerFactory();
_logger = new Logger<AliFsmnVad>(loggerFactory);
}
public SegmentEntity[] GetSegments(List<float[]> samples)
{
int waveform_nums = samples.Count;
_batchSize = Math.Min(waveform_nums, _batchSize);
SegmentEntity[] segments = new SegmentEntity[waveform_nums];
for (int beg_idx = 0; beg_idx < waveform_nums; beg_idx += _batchSize)
{
int end_idx = Math.Min(waveform_nums, beg_idx + _batchSize);
List<float[]> waveform_list = new List<float[]>();
for (int i = beg_idx; i < end_idx; i++)
{
waveform_list.Add(samples[i]);
}
List<VadInputEntity> vadInputEntitys = ExtractFeats(waveform_list);
try
{
int t_offset = 0;
int step = Math.Min(waveform_list.Max(x => x.Length), 6000);
bool is_final = true;
List<VadOutputEntity> vadOutputEntitys = Infer(vadInputEntitys);
for (int batch_num = beg_idx; batch_num < end_idx; batch_num++)
{
var scores = vadOutputEntitys[batch_num - beg_idx].Scores;
SegmentEntity[] segments_part = vadInputEntitys[batch_num].VadScorer.DefaultCall(scores, waveform_list[batch_num - beg_idx], is_final: is_final, max_end_sil: _max_end_sil, online: false);
if (segments_part.Length > 0)
{
#pragma warning disable CS8602 // 解引用可能出现空引用。
if (segments[batch_num] == null)
{
segments[batch_num] = new SegmentEntity();
}
segments[batch_num].Segment.AddRange(segments_part[0].Segment); //
#pragma warning restore CS8602 // 解引用可能出现空引用。
}
}
}
catch (OnnxRuntimeException ex)
{
_logger.LogWarning("input wav is silence or noise");
segments = null;
}
// for (int batch_num = 0; batch_num < _batchSize; batch_num++)
// {
// List<float[]> segment_waveforms = new List<float[]>();
// foreach (int[] segment in segments[beg_idx + batch_num].Segment)
// {
// // (int)(16000 * (segment[0] / 1000.0) * 2);
// int frame_length = (((6000 * 400) / 400 - 1) * 160 + 400) / 60 / 1000;
// int frame_start = segment[0] * frame_length;
// int frame_end = segment[1] * frame_length;
// float[] segment_waveform = new float[frame_end - frame_start];
// Array.Copy(waveform_list[batch_num], frame_start, segment_waveform, 0, segment_waveform.Length);
// segment_waveforms.Add(segment_waveform);
// }
// segments[beg_idx + batch_num].Waveform.AddRange(segment_waveforms);
// }
}
return segments;
}
public SegmentEntity[] GetSegmentsByStep(List<float[]> samples)
{
int waveform_nums = samples.Count;
_batchSize=Math.Min(waveform_nums, _batchSize);
SegmentEntity[] segments = new SegmentEntity[waveform_nums];
for (int beg_idx = 0; beg_idx < waveform_nums; beg_idx += _batchSize)
{
int end_idx = Math.Min(waveform_nums, beg_idx + _batchSize);
List<float[]> waveform_list = new List<float[]>();
for (int i = beg_idx; i < end_idx; i++)
{
waveform_list.Add(samples[i]);
}
List<VadInputEntity> vadInputEntitys = ExtractFeats(waveform_list);
int feats_len = vadInputEntitys.Max(x => x.SpeechLength);
List<float[]> in_cache = new List<float[]>();
in_cache = PrepareCache(in_cache);
try
{
int step = Math.Min(vadInputEntitys.Max(x => x.SpeechLength), 6000 * 400);
bool is_final = true;
for (int t_offset = 0; t_offset < (int)(feats_len); t_offset += Math.Min(step, feats_len - t_offset))
{
if (t_offset + step >= feats_len - 1)
{
step = feats_len - t_offset;
is_final = true;
}
else
{
is_final = false;
}
List<VadInputEntity> vadInputEntitys_step = new List<VadInputEntity>();
foreach (VadInputEntity vadInputEntity in vadInputEntitys)
{
VadInputEntity vadInputEntity_step = new VadInputEntity();
float[]? feats = vadInputEntity.Speech;
int curr_step = Math.Min(feats.Length - t_offset, step);
if (curr_step <= 0)
{
vadInputEntity_step.Speech = new float[32000];
vadInputEntity_step.SpeechLength = 0;
vadInputEntity_step.InCaches = in_cache;
vadInputEntity_step.Waveform = new float[(((int)(32000) / 400 - 1) * 160 + 400)];
vadInputEntitys_step.Add(vadInputEntity_step);
continue;
}
float[]? feats_step = new float[curr_step];
Array.Copy(feats, t_offset, feats_step, 0, feats_step.Length);
float[]? waveform = vadInputEntity.Waveform;
float[]? waveform_step = new float[Math.Min(waveform.Length, ((int)(t_offset + step) / 400 - 1) * 160 + 400) - t_offset / 400 * 160];
Array.Copy(waveform, t_offset / 400 * 160, waveform_step, 0, waveform_step.Length);
vadInputEntity_step.Speech = feats_step;
vadInputEntity_step.SpeechLength = feats_step.Length;
vadInputEntity_step.InCaches = vadInputEntity.InCaches;
vadInputEntity_step.Waveform = waveform_step;
vadInputEntitys_step.Add(vadInputEntity_step);
}
List<VadOutputEntity> vadOutputEntitys = Infer(vadInputEntitys_step);
for (int batch_num = 0; batch_num < _batchSize; batch_num++)
{
vadInputEntitys[batch_num].InCaches = vadOutputEntitys[batch_num].OutCaches;
var scores = vadOutputEntitys[batch_num].Scores;
SegmentEntity[] segments_part = vadInputEntitys[batch_num].VadScorer.DefaultCall(scores, vadInputEntitys_step[batch_num].Waveform, is_final: is_final, max_end_sil: _max_end_sil, online: false);
if (segments_part.Length > 0)
{
#pragma warning disable CS8602 // 解引用可能出现空引用。
if (segments[beg_idx + batch_num] == null)
{
segments[beg_idx + batch_num] = new SegmentEntity();
}
if (segments_part[0] != null)
{
segments[beg_idx + batch_num].Segment.AddRange(segments_part[0].Segment);
}
#pragma warning restore CS8602 // 解引用可能出现空引用。
}
}
}
}
catch (OnnxRuntimeException ex)
{
_logger.LogWarning("input wav is silence or noise");
segments = null;
}
// for (int batch_num = 0; batch_num < _batchSize; batch_num++)
// {
// List<float[]> segment_waveforms=new List<float[]>();
// foreach (int[] segment in segments[beg_idx + batch_num].Segment)
// {
// // (int)(16000 * (segment[0] / 1000.0) * 2);
// int frame_length = (((6000 * 400) / 400 - 1) * 160 + 400) / 60 / 1000;
// int frame_start = segment[0] * frame_length;
// int frame_end = segment[1] * frame_length;
// if(frame_end > waveform_list[batch_num].Length)
// {
// break;
// }
// float[] segment_waveform = new float[frame_end - frame_start];
// Array.Copy(waveform_list[batch_num], frame_start, segment_waveform, 0, segment_waveform.Length);
// segment_waveforms.Add(segment_waveform);
// }
// segments[beg_idx + batch_num].Waveform.AddRange(segment_waveforms);
// }
}
return segments;
}
private List<float[]> PrepareCache(List<float[]> in_cache)
{
if (in_cache.Count > 0)
{
return in_cache;
}
int fsmn_layers = _encoderConfEntity.fsmn_layers;
int proj_dim = _encoderConfEntity.proj_dim;
int lorder = _encoderConfEntity.lorder;
for (int i = 0; i < fsmn_layers; i++)
{
float[] cache = new float[1 * proj_dim * (lorder - 1) * 1];
in_cache.Add(cache);
}
return in_cache;
}
private List<VadInputEntity> ExtractFeats(List<float[]> waveform_list)
{
List<float[]> in_cache = new List<float[]>();
in_cache = PrepareCache(in_cache);
List<VadInputEntity> vadInputEntitys = new List<VadInputEntity>();
foreach (var waveform in waveform_list)
{
float[] fbanks = _wavFrontend.GetFbank(waveform);
float[] features = _wavFrontend.LfrCmvn(fbanks);
VadInputEntity vadInputEntity = new VadInputEntity();
vadInputEntity.Waveform = waveform;
vadInputEntity.Speech = features;
vadInputEntity.SpeechLength = features.Length;
vadInputEntity.InCaches = in_cache;
vadInputEntity.VadScorer = new E2EVadModel(_vad_post_conf);
vadInputEntitys.Add(vadInputEntity);
}
return vadInputEntitys;
}
/// <summary>
/// 一维数组转3维数组
/// </summary>
/// <param name="obj"></param>
/// <param name="len">一维长</param>
/// <param name="wid">二维长</param>
/// <returns></returns>
public static T[,,] DimOneToThree<T>(T[] oneDimObj, int len, int wid)
{
if (oneDimObj.Length % (len * wid) != 0)
return null;
int height = oneDimObj.Length / (len * wid);
T[,,] threeDimObj = new T[len, wid, height];
for (int i = 0; i < oneDimObj.Length; i++)
{
threeDimObj[i / (wid * height), (i / height) % wid, i % height] = oneDimObj[i];
}
return threeDimObj;
}
private List<VadOutputEntity> Infer(List<VadInputEntity> vadInputEntitys)
{
List<VadOutputEntity> vadOutputEntities = new List<VadOutputEntity>();
foreach (VadInputEntity vadInputEntity in vadInputEntitys)
{
int batchSize = 1;//_batchSize
var inputMeta = _onnxSession.InputMetadata;
var container = new List<NamedOnnxValue>();
int[] dim = new int[] { batchSize, vadInputEntity.Speech.Length / 400 / batchSize, 400 };
var tensor = new DenseTensor<float>(vadInputEntity.Speech, dim, false);
container.Add(NamedOnnxValue.CreateFromTensor<float>("speech", tensor));
int i = 0;
foreach (var cache in vadInputEntity.InCaches)
{
int[] cache_dim = new int[] { 1, 128, cache.Length / 128 / 1, 1 };
var cache_tensor = new DenseTensor<float>(cache, cache_dim, false);
container.Add(NamedOnnxValue.CreateFromTensor<float>("in_cache" + i.ToString(), cache_tensor));
i++;
}
IDisposableReadOnlyCollection<DisposableNamedOnnxValue> results = _onnxSession.Run(container);
var resultsArray = results.ToArray();
VadOutputEntity vadOutputEntity = new VadOutputEntity();
for (int j = 0; j < resultsArray.Length; j++)
{
if (resultsArray[j].Name.Equals("logits"))
{
Tensor<float> tensors = resultsArray[0].AsTensor<float>();
var _scores = DimOneToThree<float>(tensors.ToArray(), 1, tensors.Dimensions[1]);
vadOutputEntity.Scores = _scores;
}
if (resultsArray[j].Name.StartsWith("out_cache"))
{
vadOutputEntity.OutCaches.Add(resultsArray[j].AsEnumerable<float>().ToArray());
}
}
vadOutputEntities.Add(vadOutputEntity);
}
return vadOutputEntities;
}
private float[] PadSequence(List<VadInputEntity> modelInputs)
{
int max_speech_length = modelInputs.Max(x => x.SpeechLength);
int speech_length = max_speech_length * modelInputs.Count;
float[] speech = new float[speech_length];
float[,] xxx = new float[modelInputs.Count, max_speech_length];
for (int i = 0; i < modelInputs.Count; i++)
{
if (max_speech_length == modelInputs[i].SpeechLength)
{
for (int j = 0; j < xxx.GetLength(1); j++)
{
#pragma warning disable CS8602 // 解引用可能出现空引用。
xxx[i, j] = modelInputs[i].Speech[j];
#pragma warning restore CS8602 // 解引用可能出现空引用。
}
continue;
}
float[] nullspeech = new float[max_speech_length - modelInputs[i].SpeechLength];
float[]? curr_speech = modelInputs[i].Speech;
float[] padspeech = new float[max_speech_length];
// ///////////////////////////////////////////////////
var arr_neg_mean = _onnxSession.ModelMetadata.CustomMetadataMap["neg_mean"].ToString().Split(',').ToArray();
double[] neg_mean = arr_neg_mean.Select(x => (double)Convert.ToDouble(x)).ToArray();
var arr_inv_stddev = _onnxSession.ModelMetadata.CustomMetadataMap["inv_stddev"].ToString().Split(',').ToArray();
double[] inv_stddev = arr_inv_stddev.Select(x => (double)Convert.ToDouble(x)).ToArray();
int dim = neg_mean.Length;
for (int j = 0; j < max_speech_length; j++)
{
int k = new Random().Next(0, dim);
padspeech[j] = (float)((float)(0 + neg_mean[k]) * inv_stddev[k]);
}
Array.Copy(curr_speech, 0, padspeech, 0, curr_speech.Length);
for (int j = 0; j < padspeech.Length; j++)
{
#pragma warning disable CS8602 // 解引用可能出现空引用。
xxx[i, j] = padspeech[j];
#pragma warning restore CS8602 // 解引用可能出现空引用。
}
}
int s = 0;
for (int i = 0; i < xxx.GetLength(0); i++)
{
for (int j = 0; j < xxx.GetLength(1); j++)
{
speech[s] = xxx[i, j];
s++;
}
}
return speech;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (_onnxSession != null)
{
_onnxSession.Dispose();
}
if (_wavFrontend != null)
{
_wavFrontend.Dispose();
}
if (_encoderConfEntity != null)
{
_encoderConfEntity = null;
}
if (_vad_post_conf != null)
{
_vad_post_conf = null;
}
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
~AliFsmnVad()
{
Dispose(_disposed);
}
}
}
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="KaldiNativeFbankSharp" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.15.0" />
<PackageReference Include="YamlDotNet" Version="13.1.0" />
</ItemGroup>
<ItemGroup>
<None Update="speech_fsmn_vad_zh-cn-16k-common-pytorch\example\0.wav">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="speech_fsmn_vad_zh-cn-16k-common-pytorch\example\1.wav">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="speech_fsmn_vad_zh-cn-16k-common-pytorch\model.onnx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="speech_fsmn_vad_zh-cn-16k-common-pytorch\vad.mvn">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="speech_fsmn_vad_zh-cn-16k-common-pytorch\vad.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,724 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AliFsmnVadSharp.Model;
namespace AliFsmnVadSharp
{
enum VadStateMachine
{
kVadInStateStartPointNotDetected = 1,
kVadInStateInSpeechSegment = 2,
kVadInStateEndPointDetected = 3,
}
enum VadDetectMode
{
kVadSingleUtteranceDetectMode = 0,
kVadMutipleUtteranceDetectMode = 1,
}
internal class E2EVadModel
{
private VadPostConfEntity _vad_opts = new VadPostConfEntity();
private WindowDetector _windows_detector = new WindowDetector();
private bool _is_final = false;
private int _data_buf_start_frame = 0;
private int _frm_cnt = 0;
private int _latest_confirmed_speech_frame = 0;
private int _lastest_confirmed_silence_frame = -1;
private int _continous_silence_frame_count = 0;
private int _vad_state_machine = (int)VadStateMachine.kVadInStateStartPointNotDetected;
private int _confirmed_start_frame = -1;
private int _confirmed_end_frame = -1;
private int _number_end_time_detected = 0;
private int _sil_frame = 0;
private int[] _sil_pdf_ids = new int[0];
private double _noise_average_decibel = -100.0D;
private bool _pre_end_silence_detected = false;
private bool _next_seg = true;
private List<E2EVadSpeechBufWithDoaEntity> _output_data_buf;
private int _output_data_buf_offset = 0;
private List<E2EVadFrameProbEntity> _frame_probs = new List<E2EVadFrameProbEntity>();
private int _max_end_sil_frame_cnt_thresh = 800 - 150;
private float _speech_noise_thres = 0.6F;
private float[,,] _scores = null;
private int _idx_pre_chunk = 0;
private bool _max_time_out = false;
private List<double> _decibel = new List<double>();
private int _data_buf_size = 0;
private int _data_buf_all_size = 0;
public E2EVadModel(VadPostConfEntity vadPostConfEntity)
{
_vad_opts = vadPostConfEntity;
_windows_detector = new WindowDetector(_vad_opts.window_size_ms,
_vad_opts.sil_to_speech_time_thres,
_vad_opts.speech_to_sil_time_thres,
_vad_opts.frame_in_ms);
AllResetDetection();
}
private void AllResetDetection()
{
_is_final = false;
_data_buf_start_frame = 0;
_frm_cnt = 0;
_latest_confirmed_speech_frame = 0;
_lastest_confirmed_silence_frame = -1;
_continous_silence_frame_count = 0;
_vad_state_machine = (int)VadStateMachine.kVadInStateStartPointNotDetected;
_confirmed_start_frame = -1;
_confirmed_end_frame = -1;
_number_end_time_detected = 0;
_sil_frame = 0;
_sil_pdf_ids = _vad_opts.sil_pdf_ids;
_noise_average_decibel = -100.0F;
_pre_end_silence_detected = false;
_next_seg = true;
_output_data_buf = new List<E2EVadSpeechBufWithDoaEntity>();
_output_data_buf_offset = 0;
_frame_probs = new List<E2EVadFrameProbEntity>();
_max_end_sil_frame_cnt_thresh = _vad_opts.max_end_silence_time - _vad_opts.speech_to_sil_time_thres;
_speech_noise_thres = _vad_opts.speech_noise_thres;
_scores = null;
_idx_pre_chunk = 0;
_max_time_out = false;
_decibel = new List<double>();
_data_buf_size = 0;
_data_buf_all_size = 0;
ResetDetection();
}
private void ResetDetection()
{
_continous_silence_frame_count = 0;
_latest_confirmed_speech_frame = 0;
_lastest_confirmed_silence_frame = -1;
_confirmed_start_frame = -1;
_confirmed_end_frame = -1;
_vad_state_machine = (int)VadStateMachine.kVadInStateStartPointNotDetected;
_windows_detector.Reset();
_sil_frame = 0;
_frame_probs = new List<E2EVadFrameProbEntity>();
}
private void ComputeDecibel(float[] waveform)
{
int frame_sample_length = (int)(_vad_opts.frame_length_ms * _vad_opts.sample_rate / 1000);
int frame_shift_length = (int)(_vad_opts.frame_in_ms * _vad_opts.sample_rate / 1000);
if (_data_buf_all_size == 0)
{
_data_buf_all_size = waveform.Length;
_data_buf_size = _data_buf_all_size;
}
else
{
_data_buf_all_size += waveform.Length;
}
for (int offset = 0; offset < waveform.Length - frame_sample_length + 1; offset += frame_shift_length)
{
float[] _waveform_chunk = new float[frame_sample_length];
Array.Copy(waveform, offset, _waveform_chunk, 0, _waveform_chunk.Length);
float[] _waveform_chunk_pow = _waveform_chunk.Select(x => (float)Math.Pow((double)x, 2)).ToArray();
_decibel.Add(
10 * Math.Log10(
_waveform_chunk_pow.Sum() + 0.000001
)
);
}
}
private void ComputeScores(float[,,] scores)
{
_vad_opts.nn_eval_block_size = scores.GetLength(1);
_frm_cnt += scores.GetLength(1);
_scores = scores;
}
private void PopDataBufTillFrame(int frame_idx)// need check again
{
while (_data_buf_start_frame < frame_idx)
{
if (_data_buf_size >= (int)(_vad_opts.frame_in_ms * _vad_opts.sample_rate / 1000))
{
_data_buf_start_frame += 1;
_data_buf_size = _data_buf_all_size - _data_buf_start_frame * (int)(_vad_opts.frame_in_ms * _vad_opts.sample_rate / 1000);
}
}
}
private void PopDataToOutputBuf(int start_frm, int frm_cnt, bool first_frm_is_start_point,
bool last_frm_is_end_point, bool end_point_is_sent_end)
{
PopDataBufTillFrame(start_frm);
int expected_sample_number = (int)(frm_cnt * _vad_opts.sample_rate * _vad_opts.frame_in_ms / 1000);
if (last_frm_is_end_point)
{
int extra_sample = Math.Max(0, (int)(_vad_opts.frame_length_ms * _vad_opts.sample_rate / 1000 - _vad_opts.sample_rate * _vad_opts.frame_in_ms / 1000));
expected_sample_number += (int)(extra_sample);
}
if (end_point_is_sent_end)
{
expected_sample_number = Math.Max(expected_sample_number, _data_buf_size);
}
if (_data_buf_size < expected_sample_number)
{
Console.WriteLine("error in calling pop data_buf\n");
}
if (_output_data_buf.Count == 0 || first_frm_is_start_point)
{
_output_data_buf.Add(new E2EVadSpeechBufWithDoaEntity());
_output_data_buf.Last().Reset();
_output_data_buf.Last().start_ms = start_frm * _vad_opts.frame_in_ms;
_output_data_buf.Last().end_ms = _output_data_buf.Last().start_ms;
_output_data_buf.Last().doa = 0;
}
E2EVadSpeechBufWithDoaEntity cur_seg = _output_data_buf.Last();
if (cur_seg.end_ms != start_frm * _vad_opts.frame_in_ms)
{
Console.WriteLine("warning\n");
}
int out_pos = cur_seg.buffer.Length; // cur_seg.buff现在没做任何操作
int data_to_pop = 0;
if (end_point_is_sent_end)
{
data_to_pop = expected_sample_number;
}
else
{
data_to_pop = (int)(frm_cnt * _vad_opts.frame_in_ms * _vad_opts.sample_rate / 1000);
}
if (data_to_pop > _data_buf_size)
{
Console.WriteLine("VAD data_to_pop is bigger than _data_buf_size!!!\n");
data_to_pop = _data_buf_size;
expected_sample_number = _data_buf_size;
}
cur_seg.doa = 0;
for (int sample_cpy_out = 0; sample_cpy_out < data_to_pop; sample_cpy_out++)
{
out_pos += 1;
}
for (int sample_cpy_out = data_to_pop; sample_cpy_out < expected_sample_number; sample_cpy_out++)
{
out_pos += 1;
}
if (cur_seg.end_ms != start_frm * _vad_opts.frame_in_ms)
{
Console.WriteLine("Something wrong with the VAD algorithm\n");
}
_data_buf_start_frame += frm_cnt;
cur_seg.end_ms = (start_frm + frm_cnt) * _vad_opts.frame_in_ms;
if (first_frm_is_start_point)
{
cur_seg.contain_seg_start_point = true;
}
if (last_frm_is_end_point)
{
cur_seg.contain_seg_end_point = true;
}
}
private void OnSilenceDetected(int valid_frame)
{
_lastest_confirmed_silence_frame = valid_frame;
if (_vad_state_machine == (int)VadStateMachine.kVadInStateStartPointNotDetected)
{
PopDataBufTillFrame(valid_frame);
}
}
private void OnVoiceDetected(int valid_frame)
{
_latest_confirmed_speech_frame = valid_frame;
PopDataToOutputBuf(valid_frame, 1, false, false, false);
}
private void OnVoiceStart(int start_frame, bool fake_result = false)
{
if (_vad_opts.do_start_point_detection)
{
//do nothing
}
if (_confirmed_start_frame != -1)
{
Console.WriteLine("not reset vad properly\n");
}
else
{
_confirmed_start_frame = start_frame;
}
if (!fake_result || _vad_state_machine == (int)VadStateMachine.kVadInStateStartPointNotDetected)
{
PopDataToOutputBuf(_confirmed_start_frame, 1, true, false, false);
}
}
private void OnVoiceEnd(int end_frame, bool fake_result, bool is_last_frame)
{
for (int t = _latest_confirmed_speech_frame + 1; t < end_frame; t++)
{
OnVoiceDetected(t);
}
if (_vad_opts.do_end_point_detection)
{
//do nothing
}
if (_confirmed_end_frame != -1)
{
Console.WriteLine("not reset vad properly\n");
}
else
{
_confirmed_end_frame = end_frame;
}
if (!fake_result)
{
_sil_frame = 0;
PopDataToOutputBuf(_confirmed_end_frame, 1, false, true, is_last_frame);
}
_number_end_time_detected += 1;
}
private void MaybeOnVoiceEndIfLastFrame(bool is_final_frame, int cur_frm_idx)
{
if (is_final_frame)
{
OnVoiceEnd(cur_frm_idx, false, true);
_vad_state_machine = (int)VadStateMachine.kVadInStateEndPointDetected;
}
}
private int GetLatency()
{
return (int)(LatencyFrmNumAtStartPoint() * _vad_opts.frame_in_ms);
}
private int LatencyFrmNumAtStartPoint()
{
int vad_latency = _windows_detector.GetWinSize();
if (_vad_opts.do_extend != 0)
{
vad_latency += (int)(_vad_opts.lookback_time_start_point / _vad_opts.frame_in_ms);
}
return vad_latency;
}
private FrameState GetFrameState(int t)
{
FrameState frame_state = FrameState.kFrameStateInvalid;
double cur_decibel = _decibel[t];
double cur_snr = cur_decibel - _noise_average_decibel;
if (cur_decibel < _vad_opts.decibel_thres)
{
frame_state = FrameState.kFrameStateSil;
DetectOneFrame(frame_state, t, false);
return frame_state;
}
double sum_score = 0.0D;
double noise_prob = 0.0D;
Trace.Assert(_sil_pdf_ids.Length == _vad_opts.silence_pdf_num, "");
if (_sil_pdf_ids.Length > 0)
{
Trace.Assert(_scores.GetLength(0) == 1, "只支持batch_size = 1的测试"); // 只支持batch_size = 1的测试
float[] sil_pdf_scores = new float[_sil_pdf_ids.Length];
int j = 0;
foreach (int sil_pdf_id in _sil_pdf_ids)
{
sil_pdf_scores[j] = _scores[0,t - _idx_pre_chunk,sil_pdf_id];
j++;
}
sum_score = sil_pdf_scores.Length == 0 ? 0 : sil_pdf_scores.Sum();
noise_prob = Math.Log(sum_score) * _vad_opts.speech_2_noise_ratio;
double total_score = 1.0D;
sum_score = total_score - sum_score;
}
double speech_prob = Math.Log(sum_score);
if (_vad_opts.output_frame_probs)
{
E2EVadFrameProbEntity frame_prob = new E2EVadFrameProbEntity();
frame_prob.noise_prob = noise_prob;
frame_prob.speech_prob = speech_prob;
frame_prob.score = sum_score;
frame_prob.frame_id = t;
_frame_probs.Add(frame_prob);
}
if (Math.Exp(speech_prob) >= Math.Exp(noise_prob) + _speech_noise_thres)
{
if (cur_snr >= _vad_opts.snr_thres && cur_decibel >= _vad_opts.decibel_thres)
{
frame_state = FrameState.kFrameStateSpeech;
}
else
{
frame_state = FrameState.kFrameStateSil;
}
}
else
{
frame_state = FrameState.kFrameStateSil;
if (_noise_average_decibel < -99.9)
{
_noise_average_decibel = cur_decibel;
}
else
{
_noise_average_decibel = (cur_decibel + _noise_average_decibel * (_vad_opts.noise_frame_num_used_for_snr - 1)) / _vad_opts.noise_frame_num_used_for_snr;
}
}
return frame_state;
}
public SegmentEntity[] DefaultCall(float[,,] score, float[] waveform,
bool is_final = false, int max_end_sil = 800, bool online = false
)
{
_max_end_sil_frame_cnt_thresh = max_end_sil - _vad_opts.speech_to_sil_time_thres;
// compute decibel for each frame
ComputeDecibel(waveform);
ComputeScores(score);
if (!is_final)
{
DetectCommonFrames();
}
else
{
DetectLastFrames();
}
int batchSize = score.GetLength(0);
SegmentEntity[] segments = new SegmentEntity[batchSize];
for (int batch_num = 0; batch_num < batchSize; batch_num++) // only support batch_size = 1 now
{
List<int[]> segment_batch = new List<int[]>();
if (_output_data_buf.Count > 0)
{
for (int i = _output_data_buf_offset; i < _output_data_buf.Count; i++)
{
int start_ms;
int end_ms;
if (online)
{
if (!_output_data_buf[i].contain_seg_start_point)
{
continue;
}
if (!_next_seg && !_output_data_buf[i].contain_seg_end_point)
{
continue;
}
start_ms = _next_seg ? _output_data_buf[i].start_ms : -1;
if (_output_data_buf[i].contain_seg_end_point)
{
end_ms = _output_data_buf[i].end_ms;
_next_seg = true;
_output_data_buf_offset += 1;
}
else
{
end_ms = -1;
_next_seg = false;
}
}
else
{
if (!is_final && (!_output_data_buf[i].contain_seg_start_point || !_output_data_buf[i].contain_seg_end_point))
{
continue;
}
start_ms = _output_data_buf[i].start_ms;
end_ms = _output_data_buf[i].end_ms;
_output_data_buf_offset += 1;
}
int[] segment_ms = new int[] { start_ms, end_ms };
segment_batch.Add(segment_ms);
}
}
if (segment_batch.Count > 0)
{
if (segments[batch_num] == null)
{
segments[batch_num] = new SegmentEntity();
}
segments[batch_num].Segment.AddRange(segment_batch);
}
}
if (is_final)
{
// reset class variables and clear the dict for the next query
AllResetDetection();
}
return segments;
}
private int DetectCommonFrames()
{
if (_vad_state_machine == (int)VadStateMachine.kVadInStateEndPointDetected)
{
return 0;
}
for (int i = _vad_opts.nn_eval_block_size - 1; i > -1; i += -1)
{
FrameState frame_state = FrameState.kFrameStateInvalid;
frame_state = GetFrameState(_frm_cnt - 1 - i);
DetectOneFrame(frame_state, _frm_cnt - 1 - i, false);
}
_idx_pre_chunk += _scores.GetLength(1)* _scores.GetLength(0); //_scores.shape[1];
return 0;
}
private int DetectLastFrames()
{
if (_vad_state_machine == (int)VadStateMachine.kVadInStateEndPointDetected)
{
return 0;
}
try
{
for (int i = _vad_opts.nn_eval_block_size - 1; i > -1; i += -1)
{
FrameState frame_state = FrameState.kFrameStateInvalid;
frame_state = GetFrameState(_frm_cnt - 1 - i);
if (i != 0)
{
DetectOneFrame(frame_state, _frm_cnt - 1 - i, false);
}
else
{
DetectOneFrame(frame_state, _frm_cnt - 1, true);
}
}
}
catch (Exception e)
{
//
}
return 0;
}
private void DetectOneFrame(FrameState cur_frm_state, int cur_frm_idx, bool is_final_frame)
{
FrameState tmp_cur_frm_state = FrameState.kFrameStateInvalid;
if (cur_frm_state == FrameState.kFrameStateSpeech)
{
if (Math.Abs(1.0) > _vad_opts.fe_prior_thres)//Fabs
{
tmp_cur_frm_state = FrameState.kFrameStateSpeech;
}
else
{
tmp_cur_frm_state = FrameState.kFrameStateSil;
}
}
else if (cur_frm_state == FrameState.kFrameStateSil)
{
tmp_cur_frm_state = FrameState.kFrameStateSil;
}
AudioChangeState state_change = _windows_detector.DetectOneFrame(tmp_cur_frm_state, cur_frm_idx);
int frm_shift_in_ms = _vad_opts.frame_in_ms;
if (AudioChangeState.kChangeStateSil2Speech == state_change)
{
int silence_frame_count = _continous_silence_frame_count; // no used
_continous_silence_frame_count = 0;
_pre_end_silence_detected = false;
int start_frame = 0;
if (_vad_state_machine == (int)VadStateMachine.kVadInStateStartPointNotDetected)
{
start_frame = Math.Max(_data_buf_start_frame, cur_frm_idx - LatencyFrmNumAtStartPoint());
OnVoiceStart(start_frame);
_vad_state_machine = (int)VadStateMachine.kVadInStateInSpeechSegment;
for (int t = start_frame + 1; t < cur_frm_idx + 1; t++)
{
OnVoiceDetected(t);
}
}
else if (_vad_state_machine == (int)VadStateMachine.kVadInStateInSpeechSegment)
{
for (int t = _latest_confirmed_speech_frame + 1; t < cur_frm_idx; t++)
{
OnVoiceDetected(t);
}
if (cur_frm_idx - _confirmed_start_frame + 1 > _vad_opts.max_single_segment_time / frm_shift_in_ms)
{
OnVoiceEnd(cur_frm_idx, false, false);
_vad_state_machine = (int)VadStateMachine.kVadInStateEndPointDetected;
}
else if (!is_final_frame)
{
OnVoiceDetected(cur_frm_idx);
}
else
{
MaybeOnVoiceEndIfLastFrame(is_final_frame, cur_frm_idx);
}
}
else
{
return;
}
}
else if (AudioChangeState.kChangeStateSpeech2Sil == state_change)
{
_continous_silence_frame_count = 0;
if (_vad_state_machine == (int)VadStateMachine.kVadInStateStartPointNotDetected)
{ return; }
else if (_vad_state_machine == (int)VadStateMachine.kVadInStateInSpeechSegment)
{
if (cur_frm_idx - _confirmed_start_frame + 1 > _vad_opts.max_single_segment_time / frm_shift_in_ms)
{
OnVoiceEnd(cur_frm_idx, false, false);
_vad_state_machine = (int)VadStateMachine.kVadInStateEndPointDetected;
}
else if (!is_final_frame)
{
OnVoiceDetected(cur_frm_idx);
}
else
{
MaybeOnVoiceEndIfLastFrame(is_final_frame, cur_frm_idx);
}
}
else
{
return;
}
}
else if (AudioChangeState.kChangeStateSpeech2Speech == state_change)
{
_continous_silence_frame_count = 0;
if (_vad_state_machine == (int)VadStateMachine.kVadInStateInSpeechSegment)
{
if (cur_frm_idx - _confirmed_start_frame + 1 > _vad_opts.max_single_segment_time / frm_shift_in_ms)
{
_max_time_out = true;
OnVoiceEnd(cur_frm_idx, false, false);
_vad_state_machine = (int)VadStateMachine.kVadInStateEndPointDetected;
}
else if (!is_final_frame)
{
OnVoiceDetected(cur_frm_idx);
}
else
{
MaybeOnVoiceEndIfLastFrame(is_final_frame, cur_frm_idx);
}
}
else
{
return;
}
}
else if (AudioChangeState.kChangeStateSil2Sil == state_change)
{
_continous_silence_frame_count += 1;
if (_vad_state_machine == (int)VadStateMachine.kVadInStateStartPointNotDetected)
{
// silence timeout, return zero length decision
if (((_vad_opts.detect_mode == (int)VadDetectMode.kVadSingleUtteranceDetectMode) && (
_continous_silence_frame_count * frm_shift_in_ms > _vad_opts.max_start_silence_time)) || (is_final_frame && _number_end_time_detected == 0))
{
for (int t = _lastest_confirmed_silence_frame + 1; t < cur_frm_idx; t++)
{
OnSilenceDetected(t);
}
OnVoiceStart(0, true);
OnVoiceEnd(0, true, false);
_vad_state_machine = (int)VadStateMachine.kVadInStateEndPointDetected;
}
else
{
if (cur_frm_idx >= LatencyFrmNumAtStartPoint())
{
OnSilenceDetected(cur_frm_idx - LatencyFrmNumAtStartPoint());
}
}
}
else if (_vad_state_machine == (int)VadStateMachine.kVadInStateInSpeechSegment)
{
if (_continous_silence_frame_count * frm_shift_in_ms >= _max_end_sil_frame_cnt_thresh)
{
int lookback_frame = (int)(_max_end_sil_frame_cnt_thresh / frm_shift_in_ms);
if (_vad_opts.do_extend != 0)
{
lookback_frame -= (int)(_vad_opts.lookahead_time_end_point / frm_shift_in_ms);
lookback_frame -= 1;
lookback_frame = Math.Max(0, lookback_frame);
}
OnVoiceEnd(cur_frm_idx - lookback_frame, false, false);
_vad_state_machine = (int)VadStateMachine.kVadInStateEndPointDetected;
}
else if (cur_frm_idx - _confirmed_start_frame + 1 > _vad_opts.max_single_segment_time / frm_shift_in_ms)
{
OnVoiceEnd(cur_frm_idx, false, false);
_vad_state_machine = (int)VadStateMachine.kVadInStateEndPointDetected;
}
else if (_vad_opts.do_extend != 0 && !is_final_frame)
{
if (_continous_silence_frame_count <= (int)(_vad_opts.lookahead_time_end_point / frm_shift_in_ms))
{
OnVoiceDetected(cur_frm_idx);
}
}
else
{
MaybeOnVoiceEndIfLastFrame(is_final_frame, cur_frm_idx);
}
}
else
{
return;
}
}
if (_vad_state_machine == (int)VadStateMachine.kVadInStateEndPointDetected && _vad_opts.detect_mode == (int)VadDetectMode.kVadMutipleUtteranceDetectMode)
{
ResetDetection();
}
}
}
}
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
internal class CmvnEntity
{
private List<float> _means = new List<float>();
private List<float> _vars = new List<float>();
public List<float> Means { get => _means; set => _means = value; }
public List<float> Vars { get => _vars; set => _vars = value; }
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
internal class E2EVadFrameProbEntity
{
private double _noise_prob = 0.0F;
private double _speech_prob = 0.0F;
private double _score = 0.0F;
private int _frame_id = 0;
private int _frm_state = 0;
public double noise_prob { get => _noise_prob; set => _noise_prob = value; }
public double speech_prob { get => _speech_prob; set => _speech_prob = value; }
public double score { get => _score; set => _score = value; }
public int frame_id { get => _frame_id; set => _frame_id = value; }
public int frm_state { get => _frm_state; set => _frm_state = value; }
}
}
@@ -0,0 +1,98 @@
// AliFsmnVadSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// AliFsmnVadSharp.Model.E2EVadSpeechBufWithDoaEntity
internal class E2EVadSpeechBufWithDoaEntity
{
private int _start_ms = 0;
private int _end_ms = 0;
private byte[]? _buffer;
private bool _contain_seg_start_point = false;
private bool _contain_seg_end_point = false;
private int _doa = 0;
public int start_ms
{
get
{
return _start_ms;
}
set
{
_start_ms = value;
}
}
public int end_ms
{
get
{
return _end_ms;
}
set
{
_end_ms = value;
}
}
public byte[]? buffer
{
get
{
return _buffer;
}
set
{
_buffer = value;
}
}
public bool contain_seg_start_point
{
get
{
return _contain_seg_start_point;
}
set
{
_contain_seg_start_point = value;
}
}
public bool contain_seg_end_point
{
get
{
return _contain_seg_end_point;
}
set
{
_contain_seg_end_point = value;
}
}
public int doa
{
get
{
return _doa;
}
set
{
_doa = value;
}
}
public void Reset()
{
_start_ms = 0;
_end_ms = 0;
_buffer = new byte[0];
_contain_seg_start_point = false;
_contain_seg_end_point = false;
_doa = 0;
}
}
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
public class EncoderConfEntity
{
private int _input_dim=400;
private int _input_affineDim = 140;
private int _fsmn_layers = 4;
private int _linear_dim = 250;
private int _proj_dim = 128;
private int _lorder = 20;
private int _rorder = 0;
private int _lstride = 1;
private int _rstride = 0;
private int _output_dffine_dim = 140;
private int _output_dim = 248;
public int input_dim { get => _input_dim; set => _input_dim = value; }
public int input_affine_dim { get => _input_affineDim; set => _input_affineDim = value; }
public int fsmn_layers { get => _fsmn_layers; set => _fsmn_layers = value; }
public int linear_dim { get => _linear_dim; set => _linear_dim = value; }
public int proj_dim { get => _proj_dim; set => _proj_dim = value; }
public int lorder { get => _lorder; set => _lorder = value; }
public int rorder { get => _rorder; set => _rorder = value; }
public int lstride { get => _lstride; set => _lstride = value; }
public int rstride { get => _rstride; set => _rstride = value; }
public int output_affine_dim { get => _output_dffine_dim; set => _output_dffine_dim = value; }
public int output_dim { get => _output_dim; set => _output_dim = value; }
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
public class FrontendConfEntity
{
private int _fs = 16000;
private string _window = "hamming";
private int _n_mels = 80;
private int _frame_length = 25;
private int _frame_shift = 10;
private float _dither = 0.0F;
private int _lfr_m = 5;
private int _lfr_n = 1;
public int fs { get => _fs; set => _fs = value; }
public string window { get => _window; set => _window = value; }
public int n_mels { get => _n_mels; set => _n_mels = value; }
public int frame_length { get => _frame_length; set => _frame_length = value; }
public int frame_shift { get => _frame_shift; set => _frame_shift = value; }
public float dither { get => _dither; set => _dither = value; }
public int lfr_m { get => _lfr_m; set => _lfr_m = value; }
public int lfr_n { get => _lfr_n; set => _lfr_n = value; }
}
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
public class SegmentEntity
{
private List<int[]> _segment=new List<int[]>();
private List<float[]> _waveform=new List<float[]>();
public List<int[]> Segment { get => _segment; set => _segment = value; }
public List<float[]> Waveform { get => _waveform; set => _waveform = value; }
//public SegmentEntity()
//{
// int[] t=new int[0];
// _segment.Add(t);
//}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
internal class VadInputEntity
{
private float[]? _speech;
private int _speechLength;
private List<float[]> _inCaches = new List<float[]>();
private float[]? _waveform;
private E2EVadModel _vad_scorer;
public float[]? Speech { get => _speech; set => _speech = value; }
public int SpeechLength { get => _speechLength; set => _speechLength = value; }
public List<float[]> InCaches { get => _inCaches; set => _inCaches = value; }
public float[] Waveform { get => _waveform; set => _waveform = value; }
internal E2EVadModel VadScorer { get => _vad_scorer; set => _vad_scorer = value; }
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
internal class VadOutputEntity
{
private float[,,]? _scores;
private List<float[]> _outCaches=new List<float[]>();
private float[]? _waveform;
public float[,,]? Scores { get => _scores; set => _scores = value; }
public List<float[]> OutCaches { get => _outCaches; set => _outCaches = value; }
public float[] Waveform { get => _waveform; set => _waveform = value; }
}
}
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
public class VadPostConfEntity
{
private int _sample_rate= 16000;
private int _detect_mode = 1 ;
private int _snr_mode = 0;
private int _max_end_silence_time = 800;
private int _max_start_silence_time = 3000;
private bool _do_start_point_detection = true;
private bool _do_end_point_detection = true;
private int _window_size_ms = 200;
private int _sil_to_speech_time_thres = 150;
private int _speech_to_sil_time_thres = 150;
private float _speech_2_noise_ratio = 1.0F;
private int _do_extend = 1;
private int _lookback_time_start_point = 200;
private int _lookahead_time_end_point = 100;
private int _max_single_segment_time = 60000;
private int _nn_eval_block_size = 8;
private int _dcd_block_size = 4;
private float _snr_thres = -100.0F;
private int _noise_frame_num_used_for_snr = 100;
private float _decibel_thres = -100.0F;
private float _speech_noise_thres = 0.6F;
private float _fe_prior_thres = 0.0001F;
private int _silence_pdf_num = 1;
private int[] _sil_pdf_ids = new int[] {0};
private float _speech_noise_thresh_low = -0.1F;
private float _speech_noise_thresh_high = 0.3F;
private bool _output_frame_probs = false;
private int _frame_in_ms = 10;
private int _frame_length_ms = 25;
public int sample_rate { get => _sample_rate; set => _sample_rate = value; }
public int detect_mode { get => _detect_mode; set => _detect_mode = value; }
public int snr_mode { get => _snr_mode; set => _snr_mode = value; }
public int max_end_silence_time { get => _max_end_silence_time; set => _max_end_silence_time = value; }
public int max_start_silence_time { get => _max_start_silence_time; set => _max_start_silence_time = value; }
public bool do_start_point_detection { get => _do_start_point_detection; set => _do_start_point_detection = value; }
public bool do_end_point_detection { get => _do_end_point_detection; set => _do_end_point_detection = value; }
public int window_size_ms { get => _window_size_ms; set => _window_size_ms = value; }
public int sil_to_speech_time_thres { get => _sil_to_speech_time_thres; set => _sil_to_speech_time_thres = value; }
public int speech_to_sil_time_thres { get => _speech_to_sil_time_thres; set => _speech_to_sil_time_thres = value; }
public float speech_2_noise_ratio { get => _speech_2_noise_ratio; set => _speech_2_noise_ratio = value; }
public int do_extend { get => _do_extend; set => _do_extend = value; }
public int lookback_time_start_point { get => _lookback_time_start_point; set => _lookback_time_start_point = value; }
public int lookahead_time_end_point { get => _lookahead_time_end_point; set => _lookahead_time_end_point = value; }
public int max_single_segment_time { get => _max_single_segment_time; set => _max_single_segment_time = value; }
public int nn_eval_block_size { get => _nn_eval_block_size; set => _nn_eval_block_size = value; }
public int dcd_block_size { get => _dcd_block_size; set => _dcd_block_size = value; }
public float snr_thres { get => _snr_thres; set => _snr_thres = value; }
public int noise_frame_num_used_for_snr { get => _noise_frame_num_used_for_snr; set => _noise_frame_num_used_for_snr = value; }
public float decibel_thres { get => _decibel_thres; set => _decibel_thres = value; }
public float speech_noise_thres { get => _speech_noise_thres; set => _speech_noise_thres = value; }
public float fe_prior_thres { get => _fe_prior_thres; set => _fe_prior_thres = value; }
public int silence_pdf_num { get => _silence_pdf_num; set => _silence_pdf_num = value; }
public int[] sil_pdf_ids { get => _sil_pdf_ids; set => _sil_pdf_ids = value; }
public float speech_noise_thresh_low { get => _speech_noise_thresh_low; set => _speech_noise_thresh_low = value; }
public float speech_noise_thresh_high { get => _speech_noise_thresh_high; set => _speech_noise_thresh_high = value; }
public bool output_frame_probs { get => _output_frame_probs; set => _output_frame_probs = value; }
public int frame_in_ms { get => _frame_in_ms; set => _frame_in_ms = value; }
public int frame_length_ms { get => _frame_length_ms; set => _frame_length_ms = value; }
}
}
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp.Model
{
internal class VadYamlEntity
{
private int _input_size;
private string _frontend = "wav_frontend";
private FrontendConfEntity _frontend_conf=new FrontendConfEntity();
private string _model = "e2evad";
private string _encoder = "fsmn";
private EncoderConfEntity _encoder_conf=new EncoderConfEntity();
private VadPostConfEntity _vad_post_conf=new VadPostConfEntity();
public int input_size { get => _input_size; set => _input_size = value; }
public string frontend { get => _frontend; set => _frontend = value; }
public string model { get => _model; set => _model = value; }
public string encoder { get => _encoder; set => _encoder = value; }
public FrontendConfEntity frontend_conf { get => _frontend_conf; set => _frontend_conf = value; }
public EncoderConfEntity encoder_conf { get => _encoder_conf; set => _encoder_conf = value; }
public VadPostConfEntity model_conf { get => _vad_post_conf; set => _vad_post_conf = value; }
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json;
using YamlDotNet.Serialization;
namespace AliFsmnVadSharp.Utils
{
internal class YamlHelper
{
public static T ReadYaml<T>(string yamlFilePath)
{
if (!File.Exists(yamlFilePath))
{
#pragma warning disable CS8603 // 可能返回 null 引用。
return default(T);
#pragma warning restore CS8603 // 可能返回 null 引用。
}
StreamReader yamlReader = File.OpenText(yamlFilePath);
Deserializer yamlDeserializer = new Deserializer();
T info = yamlDeserializer.Deserialize<T>(yamlReader);
yamlReader.Close();
return info;
}
}
}
@@ -0,0 +1,180 @@
using AliFsmnVadSharp.Model;
using KaldiNativeFbankSharp;
namespace AliFsmnVadSharp
{
internal class WavFrontend : IDisposable
{
private bool _disposed;
private FrontendConfEntity _frontendConfEntity;
OnlineFbank _onlineFbank;
private CmvnEntity _cmvnEntity;
private static int _fbank_beg_idx = 0;
public WavFrontend(string mvnFilePath, FrontendConfEntity frontendConfEntity)
{
_frontendConfEntity = frontendConfEntity;
_fbank_beg_idx = 0;
_onlineFbank = new OnlineFbank(
dither: _frontendConfEntity.dither,
snip_edges: true,
sample_rate: _frontendConfEntity.fs,
num_bins: _frontendConfEntity.n_mels
);
_cmvnEntity = LoadCmvn(mvnFilePath);
}
public float[] GetFbank(float[] samples)
{
float sample_rate = _frontendConfEntity.fs;
samples = samples.Select((float x) => x * 32768f).ToArray();
float[] fbanks = _onlineFbank.GetFbank(samples);
return fbanks;
}
public float[] LfrCmvn(float[] fbanks)
{
float[] features = fbanks;
if (_frontendConfEntity.lfr_m != 1 || _frontendConfEntity.lfr_n != 1)
{
features = ApplyLfr(fbanks, _frontendConfEntity.lfr_m, _frontendConfEntity.lfr_n);
}
if (_cmvnEntity != null)
{
features = ApplyCmvn(features);
}
return features;
}
private float[] ApplyCmvn(float[] inputs)
{
var arr_neg_mean = _cmvnEntity.Means;
float[] neg_mean = arr_neg_mean.Select(x => (float)Convert.ToDouble(x)).ToArray();
var arr_inv_stddev = _cmvnEntity.Vars;
float[] inv_stddev = arr_inv_stddev.Select(x => (float)Convert.ToDouble(x)).ToArray();
int dim = neg_mean.Length;
int num_frames = inputs.Length / dim;
for (int i = 0; i < num_frames; i++)
{
for (int k = 0; k != dim; ++k)
{
inputs[dim * i + k] = (inputs[dim * i + k] + neg_mean[k]) * inv_stddev[k];
}
}
return inputs;
}
public float[] ApplyLfr(float[] inputs, int lfr_m, int lfr_n)
{
int t = inputs.Length / 80;
int t_lfr = (int)Math.Floor((double)(t / lfr_n));
float[] input_0 = new float[80];
Array.Copy(inputs, 0, input_0, 0, 80);
int tile_x = (lfr_m - 1) / 2;
t = t + tile_x;
float[] inputs_temp = new float[t * 80];
for (int i = 0; i < tile_x; i++)
{
Array.Copy(input_0, 0, inputs_temp, tile_x * 80, 80);
}
Array.Copy(inputs, 0, inputs_temp, tile_x * 80, inputs.Length);
inputs = inputs_temp;
float[] LFR_outputs = new float[t_lfr * lfr_m * 80];
for (int i = 0; i < t_lfr; i++)
{
if (lfr_m <= t - i * lfr_n)
{
Array.Copy(inputs, i * lfr_n * 80, LFR_outputs, i* lfr_m * 80, lfr_m * 80);
}
else
{
// process last LFR frame
int num_padding = lfr_m - (t - i * lfr_n);
float[] frame = new float[lfr_m * 80];
Array.Copy(inputs, i * lfr_n * 80, frame, 0, (t - i * lfr_n) * 80);
for (int j = 0; j < num_padding; j++)
{
Array.Copy(inputs, (t - 1) * 80, frame, (lfr_m - num_padding + j) * 80, 80);
}
Array.Copy(frame, 0, LFR_outputs, i * lfr_m * 80, frame.Length);
}
}
return LFR_outputs;
}
private CmvnEntity LoadCmvn(string mvnFilePath)
{
List<float> means_list = new List<float>();
List<float> vars_list = new List<float>();
FileStreamOptions options = new FileStreamOptions();
options.Access = FileAccess.Read;
options.Mode = FileMode.Open;
StreamReader srtReader = new StreamReader(mvnFilePath, options);
int i = 0;
while (!srtReader.EndOfStream)
{
string? strLine = srtReader.ReadLine();
if (!string.IsNullOrEmpty(strLine))
{
if (strLine.StartsWith("<AddShift>"))
{
i=1;
continue;
}
if (strLine.StartsWith("<Rescale>"))
{
i = 2;
continue;
}
if (strLine.StartsWith("<LearnRateCoef>") && i==1)
{
string[] add_shift_line = strLine.Substring(strLine.IndexOf("[") + 1, strLine.LastIndexOf("]") - strLine.IndexOf("[") - 1).Split(" ");
means_list = add_shift_line.Where(x => !string.IsNullOrEmpty(x)).Select(x => float.Parse(x.Trim())).ToList();
continue;
}
if (strLine.StartsWith("<LearnRateCoef>") && i==2)
{
string[] rescale_line = strLine.Substring(strLine.IndexOf("[") + 1, strLine.LastIndexOf("]") - strLine.IndexOf("[") - 1).Split(" ");
vars_list = rescale_line.Where(x => !string.IsNullOrEmpty(x)).Select(x => float.Parse(x.Trim())).ToList();
continue;
}
}
}
CmvnEntity cmvnEntity = new CmvnEntity();
cmvnEntity.Means = means_list;
cmvnEntity.Vars = vars_list;
return cmvnEntity;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (_onlineFbank != null)
{
_onlineFbank.Dispose();
}
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
~WavFrontend()
{
Dispose(_disposed);
}
}
}
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AliFsmnVadSharp
{
public enum FrameState
{
kFrameStateInvalid = -1,
kFrameStateSpeech = 1,
kFrameStateSil = 0
}
/// <summary>
/// final voice/unvoice state per frame
/// </summary>
public enum AudioChangeState
{
kChangeStateSpeech2Speech = 0,
kChangeStateSpeech2Sil = 1,
kChangeStateSil2Sil = 2,
kChangeStateSil2Speech = 3,
kChangeStateNoBegin = 4,
kChangeStateInvalid = 5
}
internal class WindowDetector
{
private int _window_size_ms = 0; //window_size_ms;
private int _sil_to_speech_time = 0; //sil_to_speech_time;
private int _speech_to_sil_time = 0; //speech_to_sil_time;
private int _frame_size_ms = 0; //frame_size_ms;
private int _win_size_frame = 0;
private int _win_sum = 0;
private int[] _win_state = new int[0];// * _win_size_frame; // 初始化窗
private int _cur_win_pos = 0;
private int _pre_frame_state = (int)FrameState.kFrameStateSil;
private int _cur_frame_state = (int)FrameState.kFrameStateSil;
private int _sil_to_speech_frmcnt_thres = 0; //int(sil_to_speech_time / frame_size_ms);
private int _speech_to_sil_frmcnt_thres = 0; //int(speech_to_sil_time / frame_size_ms);
private int _voice_last_frame_count = 0;
private int _noise_last_frame_count = 0;
private int _hydre_frame_count = 0;
public WindowDetector()
{
}
public WindowDetector(int window_size_ms, int sil_to_speech_time, int speech_to_sil_time, int frame_size_ms)
{
_window_size_ms = window_size_ms;
_sil_to_speech_time = sil_to_speech_time;
_speech_to_sil_time = speech_to_sil_time;
_frame_size_ms = frame_size_ms;
_win_size_frame = (int)(window_size_ms / frame_size_ms);
_win_sum = 0;
_win_state = new int[_win_size_frame];//[0] * _win_size_frame; // 初始化窗
_cur_win_pos = 0;
_pre_frame_state = (int)FrameState.kFrameStateSil;
_cur_frame_state = (int)FrameState.kFrameStateSil;
_sil_to_speech_frmcnt_thres = (int)(sil_to_speech_time / frame_size_ms);
_speech_to_sil_frmcnt_thres = (int)(speech_to_sil_time / frame_size_ms);
_voice_last_frame_count = 0;
_noise_last_frame_count = 0;
_hydre_frame_count = 0;
}
public void Reset()
{
_cur_win_pos = 0;
_win_sum = 0;
_win_state = new int[_win_size_frame];
_pre_frame_state = (int)FrameState.kFrameStateSil;
_cur_frame_state = (int)FrameState.kFrameStateSil;
_voice_last_frame_count = 0;
_noise_last_frame_count = 0;
_hydre_frame_count = 0;
}
public int GetWinSize()
{
return _win_size_frame;
}
public AudioChangeState DetectOneFrame(FrameState frameState, int frame_count)
{
_cur_frame_state = (int)FrameState.kFrameStateSil;
if (frameState == FrameState.kFrameStateSpeech)
{
_cur_frame_state = 1;
}
else if (frameState == FrameState.kFrameStateSil)
{
_cur_frame_state = 0;
}
else
{
return AudioChangeState.kChangeStateInvalid;
}
_win_sum -= _win_state[_cur_win_pos];
_win_sum += _cur_frame_state;
_win_state[_cur_win_pos] = _cur_frame_state;
_cur_win_pos = (_cur_win_pos + 1) % _win_size_frame;
if (_pre_frame_state == (int)FrameState.kFrameStateSil && _win_sum >= _sil_to_speech_frmcnt_thres)
{
_pre_frame_state = (int)FrameState.kFrameStateSpeech;
return AudioChangeState.kChangeStateSil2Speech;
}
if (_pre_frame_state == (int)FrameState.kFrameStateSpeech && _win_sum <= _speech_to_sil_frmcnt_thres)
{
_pre_frame_state = (int)FrameState.kFrameStateSil;
return AudioChangeState.kChangeStateSpeech2Sil;
}
if (_pre_frame_state == (int)FrameState.kFrameStateSil)
{
return AudioChangeState.kChangeStateSil2Sil;
}
if (_pre_frame_state == (int)FrameState.kFrameStateSpeech)
{
return AudioChangeState.kChangeStateSpeech2Speech;
}
return AudioChangeState.kChangeStateInvalid;
}
private int FrameSizeMs()
{
return _frame_size_ms;
}
}
}
+59
View File
@@ -0,0 +1,59 @@
# AliFsmnVadSharp
##### 简介:
项目中使用的VAD模型是阿里巴巴达摩院提供的FSMN-Monophone VAD模型。
**项目基于Net 6.0,使用C#编写,调用Microsoft.ML.OnnxRuntime对onnx模型进行解码,支持跨平台编译。项目以库的形式进行调用,部署非常方便。**
VAD整体流程的rtf在0.008左右。
##### 用途:
16k中文通用VAD模型:可用于检测长语音片段中有效语音的起止时间点.
FSMN-Monophone VAD是达摩院语音团队提出的高效语音端点检测模型,用于检测输入音频中有效语音的起止时间点信息,并将检测出来的有效音频片段输入识别引擎进行识别,减少无效语音带来的识别错误。
##### VAD常用参数调整说明(参考:vad.yaml文件):
max_end_silence_time:尾部连续检测到多长时间静音进行尾点判停,参数范围500ms~6000ms,默认值800ms(该值过低容易出现语音提前截断的情况)。
speech_noise_thresspeech的得分减去noise的得分大于此值则判断为speech,参数范围:(-1,1)
取值越趋于-1,噪音被误判定为语音的概率越大,FA越高
取值越趋于+1,语音被误判定为噪音的概率越大,Pmiss越高
通常情况下,该值会根据当前模型在长语音测试集上的效果取balance
##### 模型获取
##### 调用方式:
###### 1.添加项目引用
using AliFsmnVadSharp;
###### 2.初始化模型和配置
```csharp
string applicationBase = AppDomain.CurrentDomain.BaseDirectory;
string modelFilePath = applicationBase + "./speech_fsmn_vad_zh-cn-16k-common-pytorch/model.onnx";
string configFilePath = applicationBase + "./speech_fsmn_vad_zh-cn-16k-common-pytorch/vad.yaml";
string mvnFilePath = applicationBase + "./speech_fsmn_vad_zh-cn-16k-common-pytorch/vad.mvn";
int batchSize = 2;//批量解码
AliFsmnVad aliFsmnVad = new AliFsmnVad(modelFilePath, configFilePath, mvnFilePath, batchSize);
```
###### 3.调用
方法一(适用于小文件)
```csharp
SegmentEntity[] segments_duration = aliFsmnVad.GetSegments(samples);
```
方法二(适用于大文件)
```csharp
SegmentEntity[] segments_duration = aliFsmnVad.GetSegmentsByStep(samples);
```
###### 4.输出结果:
```
load model and init config elapsed_milliseconds:463.5390625
vad infer result:
[[70,2340][2620,6200][6480,23670][23950,26250][26780,28990][29950,31430][31750,37600][38210,46900][47310,49630][49910,56460][56740,59540][59820,70450]]
elapsed_milliseconds:662.796875
total_duration:70470.625
rtf:0.009405292985552491
```
输出的数据,例如:[70,2340],是以毫秒为单位的segement的起止时间,可以以此为依据对音频进行分片。其中静音噪音部分已被去除。
其他说明:
测试用例:AliFsmnVadSharp.Examples。
测试环境:windows11。
测试用例中samples的计算,使用的是NAudio库。
通过以下链接了解更多:
https://www.modelscope.cn/models/damo/speech_fsmn_vad_zh-cn-16k-common-pytorch/summary
@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiApp1"
x:Class="MauiApp1.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
@@ -0,0 +1,19 @@
namespace MauiApp1;
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new MySplashPage();
_ = EndSplash();
}
async Task EndSplash()
{
await Task.Delay(1000);
MainThread.BeginInvokeOnMainThread(() =>
{
MainPage = new AppShell();
});
}
}
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="MauiApp1.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiApp1"
Shell.FlyoutBehavior="Disabled">
<FlyoutItem FlyoutDisplayOptions="AsMultipleItems">
<ShellContent
Title="Offline"
ContentTemplate="{DataTemplate local:RecognitionForFiles}"
Route="MainPage" />
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</FlyoutItem>
</Shell>
@@ -0,0 +1,9 @@
namespace MauiApp1;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.MainPage">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Image
Source="dotnet_bot.png"
SemanticProperties.Description="Cute dot net bot waving hi to you!"
HeightRequest="66"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
@@ -0,0 +1,25 @@
using Microsoft.Maui.Storage;
using NAudio.Wave;
using System.ComponentModel;
using System.Net;
using System.Reflection;
using System.Text;
using System.Windows.Input;
using static System.Net.Mime.MediaTypeNames;
using MauiApp1.Utils;
using System;
using Microsoft.Maui.Graphics;
namespace MauiApp1;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
@@ -0,0 +1,91 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-android;net6.0-ios;net6.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net6.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net6.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<RootNamespace>MauiApp1</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Display name -->
<ApplicationTitle>ASR demo</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.manyeyes.MauiApp1</ApplicationId>
<ApplicationIdGuid>be632abf-f31d-4458-8964-b4e8787dee11</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net6.0-android|AnyCPU'">
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#F6F8F6" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#F6F8F6" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NAudio" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AliParaformerAsr\AliParaformerAsr.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="RecognitionForFiles.xaml.cs">
<DependentUpon>RecognitionForFiles.xaml</DependentUpon>
</Compile>
<Compile Update="MainPage.xaml.cs">
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="MainPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="MySplashPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
<ItemGroup>
<None Update="speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx\model_quant.onnx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="AllModels\" />
</ItemGroup>
</Project>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<IsFirstTimeProjectOpen>False</IsFirstTimeProjectOpen>
<ActiveDebugFramework>net6.0-android</ActiveDebugFramework>
<ActiveDebugProfile>Xiaomi Redmi Note 7 Pro (Android 10.0 - API 29)</ActiveDebugProfile>
<SelectedPlatformGroup>PhysicalDevice</SelectedPlatformGroup>
<DefaultDevice>pixel_5_-_api_33</DefaultDevice>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net6.0-android|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetPlatformIdentifier)'=='iOS'">
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<None Update="Platforms\Windows\Package.appxmanifest">
<SubType>Designer</SubType>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,18 @@
namespace MauiApp1;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
return builder.Build();
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.MySplashPage">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Image
Source="dotnet_bot.png"
SemanticProperties.Description="Cute dot net bot waving hi to you!"
HeightRequest="66"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
@@ -0,0 +1,25 @@
using Microsoft.Maui.Storage;
using NAudio.Wave;
using System.ComponentModel;
using System.Net;
using System.Reflection;
using System.Text;
using System.Windows.Input;
using static System.Net.Mime.MediaTypeNames;
using MauiApp1.Utils;
using System;
using Microsoft.Maui.Graphics;
namespace MauiApp1;
public partial class MySplashPage : ContentPage
{
public MySplashPage()
{
InitializeComponent();
}
}
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Required only if your app needs to access images or photos that other apps created -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- Required only if your app needs to access videos that other apps created -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Required only if your app needs to access audio files that other apps created -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RecordAudio" />
<uses-sdk />
</manifest>
@@ -0,0 +1,10 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace MauiApp1;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}
@@ -0,0 +1,39 @@
using Android.App;
using Android.Runtime;
namespace MauiApp1;
[Application]
//[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage, MaxSDKVersion = 32)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadMediaAudio)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadMediaImages)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadMediaVideo)]
[assembly: UsesPermission(Android.Manifest.Permission.ManageExternalStorage)]
// Needed for Picking photo/video
[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)]
// Needed for Taking photo/video
[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]
[assembly: UsesPermission(Android.Manifest.Permission.Camera)]
[assembly: UsesPermission(Android.Manifest.Permission.RecordAudio)]
[assembly: UsesPermission(Android.Manifest.Permission.CaptureVideoOutput)]
// Add these properties if you would like to filter out devices that do not have cameras, or set to false to make them optional
[assembly: UsesFeature("android.hardware.camera", Required = true)]
[assembly: UsesFeature("android.hardware.camera.autofocus", Required = true)]
[assembly: UsesFeature("android.hardware.recordaudio", Required = true)]
[assembly: UsesFeature("android.hardware.recordaudio.autofocus", Required = true)]
[assembly: UsesFeature("android.hardware.capturevideooutput", Required = true)]
[assembly: UsesFeature("android.hardware.capturevideooutput.autofocus", Required = true)]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>
@@ -0,0 +1,9 @@
using Foundation;
namespace MauiApp1;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>
@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace MauiApp1;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
@@ -0,0 +1,16 @@
using System;
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
namespace MauiApp1;
class Program : MauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="7" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="MauiApp1.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>
@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="MauiApp1.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:MauiApp1.WinUI">
</maui:MauiWinUIApplication>
@@ -0,0 +1,24 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace MauiApp1.WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="B8687A78-A016-4011-9FEF-910387A888E5" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MauiApp1.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>
@@ -0,0 +1,9 @@
using Foundation;
namespace MauiApp1;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>
@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace MauiApp1;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.RecognitionForFiles">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="15,15"
VerticalOptions="Start">
<StackLayout>
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="120" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="15" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Text="models not checked" Grid.Row="0" Grid.Column="0"
x:Name="ModelStatusLabel"
VerticalTextAlignment="Center" HorizontalTextAlignment="Start"/>
<Button Grid.Row="0" Grid.Column="1"
x:Name="BtnDownLoadCheck"
Text="Check"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnDownLoadCheckClicked"
HorizontalOptions="Center" />
<Button Grid.Row="0" Grid.Column="2"
x:Name="BtnDownLoadModels"
Text="Download"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnDownLoadModelsClicked"
HorizontalOptions="EndAndExpand" />
<Label Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3"
x:Name="DownloadProgressLabel"
Text="" IsVisible="False"
BackgroundColor="White"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Start"
TextColor="Red"/>
<ProgressBar IsVisible="False" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"
x:Name="DownloadProgressBar" HorizontalOptions="FillAndExpand" />
</Grid>
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
x:Name="DownloadResultsLabel"
Text="" IsVisible="False"
BackgroundColor="White"
TextColor="Red"/>
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="8*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Button Grid.Row="0" Grid.Column="0"
x:Name="BtnRecognitionExample"
Text="Example"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnBtnRecognitionExampleClicked"
HorizontalOptions="FillAndExpand" />
<Button Grid.Row="0" Grid.Column="1"
x:Name="BtnRecognitionFiles"
Text="Recognition Files"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnBtnRecognitionFilesClicked"
HorizontalOptions="FillAndExpand" />
</Grid>
</StackLayout>
<Label FlowDirection="MatchParent"
Text="Display recognition results"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I"
FontSize="15"
Background="#F6F8F6"
Padding="10,10,10,10"
x:Name="AsrResults"
HorizontalOptions="FillAndExpand"
BackgroundColor="White"
TextColor="Red" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
@@ -0,0 +1,496 @@
using Microsoft.Maui.Storage;
using NAudio.Wave;
using System.ComponentModel;
using System.Net;
using System.Reflection;
using System.Text;
using System.Windows.Input;
using static System.Net.Mime.MediaTypeNames;
using MauiApp1.Utils;
using System;
using Microsoft.Maui.Graphics;
namespace MauiApp1;
public partial class RecognitionForFiles : ContentPage
{
private AliParaformerAsr.OfflineRecognizer _offlineRecognizer = null;
public RecognitionForFiles()
{
InitializeComponent();
//GetDownloadState();
}
private async void OnDownLoadCheckClicked(object sender, EventArgs e)
{
BtnDownLoadCheck.IsEnabled = false;
TaskFactory taskFactory = new TaskFactory();
await taskFactory.StartNew(async () =>
{
DownloadCheck();
});
BtnDownLoadCheck.IsEnabled = true;
}
private async void OnDownLoadModelsClicked(object sender, EventArgs e)
{
BtnDownLoadModels.IsEnabled = false;
DownloadProgressBar.Progress = 0 / 100.0;
DownloadProgressLabel.Text = "";
TaskFactory taskFactory = new TaskFactory();
await taskFactory.StartNew(async () =>
{
DownloadModels();
});
BtnDownLoadModels.IsEnabled = true;
}
async void DownloadModels()
{
string paraformerFilejson = "[{},{}]";
string[] downloadUrls = {
"http://huggingface.co/manyeyes/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx/resolve/main/model_quant.onnx",
"http://huggingface.co/manyeyes/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx/resolve/main/am.mvn",
"http://huggingface.co/manyeyes/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx/resolve/main/asr.yaml",
"http://huggingface.co/manyeyes/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx/resolve/main/tokens.txt",
"http://huggingface.co/manyeyes/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx/resolve/main/example/0.wav" };
string[] fileNames = { "model.int8.onnx", "am.mvn", "asr.yaml", "tokens.txt", "0.wav" };
var rootFolderName = "AllModels";
var subFolderName = "speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx";
List<int> indexs = new List<int>();
DownloadHelper downloadHelper = new DownloadHelper(this.DownloadDisplay);
DownloadProgressLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressLabel.IsVisible = true;
DownloadProgressLabel.Text = "";
}));
DownloadProgressBar.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressBar.IsVisible = true;
DownloadProgressBar.Progress = 0 / 100.0;
}));
while (indexs.Count < downloadUrls.Length)
{
if (downloadHelper.IsDownloading)
{
continue;
}
for (int i = 0; i < downloadUrls.Length; i++)
{
if (indexs.Contains(i))
{
continue;
}
if (downloadHelper.IsDownloading)
{
break;
}
var downloadUrl = downloadUrls[i];
var fileName = fileNames[i];
downloadHelper.DownloadCreate(downloadUrl, fileName, rootFolderName, subFolderName);
downloadHelper.DownloadStart();
indexs.Add(i);
}
}
DownloadProgressLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressLabel.IsVisible = false;
DownloadProgressLabel.Text = "";
}));
DownloadProgressBar.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressBar.IsVisible = false;
DownloadProgressBar.Progress = 0 / 100.0;
}));
}
async void GetDownloadState()
{
string[] fileNames = { "model.int8.onnx", "am.mvn", "asr.yaml", "tokens.txt", "0.wav" };
string[] md5Strs = { "a171eff9959f2486d5c1bafce61d0b7a", "dc1dbdeeb8961f012161cfce31eaacaf", "71d32ccf12dbb85a5d8b249558f258fb", "5c8702dc2686b846fe268d9aa712be9b", "af1295e53df7298458f808bc0cd946e2" };
var rootFolderName = "AllModels";
var subFolderName = "speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx";
DownloadHelper downloadHelper = new DownloadHelper();
bool state = downloadHelper.GetDownloadState(fileNames, rootFolderName, subFolderName, md5Strs);
ModelStatusLabel.Dispatcher.Dispatch(
new Action(
delegate
{
if (state)
{
ModelStatusLabel.Text = "model is ready";
}
else
{
ModelStatusLabel.Text = "model not ready";
}
}));
}
async void DownloadCheck()
{
string[] fileNames = { "model.int8.onnx", "am.mvn", "asr.yaml", "tokens.txt", "0.wav" };
string[] md5Strs = { "a171eff9959f2486d5c1bafce61d0b7a", "dc1dbdeeb8961f012161cfce31eaacaf", "71d32ccf12dbb85a5d8b249558f258fb", "5c8702dc2686b846fe268d9aa712be9b", "af1295e53df7298458f808bc0cd946e2" };
var rootFolderName = "AllModels";
var subFolderName = "speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx";
DownloadHelper downloadHelper = new DownloadHelper(this.DownloadDisplay);
bool state = downloadHelper.GetDownloadState(fileNames, rootFolderName, subFolderName, md5Strs);
ModelStatusLabel.Dispatcher.Dispatch(
new Action(
delegate
{
if (state)
{
ModelStatusLabel.Text = "model is ready";
}
else
{
ModelStatusLabel.Text = "model not ready";
}
}));
if (!state)
{
DownloadResultsLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadResultsLabel.IsVisible = true;
DownloadResultsLabel.Text = "";
}));
for (int i = 0; i < fileNames.Length; i++)
{
downloadHelper.DownloadCheck(fileNames[i], rootFolderName, subFolderName, md5Strs[i]);
}
}
}
private void DownloadDisplay(int progress, DownloadState downloadState, string filename, string msg = "")
{
switch (downloadState)
{
case DownloadState.inprogres:
DownloadProgressBar.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressBar.Progress = progress / 100.0;
}));
DownloadProgressLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressLabel.Text = $"文件:{filename},正在下载,进度:{progress}%\n";
}));
break;
case DownloadState.cancelled:
DownloadProgressBar.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressBar.Progress = progress / 100.0;
}));
DownloadProgressLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressLabel.Text = $"文件:{filename},下载已取消\n";
}));
break;
case DownloadState.error:
DownloadProgressBar.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressBar.Progress = progress / 100.0;
}));
DownloadProgressLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressLabel.Text = $"文件:{filename},下载失败:{msg}\n";
}));
DownloadResultsLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadResultsLabel.Text += $"文件:{filename},下载失败:{msg}\n";
}));
break;
case DownloadState.completed:
DownloadProgressBar.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressBar.Progress = progress / 100.0;
}));
DownloadProgressLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressLabel.Text = $"文件:{filename},下载完成\n";
}));
DownloadResultsLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadResultsLabel.Text += $"文件:{filename},下载完成\n";
}));
break;
case DownloadState.existed:
DownloadProgressBar.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadProgressBar.Progress = progress / 100.0;
}));
DownloadResultsLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadResultsLabel.Text += $"文件:{filename},已存在\n";
}));
break;
case DownloadState.noexisted:
DownloadResultsLabel.Dispatcher.Dispatch(
new Action(
delegate
{
DownloadResultsLabel.Text += $"文件:{filename},不存在\n";
}));
break;
}
}
private async void OnBtnRecognitionExampleClicked(object sender, EventArgs e)
{
BtnRecognitionExample.IsEnabled = false;
TaskFactory taskFactory = new TaskFactory();
await taskFactory.StartNew(async () =>
{
TestAliParaformerAsrOfflineRecognizer();
});
BtnRecognitionExample.IsEnabled = true;
}
private async void OnBtnRecognitionFilesClicked(object sender, EventArgs e)
{
var customFileType = new FilePickerFileType(
new Dictionary<DevicePlatform, IEnumerable<string>>
{
{ DevicePlatform.iOS, new[] { "public.my.comic.extension" } }, // UTType values
{ DevicePlatform.Android, new[] { "audio/x-wav" } }, // MIME type
{ DevicePlatform.WinUI, new[] { ".cbr", ".cbz" } }, // file extension
{ DevicePlatform.Tizen, new[] { "*/*" } },
{ DevicePlatform.macOS, new[] { "cbr", "cbz" } }, // UTType values
});
PickOptions options = new()
{
PickerTitle = "Please select a comic file",
FileTypes = customFileType,
};
TaskFactory taskFactory = new TaskFactory();
await taskFactory.StartNew(async () =>
{
var fileResult = await PickAndShow(options);
string fullpath = fileResult.FullPath;
List<string> fullpaths = new List<string>();
fullpaths.Add(fullpath);
RecognizerFilesByAliParaformerAsrOffline(fullpaths);
});
}
public async Task<FileResult> PickAndShow(PickOptions options)
{
try
{
var result = await FilePicker.Default.PickAsync(options);
return result;
}
catch (Exception ex)
{
// The user canceled or something went wrong
}
return null;
}
public void CreateDownloadFile(string fileName)
{
var downloadFolder = FileSystem.AppDataDirectory + "/Download/";
Directory.CreateDirectory(downloadFolder);
var filePath = downloadFolder + fileName;
File.Create(filePath);
}
public AliParaformerAsr.OfflineRecognizer initAliParaformerAsrOfflineRecognizer(string modelName)
{
if (_offlineRecognizer == null)
{
try
{
string allModels = "AllModels";
string modelFilePath = SysConf.ApplicationBase + "/" + allModels + "/" + modelName + "/model.int8.onnx";
string configFilePath = SysConf.ApplicationBase + "/" + allModels + "/" + modelName + "/asr.yaml";
string mvnFilePath = SysConf.ApplicationBase + "/" + allModels + "/" + modelName + "/am.mvn";
string tokensFilePath = SysConf.ApplicationBase + "/" + allModels + "/" + modelName + "/tokens.txt";
_offlineRecognizer = new AliParaformerAsr.OfflineRecognizer(modelFilePath, configFilePath, mvnFilePath, tokensFilePath);
}
catch
{
DisplayAlert("Tips", "Please check if the model is correct", "close");
}
}
return _offlineRecognizer;
}
public void RecognizerFilesByAliParaformerAsrOffline(List<string> fullpaths)
{
string modelName = "speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx";
AliParaformerAsr.OfflineRecognizer offlineRecognizer = initAliParaformerAsrOfflineRecognizer(modelName);
if (offlineRecognizer == null) { return; }
AsrResults.Dispatcher.Dispatch(
new Action(
delegate
{
AsrResults.Text = "正在识别,请稍后……";
}
));
List<float[]>? samples = new List<float[]>();
TimeSpan total_duration = new TimeSpan(0L);
foreach (string fullpath in fullpaths)
{
string wavFilePath = string.Format(fullpath);
if (!File.Exists(wavFilePath))
{
continue;
}
AudioFileReader _audioFileReader = new AudioFileReader(wavFilePath);
byte[] datas = new byte[_audioFileReader.Length];
_audioFileReader.Read(datas, 0, datas.Length);
TimeSpan duration = _audioFileReader.TotalTime;
float[] wavdata = new float[datas.Length / 4];
Buffer.BlockCopy(datas, 0, wavdata, 0, datas.Length);
float[] sample = wavdata.Select((float x) => x * 32768f).ToArray();
samples.Add(sample);
total_duration += duration;
}
AsrResults.Dispatcher.Dispatch(
new Action(
delegate
{
TimeSpan start_time = new TimeSpan(DateTime.Now.Ticks);
foreach (var sample in samples)
{
List<float[]> temp_samples = new List<float[]>();
temp_samples.Add(sample);
List<string> results = offlineRecognizer.GetResults(temp_samples);
foreach (string result in results)
{
Console.WriteLine(result);
Console.WriteLine("");
AsrResults.Text = string.Format("result:{0}\n", result);
}
}
TimeSpan end_time = new TimeSpan(DateTime.Now.Ticks);
double elapsed_milliseconds = end_time.TotalMilliseconds - start_time.TotalMilliseconds;
double rtf = elapsed_milliseconds / total_duration.TotalMilliseconds;
AsrResults.Text += string.Format("elapsed_milliseconds:{0}\n", elapsed_milliseconds.ToString());
AsrResults.Text += string.Format("total_duration:{0}\n", total_duration.TotalMilliseconds.ToString());
AsrResults.Text += string.Format("rtf:{1}\n", "0".ToString(), rtf.ToString());
AsrResults.Text += string.Format("End!");
}));
}
public void TestAliParaformerAsrOfflineRecognizer(List<float[]>? samples = null)
{
string modelName = "speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx";
AliParaformerAsr.OfflineRecognizer offlineRecognizer = initAliParaformerAsrOfflineRecognizer(modelName);
if (offlineRecognizer == null) { return; }
AsrResults.Dispatcher.Dispatch(
new Action(
delegate
{
AsrResults.Text = "正在识别,请稍后……";
}
));
TimeSpan total_duration = new TimeSpan(0L);
if (samples == null)
{
samples = new List<float[]>();
for (int i = 0; i < 1; i++)
{
string wavFilePath = string.Format(SysConf.ApplicationBase + "/AllModels/" + modelName + "/{0}.wav", i.ToString());
if (!File.Exists(wavFilePath))
{
break;
}
AudioFileReader _audioFileReader = new AudioFileReader(wavFilePath);
byte[] datas = new byte[_audioFileReader.Length];
_audioFileReader.Read(datas, 0, datas.Length);
TimeSpan duration = _audioFileReader.TotalTime;
float[] wavdata = new float[datas.Length / 4];
Buffer.BlockCopy(datas, 0, wavdata, 0, datas.Length);
float[] sample = wavdata.Select((float x) => x * 32768f).ToArray();
samples.Add(sample);
total_duration += duration;
}
}
AsrResults.Dispatcher.Dispatch(
new Action(
delegate
{
TimeSpan start_time = new TimeSpan(DateTime.Now.Ticks);
foreach (var sample in samples)
{
List<float[]> temp_samples = new List<float[]>();
temp_samples.Add(sample);
List<string> results = offlineRecognizer.GetResults(temp_samples);
foreach (string result in results)
{
AsrResults.Text = string.Format("result:{0}\n", result);
}
}
TimeSpan end_time = new TimeSpan(DateTime.Now.Ticks);
double elapsed_milliseconds = end_time.TotalMilliseconds - start_time.TotalMilliseconds;
double rtf = elapsed_milliseconds / total_duration.TotalMilliseconds;
Console.WriteLine("elapsed_milliseconds:{0}", elapsed_milliseconds.ToString());
Console.WriteLine("total_duration:{0}", total_duration.TotalMilliseconds.ToString());
Console.WriteLine("rtf:{1}", "0".ToString(), rtf.ToString());
Console.WriteLine("Hello, World!");
AsrResults.Text += string.Format("elapsed_milliseconds:{0}\n", elapsed_milliseconds.ToString());
AsrResults.Text += string.Format("total_duration:{0}\n", total_duration.TotalMilliseconds.ToString());
AsrResults.Text += string.Format("rtf:{1}\n", "0".ToString(), rtf.ToString());
AsrResults.Text += string.Format("End!");
}
));
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#F6F8F6" />
</svg>

After

Width:  |  Height:  |  Size: 228 B

@@ -0,0 +1,492 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> <image id="image0" width="512" height="512" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAA
CXBIWXMAAAsTAAALEwEAmpwYAABrVElEQVR42u3deZxbdfU//td5J5l9JtMVSlsobactlLKKyiKb
RVFZBH+4ooJipaUsRXH7qLUqKh8tZS9U8IMK+vlYv6wiqCCLLCq7QLcpbSmlhS50ktlnknt+f7RT
pmkyuXeSzD3vd8778fAh3LzneXPe97zPuWQyCc2ceUIFAAbQ+9BDjyLomDnzBACIAaB+h9VTTz31
1FNPPcGecSkY9dRTTz311FPPn2dcCkY99dRTTz311PM3yPdMC4JRTz311FNPPfX8jcA3AJKDUU89
9dRTTz31/I1ANwDSg1FPPfXUU0899fx5vm8AbAhGPfXUU0899dTz5/m6AbAlGPXUU0899dRTz5+X
9wbApmDUU0899dRTTz1/HgWZLD0Y9dRTTz311FPPn0dBJksPRj311FNPPfXU8+dRkMnSg1FPPfXU
U0899fx5FGSy9GDUU0899dRTTz1/HgWZLD0Y9dRTTz311FPPn0dBJksPRj311FNPPfXU8+eRS8Go
p5566qmnnnr+PHIpGPXUU0899dRTz59nXApGPfXUU0899dTz5xmXglFPPfXUU0899fx5xqVg1FNP
PfXUU089f54JMll6MOqpp5566qmnnj/PuBSMeuqpp5566qnnzzMuBaOeeuqpp5566vkb5HumBcGo
p5566qmnnnr+RuAbAMnBqKeeeuqpp556/kagGwDpwainnnrqqaeeev483zcANgSjnnrqqaeeeur5
83zdANgSjHrqqaeeeuqp58/LewNgUzDqqaeeeuqpp54/j4JMlh6Meuqpp5566qnnz6Mgk6UHo556
6qmnnnrq+fMoyGTpwainnnrqqaeeev48CjJZejDqqaeeeuqpp54/j4JMlh6Meuqpp5566qnnz6Mg
k6UHo5566qmnnnrq+fPIpWDUU0899dRTTz1/HrkUjHrqqaeeeuqp588zLgWjnnrqqaeeeur584xL
wainnnrqqaeeev4841Iw6qmnnnrqqaeeP49mzjyhwpVgXPD22++nI2PVY6dSpH4cUWQCARNAGAXG
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
NCswMDowMCj2a7wAAAAASUVORK5CYII=" />
</svg>

After

Width:  |  Height:  |  Size: 37 KiB

Some files were not shown because too many files have changed in this diff Show More