Skip to content

Commit

Permalink
重构ASR部分代码,添加华为HMS语音识别;修复TTS初始化失败问题;优化长时间退出无法唤醒问题;优化界面;版本号更新至1.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Skythinker616 committed Oct 6, 2023
1 parent ca0d666 commit 24704ba
Show file tree
Hide file tree
Showing 27 changed files with 577 additions and 157 deletions.
17 changes: 17 additions & 0 deletions .idea/deploymentTargetDropDown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 41 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ GPT Assistant 是一个基于ChatGPT的安卓端语音助手,允许用户通
- 支持从**全局上下文菜单**(选中文本后弹出的系统菜单)中直接唤起
- 支持通过状态栏**快捷按钮**唤起
- 支持对**Markdown**进行渲染
- 使用百度短语音识别API进行**语音输入**
- 使用华为或百度语音API进行**语音输入**
- 调用系统TTS引擎**输出语音**

### 国内使用说明
Expand All @@ -24,17 +24,17 @@ GPT Assistant 是一个基于ChatGPT的安卓端语音助手,允许用户通
### 费用说明

本软件不会以任何方式收取费用,也不会在软件中嵌入任何广告,但使用的下述服务**可能**产生费用:
本软件不会以任何方式收取费用,也不会在软件中嵌入任何广告,但使用的下述服务**可能**会产生费用:

1. ChatGPT调用费用

- 以Chatanywhere为例,目前其**免费服务**限制对`gpt-3.5-turbo`模型的调用频率不超过**60请求/小时/IP&Key**,足够个人使用,若需要更高的调用频率或`gpt-4`模型,可以选择付费服务

2. 百度语音识别接口费用

- 目前,百度短语音识别为新用户提供**15万次 & 180天免费额度**,额度外收取¥0.0034/次的调用费用
- 目前华为HMS提供免费的语音识别接口,因此程序内置了作者的API Key以供直接使用,如无特殊情况该API将在华为免费期间一直可用

- 若不需要使用语音输入功能,可以不填写相关配置项,不会产生该项费用
- 程序也提供了对百度接口的调用以供有需要的情况下使用,目前百度短语音识别为新用户提供**15万次 & 180天免费额度**,额度外收取¥0.0034/次的调用费用

---

Expand Down Expand Up @@ -84,23 +84,21 @@ GPT Assistant 是一个基于ChatGPT的安卓端语音助手,允许用户通

直接下载最新发行版中的apk文件,安装即可

### 2. 配置 API_KEY
### 2. 配置 OpenAI

程序使用的是OpenAI API,需要用户在设置中填入自己的API_KEY,可以选择使用官方服务或第三方转发服务

**使用官方服务**
- **使用Chatanywhere转发服务**(国内推荐)

在OpenAI官网注册账号并获取API_KEY,在设置中填写网址`https://api.openai.com/`和API_KEY
Chatanywhere提供了免费和付费的OpenAI API转发服务,目前免费服务限制60请求/小时/IP&Key调用频率,付费服务则无限制,可以在国内直接访问,用户可以参照其[项目主页](https://github.com/chatanywhere/GPT_API_free)获取地址和KEY填入设置中

若在国内使用,则建议使用第三方转发服务,以下方的Chatanywhere为例
- **使用官方服务**

**使用Chatanywhere转发服务**
在OpenAI官网注册账号并获取API_KEY,在设置中填写网址`https://api.openai.com/`和API_KEY

Chatanywhere提供了免费和付费的OpenAI API转发服务,目前免费服务限制60请求/小时/IP&Key调用频率,付费服务则无限制,可以在国内直接访问,用户可以参照其[项目主页](https://github.com/chatanywhere/GPT_API_free)获取地址和KEY填入设置中
### 3. 配置百度语音识别 (可选)

### 3. 配置语音识别

> 注:如果不需要使用语音输入功能,可以跳过这一步
> 注:程序默认使用的是华为语音识别接口,如无特殊情况,不需要进行此步骤
用户可以参照[百度语音识别官方文档](https://cloud.baidu.com/doc/SPEECH/s/qknh9i8ed)注册并创建应用,然后获取AppID、API Key和Secret Key填入设置中

Expand All @@ -114,26 +112,50 @@ Chatanywhere提供了免费和付费的OpenAI API转发服务,目前免费服

1. 根据软件提示开启无障碍服务,并允许软件在后台运行

2. 对于部分厂商的较新系统,需要手动在设置中允许应用的“后台弹出界面”权限
2. 查看设置中是否存在“后台弹出界面”权限,如有该权限则允许,如无则忽略

> 若发现长按音量下键后手机震动一下但没有弹出界面,大概率是因为缺少该权限

3. 开始正常使用,可参照[效果展示](#效果展示)中的操作步骤

---

## Q&A

**Q: 长按音量下键只是在调节音量,并没有其他任何现象?**
A: 请在设置中开启本软件的无障碍服务

**Q: 长按音量下键后,手机震动了一下,但没有弹出界面?**
A: 请在设置中允许程序“后台弹出界面”权限

**Q: 隔一段时间不用就无法使用音量键唤起了?**
A: 请在设置中允许程序在后台运行

**Q: 华为和百度语音识别效果有什么区别?**
A: 经测试,华为接口拥有更好的识别准确率,尤其是在中英混说的场景下,但其断句能力则不如百度,仅适合单句识别

---

## 主要功能更新日志

- **2023.09.10** 发布第一个版本,支持基础对话、百度语音输入、TTS输出、Markdown渲染等功能
- **2023.09.13** 支持连续对话、GPT-4、百度长语音识别,上下文菜单唤起
- **2023.10.06** 添加华为HMS语音识别

---

## 测试环境

已测试的机型:

| 机型 | 系统版本 | Android 版本 | 本程序版本 |
| :--: | :-----: | :----------: | :-------: |
| 荣耀 7C | EMUI 8.0.0 | Android 8 | 1.4.0 |
| 荣耀 20 | HarmonyOS 3.0.0 | Android 10 | 1.4.0 |
| 华为 Mate 30 | HarmonyOS 3.0.0 | Android 12 | 1.2.0 |
| 荣耀 7C | EMUI 8.0.0 | Android 8 | 1.5.0 |
| 荣耀 20 | HarmonyOS 3.0.0 | Android 10 | 1.5.0 |
| 华为 Mate 30 | HarmonyOS 3.0.0 | Android 12 | 1.4.0 |
| 荣耀 Magic 4 | MagicOS 7.0 | Android 13 | 1.2.0 |
| 红米 K20 Pro | MIUI 12.5.6 | Android 11 | 1.3.0 |
| 红米 K60 Pro | MIUI 14.0.23 | Android 13 | 1.3.0 |
| 红米 K20 Pro | MIUI 12.5.6 | Android 11 | 1.5.0 |
| 红米 K60 Pro | MIUI 14.0.23 | Android 13 | 1.5.0 |
| Pixel 2 (模拟器) | Android 12 | Android 12 | 1.2.0 |

---
Expand Down
54 changes: 54 additions & 0 deletions app/agconnect-services.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"agcgw_all":{
"CN":"connect-drcn.dbankcloud.cn",
"CN_back":"connect-drcn.hispace.hicloud.com",
"DE":"connect-dre.dbankcloud.cn",
"DE_back":"connect-dre.hispace.hicloud.com",
"RU":"connect-drru.hispace.dbankcloud.ru",
"RU_back":"connect-drru.hispace.dbankcloud.cn",
"SG":"connect-dra.dbankcloud.cn",
"SG_back":"connect-dra.hispace.hicloud.com"
},
"websocketgw_all":{
"CN":"connect-ws-drcn.hispace.dbankcloud.cn",
"CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
"DE":"connect-ws-dre.hispace.dbankcloud.cn",
"DE_back":"connect-ws-dre.hispace.dbankcloud.com",
"RU":"connect-ws-drru.hispace.dbankcloud.ru",
"RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
"SG":"connect-ws-dra.hispace.dbankcloud.cn",
"SG_back":"connect-ws-dra.hispace.dbankcloud.com"
},
"client":{
"cp_id":"70086000187586270",
"product_id":"388421841221708581",
"project_id":"388421841221708581",
"app_id":"109326889",
"package_name":"com.skythinker.gptassistant"
},
"oauth_client":{
"client_id":"109326889",
"client_type":1
},
"app_info":{
"app_id":"109326889",
"package_name":"com.skythinker.gptassistant"
},
"configuration_version":"3.0",
"appInfos":[
{
"package_name":"com.skythinker.gptassistant",
"client":{
"app_id":"109326889"
},
"app_info":{
"package_name":"com.skythinker.gptassistant",
"app_id":"109326889"
},
"oauth_client":{
"client_type":1,
"client_id":"109326889"
}
}
]
}
5 changes: 3 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id 'com.android.application'
id 'com.huawei.agconnect'
}

android {
Expand All @@ -10,11 +11,10 @@ android {
minSdk 26
targetSdk 32
versionCode 1
versionName "1.4.0"
versionName "1.5.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
Expand Down Expand Up @@ -58,4 +58,5 @@ dependencies {
implementation"io.noties.markwon:syntax-highlight:$markwon_version"
annotationProcessor 'io.noties:prism4j-bundler:2.0.0'
implementation group: 'com.unfbx', name: 'chatgpt-java', version: '1.1.0'
implementation 'com.huawei.hms:ml-computer-voice-asr:3.12.0.301'
}
8 changes: 7 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<uses-permission android:name="android.permission.VIBRATE" />

Expand All @@ -14,9 +15,14 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />

<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>


<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/skythinker/gptassistant/AsrClientBase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.skythinker.gptassistant;

public abstract class AsrClientBase {
public interface IAsrCallback {
void onError(String msg);
void onResult(String result);
}
public abstract void startRecongnize();
public abstract void stopRecongnize();
public abstract void cancelRecongnize();
public abstract void setCallback(IAsrCallback callback);
public abstract void setParam(String key, Object value);
public abstract void destroy();
}
107 changes: 107 additions & 0 deletions app/src/main/java/com/skythinker/gptassistant/BaiduAsrClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.skythinker.gptassistant;

import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import com.baidu.speech.EventListener;
import com.baidu.speech.EventManager;
import com.baidu.speech.EventManagerFactory;
import com.baidu.speech.asr.SpeechConstant;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.LinkedHashMap;
import java.util.Map;

public class BaiduAsrClient extends AsrClientBase{
private EventManager asr = null;
String asrBuffer = "";
IAsrCallback callback = null;
EventListener listener = null;

public BaiduAsrClient(Context context) {
asr = EventManagerFactory.create(context, "asr");
listener = new EventListener() {
@Override
public void onEvent(String name, String params, byte[] data, int offset, int length) {
if(name.equals(SpeechConstant.CALLBACK_EVENT_ASR_PARTIAL)) {
Log.d("bd asr partial", params);
try {
JSONObject json = new JSONObject(params);
String resultType = json.getString("result_type");
if(resultType.equals("final_result")) {
String bestResult = json.getString("best_result");
asrBuffer += bestResult;
callback.onResult(asrBuffer);
}else if(resultType.equals("partial_result")){
String bestResult = json.getString("best_result");
callback.onResult(String.format("%s%s", asrBuffer, bestResult));
}
} catch (JSONException e) {
e.printStackTrace();
}
} else if(name.equals(SpeechConstant.CALLBACK_EVENT_ASR_FINISH)) {
try {
JSONObject json = new JSONObject(params);
int errorCode = json.getInt("error");
if(errorCode != 0) {
String errorMessage = json.getString("desc");
Log.d("asr error", "error code: " + errorCode + ", error message: " + errorMessage);
callback.onError(errorMessage);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
};
asr.registerListener(listener);
}

@Override
public void startRecongnize() {
Map<String, Object> params = new LinkedHashMap<>();
params.put(SpeechConstant.APP_ID, GlobalDataHolder.getAsrAppId());
params.put(SpeechConstant.APP_KEY, GlobalDataHolder.getAsrApiKey());
params.put(SpeechConstant.SECRET, GlobalDataHolder.getAsrSecretKey());
if(GlobalDataHolder.getAsrUseRealTime()){
params.put(SpeechConstant.BDS_ASR_ENABLE_LONG_SPEECH, true);
params.put(SpeechConstant.VAD, SpeechConstant.VAD_DNN);
}
else{
params.put(SpeechConstant.BDS_ASR_ENABLE_LONG_SPEECH, false);
params.put(SpeechConstant.VAD, SpeechConstant.VAD_TOUCH);
}
params.put(SpeechConstant.PID, 15374);
asr.send(SpeechConstant.ASR_START, (new JSONObject(params)).toString(), null, 0, 0);
asrBuffer = "";
}

@Override
public void stopRecongnize() {
asr.send(SpeechConstant.ASR_STOP, "{}", null, 0, 0);
}

@Override
public void cancelRecongnize() {
asr.send(SpeechConstant.ASR_CANCEL, null, null, 0, 0);
}

@Override
public void setCallback(IAsrCallback callback) {
this.callback = callback;
}

@Override
public void setParam(String key, Object value) {

}

@Override
public void destroy() {
cancelRecongnize();
asr.unregisterListener(listener);
}
}
Loading

0 comments on commit 24704ba

Please sign in to comment.