基于语音识别的智能电子病历(三)Teams BOT 机器人
Microsoft Teams 是一款基于聊天的智能团队协作工具,可以同步进行文档共享,并为成员提供包括语音、视频会议在内的即时通讯工具。(其实就是 微信或者钉钉)Microsoft Teams Bot是一种基于Microsoft Teams平台的机器人,它可以通过提及通道来启动新线程。当在Microsoft Teams中提及该机器人时,它会自动创建一个新的对话线程,以便用户可以在该线程中与机器人
背景
Microsoft Teams 是一款基于聊天的智能团队协作工具,可以同步进行文档共享,并为成员提供包括语音、视频会议在内的即时通讯工具。(其实就是 微信或者钉钉)
Microsoft Teams Bot是一种基于Microsoft Teams平台的机器人,它可以通过提及通道来启动新线程。当在Microsoft Teams中提及该机器人时,它会自动创建一个新的对话线程,以便用户可以在该线程中与机器人进行交互。(其实就是个应用)
那么Microsoft Teams Bot 和 电子病历是如何联系在一起的呢?
在医院有很多个医疗小组。每个小组又医生、护士、药师、实习生等人员组成。这些小组会使用Microsoft Teams 作为联系工具。小组往往会开一个Teams 会议,一直开着从早上到下班,有事就在会议里说。我们就开发了Teams BOT 机器人,医疗小组的成员在加入会议时,可以把Teams BOT 机器人加入到会议中。 机器人有以下3种功能:
1、实时的语音识别,将音频识别成文字。
2、音频录音,可以回放之前的音频。
3、将指定的音频内容,添加到电子病历中。
准备工作
先 github, https://microsoftgraph.github.io/microsoft-graph-comms-samples/docs/
Graph Communications Calling SDK (microsoftgraph.github.io)

文档中有详细的说明
上线一个BOT,有很多很多设置,比较繁琐,还要注意各种权限的设置。
代码
在Github 的例子里有完整的代码。可以先看相关的例子。
建议先从 RecordingBot.Tests中的代码入手,例如MediaTest\MediaStreamTest.cs
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Graph.Communications.Common.Telemetry;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using NUnit.Framework;
using RecordingBot.Services.Media;
using RecordingBot.Services.ServiceSetup;
using RecordingBot.Services.Util;
using RecordingBot.Tests.Helper;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace RecordingBot.Tests.MediaTest
{
/// <summary>
/// Defines test class MediaStreamTest.
/// Implements the <see cref="RecordingBot.Tests.TestBase" />
/// </summary>
/// <seealso cref="RecordingBot.Tests.TestBase" />
[TestFixture]
public class MediaStreamTest : TestBase
{
/// <summary>
/// The media stream
/// </summary>
private MediaStream _mediaStream;
/// <summary>
/// The settings
/// </summary>
private AzureSettings _settings;
/// <summary>
/// Sets up.
/// </summary>
[SetUp]
public void SetUp()
{
_settings = new AzureSettings
{
CaptureEvents = false,
MediaFolder = "archive",
IsStereo = false,
AudioSettings = new AudioSettings
{
WavSettings = null
},
};
var logger = new Mock<IGraphLogger>();
_mediaStream = new MediaStream(_settings, logger.Object, Guid.NewGuid().ToString(),"8");
}
/// <summary>
/// Defines the test method TestReplayStreamContentMatches.
/// </summary>
[Test]
public async Task TestReplayStreamContentMatches()
{
using (var fs = File.OpenRead(Path.Combine("TestData", "recording.zip")))
{
using (var zipInputStream = new ZipInputStream(fs))
{
while (zipInputStream.GetNextEntry() is ZipEntry zipEntry)
{
var temp = new byte[4096];
var ms = new MemoryStream();
StreamUtils.Copy(zipInputStream, ms, temp);
ms.Position = 0;
using (var bson = new BsonDataReader(ms))
{
var e = new JsonSerializer().Deserialize<SerializableAudioMediaBuffer>(bson);
var d = new DeserializeAudioMediaBuffer(e);
var p = new DeserializeParticipant().GetParticipant(e);
Assert.AreEqual(e.IsSilence, d.IsSilence);
Assert.AreEqual(e.Length, d.Length);
Assert.AreEqual(e.Timestamp, d.Timestamp);
Assert.AreEqual(e.ActiveSpeakers, d.ActiveSpeakers);
Assert.AreEqual(e.SerializableUnmixedAudioBuffers?.Length, d.UnmixedAudioBuffers?.Length);
Assert.That((d.Data != IntPtr.Zero && e.Buffer != null) || (d.Data == IntPtr.Zero && e.Buffer == null));
if (d.Data != IntPtr.Zero && e.Buffer != null)
{
var buffer = new byte[d.Length];
Marshal.Copy(d.Data, buffer, 0, (int)d.Length);
Assert.AreEqual(e.Buffer, buffer);
}
for (int i = 0; i < e.SerializableUnmixedAudioBuffers?.Length; i++)
{
Assert.AreEqual(e.SerializableUnmixedAudioBuffers.Length, d.UnmixedAudioBuffers.Length);
Assert.AreEqual(e.SerializableUnmixedAudioBuffers.Length, p.Count);
var source = e.SerializableUnmixedAudioBuffers[i];
var actual = d.UnmixedAudioBuffers[i];
var participant = p[i].Resource.Info.Identity.User;
Assert.AreEqual(source.ActiveSpeakerId, actual.ActiveSpeakerId);
Assert.AreEqual(source.Length, actual.Length);
Assert.AreEqual(source.OriginalSenderTimestamp, actual.OriginalSenderTimestamp);
var buffer = new byte[actual.Length];
Marshal.Copy(actual.Data, buffer, 0, (int)actual.Length);
Assert.AreEqual(source.Buffer, buffer);
Assert.AreEqual(source.DisplayName, participant.DisplayName);
Assert.AreEqual(source.AdditionalData, participant.AdditionalData);
Assert.AreEqual(source.AdId, participant.Id);
Assert.That(p[i].Resource.MediaStreams.Any(x => x.SourceId == source.ActiveSpeakerId.ToString()));
Assert.IsFalse(p[i].Resource.IsInLobby);
}
await _mediaStream.AppendAudioBuffer(d, p);
}
}
}
}
await _mediaStream.End();
}
/// <summary>
/// Defines the test method TestPrepareZip.
/// </summary>
[Test]
public async Task TestPrepareZip()
{
var userIds = new List<string>();
var currentAudioProcessor = new AudioProcessor(_settings,"");
foreach (var (zipEntry, zipInputStream) in ZipUtils.GetEntries(Path.Combine("TestData", "recording.zip")))
{
var temp = new byte[4096];
var ms = new MemoryStream();
StreamUtils.Copy(zipInputStream, ms, temp);
ms.Position = 0;
using (var bson = new BsonDataReader(ms))
{
var e = new JsonSerializer().Deserialize<SerializableAudioMediaBuffer>(bson);
var d = new DeserializeAudioMediaBuffer(e);
var p = new DeserializeParticipant().GetParticipant(e);
Assert.That((d.Data != IntPtr.Zero && e.Buffer != null) || (d.Data == IntPtr.Zero && e.Buffer == null));
Assert.AreEqual(e.IsSilence, d.IsSilence);
Assert.AreEqual(e.Length, d.Length);
Assert.AreEqual(e.Timestamp, d.Timestamp);
Assert.AreEqual(e.ActiveSpeakers, d.ActiveSpeakers);
Assert.AreEqual(e.SerializableUnmixedAudioBuffers?.Length, d.UnmixedAudioBuffers?.Length);
if (d.Data != IntPtr.Zero && e.Buffer != null)
{
var buffer = new byte[d.Length];
Marshal.Copy(d.Data, buffer, 0, (int)d.Length);
Assert.AreEqual(e.Buffer, buffer);
}
for (int i = 0; i < e.SerializableUnmixedAudioBuffers?.Length; i++)
{
Assert.AreEqual(e.SerializableUnmixedAudioBuffers.Length, d.UnmixedAudioBuffers.Length);
Assert.AreEqual(e.SerializableUnmixedAudioBuffers.Length, p.Count);
var source = e.SerializableUnmixedAudioBuffers[i];
var actual = d.UnmixedAudioBuffers[i];
var participant = p[i].Resource.Info.Identity.User;
Assert.AreEqual(source.ActiveSpeakerId, actual.ActiveSpeakerId);
Assert.AreEqual(source.Length, actual.Length);
Assert.AreEqual(source.OriginalSenderTimestamp, actual.OriginalSenderTimestamp);
var buffer = new byte[actual.Length];
Marshal.Copy(actual.Data, buffer, 0, (int)actual.Length);
Assert.AreEqual(source.Buffer, buffer);
Assert.AreEqual(source.DisplayName, participant.DisplayName);
Assert.AreEqual(source.AdditionalData, participant.AdditionalData);
Assert.AreEqual(source.AdId, participant.Id);
Assert.That(p[i].Resource.MediaStreams.Any(x => x.SourceId == source.ActiveSpeakerId.ToString()));
Assert.IsFalse(p[i].Resource.IsInLobby);
if (!userIds.Contains(source.AdId) && source.AdId != null)
{
userIds.Add(source.AdId);
}
}
await currentAudioProcessor.Append(e);
}
}
var lastZip = await currentAudioProcessor.Finalise();
var lastFileInfo = new FileInfo(lastZip);
var fileNames = getfileNames(lastZip).ToList();
foreach (var userId in userIds)
{
var match = fileNames.FirstOrDefault(_ => _.Contains(userId));
Assert.NotNull(match);
}
lastFileInfo.Directory?.Delete(true);
}
/// <summary>
/// Getfiles the names.
/// </summary>
/// <param name="zipFile">The zip file.</param>
/// <returns>IEnumerable<System.String>.</returns>
IEnumerable<string> getfileNames(string zipFile)
{
foreach (var file in ZipUtils.GetEntries(zipFile))
{
yield return file.Item1.Name;
}
}
}
}
AudioProcessor是核心,可以在这里增加功能。这里需要的注意的是
protected override async Task Process(SerializableAudioMediaBuffer data)中有 混音的内容,也有未混音的内容。
using NAudio.Wave;
using RecordingBot.Model.Constants;
using RecordingBot.Services.Contract;
using RecordingBot.Services.ServiceSetup;
using RecordingBot.Services.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
namespace RecordingBot.Services.Media
{
/// <summary>
/// Class AudioProcessor.
/// Implements the <see cref="RecordingBot.Services.Util.BufferBase{RecordingBot.Services.Media.SerializableAudioMediaBuffer}" />
/// </summary>
/// <seealso cref="RecordingBot.Services.Util.BufferBase{RecordingBot.Services.Media.SerializableAudioMediaBuffer}" />
public class AudioProcessor : BufferBase<SerializableAudioMediaBuffer>
{
/// <summary>
/// The writers
/// </summary>
readonly Dictionary<string, WaveFileWriter> _writers = new Dictionary<string, WaveFileWriter>();
/// <summary>
/// The processor identifier
/// </summary>
private readonly string _processorId = null;
private readonly string _callId = "";
/// <summary>
/// The settings
/// </summary>
private readonly AzureSettings _settings;
/// <summary>
/// Initializes a new instance of the <see cref="AudioProcessor" /> class.
/// </summary>
/// <param name="settings">The settings.</param>
public AudioProcessor(IAzureSettings settings,string callId)
{
_processorId = Guid.NewGuid().ToString();
_callId = callId;
_settings = (AzureSettings)settings;
}
/// <summary>
/// Processes the specified data.
/// </summary>
/// <param name="data">The data.</param>
protected override async Task Process(SerializableAudioMediaBuffer data)
{
if (data.Timestamp == 0)
{
return;
}
var path = Path.Combine(RecordingBot.Services.Media.BotOption.StoreDirectory, BotConstants.DefaultOutputFolder, _settings.MediaFolder, _processorId);
// First, write all audio buffer, unless the data.IsSilence is checked for true, into the all speakers buffer
var all = "all";
var all_writer = _writers.ContainsKey(all) ? _writers[all] : InitialiseWavFileWriter(path, all);
if (data.Buffer != null)
{
// Buffers are saved to disk even when there is silence.
// If you do not want this to happen, check if data.IsSilence == true.
await all_writer.WriteAsync(data.Buffer, 0, data.Buffer.Length).ConfigureAwait(false);
}
if (data.SerializableUnmixedAudioBuffers != null)
{
foreach (var s in data.SerializableUnmixedAudioBuffers)
{
// Write audio buffer into the WAV file for all speakers
await all_writer.WriteAsync(s.Buffer, 0, s.Buffer.Length).ConfigureAwait(false);
///2023-02-14
if (string.IsNullOrWhiteSpace(s.AdId) || string.IsNullOrWhiteSpace(s.DisplayName))
{
continue;
}
var id = s.AdId;
var writer = _writers.ContainsKey(id) ? _writers[id] : InitialiseWavFileWriter(path, id);
// Write audio buffer into the WAV file for individual speaker
await writer.WriteAsync(s.Buffer, 0, s.Buffer.Length).ConfigureAwait(false);
}
}
}
/// <summary>
/// Initialises the wav file writer.
/// </summary>
/// <param name="rootFolder">The root folder.</param>
/// <param name="id">The identifier.</param>
/// <returns>WavFileWriter.</returns>
private WaveFileWriter InitialiseWavFileWriter(string rootFolder, string id)
{
var path = AudioFileUtils.CreateFilePath(rootFolder, $"{id}.wav");
// Initialize the Wave Format using the default PCM 16bit 16K supported by Teams audio settings
var writer = new WaveFileWriter(path, new WaveFormat(
rate: AudioConstants.DefaultSampleRate,
bits: AudioConstants.DefaultBits,
channels: AudioConstants.DefaultChannels));
_writers.Add(id, writer);
return writer;
}
/// <summary>
/// Finalises the wav writing and returns a list of all the files created
/// </summary>
/// <returns>System.String.</returns>
public async Task<string> Finalise()
{
//drain the un-processed buffers on this object
while (Buffer.Count > 0)
{
await Task.Delay(200);
}
var archiveFile = Path.Combine(RecordingBot.Services.Media.BotOption.StoreDirectory, BotConstants.DefaultOutputFolder, _settings.MediaFolder, _processorId, $"{Guid.NewGuid()}.zip");
try
{
using (var stream = File.OpenWrite(archiveFile))
{
using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create))
{
// drain all the writers
foreach (var writer in _writers.Values)
{
var localFiles = new List<string>();
var localArchive = archive; //protect the closure below
var localFileName = writer.Filename;
localFiles.Add(writer.Filename);
await writer.FlushAsync();
writer.Dispose();
// Is Resampling and/or mono to stereo conversion required?
if (_settings.AudioSettings.WavSettings != null)
{
// The resampling is required
localFiles.Add(AudioFileUtils.ResampleAudio(localFileName, _settings.AudioSettings.WavSettings, _settings.IsStereo));
}
else if (_settings.IsStereo) // Is Stereo audio required?
{
// Convert mono WAV to stereo
localFiles.Add(AudioFileUtils.ConvertToStereo(localFileName));
}
// Remove temporary saved local WAV file from the disk
foreach (var localFile in localFiles)
{
await Task.Run(() =>
{
var fInfo = new FileInfo(localFile);
localArchive.CreateEntryFromFile(localFile, fInfo.Name, CompressionLevel.Optimal);
File.Delete(localFile);
}).ConfigureAwait(false);
}
}
}
}
}
finally
{
await End();
}
return archiveFile;
}
}
}
更多推荐




所有评论(0)