Sync from bytedesk-private: update

This commit is contained in:
jack ning
2024-12-14 10:43:18 +08:00
parent 476eebb101
commit 5e082909e4
3421 changed files with 812709 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'
}

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

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">FunASR</string>
</resources>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}
}