[update] use aliyun andriod sdk

This commit is contained in:
2025-08-13 16:00:23 +08:00
parent a1faca7d99
commit 9202a578cd
17 changed files with 1957 additions and 181 deletions

View File

@@ -37,8 +37,18 @@ android {
signingConfig = signingConfigs.getByName("debug")
}
}
repositories {
flatDir {
dirs("libs")
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation(files("libs/fastjson-1.1.46.android.jar"))
implementation(files("libs/nuisdk-release.aar"))
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,238 @@
package com.example.ai_chat_assistant;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.util.Log;
import java.util.concurrent.LinkedBlockingQueue;
public class AudioPlayer {
public enum PlayState {
idle,
playing,
pause,
release
}
private static final String TAG = "AudioPlayer";
private int SAMPLE_RATE = 16000;
private String ENCODE_TYPE = "pcm";
private boolean isFinishSend = false;
private AudioPlayerCallback audioPlayerCallback;
private LinkedBlockingQueue<byte[]> audioQueue = new LinkedBlockingQueue();
private PlayState playState;
private byte[] tempData;
private Thread ttsPlayerThread;
// 初始化播放器
// 此处仅使用Android系统自带的AudioTrack进行音频播放Demo演示, 客户可根据自己需要替换播放器
// 默认采样率为16000、单通道、16bit pcm格式
private int iMinBufSize = AudioTrack.getMinBufferSize(SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT) * 2;
private AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
iMinBufSize, AudioTrack.MODE_STREAM);
AudioPlayer(AudioPlayerCallback callback) {
Log.i(TAG,"Audio Player init!");
playState = PlayState.idle;
if (audioTrack == null) {
Log.e(TAG, "AudioTrack is uninited!! new again...");
iMinBufSize = AudioTrack.getMinBufferSize(SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT) * 2;
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
iMinBufSize, AudioTrack.MODE_STREAM);
}
if (audioTrack == null) {
Log.e(TAG, "AudioTrack new failed ...");
}
audioTrack.play();
audioPlayerCallback = callback;
ttsPlayerThread = new Thread(new Runnable() {
@Override
public void run() {
while (playState != PlayState.release) {
if (playState == PlayState.playing) {
if (audioQueue.size() == 0) {
if (isFinishSend) {
audioPlayerCallback.playOver();
isFinishSend = false;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
continue;
}
try {
tempData = audioQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
// int sound_level = calculateRMSLevel(tempData);
// Log.i(TAG,"sound_level: " + sound_level);
// audioPlayerCallback.playSoundLevel(sound_level);
audioTrack.write(tempData, 0, tempData.length);
} else {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} // while
Log.i(TAG,"exit ttsPlayerThread");
}
});
}
public void setAudioData(byte[] data) {
audioQueue.offer(data);
//非阻塞
}
public void isFinishSend(boolean isFinish) {
isFinishSend = isFinish;
Log.i(TAG,"Player isFinishSend:" + isFinishSend);
}
public void play() {
if (!ttsPlayerThread.isAlive()) {
ttsPlayerThread.start();
}
playState = PlayState.playing;
Log.i(TAG,"Player playState:" + playState);
isFinishSend = false;
if (audioTrack != null) {
audioTrack.play();
}
audioPlayerCallback.playStart();
}
public void stop() {
playState = PlayState.idle;
Log.i(TAG,"stop-playState :" + playState);
audioQueue.clear();
if (audioTrack != null) {
audioTrack.flush();
audioTrack.pause();
audioTrack.stop();
}
}
public void pause() {
playState = PlayState.pause;
if (audioTrack != null) {
audioTrack.pause();
}
}
public void resume() {
if (audioTrack != null) {
audioTrack.play();
}
playState = PlayState.playing;
}
public void initAudioTrack(int samplerate) {
// 初始化播放器
// 此处仅使用Android系统自带的AudioTrack进行音频播放Demo演示, 客户可根据自己需要替换播放器
// 默认采样率为16000、单通道、16bit pcm格式
Log.i(TAG,"initAudioTrack audioTrack");
int iMinBufSize = AudioTrack.getMinBufferSize(samplerate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT) * 2;
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, samplerate,
AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT,
iMinBufSize, AudioTrack.MODE_STREAM);
if (audioTrack == null) {
Log.e(TAG, "new AudioTrack failed with sr:" + samplerate + " and encode_type:" + ENCODE_TYPE);
}
}
public void releaseAudioTrack(boolean finish) {
if (audioTrack != null) {
audioTrack.stop();
if (finish) {
playState = PlayState.release;
}
audioTrack.release();
Log.i(TAG,"releaseAudioTrack audioTrack released");
}
audioTrack = null;
}
public void releaseAudioTrack() {
if (audioTrack != null) {
audioTrack.stop();
playState = PlayState.release;
audioTrack.release();
Log.i(TAG,"releaseAudioTrack audioTrack released");
}
audioTrack = null;
}
public void setSampleRate(int sampleRate) {
if (SAMPLE_RATE != sampleRate) {
releaseAudioTrack(false);
initAudioTrack(sampleRate);
SAMPLE_RATE = sampleRate;
}
}
public void setEncodeType(String type) {
// int encode_type = ENCODE_TYPE;
// if (type.equals("mp3")) {
// encode_type = AudioFormat.ENCODING_MP3;
// } else {
// encode_type = AudioFormat.ENCODING_PCM_16BIT;
// }
// if (encode_type != ENCODE_TYPE) {
// ENCODE_TYPE = encode_type;
// releaseAudioTrack();
// initAudioTrack(SAMPLE_RATE);
// }
}
// 计算给定PCM音频数据的RMS值
private int calculateRMSLevel(byte[] audioData) {
// 将byte数组转换为short数组假设是16位PCM小端序
short[] shorts = new short[audioData.length / 2];
for (int i = 0; i < shorts.length; i++) {
shorts[i] = (short) ((audioData[i * 2] & 0xFF) | (audioData[i * 2 + 1] << 8));
}
// 计算平均平方值
double rms = 1.0;
for (short s : shorts) {
rms += (double)Math.abs(s);
}
rms = rms / shorts.length;
// 计算分贝值
double db = 20 * Math.log10(rms);
db = db * 160 / 90 - 160;
if (db > 0.0) {
db = 0.0;
} else if (db < -160.0) {
db = -160;
}
// level值
int level = (int)((db + 160) * 100 / 160);
return level;
}
}

View File

@@ -0,0 +1,7 @@
package com.example.ai_chat_assistant;
public interface AudioPlayerCallback {
public void playStart();
public void playOver();
public void playSoundLevel(int level);
}

View File

@@ -0,0 +1,674 @@
package com.example.ai_chat_assistant;
import android.util.Log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.ai_chat_assistant.token.AccessToken;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
/*
* STS服务不可直接用于在线功能包括一句话识别、实时识别、语音合成等。
* 在线功能需要TOKEN可由STS临时账号通过TOKEN工具生成也可在服务端下发TOKEN。
* STS服务可用于离线功能的鉴权比如本地语音合成和唤醒。
*/
public class Auth {
public enum GetTicketMethod {
/*
* 客户远端服务端使用STS服务获得STS临时凭证然后下发给移动端侧
* 然后生成语音交互临时凭证Token。用于在线功能场景。
*/
GET_STS_ACCESS_FROM_SERVER_FOR_ONLINE_FEATURES,
/*
* 客户远端服务端使用STS服务获得STS临时凭证然后下发给移动端侧
* 同时设置sdk_code, 用于离线功能场景。
*/
GET_STS_ACCESS_FROM_SERVER_FOR_OFFLINE_FEATURES,
/*
* 客户远端服务端使用STS服务获得STS临时凭证然后下发给移动端侧
* 然后生成语音交互临时凭证Token。
* 同时设置sdk_code, 用于离线在线功能混合场景。
*/
GET_STS_ACCESS_FROM_SERVER_FOR_MIXED_FEATURES,
/*
* 客户远端服务端使用Token服务获得Token临时令牌然后下发给移动端侧
* 用于在线功能场景。
*/
GET_TOKEN_FROM_SERVER_FOR_ONLINE_FEATURES,
/*
* 客户远端服务端将账号信息ak_id和ak_secret(请加密)下发给移动端侧,
* 同时设置sdk_code, 用于离线功能场景。
*/
GET_ACCESS_FROM_SERVER_FOR_OFFLINE_FEATURES,
/*
* 客户远端服务端将账号信息ak_id和ak_secret(请加密)下发给移动端侧,
* 然后生成语音交互临时凭证Token。
* 同时设置sdk_code, 用于离线在线功能混合场景。
*/
GET_ACCESS_FROM_SERVER_FOR_MIXED_FEATURES,
/*
* 客户直接使用存储在移动端侧的Token
* 用于在线功能场景。
*/
GET_TOKEN_IN_CLIENT_FOR_ONLINE_FEATURES,
/*
* 客户直接使用存储在移动端侧的ak_id和ak_secret(请加密)
* 同时设置sdk_code, 用于离线功能场景。
*/
GET_ACCESS_IN_CLIENT_FOR_OFFLINE_FEATURES,
/*
* 客户直接使用存储在移动端侧的ak_id和ak_secret(请加密)
* 然后生成语音交互临时凭证Token。
* 同时设置sdk_code, 用于离线在线功能混合场景。
*/
GET_ACCESS_IN_CLIENT_FOR_MIXED_FEATURES,
/*
* 客户直接使用存储在移动端侧的ak_id和ak_secret(请加密)
* 用于在线功能场景。
*/
GET_ACCESS_IN_CLIENT_FOR_ONLINE_FEATURES,
/*
* 客户直接使用存储在移动端侧的STS凭证
* 然后生成语音交互临时凭证Token。用于在线功能场景。
*/
GET_STS_ACCESS_IN_CLIENT_FOR_ONLINE_FEATURES,
/*
* 客户直接使用存储在移动端侧的STS凭证
* 同时设置sdk_code, 用于离线功能场景。
*/
GET_STS_ACCESS_IN_CLIENT_FOR_OFFLINE_FEATURES,
/*
* 客户直接使用存储在移动端侧的STS凭证
* 然后生成语音交互临时凭证Token。
* 同时设置sdk_code, 用于离线在线功能混合场景。
*/
GET_STS_ACCESS_IN_CLIENT_FOR_MIXED_FEATURES;
}
private static GetTicketMethod cur_method = GetTicketMethod.GET_STS_ACCESS_FROM_SERVER_FOR_ONLINE_FEATURES;
private static String cur_appkey = "";
private static String cur_token = "";
private static String cur_sts_token = "";
private static long cur_token_expired_time = 0;
private static String cur_ak = "";
private static String cur_sk = "";
private static String cur_sdk_code = "software_nls_tts_offline_standard";
public static void setAppKey(String appkey) {
if (!appkey.isEmpty()) {
cur_appkey = appkey;
}
}
public static void setToken(String token) {
if (!token.isEmpty()) {
cur_token = token;
}
}
public static void setStsToken(String stsToken) {
cur_sts_token = stsToken;
}
public static void setAccessKey(String ak) {
if (!ak.isEmpty()) {
cur_ak = ak;
}
}
public static void setAccessKeySecret(String sk) {
if (!sk.isEmpty()) {
cur_sk = sk;
}
}
public static void setSdkCode(String sdkCode) {
if (!sdkCode.isEmpty()) {
cur_sdk_code = sdkCode;
}
}
// 将鉴权信息打包成json格式
public static JSONObject getTicket(GetTicketMethod method) {
//郑重提示:
// 语音交互服务需要先准备好账号,并开通相关服务。具体步骤请查看:
// https://help.aliyun.com/zh/isi/getting-started/start-here
//
//原始账号:
// 账号(子账号)信息主要包括AccessKey ID(后续简称为ak_id)和AccessKey Secret(后续简称为ak_secret)。
// 此账号信息一定不可存储在app代码中或移动端侧以防账号信息泄露造成资费损失。
//
//STS临时凭证:
// 由于账号信息下发给客户端存在泄露的可能阿里云提供的一种临时访问权限管理服务STS(Security Token Service)。
// STS是由账号信息ak_id和ak_secret通过请求生成临时的sts_ak_id/sts_ak_secret/sts_token
// (为了区别原始账号信息和STS临时凭证, 命名前缀sts_表示STS生成的临时凭证信息)
//什么是STShttps://help.aliyun.com/zh/ram/product-overview/what-is-sts
//STS SDK概览https://help.aliyun.com/zh/ram/developer-reference/sts-sdk-overview
//STS Python SDK调用示例https://help.aliyun.com/zh/ram/developer-reference/use-the-sts-openapi-example
//
//账号需求说明:
// 若使用离线功能(离线语音合成、唤醒), 则必须app_key、ak_id和ak_secret或app_key、sts_ak_id、sts_ak_secret和sts_token
// 若使用在线功能(语音合成、实时转写、一句话识别、录音文件转写等), 则只需app_key和token
JSONObject object = new JSONObject();
cur_method = method;
//项目创建
// 创建appkey请查看https://help.aliyun.com/zh/isi/getting-started/start-here
String APPKEY = "<您申请创建的app_key>";
if (!cur_appkey.isEmpty()) {
APPKEY = cur_appkey;
}
object.put("app_key", APPKEY); // 必填
cur_appkey = APPKEY;
if (method == GetTicketMethod.GET_STS_ACCESS_FROM_SERVER_FOR_ONLINE_FEATURES) {
//方法一,仅适合在线语音交互服务(强烈推荐):
// 客户远端服务端使用STS服务获得STS临时凭证然后下发给移动端侧详情请查看
// https://help.aliyun.com/document_detail/466615.html 使用其中方案二使用STS获取临时账号。
// 然后在移动端侧通过AccessToken()获得Token和有效期用于在线语音交互服务。
String STS_AK_ID = "STS.<服务器生成的具有时效性的临时凭证>";
String STS_AK_SECRET = "<服务器生成的具有时效性的临时凭证>";
String STS_TOKEN = "<服务器生成的具有时效性的临时凭证>";
String TOKEN = "<由STS生成的临时访问令牌>";
final AccessToken token = new AccessToken(STS_AK_ID, STS_AK_SECRET, STS_TOKEN);
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_token = TOKEN;
cur_ak = STS_AK_ID;
cur_sk = STS_AK_SECRET;
cur_sts_token = STS_TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
cur_sts_token = "";
}
} else if (method == GetTicketMethod.GET_STS_ACCESS_FROM_SERVER_FOR_OFFLINE_FEATURES) {
//方法二,仅适合离线语音交互服务(强烈推荐):
// 客户远端服务端使用STS服务获得STS临时凭证然后下发给移动端侧详情请查看
// https://help.aliyun.com/document_detail/466615.html 使用其中方案二使用STS获取临时账号。
String STS_AK_ID = "STS.<服务器生成的具有时效性的临时凭证>";
String STS_AK_SECRET = "<服务器生成的具有时效性的临时凭证>";
String STS_TOKEN = "<服务器生成的具有时效性的临时凭证>";
object.put("ak_id", STS_AK_ID); // 必填
object.put("ak_secret", STS_AK_SECRET); // 必填
object.put("sts_token", STS_TOKEN); // 必填
cur_ak = STS_AK_ID;
cur_sk = STS_AK_SECRET;
cur_sts_token = STS_TOKEN;
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
} else if (method == GetTicketMethod.GET_STS_ACCESS_FROM_SERVER_FOR_MIXED_FEATURES) {
//方法三,适合离在线语音交互服务(强烈推荐):
// 客户远端服务端使用STS服务获得STS临时凭证然后下发给移动端侧详情请查看
// https://help.aliyun.com/document_detail/466615.html 使用其中方案二使用STS获取临时账号。
// 然后在移动端侧通过AccessToken()获得Token和有效期用于在线语音交互服务。
//注意此处介绍同一个Appkey用于在线和离线功能用户可创建两个Appkey分别用于在线和离线功能。
String STS_AK_ID = "STS.<服务器生成的具有时效性的临时凭证>";
String STS_AK_SECRET = "<服务器生成的具有时效性的临时凭证>";
String STS_TOKEN = "<服务器生成的具有时效性的临时凭证>";
String TOKEN = "<由STS生成的临时访问令牌>";
final AccessToken token = new AccessToken(STS_AK_ID, STS_AK_SECRET, STS_TOKEN);
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
object.put("ak_id", STS_AK_ID); // 必填
object.put("ak_secret", STS_AK_SECRET); // 必填
object.put("sts_token", STS_TOKEN); // 必填
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_token = TOKEN;
cur_ak = STS_AK_ID;
cur_sk = STS_AK_SECRET;
cur_sts_token = STS_TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
cur_sts_token = "";
}
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
} else if (method == GetTicketMethod.GET_TOKEN_FROM_SERVER_FOR_ONLINE_FEATURES) {
//方法四,仅适合在线语音交互服务(推荐):
// 客户远端服务端使用Token服务获得Token临时令牌然后下发给移动端侧详情请查看
// https://help.aliyun.com/document_detail/466615.html 使用其中方案一获取临时令牌Token
// 获得Token方法
// https://help.aliyun.com/zh/isi/getting-started/overview-of-obtaining-an-access-token
String TOKEN = "<服务器生成的具有时效性的临时凭证>";
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
} else {
cur_token = "";
}
} else if (method == GetTicketMethod.GET_ACCESS_FROM_SERVER_FOR_OFFLINE_FEATURES) {
//方法五,仅适合离线语音交互服务(不推荐):
// 客户远端服务端将账号信息ak_id和ak_secret(请加密)下发给移动端侧。
//注意!账号信息出现在移动端侧,存在泄露风险。
String AK_ID = "<一定不可代码中存储和本地明文存储>";
String AK_SECRET = "<一定不可代码中存储和本地明文存储>";
object.put("ak_id", AK_ID); // 必填
object.put("ak_secret", AK_SECRET); // 必填
cur_ak = AK_ID;
cur_sk = AK_SECRET;
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
} else if (method == GetTicketMethod.GET_ACCESS_FROM_SERVER_FOR_MIXED_FEATURES) {
//方法六,适合离在线语音交互服务(不推荐):
// 客户远端服务端将账号信息ak_id和ak_secret(请加密)下发给移动端侧。
// 然后在移动端侧通过AccessToken()获得Token和有效期用于在线语音交互服务。
//注意!账号信息出现在移动端侧,存在泄露风险。
String AK_ID = "<一定不可代码中存储和本地明文存储>";
String AK_SECRET = "<一定不可代码中存储和本地明文存储>";
String TOKEN = "<生成的临时访问令牌>";
final AccessToken token = new AccessToken(AK_ID, AK_SECRET, "");
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
object.put("ak_id", AK_ID); // 必填
object.put("ak_secret", AK_SECRET); // 必填
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
}
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
} else if (method == GetTicketMethod.GET_TOKEN_IN_CLIENT_FOR_ONLINE_FEATURES) {
//方法七,仅适合在线语音交互服务(强烈不推荐):
// 仅仅用于开发和调试
//注意!账号信息出现在移动端侧,存在泄露风险。
String TOKEN = "<移动端写死的访问令牌,仅用于调试>";
if (!cur_token.isEmpty()) {
TOKEN = cur_token;
}
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
} else {
cur_token = "";
}
} else if (method == GetTicketMethod.GET_ACCESS_IN_CLIENT_FOR_OFFLINE_FEATURES) {
//方法八,仅适合离线语音交互服务(强烈不推荐):
// 仅仅用于开发和调试
//注意!账号信息出现在移动端侧,存在泄露风险。
String AK_ID = "<移动端写死的账号信息,仅用于调试>";
String AK_SECRET = "<移动端写死的账号信息,仅用于调试>";
if (!cur_ak.isEmpty()) {
AK_ID = cur_ak;
}
if (!cur_sk.isEmpty()) {
AK_SECRET = cur_sk;
}
object.put("ak_id", AK_ID); // 必填
object.put("ak_secret", AK_SECRET); // 必填
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
} else if (method == GetTicketMethod.GET_ACCESS_IN_CLIENT_FOR_MIXED_FEATURES) {
//方法九,适合离在线语音交互服务(强烈不推荐):
// 仅仅用于开发和调试
//注意!账号信息出现在移动端侧,存在泄露风险。
String AK_ID = "<移动端写死的账号信息,仅用于调试>";
String AK_SECRET = "<移动端写死的账号信息,仅用于调试>";
String TOKEN = "<生成的临时访问令牌>";
if (!cur_ak.isEmpty()) {
AK_ID = cur_ak;
}
if (!cur_sk.isEmpty()) {
AK_SECRET = cur_sk;
}
final AccessToken token = new AccessToken(AK_ID, AK_SECRET, "");
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
object.put("ak_id", AK_ID); // 必填
object.put("ak_secret", AK_SECRET); // 必填
cur_ak = AK_ID;
cur_sk = AK_SECRET;
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
}
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
} else if (method == GetTicketMethod.GET_ACCESS_IN_CLIENT_FOR_ONLINE_FEATURES) {
//方法十,适合在线语音交互服务(强烈不推荐):
// 仅仅用于开发和调试
//注意!账号信息出现在移动端侧,存在泄露风险。
String AK_ID = "<移动端写死的账号信息,仅用于调试>";
String AK_SECRET = "<移动端写死的账号信息,仅用于调试>";
String TOKEN = "<生成的临时访问令牌>";
if (!cur_ak.isEmpty()) {
AK_ID = cur_ak;
}
if (!cur_sk.isEmpty()) {
AK_SECRET = cur_sk;
}
final AccessToken token = new AccessToken(AK_ID, AK_SECRET, "");
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
object.put("ak_id", AK_ID); // 必填
object.put("ak_secret", AK_SECRET); // 必填
cur_ak = AK_ID;
cur_sk = AK_SECRET;
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
}
} else if (method == GetTicketMethod.GET_STS_ACCESS_IN_CLIENT_FOR_ONLINE_FEATURES) {
//方法十一,适合在线语音交互服务(强烈不推荐):
// 仅仅用于开发和调试
String STS_AK_ID = "STS.<移动端写死的账号信息,仅用于调试>";
String STS_AK_SECRET = "<移动端写死的账号信息,仅用于调试>";
String STS_TOKEN = "<移动端写死的账号信息,仅用于调试>";
if (!cur_ak.isEmpty()) {
STS_AK_ID = cur_ak;
}
if (!cur_sk.isEmpty()) {
STS_AK_SECRET = cur_sk;
}
if (!cur_sts_token.isEmpty()) {
STS_TOKEN = cur_sts_token;
}
String TOKEN = "<由STS生成的临时访问令牌>";
final AccessToken token = new AccessToken(STS_AK_ID, STS_AK_SECRET, STS_TOKEN);
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
object.put("ak_id", STS_AK_ID); // 必填
object.put("ak_secret", STS_AK_SECRET); // 必填
object.put("sts_token", STS_TOKEN); // 必填
cur_ak = STS_AK_ID;
cur_sk = STS_AK_SECRET;
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
cur_sts_token = STS_TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
cur_sts_token = "";
}
} else if (method == GetTicketMethod.GET_STS_ACCESS_IN_CLIENT_FOR_OFFLINE_FEATURES) {
//方法十二,适合离线语音交互服务(强烈不推荐):
// 仅仅用于开发和调试
String STS_AK_ID = "STS.<移动端写死的账号信息,仅用于调试>";
String STS_AK_SECRET = "<移动端写死的账号信息,仅用于调试>";
String STS_TOKEN = "<移动端写死的账号信息,仅用于调试>";
if (!cur_ak.isEmpty()) {
STS_AK_ID = cur_ak;
}
if (!cur_sk.isEmpty()) {
STS_AK_SECRET = cur_sk;
}
if (!cur_sts_token.isEmpty()) {
STS_TOKEN = cur_sts_token;
}
String TOKEN = "<由STS生成的临时访问令牌>";
final AccessToken token = new AccessToken(STS_AK_ID, STS_AK_SECRET, STS_TOKEN);
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
object.put("ak_id", STS_AK_ID); // 必填
object.put("ak_secret", STS_AK_SECRET); // 必填
object.put("sts_token", STS_TOKEN); // 必填
cur_ak = STS_AK_ID;
cur_sk = STS_AK_SECRET;
cur_sts_token = STS_TOKEN;
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
cur_sts_token = "";
}
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
} else if (method == GetTicketMethod.GET_STS_ACCESS_IN_CLIENT_FOR_MIXED_FEATURES) {
//方法十三,适合离在线语音交互服务(强烈不推荐):
// 仅仅用于开发和调试
String STS_AK_ID = "STS.<移动端写死的账号信息,仅用于调试>";
String STS_AK_SECRET = "<移动端写死的账号信息,仅用于调试>";
String STS_TOKEN = "<移动端写死的账号信息,仅用于调试>";
if (!cur_ak.isEmpty()) {
STS_AK_ID = cur_ak;
}
if (!cur_sk.isEmpty()) {
STS_AK_SECRET = cur_sk;
}
if (!cur_sts_token.isEmpty()) {
STS_TOKEN = cur_sts_token;
}
String TOKEN = "<由STS生成的临时访问令牌>";
final AccessToken token = new AccessToken(STS_AK_ID, STS_AK_SECRET, STS_TOKEN);
Thread th = new Thread() {
@Override
public void run() {
try {
token.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
};
th.start();
try {
th.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TOKEN = token.getToken();
object.put("ak_id", STS_AK_ID); // 必填
object.put("ak_secret", STS_AK_SECRET); // 必填
object.put("sts_token", STS_TOKEN); // 必填
cur_ak = STS_AK_ID;
cur_sk = STS_AK_SECRET;
cur_sts_token = STS_TOKEN;
if (TOKEN != null && !TOKEN.isEmpty()) {
object.put("token", TOKEN); // 必填
cur_appkey = APPKEY;
cur_token = TOKEN;
// token生命周期超过expired_time, 则需要重新token = new AccessToken()
cur_token_expired_time = token.getExpireTime();
} else {
cur_token = "";
cur_sts_token = "";
}
// 离线语音合成sdk_code取值精品版为software_nls_tts_offline 标准版为software_nls_tts_offline_standard
// 离线语音合成账户和sdk_code可用于唤醒
// 由创建Appkey时设置
object.put("sdk_code", cur_sdk_code); // 必填
}
return object;
}
// 也可以将鉴权信息以json格式保存至文件然后从文件里加载必须包含成员ak_id/ak_secret/app_key/device_id/sdk_code
// 该方式切换账号切换账号比较方便
public static JSONObject getTicketFromJsonFile(String fileName) {
try {
File jsonFile = new File(fileName);
FileReader fileReader = new FileReader(jsonFile);
Reader reader = new InputStreamReader(new FileInputStream(jsonFile), "utf-8");
int ch = 0;
StringBuffer sb = new StringBuffer();
while ((ch = reader.read()) != -1) {
sb.append((char) ch);
}
fileReader.close();
reader.close();
String jsonStr = sb.toString();
return JSON.parseObject(jsonStr);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static JSONObject refreshTokenIfNeed(JSONObject json, long distance_expire_time) {
if (!cur_appkey.isEmpty() && !cur_token.isEmpty() && cur_token_expired_time > 0) {
long millis = System.currentTimeMillis();
long unixTimestampInSeconds = millis / 1000;
if (cur_token_expired_time - distance_expire_time < unixTimestampInSeconds) {
String old_token = cur_token;
long old_expire_time = cur_token_expired_time;
json = getTicket(cur_method);
String new_token = cur_token;
long new_expire_time = cur_token_expired_time;
Log.i("Auth", "Refresh old token(" + old_token + " : " + old_expire_time +
") to (" + new_token + " : " + new_expire_time + ").");
}
}
return json;
}
}

View File

@@ -1,6 +1,187 @@
package com.example.ai_chat_assistant;
import android.os.Bundle;
import android.util.Log;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.idst.nui.Constants;
import com.alibaba.idst.nui.INativeStreamInputTtsCallback;
import com.alibaba.idst.nui.NativeNui;
import java.util.Map;
public class MainActivity extends FlutterActivity {
}
private static final String CHANNEL = "com.example.ai_chat_assistant/tts";
private static final String TAG = "StreamInputTts";
private static final String APP_KEY = "bXFFc1V65iYbW6EF";
private static final String TOKEN = "9ae5be92a9fa4fb3ac9bcd56eba69437";
private static final String URL = "wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1";
private final NativeNui streamInputTtsInstance = new NativeNui(Constants.ModeType.MODE_STREAM_INPUT_TTS);
private final AudioPlayer mAudioTrack = new AudioPlayer(new AudioPlayerCallback() {
@Override
public void playStart() {
Log.i(TAG, "start play");
}
@Override
public void playOver() {
Log.i(TAG, "play over");
}
@Override
public void playSoundLevel(int level) {
}
});
@Override
public void configureFlutterEngine(FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler((call, result) -> {
switch (call.method) {
case "startTts":
boolean isSuccess = startTts();
result.success(isSuccess);
break;
case "sendTts":
Map<String, Object> args = (Map<String, Object>) call.arguments;
Object textArg = args.get("text");
if (textArg == null || textArg.toString().isBlank()) {
return;
}
sendTts(textArg.toString());
break;
case "stopTts":
stopTts();
result.success("已停止");
break;
default:
result.notImplemented();
break;
}
});
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
mAudioTrack.stop();
mAudioTrack.releaseAudioTrack();
streamInputTtsInstance.stopStreamInputTts();
}
private boolean startTts() {
int ret = streamInputTtsInstance.startStreamInputTts(new INativeStreamInputTtsCallback() {
@Override
public void onStreamInputTtsEventCallback(INativeStreamInputTtsCallback.StreamInputTtsEvent event, String task_id, String session_id, int ret_code, String error_msg, String timestamp, String all_response) {
Log.i(TAG, "stream input tts event(" + event + ") session id(" + session_id + ") task id(" + task_id + ") retCode(" + ret_code + ") errMsg(" + error_msg + ")");
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED");
mAudioTrack.play();
Log.i(TAG, "start play");
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS:" + timestamp);
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE || event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
/*
* 提示: STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE事件表示TTS已经合成完并通过回调传回了所有音频数据, 而不是表示播放器已经播放完了所有音频数据。
*/
Log.i(TAG, "play end");
// 表示推送完数据, 当播放器播放结束则会有playOver回调
mAudioTrack.isFinishSend(true);
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
Log.e(TAG, "STREAM_INPUT_TTS_EVENT_TASK_FAILED: " + "error_code(" + ret_code + ") error_message(" + error_msg + ")");
}
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN:" + all_response);
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_END) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_END:" + all_response);
}
}
@Override
public void onStreamInputTtsDataCallback(byte[] data) {
if (data.length > 0) {
mAudioTrack.setAudioData(data);
}
}
@Override
public void onStreamInputTtsLogTrackCallback(Constants.LogLevel level, String log) {
Log.i(TAG, "onStreamInputTtsLogTrackCallback log level:" + level + ", message -> " + log);
}
}, genTicket(), genParameters(), "", Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_VERBOSE), true);
if (Constants.NuiResultCode.SUCCESS != ret) {
Log.i(TAG, "start tts failed " + ret);
return false;
} else {
return true;
}
}
private void sendTts(String text) {
streamInputTtsInstance.sendStreamInputTts(text);
}
private void stopTts() {
streamInputTtsInstance.cancelStreamInputTts();
mAudioTrack.stop();
}
private String genTicket() {
String str = "";
try {
//获取账号访问凭证:
Auth.GetTicketMethod method = Auth.GetTicketMethod.GET_TOKEN_IN_CLIENT_FOR_ONLINE_FEATURES;
Auth.setAppKey(APP_KEY);
Auth.setToken(TOKEN);
Log.i(TAG, "Use method:" + method);
JSONObject object = Auth.getTicket(method);
if (!object.containsKey("token") && !object.containsKey("sts_token")) {
Log.e(TAG, "Cannot get token or sts_token!!!");
}
object.put("url", URL);
//过滤SDK内部日志通过回调送回到用户层
object.put("log_track_level", String.valueOf(Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_INFO)));
str = object.toString();
} catch (JSONException e) {
e.printStackTrace();
}
Log.i(TAG, "user ticket:" + str);
return str;
}
private String genParameters() {
String str = "";
try {
JSONObject object = new JSONObject();
object.put("enable_subtitle", true);
object.put("voice", "zhixiaoxia");
object.put("format", "pcm");
object.put("sample_rate", 16000);
object.put("volume", 50);
object.put("speech_rate", 0);
object.put("pitch_rate", 0);
str = object.toString();
} catch (JSONException e) {
e.printStackTrace();
}
Log.i(TAG, "user parameters:" + str);
return str;
}
}

View File

@@ -0,0 +1,97 @@
package com.example.ai_chat_assistant.token;
import android.util.Log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
/**
* 用来获取访问令牌的工具类
*
* @author xuebin
*/
public class AccessToken{
private static final String TAG="AliSpeechSDK";
private static final String NODE_TOKEN = "Token";
private String accessKeyId;
private String accessKeySecret;
private String securityToken;
private String token;
private long expireTime;
private int statusCode;
private String errorMessage;
private String domain = "nls-meta.cn-shanghai.aliyuncs.com";
private String regionId= "cn-shanghai";
private String version ="2019-02-28";
private String action = "CreateToken";
/**
* 构造实例
* @param accessKeyId
* @param accessKeySecret
*/
public AccessToken(String accessKeyId, String accessKeySecret, String securityToken) {
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.securityToken = securityToken;
}
/**
* 构造实例
* @param accessKeyId
* @param accessKeySecret
*/
public AccessToken(String accessKeyId, String accessKeySecret, String securityToken,
String domain, String regionId, String version) {
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.securityToken = securityToken;
this.domain = domain;
this.regionId = regionId;
this.version = version;
}
/**
* 向服务端申请访问令牌申请成功后会更新token和expireTime然后返回
* @throws IOException https调用出错或返回数据解析出错
*/
public void apply() throws IOException {
HttpRequest request = new HttpRequest(accessKeyId, accessKeySecret, securityToken,
this.domain, this.regionId, this.version);
request.authorize();
HttpResponse response = HttpUtil.send(request);
Log.i(TAG,"Get response token info :" + JSON.toJSONString(response));
if (response.getErrorMessage() == null) {
String result = response.getResult();
try {
JSONObject jsonObject = JSON.parseObject(result);
if (jsonObject.containsKey(NODE_TOKEN)) {
this.token = jsonObject.getJSONObject(NODE_TOKEN).getString("Id");
this.expireTime = jsonObject.getJSONObject(NODE_TOKEN).getIntValue("ExpireTime");
} else {
statusCode = 500;
errorMessage = "Received unexpected result: " + result;
}
} catch (JSONException e) {
throw new IOException("Failed to parse result: " + result, e);
}
} else {
Log.e(TAG, response.getErrorMessage());
statusCode = response.getStatusCode();
errorMessage = response.getErrorMessage();
}
}
public String getToken() {
return token;
}
public long getExpireTime() {
return expireTime;
}
}

View File

@@ -0,0 +1,272 @@
package com.example.ai_chat_assistant.token;
import android.util.Log;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.UUID;
/**
* Http request
*
* @author xuebin
*/
public class HttpRequest {
public static final String TAG= "SIGN";
public static final String CHARSET_UTF8 = "UTF-8";
public static final String METHOD_POST = "POST";
public static final String METHOD_GET = "GET";
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_ACCEPT = "Accept";
// public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
public static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
public static final String ACCEPT = "application/json";
public static final String Format = "JSON";
public static final String SignatureMethod = "HMAC-SHA1";
public static final String SignatureVersion = "1.0";
private String AccessKeyId;
private String AccessKeySecret;
private String SecurityToken;
private static final String ENCODING = "UTF-8";
private final static String FORMAT_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private final static String TIME_ZONE = "GMT";
protected Map<String, String> header;
private String domain;
private String regionId;
private String queryString;
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getRegionId() {
return regionId;
}
public void setRegionId(String regionId) {
this.regionId = regionId;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
private String action = "CreateToken";
private String version;
private String url;
public HttpRequest(String accessKeyId, String accessKeySecret, String securityToken,
String domain, String regionId, String version) {
AccessKeyId = accessKeyId;
AccessKeySecret = accessKeySecret;
SecurityToken = securityToken;
setDomain(domain);
setVersion(version);
setRegionId(regionId);
header = new HashMap<String, String>();
header.put(HEADER_ACCEPT, ACCEPT);
header.put(HEADER_CONTENT_TYPE, CONTENT_TYPE);
// POP doesn't support gzip
// header.put(HEADER_ACCEPT_ENCODING, "identity");
header.put("HOST", domain);
}
/**
* 返回此 request 对应的 url 地址
* @return url 地址
*/
public String getUrl() {
return url;
}
/**
* 返回 http body
* @return 二进制形式的http body
*/
public byte[] getBodyBytes() {
return null;
}
public String getMethod() {
return METHOD_GET;
}
/**
* 解析http返回结果,构建response对象
* @param statusCode http 状态码
* @param bytes 返回的二进制数据
* @return response对象
*/
public HttpResponse parse(int statusCode, byte[] bytes) throws IOException {
HttpResponse response = new HttpResponse();
response.setStatusCode(statusCode);
String result = new String(bytes, HttpRequest.CHARSET_UTF8);
if (response.getStatusCode() == HttpUtil.STATUS_OK){
response.setResult(result);
}else {
response.setErrorMessage(result);
}
return response;
}
public Map<String, String> getHeader() {
return header;
}
public void authorize(){
Map<String, String> queryParamsMap = new HashMap<String, String>();
queryParamsMap.put("AccessKeyId", this.AccessKeyId);
if (this.SecurityToken != null && !this.SecurityToken.trim().isEmpty()) {
queryParamsMap.put("SecurityToken", this.SecurityToken);
}
queryParamsMap.put("Action", this.action);
queryParamsMap.put("Version", this.version);
queryParamsMap.put("RegionId", this.regionId);
queryParamsMap.put("Timestamp", getISO8601Time(null));
queryParamsMap.put("Format", Format);
queryParamsMap.put("SignatureMethod", SignatureMethod);
queryParamsMap.put("SignatureVersion", SignatureVersion);
queryParamsMap.put("SignatureNonce", getUniqueNonce());
String queryString = canonicalizedQuery(queryParamsMap);
if (null == queryString) {
Log.e(TAG,"create the canonicalized query failed");
return;
}
String method = "GET";
String urlPath = "/";
String stringToSign = createStringToSign(method, urlPath, queryString);
Log.i(TAG,"String to sign is :"+ stringToSign);
// 2.计算 HMAC-SHA1
String signature = Signer.signString(stringToSign, AccessKeySecret+"&");
// System.out.println(signature);
// 3.得到 authorization header
String queryStringWithSign = null;
try {
queryStringWithSign = "Signature=" + percentEncode(signature) + "&" + queryString;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
this.queryString = queryStringWithSign;
}
public String getQueryString(){
return this.queryString;
}
/**
* 获取时间戳
* 必须符合ISO8601规范并需要使用UTC时间时区为+0
*/
private String getISO8601Time(Date date) {
Date nowDate = date;
if (null == date) {
nowDate = new Date();
}
SimpleDateFormat df = new SimpleDateFormat(FORMAT_ISO8601);
df.setTimeZone(new SimpleTimeZone(0, TIME_ZONE));
return df.format(nowDate);
}
/**
* 获取UUID
*/
private String getUniqueNonce() {
UUID uuid = UUID.randomUUID();
return uuid.toString();
}
/***
* 将参数排序后,进行规范化设置,组合成请求字符串
* @param queryParamsMap 所有请求参数
* @return 规范化的请求字符串
*/
private String canonicalizedQuery(Map<String, String> queryParamsMap) {
String[] sortedKeys = queryParamsMap.keySet().toArray(new String[] {});
Arrays.sort(sortedKeys);
String queryString = null;
try {
StringBuilder canonicalizedQueryString = new StringBuilder();
for (String key : sortedKeys) {
canonicalizedQueryString.append("&")
.append(percentEncode(key)).append("=")
.append(percentEncode(queryParamsMap.get(key)));
}
queryString = canonicalizedQueryString.toString().substring(1);
} catch (UnsupportedEncodingException e) {
Log.e(TAG,"UTF-8 encoding is not supported.");
e.printStackTrace();
}
return queryString;
}
/**
* URL编码
* 使用UTF-8字符集按照 RFC3986 规则编码请求参数和参数取值
*/
private String percentEncode(String value) throws UnsupportedEncodingException {
return value != null ? URLEncoder.encode(value, ENCODING).replace("+", "%20")
.replace("*", "%2A").replace("%7E", "~") : null;
}
/***
* 构造签名字符串
* @param method HTTP请求的方法
* @param urlPath HTTP请求的资源路径
* @param queryString 规范化的请求字符串
* @return 签名字符串
*/
private String createStringToSign(String method, String urlPath, String queryString) {
String stringToSign = null;
try {
StringBuilder strBuilderSign = new StringBuilder();
strBuilderSign.append(method);
strBuilderSign.append("&");
strBuilderSign.append(percentEncode(urlPath));
strBuilderSign.append("&");
strBuilderSign.append(percentEncode(queryString));
stringToSign = strBuilderSign.toString();
} catch (UnsupportedEncodingException e) {
Log.e(TAG,"UTF-8 encoding is not supported.");
e.printStackTrace();
}
return stringToSign;
}
}

View File

@@ -0,0 +1,47 @@
package com.example.ai_chat_assistant.token;
/**
* Say something
*
* @author xuebin
*/
public class HttpResponse {
private int statusCode;
private String errorMessage;
String result;
String text;
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}

View File

@@ -0,0 +1,91 @@
package com.example.ai_chat_assistant.token;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
/**
* Say something
*
* @author xuebin
*/
public class HttpUtil {
public static final int STATUS_OK = 200;
/**
* 发送请求
* @param request 请求
* @return response 结果
*/
public static HttpResponse send(HttpRequest request) throws IOException {
OutputStream out = null;
InputStream inputStream = null;
try {
String url = "https://"+ request.getDomain() + "/?" +request.getQueryString();
Log.i("SIGN","request url is :"+url);
URL realUrl = new URL(url);
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
// 打开和URL之间的连接
HttpsURLConnection conn = (HttpsURLConnection) realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestMethod(request.getMethod());
if (HttpRequest.METHOD_POST.equals(request.getMethod())){
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
}
conn.setUseCaches(false);
final byte[] bodyBytes = request.getBodyBytes();
if (bodyBytes != null) {
// 获取URLConnection对象对应的输出流
out = conn.getOutputStream();
// 发送请求参数
out.write(bodyBytes);
// flush输出流的缓冲
out.flush();
}
final int code = conn.getResponseCode();
Map<String, List<String>> headerFields = conn.getHeaderFields();
String responseMessage = conn.getResponseMessage();
Log.d("HttpUtil",responseMessage);
if (code ==STATUS_OK){
inputStream = conn.getInputStream();
}else {
inputStream = conn.getErrorStream();
}
HttpResponse requestResponse = request.parse(code, readAll(inputStream));
return requestResponse;
} finally { // 使用finally块来关闭输出流、输入流
try {
if (out != null) {
out.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private static byte[] readAll(InputStream inputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
while (len > 0){
byteArrayOutputStream.write(bytes, 0, len);
len = inputStream.read(bytes);
}
return byteArrayOutputStream.toByteArray();
}
}

View File

@@ -0,0 +1,76 @@
package com.example.ai_chat_assistant.token;
import android.util.Base64;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.SimpleTimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* Code from Aliyun POP core SDK.
* Just replace javax.xml.bind.DatatypeConverter with android.util.Base64
*
* @author xuebin
*/
public class Signer {
private static final String ALGORITHM_NAME = "HmacSHA1";
public static final String ENCODING = "UTF-8";
public static String signString(String stringToSign, String accessKeySecret) {
try {
Mac mac = Mac.getInstance(ALGORITHM_NAME);
mac.init(new SecretKeySpec(
accessKeySecret.getBytes(ENCODING),
ALGORITHM_NAME
));
byte[] signData = mac.doFinal(stringToSign.getBytes(ENCODING));
String encodeToString = Base64.encodeToString(signData, Base64.DEFAULT);
return encodeToString.substring(0, encodeToString.length()-1);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e.toString());
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e.toString());
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e.toString());
}
}
public String getSignerName() {
return "HMAC-SHA1";
}
public String getSignerVersion() {
return "1.0";
}
public String getSignerType() {
return null;
}
/**
* 等同于javaScript中的 new Date().toUTCString();
*/
public static String toGMTString() {
return toGMTString(new Date());
}
private static String toGMTString(Date date) {
SimpleDateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.UK);
df.setTimeZone(new SimpleTimeZone(0, "GMT"));
String text = df.format(date);
if (!text.endsWith("GMT")){
// delete +00:00 from the end of string: Sun, 30 Sep 2018 08:42:06 GMT+00:00
text = text.substring(0, text.length()-6);
}
return text;
}
}

View File

@@ -1,6 +0,0 @@
package com.example.app003;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,12 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/services.dart';
import '../utils/common_util.dart';
class ChatSseService {
static const MethodChannel _channel = MethodChannel('com.example.ai_chat_assistant/tts');
// 缓存用户ID和会话ID
String? _cachedUserId;
String? _cachedConversationId;
@@ -11,6 +16,7 @@ class ChatSseService {
HttpClient? _currentClient;
HttpClientResponse? _currentResponse;
bool _isAborted = true;
bool isTtsStarted = false;
String? get conversationId => _cachedConversationId;
@@ -42,12 +48,21 @@ class ChatSseService {
request.add(utf8.encode(json.encode(body)));
_currentResponse = await request.close();
if (_currentResponse!.statusCode == 200) {
bool isFirst = true;
await for (final line in _currentResponse!
.transform(utf8.decoder)
.transform(const LineSplitter())) {
if (_isAborted) {
break;
}
if (isFirst) {
if (!isTtsStarted) {
if (await _channel.invokeMethod<bool>("startTts") == true) {
isTtsStarted = true;
}
}
isFirst = false;
}
if (line.startsWith('data:')) {
final jsonStr = line.substring(5).trim();
if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) {
@@ -63,12 +78,16 @@ class ChatSseService {
if (jsonData['event'].toString().contains('message')) {
final textChunk =
jsonData.containsKey('answer') ? jsonData['answer'] : '';
if (_isAborted) {
break;
}
_channel.invokeMethod("sendTts", {"text": CommonUtil.cleanText(textChunk, true)});
responseText += textChunk;
tempText += textChunk;
int endIndex = _getCompleteTextEndIndex(tempText);
final completeText = tempText.substring(0, endIndex).trim();
tempText = tempText.substring(endIndex).trim();
onStreamResponse(messageId, responseText, completeText, false);
// tempText += textChunk;
// int endIndex = _getCompleteTextEndIndex(tempText);
// final completeText = tempText.substring(0, endIndex).trim();
// tempText = tempText.substring(endIndex).trim();
onStreamResponse(messageId, responseText, "completeText", false);
}
} catch (e) {
print('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
@@ -81,12 +100,15 @@ class ChatSseService {
} catch (e) {
// todo
} finally {
isTtsStarted = false;
resetRequest();
}
}
Future<void> abort() async {
_isAborted = true;
_channel.invokeMethod("stopTts");
isTtsStarted = false;
resetRequest();
}

View File

@@ -27,7 +27,7 @@ class MessageService extends ChangeNotifier {
MessageService._internal();
final ChatSseService _chatSseService = ChatSseService();
final LocalTtsService _ttsService = LocalTtsService();
// final LocalTtsService _ttsService = LocalTtsService();
final AudioRecorderService _audioService = AudioRecorderService();
final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
final TextClassificationService _classificationService =
@@ -213,10 +213,10 @@ class MessageService extends ChangeNotifier {
id: _latestAssistantMessageId!,
text: vehicleCommandResponse.tips!,
status: MessageStatus.executing);
if (!_isReplyAborted) {
_ttsService.pushTextForStreamTTS(vehicleCommandResponse.tips!);
_ttsService.markSSEStreamCompleted();
}
// if (!_isReplyAborted) {
// _ttsService.pushTextForStreamTTS(vehicleCommandResponse.tips!);
// _ttsService.markSSEStreamCompleted();
// }
bool containOpenAC = false;
for (var command in vehicleCommandResponse.commands) {
if (_isReplyAborted) {
@@ -332,19 +332,19 @@ class MessageService extends ChangeNotifier {
}
try {
if (isComplete) {
if (completeText.isNotEmpty) {
_ttsService.pushTextForStreamTTS(completeText);
}
_ttsService.markSSEStreamCompleted();
// if (completeText.isNotEmpty) {
// _ttsService.pushTextForStreamTTS(completeText);
// }
// _ttsService.markSSEStreamCompleted();
replaceMessage(
id: messageId,
text: responseText,
status: MessageStatus.completed,
);
} else {
if (completeText.isNotEmpty) {
_ttsService.pushTextForStreamTTS(completeText);
}
// if (completeText.isNotEmpty) {
// _ttsService.pushTextForStreamTTS(completeText);
// }
replaceMessage(
id: messageId, text: responseText, status: MessageStatus.thinking);
}
@@ -355,7 +355,7 @@ class MessageService extends ChangeNotifier {
Future<void> abortReply() async {
_isReplyAborted = true;
_ttsService.stop();
// _ttsService.stop();
_chatSseService.abort();
int index = findMessageIndexById(_latestAssistantMessageId);
if (index == -1 || messages[index].status != MessageStatus.thinking) {

View File

@@ -1,86 +1,134 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "http://175.24.250.68:4000"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.13.0"
boolean_selector:
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
url: "http://175.24.250.68:4000"
source: hosted
version: "5.2.1"
audioplayers_android:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
name: audioplayers_android
sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
url: "http://175.24.250.68:4000"
source: hosted
version: "2.1.2"
version: "4.0.3"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
url: "http://175.24.250.68:4000"
source: hosted
version: "5.0.2"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
url: "http://175.24.250.68:4000"
source: hosted
version: "3.1.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
url: "http://175.24.250.68:4000"
source: hosted
version: "6.1.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
url: "http://175.24.250.68:4000"
source: hosted
version: "4.1.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
url: "http://175.24.250.68:4000"
source: hosted
version: "3.1.0"
basic_intl:
dependency: "direct main"
description:
name: basic_intl
sha256: "15ba8447fb069bd80f0b0c74c71faf5dea45874e9566a68e4e30c897ab2b07c4"
url: "http://175.24.250.68:4000"
source: hosted
version: "0.2.0"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.4.0"
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "http://175.24.250.68:4000"
source: hosted
version: "1.19.1"
version: "1.19.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.1.4"
fixnum:
version: "2.1.3"
file:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "http://175.24.250.68:4000"
source: hosted
version: "1.1.1"
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
@@ -91,14 +139,25 @@ packages:
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "5.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
url: "http://175.24.250.68:4000"
source: hosted
version: "0.7.7+1"
flutter_tts:
dependency: "direct main"
description:
name: flutter_tts
sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
url: "http://175.24.250.68:4000"
source: hosted
version: "4.2.3"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -109,87 +168,79 @@ packages:
description:
name: fluttertoast
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "8.2.12"
http:
dependency: "direct main"
description:
name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev"
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "http://175.24.250.68:4000"
source: hosted
version: "1.4.0"
version: "1.5.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "4.1.2"
leak_tracker:
js:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "http://175.24.250.68:4000"
source: hosted
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "0.6.7"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "5.1.1"
matcher:
markdown:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
name: markdown
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "http://175.24.250.68:4000"
source: hosted
version: "0.12.17"
version: "7.3.0"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "http://175.24.250.68:4000"
source: hosted
version: "1.16.0"
version: "1.15.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.9.1"
path_provider:
@@ -197,7 +248,7 @@ packages:
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.1.5"
path_provider_android:
@@ -205,7 +256,7 @@ packages:
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.2.17"
path_provider_foundation:
@@ -213,7 +264,7 @@ packages:
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.4.1"
path_provider_linux:
@@ -221,7 +272,7 @@ packages:
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
@@ -229,7 +280,7 @@ packages:
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.1.2"
path_provider_windows:
@@ -237,15 +288,55 @@ packages:
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
url: "http://175.24.250.68:4000"
source: hosted
version: "10.4.5"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
url: "http://175.24.250.68:4000"
source: hosted
version: "10.3.6"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
url: "http://175.24.250.68:4000"
source: hosted
version: "9.1.4"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
url: "http://175.24.250.68:4000"
source: hosted
version: "3.12.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
url: "http://175.24.250.68:4000"
source: hosted
version: "0.1.3"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "3.1.6"
plugin_platform_interface:
@@ -253,15 +344,23 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
url: "http://175.24.250.68:4000"
source: hosted
version: "6.1.5"
record:
dependency: "direct main"
description:
name: record
sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "6.0.0"
record_android:
@@ -269,7 +368,7 @@ packages:
description:
name: record_android
sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.3.3"
record_ios:
@@ -277,47 +376,47 @@ packages:
description:
name: record_ios
sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.0.0"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: "29e7735b05c1944bb6c9b72a36c08d4a1b24117e712d6a9523c003bde12bf484"
url: "https://pub.dev"
sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.1.0"
version: "1.1.1"
record_macos:
dependency: transitive
description:
name: record_macos
sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.0.0"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4"
url: "https://pub.dev"
sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89
url: "http://175.24.250.68:4000"
source: hosted
version: "1.2.0"
version: "1.3.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: "024c81eb7f51468b1833a3eca8b461c7ca25c04899dba37abe580bb57afd32e4"
url: "https://pub.dev"
sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb
url: "http://175.24.250.68:4000"
source: hosted
version: "1.1.8"
version: "1.1.9"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.0.6"
sky_engine:
@@ -330,95 +429,63 @@ packages:
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "http://175.24.250.68:4000"
source: hosted
version: "3.3.0+3"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
url: "http://175.24.250.68:4000"
source: hosted
version: "4.5.1"
version: "3.0.7"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.1.1"
xdg_directories:
@@ -426,9 +493,9 @@ packages:
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
url: "http://175.24.250.68:4000"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.8.1 <4.0.0"
dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.0"