diff --git a/Configurations/XAiApiConfiguration.cs b/Configurations/XAiApiConfiguration.cs index 32a2418..2153bc2 100644 --- a/Configurations/XAiApiConfiguration.cs +++ b/Configurations/XAiApiConfiguration.cs @@ -19,5 +19,10 @@ namespace xAiApi.Configurations /// OCR Configuration ... /// public XAiOCRDescriptor OCR { get; set; } + + /// + /// Transcript Configuration ... + /// + public XAiTransciptDescriptor Transcript { get; set; } } } \ No newline at end of file diff --git a/Models/XAiTransciptDescriptor.cs b/Models/XAiTransciptDescriptor.cs new file mode 100644 index 0000000..7a54bee --- /dev/null +++ b/Models/XAiTransciptDescriptor.cs @@ -0,0 +1,8 @@ +namespace xAiApi.Models +{ + public class XAiTransciptDescriptor + { + public string Language { get; set; } + public string ModelsPath { get; set; } + } +} \ No newline at end of file diff --git a/Providers/Extractors/XAudioFileContentExtractor.cs b/Providers/Extractors/XAudioFileContentExtractor.cs index 02aad55..8f6e18a 100644 --- a/Providers/Extractors/XAudioFileContentExtractor.cs +++ b/Providers/Extractors/XAudioFileContentExtractor.cs @@ -2,11 +2,15 @@ using System; using System.IO; using System.Linq; using Whisper.net; +using NAudio.Wave; using System.Threading; using xAiModels.Models; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; using System.Threading.Tasks; +using NAudio.MediaFoundation; using xAiApi.Interfaces.Extractors; -using DocumentFormat.OpenXml.EMMA; namespace xAiApi.Providers.Extractors { @@ -24,29 +28,30 @@ namespace xAiApi.Providers.Extractors "audio/mp3", "audio/wav", "audio/wave", + "audio/x-wav", "audio/ogg", "audio/m4a", "audio/mp4", + "audio/aac", + "audio/x-m4a", "audio/webm" ]; - private readonly string language; - private readonly string whisperModelPath; - - public XAudioFileContentExtractor() : this( - language: "fa", - whisperModelPath: "Models/ggml-base.bin" - ) - { } + private readonly XAiApiConfiguration configuration; public XAudioFileContentExtractor( - string whisperModelPath = "Models/ggml-base.bin", - string language = "fa" + XAiApiConfiguration configuration ) { // - this.language = language; - this.whisperModelPath = whisperModelPath; + if (configuration.Transcript.IsNullOrDefault() || + configuration.Transcript.ModelsPath.IsNullOrEmpty()) + { + XException.InvalidConfiguration.Throw(); + } + + // + this.configuration = configuration; } /// @@ -113,14 +118,15 @@ namespace xAiApi.Providers.Extractors // using var wavStream = await ConvertToWavAsync( - memoryStream, - cancellationToken + mimeType: mimeType, + inputStream: memoryStream, + cancellationToken: cancellationToken ); // - using var factory = WhisperFactory.FromPath(whisperModelPath); + using var factory = WhisperFactory.FromPath(configuration.Transcript.ModelsPath); using var processor = factory.CreateBuilder() - .WithLanguage(language) + .WithLanguage(configuration.Transcript.Language) .Build(); // @@ -143,17 +149,186 @@ namespace xAiApi.Providers.Extractors return result; } + // + #region Private ... /// - /// Audio Format Converting ... + /// Audio Format Converting to WAV 16kHz Mono 16-bit PCM ... /// private async Task ConvertToWavAsync( Stream inputStream, - CancellationToken cancellationToken + string mimeType, + CancellationToken cancellationToken = default ) { // - // TODO: Complete this ... - return await Task.FromResult(inputStream); + var result = await Task.Run(() => + { + // + var memoryStream = new MemoryStream(); + inputStream.CopyTo(memoryStream); + memoryStream.Position = 0; + + // + var targetFormat = new WaveFormat(16000, 16, 1); + string tempFilePath = null; + + // + try + { + // + WaveStream reader = GetAudioReader(memoryStream, mimeType, ref tempFilePath); + if (reader == null) + { + // + XException.NotAllowed.Throw( + $"Unsupported audio format: {mimeType}" + ); + } + + // + var needsConversion = + reader.WaveFormat.SampleRate != 16000 || + reader.WaveFormat.Channels != 1 || + reader.WaveFormat.BitsPerSample != 16 || + reader.WaveFormat.Encoding != WaveFormatEncoding.Pcm; + var outputStream = new MemoryStream(); + if (!needsConversion) + { + // + reader.CopyTo(outputStream); + reader.Dispose(); + } + else + { + // + MediaFoundationApi.Startup(); + using var resampler = new MediaFoundationResampler( + reader, + targetFormat + ); + resampler.ResamplerQuality = 60; + + // + WaveFileWriter.WriteWavFileToStream(outputStream, resampler); + reader.Dispose(); + } + + // + outputStream.Position = 0; + return outputStream; + } + finally + { + // + if (!string.IsNullOrEmpty(tempFilePath) && + File.Exists(tempFilePath)) + { + // + try + { + File.Delete(tempFilePath); + } + catch { } + } + + // + memoryStream.Dispose(); + } + }, cancellationToken); + + // + return result; } + + /// + /// Get appropriate WaveStream reader based on MIME type ... + /// + /// Input stream (MemoryStream) + /// MIME type of audio file + /// Path to temp file (if created) + /// WaveStream reader or null if unsupported + private WaveStream GetAudioReader( + MemoryStream stream, + string mimeType, + ref string tempFilePath + ) + { + // + try + { + // + var normalizedMime = mimeType?.ToLowerInvariant() ?? string.Empty; + switch (normalizedMime) + { + // + case "audio/wav": + case "audio/wave": + case "audio/x-wav": + return new WaveFileReader(stream); + + // + case "audio/mpeg": + case "audio/mp3": + return new Mp3FileReader(stream); + + // + case "audio/m4a": + case "audio/mp4": + case "audio/aac": + case "audio/ogg": + case "audio/webm": + case "audio/x-m4a": + // + var extension = normalizedMime switch + { + "audio/m4a" or "audio/mp4" or "audio/aac" or "audio/x-m4a" => ".m4a", + "audio/ogg" => ".ogg", + "audio/webm" => ".webm", + _ => ".tmp" + }; + tempFilePath = Path.Combine( + Path.GetTempPath(), + $"audio_{Guid.NewGuid()}{extension}" + ); + + // + stream.Position = 0; + using (var fileStream = File.Create(tempFilePath)) + { + stream.CopyTo(fileStream); + } + + // + MediaFoundationApi.Startup(); + return new MediaFoundationReader(tempFilePath); + + default: + // + tempFilePath = Path.Combine( + Path.GetTempPath(), + $"audio_{Guid.NewGuid()}.tmp" + ); + stream.Position = 0; + using (var fileStream = File.Create(tempFilePath)) + { + stream.CopyTo(fileStream); + } + try + { + MediaFoundationApi.Startup(); + return new MediaFoundationReader(tempFilePath); + } + catch + { + return null; + } + } + } + catch (Exception) + { + return null; + } + } + #endregion } } \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json index cfd8ad5..ea92863 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -84,6 +84,10 @@ "EngineMode": "LstmOnly", "EnableOcrFallback": true, "DefaultLanguage": "fas+eng" + }, + "Transcript": { + "Language": "fa", + "ModelsPath": "Models/ggml-base.bin" } }, "Logging": { diff --git a/xAiApi.csproj b/xAiApi.csproj index 2e72bf9..71e53bb 100644 --- a/xAiApi.csproj +++ b/xAiApi.csproj @@ -27,13 +27,6 @@ - - - @@ -53,22 +46,22 @@ - - - + + - - - - - - - - - + + + + + + + + + +