背景

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&lt;System.String&gt;.</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;
        }
    }
}

Logo

一站式 AI 云服务平台

更多推荐