媒介
前段时间太忙了博客不停都没来得及更新,但是不代表我已经停止开发了,刚好最近把语音部分给调整了一下,所以就来分享一下具体的内容了。我想说一下,更新晚还是有好处的,社区已经有很多的小伙伴自己实现了一些语音对话功能的案例,比如小智也有.NET客户端了,还有就是一些树莓派对接实时语音api实现对话的功能,这些都是挺好的案例,很适合有兴趣的小伙伴来学习使用。
我做的还是比较传统的对话,通过Azure的语音服务进行关键字的训练,然后通过文本转语音和语音转文本再联合BotSharp智能体框架可以做到不集成小智服务实现对话的本领,而且拥有会话管理,提示词管理还有一些工具调用的本领。我已经如饥似渴的想分享给各人了。
问题解答
为啥选择树莓派不是单片机
有朋友以为树莓派代价贵,还说单片机就能完成开发之类的,我给各人阐明下我们目前做的是针对Linux系统下的.NET的一些实践,如果以为树莓派贵可以买一些国产的板子平替,有本领的可以自己搞库映射。至于单片机开发的事情,如果真的感兴趣单片机开发,那我们可以后期出一些文章讲这个做一些有趣的玩具。
表情播放目前的方案选择是啥
上一篇文章有讲什么是lottie动画,文章有说效果不好,但是我检查代码之后发现是代码写的有bug才导致播放很差,所以就不消转成文本的mp4了,lottie动画文件很小,适合放到板子上。
下图上面是在电脑上使用Lottie动画播放的效果。
名词解释
我是真的想让各人亲自上手试试,所以文章会讲一些基础的内容,各人不要嫌啰嗦,能看到这篇文章的小伙伴阐明各人至少应该有个板子而且点亮了屏幕。
BotSharp
BotSharp 是一个开源的多智能体应用开发框架,从简单的聊天呆板人,再到多智能体协作,以及复杂的任务如【Text To Sql】框架都提供了开箱即用的使用方法,可以快速的将大模子的本领接入到现有的业务系统中,而且内置知识库和会话管理功能等。
在我们的智能桌面呆板人项目中,BotSharp提供会话管理和多智能体调用以及工具调用的功能。
Azure语音服务
Azure语音服务是微软提供的云端AI服务,支持语音转文本、文本转语音、语音翻译等功能。在我们的智能桌面呆板人项目中,Azure语音服务用于将用户的语音指令转换为文本(用于理解用户意图)以及将呆板人的文本复兴转换为自然语音输出。此外,它还支持关键词辨认功能,可用于唤醒我们的呆板人。
ALSA
ALSA(Advanced Linux Sound Architecture)是Linux系统中的音频架构,提供了对声卡硬件的驱动功能和API。在树莓派等Linux设备上,ALSA是处理声音输入输出的基础系统。我们的呆板人项目需要ALSA来管理麦克风输入和扬声器输出。
aplay
aplay是ALSA提供的命令行工具,用于播放音频文件。在Linux系统中,可以使用aplay命令来测试和播放各种格式的音频文件,验证扬声器是否正常工作。在我们的项目中,可以通过步伐调用aplay来播放合成的语音文件。
arecord
arecord是ALSA的录音工具,用于从麦克风或其他音频输入设备捕捉音频。在树莓派上,我们可以使用arecord来录制用户的语音,然后将录制的音频文件发送给语音辨认服务进行处理。
amixer
amixer是ALSA提供的另一个命令行工具,用于控制音频混合器设置,如音量调治、输入输出设备选择等。在我们的呆板人项目中,可以使用amixer来调整麦克风和扬声器的音量,确保语音交互体验良好。
准备工作
首先准备麦克风和喇叭
可以通过购买现成的声卡,接到树莓派上的USB口或者OTG口,而且根据上面的aplay和arecord工具测试麦克风和喇叭是否正常。- cat /proc/asound/cards #查看声卡设备
- cat /proc/asound/devices #查看设备
- arecord -l # 列出设备
- aplay -l # 列出设备
- arecord -D "plughw:0,0" -f S16_LE -r 16000 -d 5 -t wav test.wav #录制一段测试声音
- aplay -D "plughw:0,0" test.wav # 播放测试声音
- alsamixer #调节系统的音量
复制代码 部分指令测试图如下:
获取Azure语音服务的API KEY
需要在微软的Azure服务中创建语音资源,获取Api Key。
获取微软的Azure OpenAI或者其他的大模子的API KEY
目前微软新出的Azure AI Foundry服务,可以在里面创建并使用多种模子,适合有订阅的用户使用,代价还算合适各人可以试试,流行的一些模子都可以在上面使用。
如果没有各人也可以申请国内的一些Key,例如阿里的百炼平台和DeepSeek的一些API KEY。
BotSharp简单上手
BotSharp
BotSharp 是一个开源的多智能体应用开发框架,从简单的聊天呆板人,再到多智能体协作,以及复杂的任务如【Text To Sql】框架都提供了开箱即用的使用方法,可以快速的将大模子的本领接入到现有的业务系统中,而且内置知识库和会话管理功能等。
大语言模子的函数调用(这个是理解BotSharp框架的核心知识点)
函数调用答应您将模子连接到外部工具和系统。这对于许多事情都很有用,例如为 AI 助手提供功能,或在应用步伐和模子之间构建深度集成。
openai官方文档函数调用介绍文档
运行BotSharp源码
首先准备一台安装了Visual Studio并安装了Aspire组件的电脑,电脑再安装nodejs环境用来运行前端UI项目。
克隆前后端代码到同一目次。
BotSharp C#源码克隆指令如下:- git clone https://github.com/SciSharp/BotSharp.git
复制代码 BotSharp前端UI代码克隆指令如下:- git clone https://github.com/SciSharp/BotSharp-UI.git
复制代码 命令行进入到BotSharpUI的代码目次执行npm install 安装依靠
确保前端连接的后台服务地址为当地服务
然后Visual Studio打开BotSharp办理方案,配置大模子的api key的然后启动图上的项目
详细的使用文档请查看社区的文档链接:BotSharp社区文档
ElectronBot.Standalone 项目整体代码讲解
主流程
关键词的唤醒和BotSharp嵌入的流程图如下:
图上有些功能并没有实现,但是辅助各人理解是足够的了。

图上流程图对应的代码在下图所示的HostedService类中
 - using BotSharp.Abstraction.Agents.Enums;
- using BotSharp.Abstraction.Conversations;
- using BotSharp.Abstraction.Conversations.Models;
- using BotSharp.Abstraction.Routing;
- using ElectronBot.Standalone.Core.Contracts;
- using ElectronBot.Standalone.Core.Enums;
- using ElectronBot.Standalone.Core.Models;
- using ElectronBot.Standalone.Core.Repositories;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Hosting;
- using Microsoft.Extensions.Logging;
- using NetCoreAudio;
- namespace ElectronBot.Standalone.Core.Services;
- /// <summary>
- /// A hosted service providing the primary conversation loop for Semantic Kernel with OpenAI ChatGPT.
- /// </summary>
- public class HostedService : IHostedService, IDisposable
- {
- private readonly ILogger<HostedService> _logger;
- private readonly IWakeWordListener _wakeWordListener;
- private readonly IServiceProvider _serviceProvider;
- private BotSpeechSetting _options;
- private Task? _executeTask;
- private readonly CancellationTokenSource _cancelToken = new();
- // Notification sound support
- private readonly string _notificationSoundFilePath;
- private readonly Player _player;
- private readonly IServiceScopeFactory _serviceScopeFactory;
- private readonly IBotPlayer _botPlayer;
- /// <summary>
- /// Constructor
- /// </summary>
- public HostedService(IWakeWordListener wakeWordListener,
- ILogger<HostedService> logger,
- IServiceProvider serviceProvider,
- BotSpeechSetting options,
- IServiceScopeFactory serviceScopeFactory,
- IBotPlayer botPlayer)
- {
- _logger = logger;
- _wakeWordListener = wakeWordListener;
- _notificationSoundFilePath = Path.Combine(AppContext.BaseDirectory, "Asserts", "bing.mp3");
- _player = new Player();
- _serviceProvider = serviceProvider;
- _options = options;
- _serviceScopeFactory = serviceScopeFactory;
- _botPlayer = botPlayer;
- }
- /// <summary>
- /// Start the service.
- /// </summary>
- public Task StartAsync(CancellationToken cancellationToken)
- {
- _botPlayer.ShowDateToSubScreenAsync();
- _executeTask = ExecuteAsync(_cancelToken.Token);
- return Task.CompletedTask;
- }
- /// <summary>
- /// Primary service logic loop.
- /// </summary>
- public async Task ExecuteAsync(CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- try
- {
- // Play a notification to let the user know we have started listening for the wake phrase.
- await _player.Play(_notificationSoundFilePath);
- var botSpeech = _serviceProvider.GetRequiredService<IBotSpeecher>();
- // Wait for wake word or phrase
- if (!await _wakeWordListener.WaitForWakeWordAsync(cancellationToken))
- {
- continue;
- }
- await _player.Play(_notificationSoundFilePath);
- var helloString = _options.AnswerText;
- // Say hello on startup
- await botSpeech.SpeakAsync(helloString ?? "Hello!", cancellationToken);
- // Start listening
- while (!cancellationToken.IsCancellationRequested)
- {
- // Listen to the user
- var userSpoke = await botSpeech.ListenAsync(cancellationToken);
- await _botPlayer.StopLottiePlaybackAsync();
- _logger.LogInformation($"User spoke: {userSpoke}");
- // Get a reply from the AI and add it to the chat history.
- var reply = string.Empty;
- using (var scope = _serviceScopeFactory.CreateScope())
- {
- var serviceProvider = scope.ServiceProvider;
- var repo = serviceProvider.GetRequiredService<IBraincaseRepository>();
- var setting = await repo.GetSettingAsync();
- var inputMsg = new RoleDialogModel(AgentRole.User, userSpoke)
- {
- MessageId = Guid.NewGuid().ToString(),
- CreatedAt = DateTime.UtcNow
- };
- var conversationService = serviceProvider.GetRequiredService<IConversationService>();
- var routing = serviceProvider.GetRequiredService<IRoutingService>();
- routing.Context.SetMessageId(setting.CurrentConversationId, inputMsg.MessageId);
- conversationService.SetConversationId(setting.CurrentConversationId, new());
- try
- {
- // 启动动画但不阻塞当前执行流程
- var animationTask = _botPlayer.PlayLottieByNameIdAsync("think", -1);
- // 可以选择添加异常处理
- animationTask?.ContinueWith(t =>
- {
- if (t.IsFaulted)
- {
- _logger.LogError($"Animation playback failed: {t.Exception}");
- }
- }, TaskContinuationOptions.OnlyOnFaulted);
- }
- catch (Exception ex)
- {
- _logger.LogError($"Failed to start animation: {ex.Message}");
- await _botPlayer.StopLottiePlaybackAsync();
- // 根据需要处理异常
- }
- await Task.Run(async () =>
- {
- await conversationService.SendMessage(VerdureAgentId.VerdureChatId, inputMsg,
- replyMessage: null,
- msg =>
- {
- reply = msg.Content;
- return Task.CompletedTask;
- });
- });
- await _botPlayer.StopLottiePlaybackAsync();
- // Speak the AI's reply
- await botSpeech.SpeakAsync(reply, cancellationToken);
- // If the user said "Goodbye" - stop listening and wait for the wake work again.
- if (userSpoke.StartsWith("再见") || userSpoke.StartsWith("goodbye", StringComparison.InvariantCultureIgnoreCase))
- {
- break;
- }
- }
- }
- }
- catch (Exception aiex)
- {
- await _botPlayer.StopLottiePlaybackAsync();
- _logger.LogError($"OpenAI returned an error.{aiex.Message}");
- }
- }
- }
- /// <summary>
- /// Stop a running service.
- /// </summary>
- public Task StopAsync(CancellationToken cancellationToken)
- {
- _cancelToken.Cancel();
- return Task.CompletedTask;
- }
- /// <inheritdoc/>
- public virtual void Dispose()
- {
- _cancelToken.Dispose();
- _wakeWordListener.Dispose();
- }
- }
复制代码 语音服务
微软的Azure认知服务提供了开箱即用的C#接入SDK,我们直接集成代码而且配置我们的麦克风设备和语音服务的apikey就行了。主要是要配置麦克风设备,因为在树莓派上的录音设备如果不是默认的,需要我们指定具体的设备,不然听不到声音,我配置的设备信息如下可以作为参考。
ElectronBot.Standalone 项目怎样集成BotSharp
对话呆板人项目地址如下:
https://github.com/maker-community/ElectronBot.Standalone
项目是使用Nuget包的情势集成BotSharp,宿主是WebAPI项目,当然也可以是桌面项目或者控制台项目。

项目的启动代码如下:- using BotSharp.Abstraction.Messaging.JsonConverters;
- using BotSharp.Abstraction.Repositories;
- using BotSharp.Abstraction.Users;
- using BotSharp.Core;
- using BotSharp.Logger;
- using ElectronBot.Standalone.Core.Contracts;
- using ElectronBot.Standalone.Core.Handlers;
- using ElectronBot.Standalone.Core.Models;
- using ElectronBot.Standalone.Core.Repositories;
- using ElectronBot.Standalone.Core.Services;
- using ElectronBot.Standalone.DataStorage;
- using ElectronBot.Standalone.DataStorage.Repository;
- using Verdure.Braincase.Copilot.Plugin.Services.BotSharp;
- var builder = WebApplication.CreateBuilder(args);
- // Add services to the container.
- builder.Services.AddControllers();
- // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
- builder.Services.AddEndpointsApiExplorer();
- builder.Services.AddSwaggerGen();
- var dbSettings = new BotSharpDatabaseSettings();
- builder.Configuration.Bind("Database", dbSettings);
- builder.Services.AddSingleton(dbSettings);
- var brainSettings = new BraincaseDatabaseSettings();
- builder.Configuration.Bind("Database", brainSettings);
- brainSettings.BraincaseLiteDB = "braincase.db";
- builder.Services.AddSingleton(brainSettings);
- var botSpeechSettings = new BotSpeechSetting();
- builder.Configuration.Bind("BotSpeechSetting", botSpeechSettings);
- builder.Services.AddSingleton(botSpeechSettings);
- builder.Services.AddScoped<BraincaseLiteDBContext>();
- builder.Services.AddSingleton<IBotPlayer, DefaultBotPlayer>();
- builder.Services.AddSingleton<IBotSpeecher, AzBotSpeecher>();
- builder.Services.AddSingleton<IWakeWordListener, AzCognitiveServicesWakeWordListener>();
- builder.Services.AddScoped<IBotCopilot, DefaultBotCopilot>();
- builder.Services.AddScoped<IBraincaseRepository, BraincaseRepository>();
- builder.Services.AddHostedService<HostedService>();
- builder.Services.AddBotSharpCore(builder.Configuration, options =>
- {
- options.JsonSerializerOptions.Converters.Add(new RichContentJsonConverter());
- options.JsonSerializerOptions.Converters.Add(new TemplateMessageJsonConverter());
- }).AddBotSharpLogger(builder.Configuration);
- builder.Services.AddHttpContextAccessor();
- builder.Services.AddScoped<IUserIdentity, BotUserIdentity>();
- var app = builder.Build();
- // Configure the HTTP request pipeline.
- if (app.Environment.IsDevelopment())
- {
- app.UseSwagger();
- app.UseSwaggerUI();
- }
- app.UseAuthorization();
- app.MapControllers();
- // Use BotSharp
- //app.UseBotSharp();
- // Add startup code
- app.Lifetime.ApplicationStarted.Register(async () =>
- {
- // Your startup code here
- Console.WriteLine("Application has started.");
- // Retrieve IBotCopilot from DI container
- using (var scope = app.Services.CreateScope())
- {
- var botCopilot = scope.ServiceProvider.GetRequiredService<IBotCopilot>();
- await botCopilot.InitCopilotAsync();
- }
- });
- app.Run();
复制代码 ElectronBot.Standalone 代码测试
我们配置完Azure语音服务的Key和大模子的key并设置到对应的智能体上,默认使用的是Azure AI Foundry里的gpt-4o-mini的模子,如果需要修改,请根据下图的配置进行修改。
我训练的唤醒关键词为 小娜,可以用你好小娜进行关键词唤醒。
启动项目便会将配置更新到LiteDB存储里,各人也可以直接修改数据库里的数据。
代码做了兼容,意味着在windows系统下也可以进行测试。
测试效果如下:
代码到这里就算是讲解完了,实际的效果各人可以亲自测试下。
总结感悟
写这篇文章距离上一篇隔了有一两个月了,不禁感慨AI行业发展的是真的快,之前的MCP协议还不是很火,随着智能体框架的兴起以及一些大厂的跟进,MCP也火了起来,.NET社区针对MCP的支持也迅速美满了。
说起DeepSeek,他们最近也更新了V3版本的模子,虽然是小版本号升级,但是本领提拔却很大了,之前不能胜任的一些操作现在也可以很好的支持了。我使用了BotSharp框架里比较复杂的功能测试DeepSeek V3的新模子发现效果出奇的好,国产大模子真是越来越好了。
OpenAI 也发布了新的生图功能,针对按指令生图修图完成的都很好了,说句实话,感觉如许下去,文章都不消写了,各人直接跟着AI学习就好了。
总而言之盼望各人都能进步,这篇文章也能给各人带来一些启发。
参考保举文档项目如下:
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|