前言
前段时间太忙了博客一直都没来得及更新,但是不代表我已经停止开发了,刚好最近把语音部分给调整了一下,所以就来分享一下具体的内容了。我想说一下,更新晚还是有好处的,社区已经有很多的小伙伴自己实现了一些语音对话功能的案例,比如小智也有.NET客户端了,还有就是一些树莓派对接实时语音api实现对话的功能,这些都是挺好的案例,很适合有兴趣的小伙伴来学习使用。
我做的还是比较传统的对话,通过Azure的语音服务进行关键字的训练,然后通过文本转语音和语音转文本再结合BotSharp智能体框架可以做到不集成小智服务实现对话的能力,并且拥有会话管理,提示词管理还有一些工具调用的能力。我已经迫不及待的想分享给大家了。
问题解答
为啥选择树莓派不是单片机
有朋友觉得树莓派价格贵,还说单片机就能完成开发之类的,我给大家说明下我们目前做的是针对Linux系统下的.NET的一些实践,如果觉得树莓派贵可以买一些国产的板子平替,有能力的可以自己搞库映射。至于单片机开发的事情,如果真的感兴趣单片机开发,那我们可以后期出一些文章讲这个做一些有趣的玩具。
表情播放目前的方案选择是啥
上一篇文章有讲什么是lottie动画,文章有说效果不好,但是我检查代码之后发现是代码写的有bug才导致播放很差,所以就不用转成文本的mp4了,lottie动画文件很小,适合放到板子上。
下图上面是在电脑上使用Lottie动画播放的效果。
名词解释
我是真的想让大家亲自上手试试,所以文章会讲一些基础的内容,大家不要嫌啰嗦,能看到这篇文章的小伙伴说明大家至少应该有个板子并且点亮了屏幕。
BotSharp
BotSharp 是一个开源的多智能体应用开发框架,从简单的聊天机器人,再到多智能体协作,以及复杂的任务如【Text To Sql】框架都提供了开箱即用的使用方法,可以快速的将大模型的能力接入到现有的业务系统中,并且内置知识库和会话管理功能等。
在我们的智能桌面机器人项目中,BotSharp提供会话管理和多智能体调用以及工具调用的功能。
- BotSharp官方GitHub
- BotSharp文档
Azure语音服务
Azure语音服务是微软提供的云端AI服务,支持语音转文本、文本转语音、语音翻译等功能。在我们的智能桌面机器人项目中,Azure语音服务用于将用户的语音指令转换为文本(用于理解用户意图)以及将机器人的文本回复转换为自然语音输出。此外,它还支持关键词识别功能,可用于唤醒我们的机器人。
- Azure语音服务官方文档
- .NET中使用Azure语音服务指南
- 关键词识别文档
ALSA
ALSA(Advanced Linux Sound Architecture)是Linux系统中的音频架构,提供了对声卡硬件的驱动功能和API。在树莓派等Linux设备上,ALSA是处理声音输入输出的基础系统。我们的机器人项目需要ALSA来管理麦克风输入和扬声器输出。
- ALSA项目主页
- ALSA Wiki
aplay
aplay是ALSA提供的命令行工具,用于播放音频文件。在Linux系统中,可以使用aplay命令来测试和播放各种格式的音频文件,验证扬声器是否正常工作。在我们的项目中,可以通过程序调用aplay来播放合成的语音文件。
- aplay命令手册
arecord
arecord是ALSA的录音工具,用于从麦克风或其他音频输入设备捕获音频。在树莓派上,我们可以使用arecord来录制用户的语音,然后将录制的音频文件发送给语音识别服务进行处理。
- arecord命令手册
- 使用arecord录音指南
amixer
amixer是ALSA提供的另一个命令行工具,用于控制音频混合器设置,如音量调节、输入输出设备选择等。在我们的机器人项目中,可以使用amixer来调整麦克风和扬声器的音量,确保语音交互体验良好。
- 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 supportprivate 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 phraseif (!await _wakeWordListener.WaitForWakeWordAsync(cancellationToken)){continue;}await _player.Play(_notificationSoundFilePath);var helloString = _options.AnswerText;// Say hello on startupawait botSpeech.SpeakAsync(helloString ?? "Hello!", cancellationToken);// Start listeningwhile (!cancellationToken.IsCancellationRequested){// Listen to the uservar 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 replyawait 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 hereConsole.WriteLine("Application has started.");// Retrieve IBotCopilot from DI containerusing (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学习就好了。
总而言之希望大家都能进步,这篇文章也能给大家带来一些启发。
参考推荐文档项目如下:
- 创客社区地址
- BotSharp文档
- 桌面桌面机器人仓库地址
- .NET IoT库源码地址
- .NET IoT官方地址
- .NET IoT社区人员张高兴推荐
- 什么是lottie动画?
- 参考的语音对话项目conversational-speaker
- 小智C#客户端
- C# MCP SDK
- 树莓派学习之旅(2)— 使用USB声卡进行录音
- BotSharp官方GitHub
- EdgeVoice AI 聊天机器人