工作愉快

*实现agent ReAct与LLM chain自动切换逻辑✓

聊天窗区分任务消息✓

修复删除日程bug✓

优化远程音频逻辑✓

等待处理引入加载中效果✓

优化prompt以解决日程任务递归调用问题✓

修复一次性日程清除的bug✓
This commit is contained in:
xszyou 2023-12-25 22:13:09 +08:00
parent e48b8d8a2d
commit 01c19c13e8
59 changed files with 291 additions and 1387 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
agent/tools/IotmService.py

View File

@ -11,7 +11,7 @@
**12月迟来的报到Fay数字人 AI Agent版含智慧农业箱的操作demo代码如果你需要完整代码可以公众号留言申请获取4版正式上传!** **12月迟来的报到Fay数字人 AI Agent版含智慧农业箱的操作demo代码如果你需要完整代码可以公众号留言申请获取5版正式上传!**
如果你需要是一个线上线下的销售员,请移步[`带货完整版`](https://github.com/TheRamU/Fay/tree/fay-sales-edition) 如果你需要是一个线上线下的销售员,请移步[`带货完整版`](https://github.com/TheRamU/Fay/tree/fay-sales-edition)
@ -55,9 +55,29 @@ python main.py
### **启动数字人(非必须)** ### **启动数字人(非必须)**
+ 启动数字人[xszyou/fay-ue5: 可对接fay数字人的ue5工程 (github.com)](https://github.com/xszyou/fay-ue5) + 仓库地址https://github.com/xszyou/fay-ue5
### **启动android 连接器(非必须)**
+ 仓库地址https://github.com/xszyou/fay-android
### **更新日志**
2023.12.25:
*实现agent ReAct与LLM chain自动切换逻辑✓
聊天窗区分任务消息✓
修复删除日程bug✓
优化远程音频逻辑✓
等待处理引入加载中效果✓
优化prompt以解决日程任务递归调用问题✓
修复一次性日程清除的bug✓
### **联系** ### **联系**

View File

@ -11,7 +11,7 @@
**Belated December announcement, the 4th edition of Fay Digital Human AI Agent Version (complete code for smart agriculture box can be requested via our public channel) is officially uploaded!** **Belated December announcement, the 5th edition of Fay Digital Human AI Agent Version (complete code for smart agriculture box can be requested via our public channel) is officially uploaded!**
If you need an online and offline salesperson, please go to [`Complete Retail Version`](https://github.com/TheRamU/Fay/tree/fay-sales-edition) If you need an online and offline salesperson, please go to [`Complete Retail Version`](https://github.com/TheRamU/Fay/tree/fay-sales-edition)
@ -52,9 +52,29 @@ python main.py
### **Launching the Digital Human (Optional)** ### **Launching the Digital Human (Optional)**
+ Launch the digital human[xszyou/fay-ue5: 可对接fay数字人的ue5工程 (github.com)](https://github.com/xszyou/fay-ue5) Repository URL:https://github.com/xszyou/fay-ue5
### **Launch of Android Connector (Optional)**
Repository URL: https://github.com/xszyou/fay-android
### **Changelog**
2023.12.25:
Implemented the automatic switching logic between agent ReAct and LLM chain ✓
Distinguished task messages in the chat window ✓
Fixed the bug in deleting schedules ✓
Optimized remote audio logic ✓
Introduced loading effects for pending processes ✓
Optimized prompts to resolve recursive calling issues in schedule tasks ✓
Fixed the bug in clearing one-time schedules ✓
### **Contact** ### **Contact**

View File

@ -8,6 +8,7 @@ scheduled_tasks = {}
agent_running = False agent_running = False
agent = FayAgentCore() agent = FayAgentCore()
# 数据库初始化 # 数据库初始化
def init_db(): def init_db():
conn = sqlite3.connect('timer.db') conn = sqlite3.connect('timer.db')
@ -51,8 +52,7 @@ def parse_repeat_rule(rule, task_time):
# 执行任务 # 执行任务
def execute_task(task_time, id, content): def execute_task(task_time, id, content):
content = "执行任务->现在" + content agent.run("执行任务->立刻" + content)
agent.run(content)
del scheduled_tasks[id] del scheduled_tasks[id]
# 如果不重复,执行后删除记录 # 如果不重复,执行后删除记录
conn = sqlite3.connect('timer.db') conn = sqlite3.connect('timer.db')
@ -90,7 +90,6 @@ def agent_start():
agent_running = True agent_running = True
init_db() init_db()
# insert_test_data()
check_and_execute_thread = threading.Thread(target=check_and_execute) check_and_execute_thread = threading.Thread(target=check_and_execute)
check_and_execute_thread.start() check_and_execute_thread.start()

View File

@ -6,6 +6,7 @@ from langchain.docstore import InMemoryDocstore
from langchain.vectorstores import FAISS from langchain.vectorstores import FAISS
from langchain.agents import AgentExecutor, Tool, ZeroShotAgent, initialize_agent, agent_types from langchain.agents import AgentExecutor, Tool, ZeroShotAgent, initialize_agent, agent_types
from langchain.chains import LLMChain from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from agent.tools.MyTimer import MyTimer from agent.tools.MyTimer import MyTimer
from agent.tools.QueryTime import QueryTime from agent.tools.QueryTime import QueryTime
@ -19,6 +20,8 @@ from agent.tools.QueryTimerDB import QueryTimerDB
from agent.tools.DeleteTimer import DeleteTimer from agent.tools.DeleteTimer import DeleteTimer
from agent.tools.GetSwitchLog import GetSwitchLog from agent.tools.GetSwitchLog import GetSwitchLog
from agent.tools.getOnRunLinkage import getOnRunLinkage from agent.tools.getOnRunLinkage import getOnRunLinkage
from agent.tools.SetChatStatus import SetChatStatus
import utils.config_util as utils import utils.config_util as utils
from core.content_db import Content_Db from core.content_db import Content_Db
@ -26,6 +29,7 @@ from core import wsa_server
import os import os
class FayAgentCore(): class FayAgentCore():
def __init__(self): def __init__(self):
@ -64,6 +68,7 @@ class FayAgentCore():
delete_timer_tool = DeleteTimer() delete_timer_tool = DeleteTimer()
get_switch_log = GetSwitchLog() get_switch_log = GetSwitchLog()
get_on_run_linkage = getOnRunLinkage() get_on_run_linkage = getOnRunLinkage()
set_chat_status_tool = SetChatStatus()
tools = [ tools = [
Tool( Tool(
@ -126,22 +131,57 @@ class FayAgentCore():
func=get_on_run_linkage.run, func=get_on_run_linkage.run,
description=get_on_run_linkage.description description=get_on_run_linkage.description
), ),
Tool(
name=set_chat_status_tool.name,
func=set_chat_status_tool.run,
description=set_chat_status_tool.description
),
] ]
#agent用于执行任务
self.agent = initialize_agent(agent_types=agent_types.AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, self.agent = initialize_agent(agent_types=agent_types.AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
tools=tools, llm=llm, verbose=True, tools=tools, llm=llm, verbose=True,
max_history=5, max_history=5,
memory=memory, handle_parsing_errors=True) memory=memory, handle_parsing_errors=True)
#创建llm chain用于聊天
tools_prompt = "["
tool_names = [tool.name for tool in tools if tool.name != set_chat_status_tool.name and tool.name != say_tool.name]
tools_prompt += "".join(tool_names) + "]"
template = """你是一个智慧农业实验箱里的ai你的责任是陪伴主人生活、工作以及协助主人打理好农业种植箱里的农作物.现在主人正在和你聊天若你在聊天过程中感觉到主人想使用以下工具请按“agent:'主人刚刚的说话'”这样的格式回复,否则请直接回复我文字内容。工具如下:
""" + tools_prompt +"""
{chat_history}
Human: {human_input}
AI:"""
prompt = PromptTemplate(
input_variables=["chat_history", "human_input"], template=template
)
self.llm_chain = LLMChain(
llm=llm,
prompt=prompt,
verbose=True,
memory=memory
)
self.is_chat = False#聊天状态
def run(self, input_text): def run(self, input_text):
#消息保存 #消息保存
contentdb = Content_Db() contentdb = Content_Db()
contentdb.add_content('member', 'agent', input_text.replace('主人语音说了:', '').replace('主人文字说了:', '')) contentdb.add_content('member', 'agent', input_text.replace('主人语音说了:', '').replace('主人文字说了:', ''))
wsa_server.get_web_instance().add_cmd({"panelReply": {"type":"member","content":input_text.replace('主人语音说了:', '').replace('主人文字说了:', '')}}) wsa_server.get_web_instance().add_cmd({"panelReply": {"type":"member","content":input_text.replace('主人语音说了:', '').replace('主人文字说了:', '')}})
result = None result = ""
try: try:
result = self.agent.run(input_text) #判断执行聊天模式还是agent模式双模式在运行过程中会主动切换
if self.is_chat:
result = self.llm_chain.predict(human_input=input_text.replace('主人语音说了:', '').replace('主人文字说了:', ''))
if "agent:" in result.lower() or not self.is_chat:
print(result)
print(self.is_chat)
self.is_chat = False
input_text = result if result.lower().replace("agent:", "") else input_text
result = self.agent.run(input_text)
except Exception as e: except Exception as e:
print(e) print(e)
result = "执行完毕" if result is None or result == "N/A" else result result = "执行完毕" if result is None or result == "N/A" else result

View File

@ -2,11 +2,11 @@ import os
from typing import Any from typing import Any
from langchain.tools import BaseTool from langchain.tools import BaseTool
import agent.tools.IotmService as IotmService import tools.IotmService as IotmService
class GetSwitchLog(BaseTool): class GetSwitchLog(BaseTool):
name = "GetSwitchLog" name = "GetSwitchLog"
description = "此工具用于查询农业箱的设备开关操作历史记录设备序号小风扇1、电热风扇(2)、制冷风扇(3)、肥料开关(4)、补光设备(5)、植物生长灯(6)、二氧化碳(7)" description = "此工具用于查询农业箱的设备开关当天的操作历史记录"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -16,10 +16,31 @@ class GetSwitchLog(BaseTool):
pass pass
def _run(self, para: str) -> str: def _run(self, para: str):
infos = IotmService.get_switch_log() logs = IotmService.get_switch_log()
device_logs = {}
return infos
switch_mapping = {
1: '小风扇',
2: '电热风扇',
3: '制冷风扇',
4: '水开关',
5: '肥料开关',
6: '植物生长灯',
7: '二氧化碳'
}
for val in logs:
switch_name = switch_mapping[val['number']]
status = 'on' if val['status'] == 1 else 'off'
info = val['timetText']
if switch_name not in device_logs:
device_logs[switch_name] = {'on': [], 'off': []}
device_logs[switch_name][status].append(info)
return device_logs
if __name__ == "__main__": if __name__ == "__main__":
tool = GetSwitchLog() tool = GetSwitchLog()

View File

@ -7,7 +7,7 @@ from langchain.tools import BaseTool
class Knowledge(BaseTool): class Knowledge(BaseTool):
name = "Knowledge" name = "Knowledge"
description = """此工具用于查询箱内植物的专业知识,可以作为补充参考,使用时请传入相关问题作为参数,例如:“草梅最适合的生长温度”""" description = """此工具用于查询箱内植物的专业知识,使用时请传入相关问题作为参数,例如:“草梅最适合的生长温度”"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

@ -5,11 +5,12 @@ import requests
from langchain.tools import BaseTool from langchain.tools import BaseTool
import fay_booter import fay_booter
from core.interact import Interact from core.interact import Interact
from agent import agent_service
class Say(BaseTool): class Say(BaseTool):
name = "Say" name = "语音输出工具"
description = """此工具用于沟通及提醒,使用时请传入说话内容作为参数,例如:“该下班了,请注意休息”""" description = """此工具用于语音输出,使用时请传入说话内容作为参数,例如:“该下班了,请注意休息”"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -20,8 +21,9 @@ class Say(BaseTool):
def _run(self, para: str) -> str: def _run(self, para: str) -> str:
interact = Interact("agent", 1, {'user': '', 'msg': para}) interact = Interact("audio", 1, {'user': '', 'msg': para})
fay_booter.feiFei.on_interact(interact) fay_booter.feiFei.on_interact(interact)
agent_service.agent.is_chat = True
return "语音输出了:" + para return "语音输出了:" + para

View File

@ -0,0 +1,29 @@
import os
from typing import Any
from langchain.tools import BaseTool
from agent import agent_service
class SetChatStatus(BaseTool):
name = "SetChatStatus"
description = """此工具用于设置聊天状态,当识别到主人想进行交流聊天时使用此工具"""
def __init__(self):
super().__init__()
async def _arun(self, *args: Any, **kwargs: Any) -> Any:
# 用例中没有用到 arun 不予具体实现
pass
def _run(self, para: str) -> str:
agent_service.agent.is_chat = True
return "设置聊天状态成功"
if __name__ == "__main__":
tool = SetChatStatus()
info = tool.run("该下班了,请注意休息")
print(info)

View File

@ -1,12 +1,13 @@
import os import os
from typing import Any from typing import Any
from itertools import groupby
from langchain.tools import BaseTool from langchain.tools import BaseTool
import agent.tools.IotmService as IotmService import tools.IotmService as IotmService
class getOnRunLinkage(BaseTool): class getOnRunLinkage(BaseTool):
name = "getOnRunLinkage" name = "getOnRunLinkage"
description = "此工具用于查询农业箱当前在运行的联动设备序号小风扇1、电热风扇(2)、制冷风扇(3)、肥料开关(4)、补光设备(5)、植物生长灯(6)、二氧化碳(7)" description = "此工具用于查询农业箱当前在运行的联动"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -17,9 +18,45 @@ class getOnRunLinkage(BaseTool):
def _run(self, para: str) -> str: def _run(self, para: str) -> str:
infos = IotmService.get_on_run_linkage() logs = IotmService.get_on_run_linkage()
return infos desc_list = {
'co2_S36': '二氧化碳',
'light_bh1': '箱内的光照强度',
'air_S37': '污染气体',
'nh3_S37': '氨气',
'temperature_MP14': '箱外温度',
'temperature_MP21': '箱内温度',
'humidity_MP14': '箱外湿度',
'humidity_S34': '箱内土壤的湿度',
}
infos = {}
logs.sort(key=lambda x: (x['label'], x['port']))
for (sensor_type, port), group in groupby(logs, key=lambda x: (x['label'], x['port'])):
group_infos = []
for val in group:
onoff = '开启设备开关' if val['onoff'] == 1 else '关闭设备开关'
info = {
'max': val['maxVal'],
'min': val['minVal'],
'onoff': onoff,
}
if float(val['keeptime']) > 0:
info["持续时间(若需执行开启设备,持续时间过后执行关闭),单位为分钟"] = val['keeptime']
if float(val['delaytime']) > 0:
info["执行后下次检查相距时间,单位为分钟"] = val['delaytime']
group_infos.append(info)
key_str = f"{sensor_type}_{port}"
infos[desc_list.get(key_str, 'Unknown')] = group_infos
return infos
if __name__ == "__main__": if __name__ == "__main__":
tool = getOnRunLinkage() tool = getOnRunLinkage()
info = tool.run("") info = tool.run("")

View File

@ -1,15 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="..\:/android/projects/fayConnectorDemo/app/src/main/res/layout/activity_main.xml" value="0.358695652173913" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -1,38 +0,0 @@
plugins {
id 'com.android.application'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.yaheen.fayconnectordemo"
minSdk 29
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

@ -1,21 +0,0 @@
# 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

@ -1,26 +0,0 @@
package com.yaheen.fayconnectordemo;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.yaheen.fayconnectordemo", appContext.getPackageName());
}
}

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.yaheen.fayconnectordemo">
<uses-permission android:name="android.permission.INTERNET"/><!--网络访问-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FayConnectorDemo"
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>
<service android:name=".FayConnectorService" />
</application>
</manifest>

View File

@ -1,343 +0,0 @@
package com.yaheen.fayconnectordemo;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.BitmapFactory;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import com.google.android.material.snackbar.Snackbar;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Date;
public class FayConnectorService extends Service {
private AudioRecord record;
private int recordBufsize = 0;
private Socket socket = null;
private InputStream in = null;
private OutputStream out = null;
public static boolean running = false;
private File cacheDir = null;
private String channelId = null;
private PendingIntent pendingIntent = null;
private NotificationManagerCompat notificationManager = null;
private long totalrece = 0;
private long totalsend = 0;
private AudioManager mAudioManager = null;
private boolean isPlay = false;
//创建通知
private String createNotificationChannel(String channelID, String channelNAME, int level) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, level);
manager.createNotificationChannel(channel);
return channelID;
} else {
return null;
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, START_FLAG_REDELIVERY, startId);
return Service.START_STICKY;
}
@Override
public void onCreate() {
super.onCreate();
Log.d("fay", "服务启动");
//开启蓝牙传输
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mAudioManager.startBluetoothSco();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
if (AudioManager.SCO_AUDIO_STATE_CONNECTED == state) {
Log.d("fay", "蓝牙sco连接成功");
}
}
};
this.registerReceiver(receiver, intentFilter);
running = true;
this.cacheDir = getApplicationContext().getFilesDir();//getCacheDir();
Thread sendThread = new Thread(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(FayConnectorService.this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
if (record == null) {
recordBufsize = AudioRecord
.getMinBufferSize(16000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
record = new AudioRecord(MediaRecorder.AudioSource.MIC,
16000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
recordBufsize);
}
try {
socket = new Socket("192.168.1.101", 10001);
in = socket.getInputStream();
out = socket.getOutputStream();
Log.d("fay", "fay控制器连接成功");
} catch (IOException e) {
Log.d("fay", "socket连接失败");
running = false;
return;
}
byte[] data = new byte[1024];
record.startRecording();
Log.d("fay", "麦克风启动成功");
try {
Log.d("fay", "开始传输音频");
while (running) {
if (isPlay){
continue;
}
int size = record.read(data, 0, 1024);
if (size > 0) {
out.write(data);
totalsend += data.length / 1024;
}else{//录音异常等待60秒重新录取
try {
Thread.sleep(60000);
record.stop();
record.startRecording();
}catch (Exception e){
}
}
}
} catch (Exception e) { //通过异常关退出循环
Log.d("fay", "服务端关闭:" + e.toString());
} finally {
running = false;
record.stop();
record = null;
((AudioManager) getSystemService(Context.AUDIO_SERVICE)).stopBluetoothSco();
try {
socket.close();
} catch (Exception e) {
}
socket = null;
Log.d("fay", "send线程结束");
}
}
}
}
});
Thread receThread = new Thread(new Runnable() {
@Override
public void run() {
try {
while (running) {
while (socket != null && !socket.isClosed()) {
byte[] data = new byte[9];
byte[] wavhead = new byte[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};//文件传输开始标记
in.read(data);
if (Arrays.equals(wavhead, data)) {
Log.d("fay", "开始接收音频文件");
String filedata = "";
data = new byte[1024];
int len = 0;
while ((len = in.read(data)) != -1) {
byte[] temp = new byte[len];
System.arraycopy(data, 0, temp, 0, len);
filedata += MainActivity.bytesToHexString(temp);
int index = filedata.indexOf("080706050403020100");
if (filedata.length() > 9 && index > 0) {//wav文件结束标记
filedata = filedata.substring(0, index).replaceAll("F0F1F2F3F4F5F6F7F8", "");
File wavFile = new File(cacheDir, String.format("sample-%s.wav", new Date().getTime() + ""));
wavFile.createNewFile();
FileOutputStream fos = new FileOutputStream(wavFile);
fos.write(MainActivity.decodeHexBytes(filedata.toCharArray()));
fos.close();
totalrece += filedata.length() / 2 / 1024;
Log.d("fay", "wav文件接收完成:" + wavFile.getAbsolutePath() + "," + filedata.length() / 2);
try {
MediaPlayer player = new MediaPlayer();
player.setDataSource(wavFile.getAbsolutePath());
player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.d("fay", "开始播放");
if (mAudioManager.isBluetoothScoOn()){
mAudioManager.stopBluetoothSco();
mAudioManager.setBluetoothScoOn(false);
mAudioManager.setMode(mAudioManager.MODE_NORMAL);
}
try {
Thread.sleep(500);
}catch (Exception e){
}
isPlay = true;
mp.start();
}
});
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.d("fay", "播放完成");
isPlay = false;
mp.release();
mAudioManager.startBluetoothSco();
mAudioManager.setMode(mAudioManager.MODE_IN_CALL);
mAudioManager.setBluetoothScoOn(true);
}
});
player.setVolume(1,1);
player.setLooping(false);
player.prepareAsync();
} catch (IOException e) {
Log.e("fay", e.toString());
}
break;
}
}
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
}
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
} catch (Exception e) {//通过异常判断socket已经关闭退出循环
} finally {
Log.d("fay", "rece线程结束");
}
}
});
sendThread.start();
receThread.start();
//通知栏
new Thread(new Runnable() {
@Override
public void run() {
try{
while (running) {
Thread.sleep(3000);
if (totalsend + totalrece > 2048){
inotify("fay connector demo", "已经连接fay控制器累计接收/发送:" + String.format("%.2f", (double)totalrece / 1024) + "/" + String.format("%.2f", (double)totalsend / 1024) + "MB");
} else {
inotify("fay connector demo", "已经连接fay控制器累计接收/发送:" + totalrece + "/" + totalsend + "KB");
}
}
inotify("fay connector demo", "已经断开fay控制器");
}catch (Exception e){
Log.e("fay", e.toString());
}finally {
FayConnectorService.this.stopSelf();
}
}
}).start();
}
private void inotify(String title, String content){
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (pendingIntent == null){
pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
if (channelId == null){
channelId = createNotificationChannel("my_channel_ID", "my_channel_NAME", NotificationManager.IMPORTANCE_HIGH);
}
if (notificationManager == null){
notificationManager = NotificationManagerCompat.from(this);
}
NotificationCompat.Builder notification2 = new NotificationCompat.Builder(FayConnectorService.this, channelId)
.setContentTitle(title)
.setContentText(content)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
//notificationManager.notify(100, notification2.build());
startForeground(100, notification2.build());
}
@Override
public void onDestroy() {
Log.d("fay", "服务关闭");
super.onDestroy();
mAudioManager.stopBluetoothSco();
running = false;
stopForeground(true);
}
}

View File

@ -1,137 +0,0 @@
package com.yaheen.fayconnectordemo;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.app.ActivityManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import com.google.android.material.snackbar.Snackbar;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private TextView tv = null;
private boolean running = false;
private Intent serviceIntent = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = this.findViewById(R.id.tv);
serviceIntent = new Intent(this, FayConnectorService.class);
//按钮点击
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d("fay","onclick");
running = FayConnectorService.running;//isServiceRunning();//同步service的运行状态,不好使
if (!running){//运行
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//开启
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.RECORD_AUDIO)) {
Log.d("fay", "用户彻底拒绝了权限");
return;
} else {
// 用户未彻底拒绝授予权限
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.RECORD_AUDIO}, 1);
}
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
Log.d("fay","权限ok");
Snackbar.make(view, "正在连接fay控制器", Snackbar.LENGTH_SHORT)
.setAction("Action", null).show();
startForegroundService(serviceIntent);
running = true;
}
}
} else{//关闭
stopService(serviceIntent);
Snackbar.make(view, "已经断开fay控制器", Snackbar.LENGTH_SHORT)
.setAction("Action", null).show();
running = false;
}
}
});
}
public static String bytesToHexString(byte[] data){
String result="";
for (int i = 0; i < data.length; i++) {
result+=Integer.toHexString((data[i] & 0xFF) | 0x100).toUpperCase().substring(1, 3);
}
return result;
}
public static byte[] decodeHexBytes(char[] data) {
int len = data.length;
if ((len & 0x01) != 0) {
throw new RuntimeException("未知的字符");
}
byte[] out = new byte[len >> 1];
for (int i = 0, j = 0; j < len; i++) {
int f = toDigit(data[j], j) << 4;
j++;
f = f | toDigit(data[j], j);
j++;
out[i] = (byte) (f & 0xFF);
}
return out;
}
protected static int toDigit(char ch, int index) {
int digit = Character.digit(ch, 16);
if (digit == -1) {
throw new RuntimeException("非法16进制字符 " + ch
+ " 在索引 " + index);
}
return digit;
}
private boolean isServiceRunning() {
ActivityManager activityManager = (ActivityManager) this.getApplicationContext()
.getSystemService(Context.ACTIVITY_SERVICE);
ComponentName serviceName = new ComponentName("com.yaheen.fayconnectordemo", ".FayConnectorService");
PendingIntent intent = activityManager.getRunningServiceControlPanel(serviceName);
if (intent == null){
return false;
}
return true;
}
}

View File

@ -1,30 +0,0 @@
<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>

View File

@ -1,170 +0,0 @@
<?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

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接/断开fay控制器"
android:id="@+id/tv"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +0,0 @@
<?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" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?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" />
</adaptive-icon>

View File

@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.FayConnectorDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,10 +0,0 @@
<?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

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

View File

@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.FayConnectorDemo" 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" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,13 +0,0 @@
<?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

@ -1,19 +0,0 @@
<?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>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -1,17 +0,0 @@
package com.yaheen.fayconnectordemo;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

@ -1,9 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -1,21 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

@ -1,6 +0,0 @@
#Fri Jan 20 09:27:45 CST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,16 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "fayConnectorDemo"
include ':app'

View File

@ -33,7 +33,6 @@ if platform.system() == "Windows":
from test_olipsync import LipSyncGenerator from test_olipsync import LipSyncGenerator
#文本消息处理20231211增加agent操作 #文本消息处理20231211增加agent操作
def send_for_answer(msg): def send_for_answer(msg):
#记录运行时间 #记录运行时间
@ -53,6 +52,11 @@ def send_for_answer(msg):
#agent 处理 #agent 处理
text = agent_service.agent.run(msg) text = agent_service.agent.run(msg)
#聊天模式语音输入语音输出
if text and "语音" in msg and agent_service.agent.is_chat:
interact = Interact("audio", 1, {'user': '', 'msg': text})
fay_booter.feiFei.on_interact(interact)
#推送数字人 #推送数字人
if not cfg.config["interact"]["playSound"]: if not cfg.config["interact"]["playSound"]:
content = {'Topic': 'Unreal', 'Data': {'Key': 'log', 'Value': text}} content = {'Topic': 'Unreal', 'Data': {'Key': 'log', 'Value': text}}
@ -240,7 +244,7 @@ class FeiFei:
# if audio_length <= config_util.config["interact"]["maxInteractTime"] or say_type == "script": # if audio_length <= config_util.config["interact"]["maxInteractTime"] or say_type == "script":
if config_util.config["interact"]["playSound"]: # 展板播放 if config_util.config["interact"]["playSound"]: # 展板播放
self.__play_sound(file_url) self.__play_sound(file_url)
else:#发送音频给ue和socket else:#发送音频给ue
#推送ue #推送ue
content = {'Topic': 'Unreal', 'Data': {'Key': 'audio', 'Value': os.path.abspath(file_url), 'Text': self.a_msg, 'Time': audio_length, 'Type': say_type}} content = {'Topic': 'Unreal', 'Data': {'Key': 'audio', 'Value': os.path.abspath(file_url), 'Text': self.a_msg, 'Time': audio_length, 'Type': say_type}}
#计算lips #计算lips
@ -254,22 +258,22 @@ class FeiFei:
util.log(1, "唇型数字生成失败无法使用新版ue5工程") util.log(1, "唇型数字生成失败无法使用新版ue5工程")
wsa_server.get_instance().add_cmd(content) wsa_server.get_instance().add_cmd(content)
#推送远程音频 #推送远程音频
if self.deviceConnect is not None: if self.deviceConnect is not None:
try: try:
self.deviceConnect.send(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08') # 发送音频开始标志,同时也检查设备是否在线 self.deviceConnect.send(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08') # 发送音频开始标志,同时也检查设备是否在线
wavfile = open(os.path.abspath(file_url),'rb') wavfile = open(os.path.abspath(file_url),'rb')
data = wavfile.read(1024)
total = 0
while data:
total += len(data)
self.deviceConnect.send(data)
data = wavfile.read(1024) data = wavfile.read(1024)
total = 0 time.sleep(0.001)
while data: self.deviceConnect.send(b'\x08\x07\x06\x05\x04\x03\x02\x01\x00')# 发送音频结束标志
total += len(data) util.log(1, "远程音频发送完成:{}".format(total))
self.deviceConnect.send(data) except socket.error as serr:
data = wavfile.read(1024) util.log(1,"远程音频输入输出设备已经断开:{}".format(serr))
time.sleep(0.001)
self.deviceConnect.send(b'\x08\x07\x06\x05\x04\x03\x02\x01\x00')# 发送音频结束标志
util.log(1, "远程音频发送完成:{}".format(total))
except socket.error as serr:
util.log(1,"远程音频输入输出设备已经断开:{}".format(serr))
time.sleep(audio_length + 0.5) time.sleep(audio_length + 0.5)
wsa_server.get_web_instance().add_cmd({"panelMsg": ""}) wsa_server.get_web_instance().add_cmd({"panelMsg": ""})

View File

@ -155,8 +155,7 @@ class Recorder:
if not data: if not data:
continue continue
if cfg.config['source']['record']['enabled'] and not self.is_remote():
if cfg.config['source']['record']['enabled']:
if len(cfg.config['source']['record'])<3: if len(cfg.config['source']['record'])<3:
channels = 1 channels = 1
else: else:
@ -167,7 +166,6 @@ class Recorder:
mono = data[:, 0] # taking the first channel mono = data[:, 0] # taking the first channel
data = mono.tobytes() data = mono.tobytes()
level = audioop.rms(data, 2) level = audioop.rms(data, 2)
if len(self.__history_data) >= 5: if len(self.__history_data) >= 5:
self.__history_data.pop(0) self.__history_data.pop(0)
@ -229,3 +227,8 @@ class Recorder:
@abstractmethod @abstractmethod
def get_stream(self): def get_stream(self):
pass pass
#TODO Edit by xszyou on 20231225:子类实现返回是否远程音频
@abstractmethod
def is_remote(self):
pass

View File

@ -71,7 +71,9 @@ class RecorderListener(Recorder):
except Exception as e: except Exception as e:
print(e) print(e)
util.log(1, "请检查设备是否有误,再重新启动!") util.log(1, "请检查设备是否有误,再重新启动!")
def is_remote(self):
return False
@ -129,6 +131,9 @@ class DeviceInputListener(Recorder):
def start_ngrok(self, clientId): def start_ngrok(self, clientId):
self.ngrok = ngrok_util.NgrokCilent(clientId) self.ngrok = ngrok_util.NgrokCilent(clientId)
self.ngrok.start() self.ngrok.start()
def is_remote(self):
return True

View File

@ -417,4 +417,32 @@ textarea {
border-radius: 10px; border-radius: 10px;
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
-webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5); -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
} }
/* CSS 加载动画样式 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
border: 5px solid #f3f3f3; /* 轻颜色 */
border-top: 5px solid #3498db; /* 蓝色 */
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 2s linear infinite;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #3498db;
width: 10px;
height: 10px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@ -74,7 +74,8 @@ new Vue({
msg_list:[], msg_list:[],
is_connect: false, is_connect: false,
wake_word_enabled:false, wake_word_enabled:false,
wake_word: '' wake_word: '',
loading: false
} }
}, },
@ -535,6 +536,12 @@ new Vue({
'type' : data['type'] , 'type' : data['type'] ,
'way' : 'send' 'way' : 'send'
} }
console.log(info)
if (data['type'] == 'fay'){
_this.loading = false;
}else{
_this.loading = true;
}
_this.msg_list.push(info); _this.msg_list.push(info);
this.timer = setTimeout(()=>{ //设置延迟执行 this.timer = setTimeout(()=>{ //设置延迟执行

View File

@ -157,28 +157,32 @@
<div class="right_main"> <div class="right_main">
<div class="container"> <div class="container">
<div class="content"> <div class="content">
<div v-for="item in msg_list"> <div v-for="(item, index) in msg_list" :key="index">
<div class="item item-center"><span>[[item.timetext]]</span></div> <div class="item item-center"><span>[[item.timetext]]</span></div>
<div class="item item-left" v-if="item.type == 'fay'">
<div class="avatar"><img src="{{ url_for('static',filename='to.jpg') }}" />
</div>
<div class="bubble bubble-left">[[item.content]]
</div>
</div>
<div class="item item-right" v-else>
<div class="bubble bubble-right">[[item.content]]</div>
<div class="avatar"><img src="{{ url_for('static',filename='from.jpg') }}" />
</div>
</div>
</div>
</div> <div class="item item-left" v-if="item.type == 'fay'">
<div class="avatar"><img src="{{ url_for('static',filename='to.jpg') }}" />
</div>
<div class="bubble bubble-left">[[item.content]]
</div>
</div>
<div class="item item-right" v-else>
<div class="bubble bubble-right" >[[item.content]]<div v-if="loading && index === msg_list.length - 1" class="spinner"></div></div>
<div class="avatar"><img src="{{ url_for('static',filename='from.jpg') }}" />
</div>
</div>
</div>
<div>
</div>
</div>
<div class="input-area"> <div class="input-area">
<textarea v-model="send_msg" name="text" id="textarea" placeholder="发送些内容给Fay..."></textarea> <textarea v-model="send_msg" name="text" id="textarea" placeholder="发送些内容给Fay..."></textarea>
<div class="button-area"> <div class="button-area">

View File

@ -35,7 +35,7 @@ def receive_audio(client):
break break
print("receive audio end:{}".format(len(filedata)), end="") print("receive audio end:{}".format(len(filedata)), end="")
filename = "sample/recv_{}.mp3".format(time.time()) filename = "samples/recv_{}.mp3".format(time.time())
with open(filename, "wb") as f: with open(filename, "wb") as f:
f.write(filedata) f.write(filedata)
f.close() f.close()