Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
lxm 1 year ago
parent
commit
963d82dfa1
  1. 72
      lib/core/widgets/intro_overlay_builder.dart
  2. 15
      lib/core/widgets/select_location_map.dart
  3. 44
      lib/data/services/novice_guide_service.dart
  4. 344
      lib/modules/agent/views/agent_view.dart
  5. 35
      lib/modules/home/views/home_view.dart
  6. 59
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

72
lib/core/widgets/intro_overlay_builder.dart

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class IntroOverlayBuilder {
static Widget aiBuilder(
StepWidgetParams params,
String text, {
String buttonText = '下一步',
bool alignment = true,
Function? onNext,
Function? onFinish,
}) {
return Column(
crossAxisAlignment:
alignment ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.only(bottom: 10.w),
child: Text(
text,
style: TextStyle(
fontSize: 15.sp,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Row(
children: [
if (onNext != null)
_button(
buttonText,
() {
params.onNext!();
onNext();
},
),
if (onFinish != null)
_button(
buttonText,
() {
params.onFinish();
onFinish();
},
),
],
),
],
);
}
static Widget _button(String text, Function() onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white),
),
child: Text(
text,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
),
),
),
);
}
}

15
lib/core/widgets/select_location_map.dart

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class SelectLocationMap extends StatefulWidget {
const SelectLocationMap({super.key});
@override
State<SelectLocationMap> createState() => _SelectLocationMapState();
}
class _SelectLocationMapState extends State<SelectLocationMap> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

44
lib/data/services/novice_guide_service.dart

@ -8,6 +8,47 @@ class NoviceGuideService extends GetxService {
bool isHomeGuide = true;
bool isAIGuide = true;
int aiStep = 0;
List aiStepsList = [
{
'start': '对小言说:“今天天气怎么样”',
'input': '今天天气怎么样',
'send': '小言会为你实时为你播报天气',
'end': '小言能实时为你播报天气,试试其他指令吧',
},
{
'start': '对小言说:“请播放一组摇滚乐”',
'input': '请播放一组摇滚乐',
'send': '小言会为你搜寻到一组音乐',
'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧',
},
{
'start': '对小言说:“导航到深圳机场”',
'input': '导航到深圳机场',
'send': '小言会为你规划路线',
'end': '小言已成功为您导航到深圳机场,试试其他指令吧',
},
{
'start': '对小言说:“明天下午3点创建会议提醒”',
'input': '明天下午3点创建会议提醒',
'send': '小言会为你创建会议提醒',
'end': '小言已成功为您创建会议提醒,试试其他指令吧',
},
{
'start': '对小言说:“拨打妈妈的电话”',
'input': '拨打妈妈的电话',
'send': '小言会为你拨打电话',
'end': '小言已成功为您拨打电话,试试其他指令吧',
},
{
'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”',
'input': '给爸爸发短信说晚上回家吃饭’',
'send': '小言会为你发送短信',
'end': '短信已发送,语音操控更便捷',
},
];
@override
void onInit() {
super.onInit();
@ -15,8 +56,9 @@ class NoviceGuideService extends GetxService {
// isAIGuide = _storage.read('ai_guide') ?? false;
}
void startHomeGuide() {
void startHomeGuide() async {
if (!isHomeGuide) {
await Future.delayed(const Duration(milliseconds: 200));
isHomeGuide = true;
_storage.write('home_guide', true);
Intro.of(Get.context!).start(group: 'home');

344
lib/modules/agent/views/agent_view.dart

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/agent_controller.dart';
import '../controllers/message_action_controller.dart';
import 'message_bubble.dart';
@ -197,47 +198,79 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
children: [
// 聊天内容
Expanded(
child: Obx(
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller
.messages[controller.messages.length - 1 - index];
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
));
child: Stack(
children: [
IntroStepBuilder(
group: 'ai',
order: 3,
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
controller.noviceGuideService.aiStepsList[
controller.noviceGuideService.aiStep]['end'],
onFinish: () async {
if (controller.noviceGuideService.aiStep < 5) {
await Future.delayed(
const Duration(milliseconds: 300));
controller.noviceGuideService.aiStep++;
Intro.of(Get.context!)
.start(group: 'ai', reset: true);
}
},
);
},
builder: (BuildContext context, GlobalKey key) => Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
key: key,
width: double.infinity,
height: 430.w,
),
),
),
),
Obx(
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller.messages[
controller.messages.length - 1 - index];
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
));
},
),
),
),
],
),
),
@ -392,7 +425,24 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
return IntroStepBuilder(
group: 'ai',
order: 1,
text: '对小言说“今天天气怎么样”',
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
controller.noviceGuideService
.aiStepsList[
controller.noviceGuideService
.aiStep]['start'],
buttonText: '确定输入',
onNext: () {
controller.textController.text =
controller.noviceGuideService
.aiStepsList[
controller
.noviceGuideService
.aiStep]['input'];
},
);
},
onWidgetLoad: () {
controller.noviceGuideService
.startAIGuide();
@ -630,111 +680,143 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
SizedBox(width: 8.w),
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Obx(() {
final hasText = controller.hasTextInput.value;
final hasImage =
controller.isImageInputActive.value;
final shouldShowSend = hasText || hasImage;
final shouldShowExpand =
controller.shouldShowExpandButton.value;
Builder(builder: (context) {
StepWidgetParams? widgetParams;
return IntroStepBuilder(
group: 'ai',
order: 2,
onHighlightWidgetTap: () {
widgetParams!.onNext!();
controller.sendMessage();
},
overlayBuilder: (params) {
widgetParams = params;
return IntroOverlayBuilder.aiBuilder(
params,
'点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}',
alignment: false,
);
},
builder:
(BuildContext context, GlobalKey key) =>
Obx(
key: key,
() {
final hasText =
controller.hasTextInput.value;
final hasImage =
controller.isImageInputActive.value;
final shouldShowSend =
hasText || hasImage;
final shouldShowExpand = controller
.shouldShowExpandButton.value;
if (shouldShowSend) {
// 显示发送按钮(可能还有展开按钮)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText)
Padding(
padding:
EdgeInsets.only(bottom: 40.h),
child: GestureDetector(
if (shouldShowSend) {
// 显示发送按钮(可能还有展开按钮)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText)
Padding(
padding: EdgeInsets.only(
bottom: 40.h),
child: GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller
.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor:
Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller
.closeFullScreenEdit();
});
},
child: Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
borderRadius:
BorderRadius.circular(
16.w),
),
child: Icon(
Icons.expand_less,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20.sp,
),
),
),
),
// 发送按钮
GestureDetector(
onTap: () =>
controller.sendMessage(),
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(
22.w),
),
child: Icon(
Icons.send,
color: Colors.white,
size: 22.sp,
),
),
),
],
);
} else {
// 显示'+'选择按钮
return Obx(() {
final isExpanded = controller
.isPlusOptionsExpanded.value;
return GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor:
Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller
.closeFullScreenEdit();
});
controller.togglePlusOptions();
},
child: Container(
width: 32.w,
height: 32.w,
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(16.w),
BorderRadius.circular(22.w),
),
child: Icon(
Icons.expand_less,
isExpanded
? Icons.close
: Icons.add,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20.sp,
size: 22.sp,
),
),
),
),
// 发送按钮
GestureDetector(
onTap: () => controller.sendMessage(),
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(22.w),
),
child: Icon(
Icons.send,
color: Colors.white,
size: 22.sp,
),
),
),
],
);
} else {
// 显示'+'选择按钮
return Obx(() {
final isExpanded =
controller.isPlusOptionsExpanded.value;
return GestureDetector(
onTap: () {
controller.togglePlusOptions();
},
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(22.w),
),
child: Icon(
isExpanded ? Icons.close : Icons.add,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22.sp,
),
),
);
});
}
);
});
}
},
),
);
}),
],
),

35
lib/modules/home/views/home_view.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/home_controller.dart';
import '../../../routes/app_routes.dart';
import '../../../core/widgets/common_bottom_nav.dart';
@ -84,31 +85,46 @@ class HomeView extends GetView<HomeController> {
_buildAppBar(isDarkMode),
// 顶部图片
_buildEarbudsImage(),
SizedBox(height: 20.h),
// 设备状态卡片
IntroStepBuilder(
group: 'home',
order: 1,
text: '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onWidgetLoad: () {
controller.noviceGuideService.startHomeGuide();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildEarbudsImage(),
child: _buildDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
// 设备状态卡片
IntroStepBuilder(
group: 'home',
order: 2,
text: '对着麦克风说“小言小言”',
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'对着麦克风说“小言小言”',
onNext: () {},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
// 热门AI体验
_buildSectionTitle(isDarkMode),
@ -645,11 +661,16 @@ class HomeView extends GetView<HomeController> {
child: IntroStepBuilder(
group: 'home',
order: 3,
text: '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧',
onHighlightWidgetTap: () {
Intro.of(Get.context!).dispose();
controller.exploreAIFeatures();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧',
);
},
builder: (BuildContext context, GlobalKey key) => _buildFeatureCard(
key: key,
title: 'aiAssistant'.tr, // AI全能助手

59
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -26,22 +26,32 @@ class SwiftOpusAudioProcessor: NSObject {
cleanup()
}
/// 线程安全的解码器初始化
private func initializeOpusDecoder() -> Bool {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if decoder != nil {
return true
}
var error: Int32 = 0
decoder = opus_decoder_create(sampleRate, channels, &error)
let newDecoder = opus_decoder_create(sampleRate, channels, &error)
guard error == OPUS_OK, decoder != nil else {
guard error == OPUS_OK, newDecoder != nil else {
print("Opus解码器创建失败,错误代码: \(error)")
return false
}
decoder = newDecoder
return true
}
/// 线程安全的解码器销毁
private func destroyOpusDecoder() {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if let decoder = decoder {
opus_decoder_destroy(decoder)
self.decoder = nil
@ -112,23 +122,44 @@ class SwiftOpusAudioProcessor: NSObject {
}
}
// 简化的 Opus 解码方法,重点是容错处理
/// 改进的 Opus 解码方法,增强容错和线程安全
private func decodeOpusPacket(_ packet: Data) {
guard let decoder = decoder else { return }
// 添加线程安全锁
objc_sync_enter(self)
defer { objc_sync_exit(self) }
// 验证解码器状态
guard let decoder = decoder, isDecodingActive else {
print("解码器未初始化或已停止")
return
}
let maxSamples = Int(maxFrameSize)
var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels))
let decodedSamples = packet.withUnsafeBytes { packetPtr in
let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress
return opus_decode(
decoder,
uint8Ptr,
opus_int32(packet.count),
&pcmBuffer,
Int32(maxSamples),
0
)
// 确保指针有效
guard let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress else {
print("无法获取数据包指针")
return opus_int32(-1)
}
// 添加异常捕获机制
var result: opus_int32 = -1
// 使用 autoreleasepool 确保内存管理
autoreleasepool {
result = opus_decode(
decoder,
uint8Ptr,
opus_int32(packet.count),
&pcmBuffer,
Int32(maxSamples),
0
)
}
return result
}
// 增强的错误处理:检查各种错误情况
@ -237,4 +268,4 @@ class SwiftOpusAudioProcessor: NSObject {
func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) {
return (sampleRate, channels)
}
}
}

Loading…
Cancel
Save