diff --git a/Documents/Docs/Developer/15_AudioFileContentExtractor.html b/Documents/Docs/Developer/15_AudioFileContentExtractor.html new file mode 100644 index 0000000..26a6e69 --- /dev/null +++ b/Documents/Docs/Developer/15_AudioFileContentExtractor.html @@ -0,0 +1,701 @@ + + + + + +تکمیل سرویس XAudioFileContentExtractor - فن آوران ساحر علم + + + +
+ +
+

🎙️ تکمیل سرویس XAudioFileContentExtractor

+
پیاده‌سازی کامل تبدیل صوت به متن با Whisper و NAudio
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: یکشنبه ۱۳ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🔍 تحلیل وضعیت فعلی

+

در نسخه فعلی XAudioFileContentExtractor، متد ConvertToWavAsync به صورت TODO رها شده و صرفاً استریم ورودی را بدون هیچ تبدیلی برمی‌گرداند:

+ +
// ❌ کد فعلی - ناقص
+private async Task<Stream> ConvertToWavAsync(
+    Stream inputStream,
+    CancellationToken cancellationToken
+)
+{
+    // TODO: Complete this ...
+    return await Task.FromResult(inputStream);
+}
+ +
+ ⚠️ مشکل: مدل Whisper فقط فرمت WAV 16kHz Mono PCM 16-bit را قبول می‌کند. اگر فایل ورودی MP3، M4A، OGG یا هر فرمت دیگری باشد، Whisper خطا می‌دهد یا خروجی نادرست تولید می‌کند. +
+ +

فرمت‌های پشتیبانی شده و وضعیت فعلی:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
فرمتMIME Typeوضعیت فعلینیاز به تبدیل
MP3audio/mpeg❌ بدون تبدیل✅ بله
WAVaudio/wav⚠️ ممکن است نیاز به resample داشته باشد⚠️ شاید
OGGaudio/ogg❌ بدون تبدیل✅ بله
M4Aaudio/m4a❌ بدون تبدیل✅ بله
MP4 Audioaudio/mp4❌ بدون تبدیل✅ بله
WebM Audioaudio/webm❌ بدون تبدیل✅ بله
+
+ + +
+

📦 گام ۱: نصب پکیج‌های NuGet

+

برای تبدیل فرمت‌های صوتی به WAV 16kHz، از کتابخانه NAudio استفاده می‌کنیم:

+ +
# در Package Manager Console:
+Install-Package NAudio
+
+# یا در .NET CLI:
+dotnet add package NAudio
+ +
+ 💡 چرا NAudio؟ +
    +
  • پشتیبانی از MP3, WAV, AIFF و فرمت‌های Windows Media
  • +
  • قابلیت Resampling به هر نرخ نمونه‌برداری
  • +
  • تبدیل Stereo به Mono
  • +
  • استفاده از Media Foundation ویندوز برای فرمت‌های M4A, WMA, OGG
  • +
  • بدون نیاز به نصب نرم‌افزار جانبی
  • +
+
+
+ + +
+

🛠️ گام ۲: کد کامل XAudioFileContentExtractor

+ +
+ MODIFY + xAiApi/Providers/Extractors/XAudioFileContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Linq;
+using NAudio.Wave;
+using Whisper.net;
+using System.Threading;
+using xAiModels.Models;
+using NAudio.MediaFoundation;
+using System.Threading.Tasks;
+using xAiApi.Interfaces.Extractors;
+
+namespace xAiApi.Providers.Extractors
+{
+    /// <summary>
+    /// Extracts text content from Audio files using Whisper ...
+    /// </summary>
+    public class XAudioFileContentExtractor : IXAudioFileContentExtractor
+    {
+        /// <summary>
+        /// Supported MIME Types ...
+        /// </summary>
+        private static readonly string[] SupportedMimeTypes =
+        [
+            "audio/mpeg",
+            "audio/mp3",
+            "audio/wav",
+            "audio/wave",
+            "audio/ogg",
+            "audio/m4a",
+            "audio/mp4",
+            "audio/webm"
+        ];
+
+        private readonly string language;
+        private readonly string whisperModelPath;
+
+        public XAudioFileContentExtractor() : this(
+            language: "fa",
+            whisperModelPath: "Models/ggml-base.bin"
+        )
+        { }
+
+        public XAudioFileContentExtractor(
+            string whisperModelPath = "Models/ggml-base.bin",
+            string language = "fa"
+        )
+        {
+            this.language = language;
+            this.whisperModelPath = whisperModelPath;
+        }
+
+        /// <summary>
+        /// Check if this extractor supports the specified MIME type ...
+        /// </summary>
+        public bool CanExtract(string mimeType)
+        {
+            return SupportedMimeTypes.Contains(
+                mimeType?.ToLowerInvariant() ?? string.Empty
+            );
+        }
+
+        /// <summary>
+        /// Extract text content from file stream ...
+        /// </summary>
+        public async Task<string> ExtractAsync(
+            Stream fileStream,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = await ExtractRichAsync(
+                fileStream,
+                "audio",
+                mimeType,
+                cancellationToken
+            );
+            return result.AudioTranscript;
+        }
+
+        /// <summary>
+        /// Extract content from stream as Rich Result ...
+        /// </summary>
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType
+            };
+
+            try
+            {
+                // ۱. تبدیل فرمت صوتی به WAV 16kHz Mono
+                using var wavStream = await ConvertToWavAsync(
+                    fileStream,
+                    mimeType,
+                    cancellationToken
+                );
+
+                // ۲. بررسی وجود مدل Whisper
+                if (!File.Exists(whisperModelPath))
+                {
+                    result.ErrorMessage =
+                        $"Whisper model not found at: {whisperModelPath}. " +
+                        "Please download from https://huggingface.co/ggerganov/whisper.cpp/tree/main";
+                    return result;
+                }
+
+                // ۳. انجام Speech-to-Text با Whisper
+                using var factory = WhisperFactory.FromPath(whisperModelPath);
+                using var processor = factory.CreateBuilder()
+                    .WithLanguage(language)
+                    .Build();
+
+                var segments = new System.Text.StringBuilder();
+                await foreach (var segment in processor.ProcessAsync(
+                    wavStream, 
+                    cancellationToken))
+                {
+                    segments.Append(segment.Text);
+                }
+
+                result.AudioTranscript = segments.ToString().Trim();
+                result.Text = result.AudioTranscript;
+            }
+            catch (OperationCanceledException)
+            {
+                throw;
+            }
+            catch (Exception ex)
+            {
+                result.ErrorMessage = $"Audio extraction failed: {ex.Message}";
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Convert any audio format to WAV 16kHz Mono 16-bit PCM
+        /// (required format for Whisper) ...
+        /// </summary>
+        private async Task<Stream> ConvertToWavAsync(
+            Stream inputStream,
+            string mimeType,
+            CancellationToken cancellationToken
+        )
+        {
+            return await Task.Run(() =>
+            {
+                // کپی به MemoryStream برای NAudio (نیاز به Seekable Stream)
+                var memoryStream = new MemoryStream();
+                inputStream.CopyTo(memoryStream);
+                memoryStream.Position = 0;
+
+                // فرمت هدف: 16kHz, 16-bit, Mono (الزامی برای Whisper)
+                var targetFormat = new WaveFormat(16000, 16, 1);
+
+                // خواندن فایل صوتی بر اساس فرمت
+                WaveStream reader = GetAudioReader(memoryStream, mimeType);
+
+                if (reader == null)
+                {
+                    // Fallback: تلاش با MediaFoundationReader برای فرمت‌های ناشناخته
+                    try
+                    {
+                        memoryStream.Position = 0;
+                        MediaFoundationApi.Startup();
+                        reader = new MediaFoundationReader(memoryStream);
+                    }
+                    catch
+                    {
+                        memoryStream.Position = 0;
+                        return memoryStream;
+                    }
+                }
+
+                // بررسی آیا تبدیل لازم است یا خیر
+                var needsConversion =
+                    reader.WaveFormat.SampleRate != 16000 ||
+                    reader.WaveFormat.Channels != 1 ||
+                    reader.WaveFormat.BitsPerSample != 16 ||
+                    reader.WaveFormat.Encoding != WaveFormatEncoding.Pcm;
+
+                if (!needsConversion)
+                {
+                    // فرمت صحیح است، نیازی به تبدیل نیست
+                    reader.Dispose();
+                    memoryStream.Position = 0;
+                    return memoryStream;
+                }
+
+                // تبدیل فرمت با Resampler
+                var outputStream = new MemoryStream();
+                try
+                {
+                    MediaFoundationApi.Startup();
+                    using var resampler = new MediaFoundationResampler(
+                        reader, 
+                        targetFormat
+                    );
+                    resampler.ResamplerQuality = 60;
+
+                    WaveFileWriter.WriteWavFileToStream(outputStream, resampler);
+                    outputStream.Position = 0;
+
+                    reader.Dispose();
+                    memoryStream.Dispose();
+                }
+                catch
+                {
+                    // در صورت خطا در Resampler، استریم اصلی را برگردان
+                    outputStream.Dispose();
+                    reader.Dispose();
+                    memoryStream.Position = 0;
+                    return memoryStream;
+                }
+
+                return outputStream;
+            }, cancellationToken);
+        }
+
+        /// <summary>
+        /// Get appropriate WaveStream reader based on MIME type ...
+        /// </summary>
+        private WaveStream GetAudioReader(
+            MemoryStream stream,
+            string mimeType
+        )
+        {
+            try
+            {
+                var normalizedMime = mimeType?.ToLowerInvariant() ?? string.Empty;
+
+                switch (normalizedMime)
+                {
+                    case "audio/mpeg":
+                    case "audio/mp3":
+                        return new Mp3FileReader(stream);
+
+                    case "audio/wav":
+                    case "audio/wave":
+                        return new WaveFileReader(stream);
+
+                    case "audio/ogg":
+                    case "audio/m4a":
+                    case "audio/mp4":
+                    case "audio/webm":
+                        // استفاده از MediaFoundation برای فرمت‌های پیشرفته
+                        MediaFoundationApi.Startup();
+                        return new MediaFoundationReader(stream);
+
+                    default:
+                        return null;
+                }
+            }
+            catch
+            {
+                return null;
+            }
+        }
+    }
+}
+
+ + +
+

📥 گام ۳: آماده‌سازی مدل Whisper

+ +

۳.۱. دانلود مدل:

+

مدل‌های Whisper را از لینک زیر دانلود کنید:

+

https://huggingface.co/ggerganov/whisper.cpp/tree/main

+ +

۳.۲. مدل‌های پیشنهادی:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
مدلحجمسرعتدقت فارسیکاربرد
ggml-tiny.bin~75 MB⭐⭐⭐⭐⭐⭐⭐تست و توسعه
ggml-base.bin~142 MB⭐⭐⭐⭐⭐⭐⭐استفاده عمومی (پیش‌فرض)
ggml-small.bin~466 MB⭐⭐⭐⭐⭐⭐⭐دقت بالاتر
ggml-medium.bin~1.5 GB⭐⭐⭐⭐⭐⭐⭐Production
ggml-large-v3.bin~3 GB⭐⭐⭐⭐⭐⭐حداکثر دقت
+ +

۳.۳. ساختار پوشه‌ها:

+
xAiApi/
+└── Models/
+    ├── ggml-base.bin      ← پیش‌فرض
+    └── ggml-small.bin     ← اختیاری (دقت بالاتر)
+
+ + +
+

⚙️ گام ۴: پیکربندی appsettings.json

+ +
{
+  "AiApiConfiguration": {
+    "Models": [ ... ],
+    "Prompts": [ ... ],
+    "OCR": {
+      "DataPath": "tessdata",
+      "EngineMode": "LstmOnly",
+      "EnableOcrFallback": true,
+      "DefaultLanguage": "fas+eng"
+    },
+    "Audio": {
+      "WhisperModelPath": "Models/ggml-base.bin",
+      "Language": "fa"
+    }
+  }
+}
+ +
+ 💡 نکته: اگر از پیکربندی Audio استفاده می‌کنید، باید Constructor کلاس XAudioFileContentExtractor را به صورت Factory در Startup.cs ثبت کنید تا مقادیر از IConfiguration خوانده شوند. +
+
+ + +
+

📋 خلاصه تغییرات

+ + + + + + + + + + + + + + + + + + + + + + +
فایلتغییرتوضیح
XAudioFileContentExtractor.csMODIFYپیاده‌سازی کامل ConvertToWavAsync با NAudio
NuGet PackagesINSTALLنصب NAudio
Models/ADDدانلود مدل ggml-base.bin از HuggingFace
+ +

ویژگی‌های کلیدی پیاده‌سازی:

+
+
+

🔄 تبدیل فرمت خودکار

+
    +
  • MP3, OGG, M4A, WebM → WAV 16kHz
  • +
  • استفاده از MediaFoundationResampler
  • +
  • تبدیل Stereo به Mono
  • +
+
+
+

⚡ بهینه‌سازی عملکرد

+
    +
  • بررسی نیاز به تبدیل قبل از پردازش
  • +
  • رد کردن تبدیل اگر فرمت صحیح باشد
  • +
  • اجرای async در Thread جداگانه
  • +
+
+
+

🛡️ مدیریت خطا

+
    +
  • بررسی وجود مدل Whisper
  • +
  • Fallback در صورت خطای Resampler
  • +
  • پشتیبانی از CancellationToken
  • +
+
+
+

🌐 پشتیبانی چند فرمتی

+
    +
  • Mp3FileReader برای MP3
  • +
  • WaveFileReader برای WAV
  • +
  • MediaFoundationReader برای M4A/OGG/WebM
  • +
+
+
+ +
+ ✅ نتیجه نهایی: +

سرویس XAudioFileContentExtractor اکنون به صورت کامل قادر است:

+
    +
  • هر فرمت صوتی رایج را به WAV 16kHz Mono تبدیل کند
  • +
  • متن فارسی و انگلیسی را از فایل صوتی استخراج کند
  • +
  • بدون نیاز به نرم‌افزار جانبی (ffmpeg و غیره) کار کند
  • +
  • در محیط Production با اطمینان عمل کند
  • +
+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/15_AudioFileContentExtractor_1.html b/Documents/Docs/Developer/15_AudioFileContentExtractor_1.html new file mode 100644 index 0000000..f861f2f --- /dev/null +++ b/Documents/Docs/Developer/15_AudioFileContentExtractor_1.html @@ -0,0 +1,712 @@ + + + + + +تکمیل سرویس XAudioFileContentExtractor - فن آوران ساحر علم + + + +
+ +
+

🎙️ تکمیل سرویس XAudioFileContentExtractor

+
پیاده‌سازی کامل تبدیل صوت به متن با Whisper و NAudio
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: یکشنبه ۱۳ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🔍 گام ۱: تحلیل وضعیت فعلی

+

با بررسی آخرین نسخه کد XAudioFileContentExtractor، موارد زیر شناسایی شد:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
بخشوضعیتتوضیح
SupportedMimeTypes✅ کاملپشتیبانی از MP3, WAV, OGG, M4A, WebM
CanExtract()✅ کاملتشخیص صحیح MIME Type
ExtractAsync()✅ کاملDelegate به ExtractRichAsync
ExtractRichAsync()⚠️ نیمه‌کارهساختار اصلی وجود دارد ولی نیاز به تکمیل دارد
ConvertToWavAsync()❌ خالی (TODO)فقط placeholder است و باید پیاده‌سازی شود
ILogger❌ موجود نیستنیاز به تزریق Logger برای ثبت وقایع
+
+ + +
+

📦 گام ۲: نصب پکیج‌های NuGet

+
# پکیج اصلی Whisper برای Speech-to-Text
+Install-Package Whisper.net
+
+# Runtime مورد نیاز Whisper (برای CPU)
+Install-Package Whisper.net.Runtime
+
+# کتابخانه NAudio برای تبدیل فرمت صوتی
+Install-Package NAudio
+ +
+ 💡 نکته: اگر از GPU NVIDIA استفاده می‌کنید، به جای Whisper.net.Runtime پکیج Whisper.net.Runtime.Cuda را نصب کنید تا سرعت پردازش چندین برابر شود. +
+
+ + +
+

📥 گام ۳: دانلود مدل Whisper

+

مدل‌های Whisper در اندازه‌های مختلف موجود هستند. برای زبان فارسی، مدل base یا small پیشنهاد می‌شود:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
مدلحجمدقت فارسیسرعتVRAM مورد نیاز
ggml-tiny.bin~75 MB⭐⭐⭐⭐⭐⭐⭐~1 GB
ggml-base.bin~142 MB⭐⭐⭐⭐⭐⭐⭐~1 GB
ggml-small.bin~466 MB⭐⭐⭐⭐⭐⭐⭐~2 GB
ggml-medium.bin~1.5 GB⭐⭐⭐⭐⭐⭐⭐~5 GB
+ +

دانلود از:

+
# لینک مستقیم دانلود مدل base:
+https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin
+
+# قرار دادن در مسیر:
+xAiApi/Models/ggml-base.bin
+
+ + +
+

⚙️ گام ۴: پیکربندی appsettings.json

+
{
+  "AiApiConfiguration": {
+    "FileExtraction": {
+      "Audio": {
+        "WhisperModelPath": "Models/ggml-base.bin",
+        "DefaultLanguage": "fa"
+      }
+    }
+  }
+}
+
+ + +
+

🛠️ گام ۵: کد کامل XAudioFileContentExtractor

+ +
+ MODIFY + xAiApi/Providers/Extractors/XAudioFileContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using NAudio.Wave;
+using NAudio.Wave.SampleProviders;
+using Whisper.net;
+using System.Threading;
+using xAiModels.Models;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using xAiApi.Interfaces.Extractors;
+
+namespace xAiApi.Providers.Extractors
+{
+    /// <summary>
+    /// Extracts text content from Audio files using Whisper ...
+    /// </summary>
+    public class XAudioFileContentExtractor : IXAudioFileContentExtractor
+    {
+        /// <summary>
+        /// Supported MIME Types ...
+        /// </summary>
+        private static readonly string[] SupportedMimeTypes =
+        [
+            "audio/mpeg",
+            "audio/mp3",
+            "audio/wav",
+            "audio/wave",
+            "audio/ogg",
+            "audio/m4a",
+            "audio/mp4",
+            "audio/webm"
+        ];
+
+        /// <summary>
+        /// Language for Whisper STT ...
+        /// </summary>
+        private readonly string language;
+
+        /// <summary>
+        /// Path to Whisper GGML model file ...
+        /// </summary>
+        private readonly string whisperModelPath;
+
+        /// <summary>
+        /// Logger ...
+        /// </summary>
+        private readonly ILogger<XAudioFileContentExtractor> logger;
+
+        /// <summary>
+        /// Default Constructor ...
+        /// </summary>
+        public XAudioFileContentExtractor()
+            : this(
+                  logger: null,
+                  whisperModelPath: "Models/ggml-base.bin",
+                  language: "fa"
+              )
+        { }
+
+        /// <summary>
+        /// Constructor with configuration ...
+        /// </summary>
+        public XAudioFileContentExtractor(
+            ILogger<XAudioFileContentExtractor> logger,
+            string whisperModelPath = "Models/ggml-base.bin",
+            string language = "fa"
+        )
+        {
+            this.logger = logger;
+            this.language = language;
+            this.whisperModelPath = whisperModelPath;
+        }
+
+        /// <summary>
+        /// Check if this extractor supports the specified MIME type ...
+        /// </summary>
+        public bool CanExtract(string mimeType)
+        {
+            return SupportedMimeTypes.Contains(
+                mimeType?.ToLowerInvariant() ?? string.Empty
+            );
+        }
+
+        /// <summary>
+        /// Extract text content from file stream ...
+        /// </summary>
+        public async Task<string> ExtractAsync(
+            Stream fileStream,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = await ExtractRichAsync(
+                fileStream,
+                "audio",
+                mimeType,
+                cancellationToken
+            );
+            return result.Text;
+        }
+
+        /// <summary>
+        /// Extract content from stream as Rich Result ...
+        /// </summary>
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType
+            };
+
+            try
+            {
+                // ۱. کپی Stream ورودی به MemoryStream
+                using var memoryStream = new MemoryStream();
+                await fileStream.CopyToAsync(memoryStream, cancellationToken);
+                memoryStream.Position = 0;
+
+                // ۲. تبدیل فرمت صوتی به WAV 16kHz Mono (مورد نیاز Whisper)
+                using var wavStream = await ConvertToWavAsync(
+                    inputStream: memoryStream,
+                    mimeType: mimeType,
+                    cancellationToken: cancellationToken
+                );
+
+                // ۳. بررسی وجود فایل مدل Whisper
+                if (!File.Exists(whisperModelPath))
+                {
+                    throw new FileNotFoundException(
+                        $"Whisper model not found at: {whisperModelPath}. " +
+                        "Please download from https://huggingface.co/ggerganov/whisper.cpp/tree/main"
+                    );
+                }
+
+                // ۴. انجام Speech-to-Text با Whisper
+                using var factory = WhisperFactory.FromPath(whisperModelPath);
+                using var processor = factory.CreateBuilder()
+                    .WithLanguage(language)
+                    .Build();
+
+                var segments = new StringBuilder();
+
+                await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken))
+                {
+                    cancellationToken.ThrowIfCancellationRequested();
+                    segments.Append(segment.Text);
+                }
+
+                // ۵. تنظیم نتیجه
+                result.AudioTranscript = segments.ToString().Trim();
+                result.Text = result.AudioTranscript;
+
+                logger?.LogInformation(
+                    "Audio extraction completed for {FileName}: {Length} characters extracted",
+                    fileName,
+                    result.Text.Length
+                );
+            }
+            catch (OperationCanceledException)
+            {
+                logger?.LogWarning("Audio extraction cancelled for file: {FileName}", fileName);
+                throw;
+            }
+            catch (Exception ex)
+            {
+                logger?.LogError(ex, "Audio extraction failed for file: {FileName}", fileName);
+                result.ErrorMessage = $"Audio extraction failed: {ex.Message}";
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// تبدیل فرمت صوتی به WAV 16kHz 16-bit Mono ...
+        /// فرمت استاندارد مورد نیاز موتور Whisper
+        /// </summary>
+        private async Task<Stream> ConvertToWavAsync(
+            Stream inputStream,
+            string mimeType,
+            CancellationToken cancellationToken
+        )
+        {
+            return await Task.Run(() =>
+            {
+                WaveStream reader = null;
+
+                try
+                {
+                    inputStream.Position = 0;
+
+                    // انتخاب Reader مناسب بر اساس MIME Type
+                    reader = CreateWaveReader(inputStream, mimeType);
+
+                    // فرمت هدف: 16kHz, 16-bit, Mono
+                    var targetFormat = new WaveFormat(16000, 16, 1);
+
+                    // تبدیل به SampleProvider برای پردازش
+                    ISampleProvider sampleProvider = reader.ToSampleProvider();
+
+                    // تبدیل به Mono اگر چند کاناله باشد
+                    if (reader.WaveFormat.Channels > 1)
+                    {
+                        sampleProvider = sampleProvider.ToMono();
+                    }
+
+                    // Resample به 16kHz اگر نرخ نمونه‌برداری متفاوت باشد
+                    if (reader.WaveFormat.SampleRate != 16000)
+                    {
+                        sampleProvider = new WdlResamplingSampleProvider(
+                            sampleProvider,
+                            16000
+                        );
+                    }
+
+                    // تبدیل به WaveProvider 16-bit
+                    var waveProvider = sampleProvider.ToWaveProvider16();
+
+                    // نوشتن به MemoryStream موقت
+                    var tempStream = new MemoryStream();
+                    using (var writer = new WaveFileWriter(tempStream, targetFormat))
+                    {
+                        var buffer = new byte[4096];
+                        int bytesRead;
+
+                        while ((bytesRead = waveProvider.Read(buffer, 0, buffer.Length)) > 0)
+                        {
+                            writer.Write(buffer, 0, bytesRead);
+                        }
+                    }
+
+                    // ساخت MemoryStream جدید از داده‌های نهایی
+                    // (چون WaveFileWriter پس از Dispose، stream را می‌بندد)
+                    var resultStream = new MemoryStream(tempStream.ToArray());
+                    tempStream.Dispose();
+
+                    return resultStream;
+                }
+                finally
+                {
+                    reader?.Dispose();
+                }
+            }, cancellationToken);
+        }
+
+        /// <summary>
+        /// ایجاد WaveStream Reader مناسب بر اساس نوع فایل صوتی ...
+        /// </summary>
+        private WaveStream CreateWaveReader(Stream stream, string mimeType)
+        {
+            return mimeType?.ToLowerInvariant() switch
+            {
+                "audio/mpeg" or "audio/mp3"
+                    => new Mp3FileReader(stream),
+
+                "audio/wav" or "audio/wave"
+                    => new WaveFileReader(stream),
+
+                // برای سایر فرمت‌ها، ابتدا به عنوان MP3 تلاش می‌کنیم
+                // در صورت خطا، باید از FFmpeg استفاده شود
+                _ => new Mp3FileReader(stream)
+            };
+        }
+    }
+}
+
+ + +
+

🔌 گام ۶: ثبت در Startup.cs

+ +
+ MODIFY + xAiApi/Startup.cs +
+ +
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (سایر ثبت‌ها)
+
+    // ✅ ثبت XAudioFileContentExtractor با پیکربندی
+    services.AddSingleton<IXFileContentExtractorBase>(sp =>
+    {
+        var logger = sp.GetRequiredService<ILogger<XAudioFileContentExtractor>>();
+        var configuration = sp.GetRequiredService<IConfiguration>();
+
+        var modelPath = configuration.GetValue<string>(
+            "AiApiConfiguration:FileExtraction:Audio:WhisperModelPath"
+        ) ?? "Models/ggml-base.bin";
+
+        var language = configuration.GetValue<string>(
+            "AiApiConfiguration:FileExtraction:Audio:DefaultLanguage"
+        ) ?? "fa";
+
+        return new XAudioFileContentExtractor(
+            logger: logger,
+            whisperModelPath: modelPath,
+            language: language
+        );
+    });
+
+    // ... (سایر ثبت‌ها)
+}
+
+ + +
+

💡 گام ۷: نکات و عیب‌یابی

+ +

۷.۱. مشکلات رایج:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
مشکلعلتراه‌حل
FileNotFoundExceptionفایل مدل Whisper یافت نشددانلود مدل و قرار دادن در مسیر Models/ggml-base.bin
InvalidOperationException در NAudioفرمت صوتی پشتیبانی نمی‌شودنصب MediaFoundation یا استفاده از FFmpeg برای فرمت‌های OGG/M4A
دقت پایین STT فارسیمدل base برای فارسی محدود استاستفاده از مدل ggml-small.bin یا ggml-medium.bin
کندی پردازشاجرا روی CPUنصب Whisper.net.Runtime.Cuda برای GPU
OutOfMemoryExceptionفایل صوتی بسیار بزرگمحدود کردن حجم فایل ورودی یا تقسیم به بخش‌های کوچکتر
+ +

۷.۲. پشتیبانی از فرمت‌های OGG و M4A:

+

کتابخانه NAudio به صورت پیش‌فرض از فرمت‌های OGG و M4A پشتیبانی نمی‌کند. برای این فرمت‌ها دو راه‌حل وجود دارد:

+ +
+
+

راه‌حل ۱: استفاده از FFmpeg

+
    +
  • نصب FFmpeg روی سرور
  • +
  • تبدیل فرمت با Process.Start
  • +
  • پشتیبانی از تمام فرمت‌ها
  • +
+
+
+

راه‌حل ۲: NAudio + MediaFoundation

+
    +
  • فقط در Windows کار می‌کند
  • +
  • پشتیبانی از M4A و برخی فرمت‌ها
  • +
  • بدون نیاز به نصب اضافی
  • +
+
+
+ +

۷.۳. ساختار پوشه نهایی:

+
xAiApi/
+├── Models/
+│   └── ggml-base.bin          ← مدل Whisper
+├── tessdata/
+│   ├── fas.traineddata        ← مدل Tesseract فارسی
+│   └── eng.traineddata        ← مدل Tesseract انگلیسی
+├── appsettings.json           ← پیکربندی
+└── Providers/
+    └── Extractors/
+        ├── XAudioFileContentExtractor.cs   ← ✅ تکمیل شده
+        ├── XImageFileContentExtractor.cs   ← ✅ تکمیل شده
+        ├── XPdfFileContentExtractor.cs
+        ├── XDocxFileContentExtractor.cs
+        ├── XExcelFileContentExtractor.cs
+        ├── XPlainTextFileContentExtractor.cs
+        ├── XVisionFileContentExtractor.cs
+        └── XFileContentExtractor.cs
+ +
+ ✅ خلاصه تغییرات: +
    +
  • 🎙️ STT کامل: تبدیل صوت به متن با Whisper
  • +
  • 🔄 تبدیل فرمت: تبدیل خودکار MP3/WAV به 16kHz Mono با NAudio
  • +
  • 🌐 پشتیبانی فارسی: زبان پیش‌فرض فارسی
  • +
  • 🛡️ مدیریت خطا: بررسی وجود مدل و مدیریت استثناها
  • +
  • 📝 Logging: ثبت موفقیت/شکست با جزئیات
  • +
  • ⚡ بهینه: اجرای تبدیل فرمت در Thread جداگانه
  • +
  • 🔄 قابل لغو: پشتیبانی از CancellationToken
  • +
+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/16_AudioFileContentExtractor_Fix_Foundation.html b/Documents/Docs/Developer/16_AudioFileContentExtractor_Fix_Foundation.html new file mode 100644 index 0000000..d0747d0 --- /dev/null +++ b/Documents/Docs/Developer/16_AudioFileContentExtractor_Fix_Foundation.html @@ -0,0 +1,911 @@ + + + + + +رفع خطای MediaFoundationReader در XAudioFileContentExtractor - فن آوران ساحر علم + + + +
+ +
+

🔧 رفع خطای MediaFoundationReader

+
پیاده‌سازی کامل ConvertToWavAsync با پشتیبانی از تمام فرمت‌های صوتی
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: یکشنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🔍 گام ۱: تحلیل ریشه خطا

+

در کتابخانه NAudio، کلاس MediaFoundationReader فقط مسیر فایل را به عنوان ورودی می‌پذیرد و constructor ای برای Stream ندارد:

+ +
+ ❌ خطای کامپایل: +
// ❌ این کد کار نمی‌کند
+new MediaFoundationReader(memoryStream);  // Error: No constructor takes Stream
+
+ +

Constructor های موجود MediaFoundationReader:

+ + + + + + + + + + + + + + + + + +
Constructorوضعیت
MediaFoundationReader(string audioFile)✅ موجود
MediaFoundationReader(string audioFile, MediaFoundationReaderSettings settings)✅ موجود
MediaFoundationReader(Stream stream)❌ موجود نیست
+
+ + +
+

💡 گام ۲: استراتژی اصلاح

+

برای حل این مشکل، از یک رویکرد ترکیبی بر اساس نوع فرمت صوتی استفاده می‌کنیم:

+ +
+
+

🎵 WAV (PCM)

+
    +
  • استفاده مستقیم از WaveFileReader
  • +
  • بدون نیاز به فایل موقت
  • +
  • فقط در صورت نیاز Resample
  • +
+
+
+

🎤 MP3

+
    +
  • استفاده از Mp3FileReader
  • +
  • پشتیبانی از MemoryStream
  • +
  • بدون نیاز به فایل موقت
  • +
+
+
+

📦 M4A / OGG / WebM

+
    +
  • نوشتن به فایل موقت
  • +
  • استفاده از MediaFoundationReader(path)
  • +
  • حذف فایل پس از پردازش
  • +
+
+
+

🔄 Resample

+
    +
  • تبدیل به 16kHz Mono 16-bit
  • +
  • استفاده از MediaFoundationResampler
  • +
  • خروجی WAV استاندارد
  • +
+
+
+
+ + +
+

🛠️ گام ۳: پیاده‌سازی ConvertToWavAsync

+ +
+ MODIFY + xAiApi/Providers/Extractors/XAudioFileContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Linq;
+using NAudio.Wave;
+using Whisper.net;
+using System.Threading;
+using xAiModels.Models;
+using NAudio.MediaFoundation;
+using System.Threading.Tasks;
+using xAiApi.Interfaces.Extractors;
+
+namespace xAiApi.Providers.Extractors
+{
+    public class XAudioFileContentExtractor : IXAudioFileContentExtractor
+    {
+        // ... (کدهای قبلی)
+
+        /// <summary>
+        /// Audio Format Converting to WAV 16kHz Mono 16-bit PCM ...
+        /// </summary>
+        private async Task<Stream> ConvertToWavAsync(
+            Stream inputStream,
+            string mimeType,
+            CancellationToken cancellationToken
+        )
+        {
+            return await Task.Run(() =>
+            {
+                // ۱. کپی به MemoryStream برای Seekable بودن
+                var memoryStream = new MemoryStream();
+                inputStream.CopyTo(memoryStream);
+                memoryStream.Position = 0;
+
+                // ۲. فرمت هدف: 16kHz, 16-bit, Mono (الزامی برای Whisper)
+                var targetFormat = new WaveFormat(16000, 16, 1);
+                string tempFilePath = null;
+
+                try
+                {
+                    // ۳. دریافت Reader مناسب بر اساس MIME Type
+                    WaveStream reader = GetAudioReader(memoryStream, mimeType, ref tempFilePath);
+
+                    if (reader == null)
+                    {
+                        throw new NotSupportedException(
+                            $"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
+                    {
+                        // ۵. Resample به فرمت هدف
+                        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 { /* Ignore cleanup errors */ }
+                    }
+
+                    memoryStream.Dispose();
+                }
+            }, cancellationToken);
+        }
+    }
+}
+
+ + +
+

🎯 گام ۴: متد کمکی GetAudioReader

+

این متد بر اساس MIME Type، Reader مناسب را انتخاب می‌کند:

+ +
/// <summary>
+/// Get appropriate WaveStream reader based on MIME type ...
+/// </summary>
+/// <param name="stream">Input stream (MemoryStream)</param>
+/// <param name="mimeType">MIME type of audio file</param>
+/// <param name="tempFilePath">Path to temp file (if created)</param>
+/// <returns>WaveStream reader or null if unsupported</returns>
+private WaveStream GetAudioReader(
+    MemoryStream stream,
+    string mimeType,
+    ref string tempFilePath
+)
+{
+    try
+    {
+        var normalizedMime = mimeType?.ToLowerInvariant() ?? string.Empty;
+
+        switch (normalizedMime)
+        {
+            // WAV: استفاده مستقیم از MemoryStream
+            case "audio/wav":
+            case "audio/wave":
+            case "audio/x-wav":
+                return new WaveFileReader(stream);
+
+            // MP3: استفاده مستقیم از MemoryStream
+            case "audio/mpeg":
+            case "audio/mp3":
+                return new Mp3FileReader(stream);
+
+            // M4A, OGG, WebM, MP4: نیاز به فایل موقت
+            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 به فایل موقت
+                stream.Position = 0;
+                using (var fileStream = File.Create(tempFilePath))
+                {
+                    stream.CopyTo(fileStream);
+                }
+
+                // ۳. استفاده از MediaFoundationReader با مسیر فایل
+                MediaFoundationApi.Startup();
+                return new MediaFoundationReader(tempFilePath);
+
+            default:
+                // تلاش عمومی با MediaFoundationReader
+                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;
+    }
+}
+}
+
+ + +
+

🔄 گام ۵: به‌روزرسانی ExtractRichAsync

+

متد ExtractRichAsync باید mimeType را به ConvertToWavAsync ارسال کند:

+ +
public async Task<XFileExtractionResult> ExtractRichAsync(
+    Stream fileStream,
+    string fileName,
+    string mimeType,
+    CancellationToken cancellationToken = default
+)
+{
+    var result = new XFileExtractionResult
+    {
+        FileName = fileName,
+        MimeType = mimeType
+    };
+
+    try
+    {
+        using var memoryStream = new MemoryStream();
+        await fileStream.CopyToAsync(memoryStream, cancellationToken);
+        memoryStream.Position = 0;
+
+        // ✅ ارسال mimeType به ConvertToWavAsync
+        using var wavStream = await ConvertToWavAsync(
+            memoryStream,
+            mimeType,
+            cancellationToken
+        );
+
+        using var factory = WhisperFactory.FromPath(whisperModelPath);
+        using var processor = factory.CreateBuilder()
+            .WithLanguage(language)
+            .Build();
+
+        var segments = new System.Text.StringBuilder();
+        await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken))
+        {
+            segments.Append(segment.Text);
+        }
+
+        result.AudioTranscript = segments.ToString().Trim();
+        result.Text = result.AudioTranscript;
+    }
+    catch (Exception ex)
+    {
+        result.ErrorMessage = $"Audio extraction failed: {ex.Message}";
+    }
+
+    return result;
+}
+
+ + +
+

📋 گام ۶: کد کامل کلاس XAudioFileContentExtractor

+ +
using System;
+using System.IO;
+using System.Linq;
+using NAudio.Wave;
+using Whisper.net;
+using System.Threading;
+using xAiModels.Models;
+using NAudio.MediaFoundation;
+using System.Threading.Tasks;
+using xAiApi.Interfaces.Extractors;
+
+namespace xAiApi.Providers.Extractors
+{
+    /// <summary>
+    /// Extracts text content from Audio files using Whisper ...
+    /// </summary>
+    public class XAudioFileContentExtractor : IXAudioFileContentExtractor
+    {
+        /// <summary>
+        /// Supported MIME Types ...
+        /// </summary>
+        private static readonly string[] SupportedMimeTypes =
+        [
+            "audio/mpeg",
+            "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"
+        )
+        { }
+
+        public XAudioFileContentExtractor(
+            string whisperModelPath = "Models/ggml-base.bin",
+            string language = "fa"
+        )
+        {
+            this.language = language;
+            this.whisperModelPath = whisperModelPath;
+        }
+
+        /// <summary>
+        /// Check if this extractor supports the specified MIME type ...
+        /// </summary>
+        public bool CanExtract(string mimeType)
+        {
+            return SupportedMimeTypes.Contains(
+                mimeType?.ToLowerInvariant() ?? string.Empty
+            );
+        }
+
+        /// <summary>
+        /// Extract text content from file stream ...
+        /// </summary>
+        public async Task<string> ExtractAsync(
+            Stream fileStream,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = await ExtractRichAsync(
+                fileStream,
+                "audio",
+                mimeType,
+                cancellationToken
+            );
+            return result.AudioTranscript;
+        }
+
+        /// <summary>
+        /// Extract content from stream as Rich Result ...
+        /// </summary>
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType
+            };
+
+            try
+            {
+                using var memoryStream = new MemoryStream();
+                await fileStream.CopyToAsync(memoryStream, cancellationToken);
+                memoryStream.Position = 0;
+
+                using var wavStream = await ConvertToWavAsync(
+                    memoryStream,
+                    mimeType,
+                    cancellationToken
+                );
+
+                using var factory = WhisperFactory.FromPath(whisperModelPath);
+                using var processor = factory.CreateBuilder()
+                    .WithLanguage(language)
+                    .Build();
+
+                var segments = new System.Text.StringBuilder();
+                await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken))
+                {
+                    segments.Append(segment.Text);
+                }
+
+                result.AudioTranscript = segments.ToString().Trim();
+                result.Text = result.AudioTranscript;
+            }
+            catch (Exception ex)
+            {
+                result.ErrorMessage = $"Audio extraction failed: {ex.Message}";
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Audio Format Converting to WAV 16kHz Mono 16-bit PCM ...
+        /// </summary>
+        private async Task<Stream> ConvertToWavAsync(
+            Stream inputStream,
+            string mimeType,
+            CancellationToken cancellationToken
+        )
+        {
+            return 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)
+                    {
+                        throw new NotSupportedException(
+                            $"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);
+        }
+
+        /// <summary>
+        /// Get appropriate WaveStream reader based on MIME type ...
+        /// </summary>
+        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/x-m4a":
+                    case "audio/ogg":
+                    case "audio/webm":
+                        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
+            {
+                return null;
+            }
+        }
+    }
+}
+
+ + +
+

⚠️ گام ۷: نکات مهم و ملاحظات

+ +
+
+

🪟 محدودیت ویندوز

+
    +
  • MediaFoundationReader فقط روی ویندوز کار می‌کند
  • +
  • برای Linux/Mac نیاز به ffmpeg است
  • +
  • در Production حتماً بررسی کنید
  • +
+
+
+

📦 پکیج‌های NuGet

+
    +
  • NAudio - کتابخانه اصلی
  • +
  • NAudio.MediaFoundation - برای M4A/OGG
  • +
  • Whisper.net - برای Speech-to-Text
  • +
+
+
+

🗑️ مدیریت فایل موقت

+
    +
  • فایل‌های موقت در Path.GetTempPath()
  • +
  • در بلوک finally حذف می‌شوند
  • +
  • حتی در صورت خطا پاکسازی انجام می‌شود
  • +
+
+
+

🎯 فرمت هدف Whisper

+
    +
  • Sample Rate: 16000 Hz
  • +
  • Channels: 1 (Mono)
  • +
  • Bits Per Sample: 16
  • +
  • Encoding: PCM
  • +
+
+
+ +

جدول فرمت‌ها و روش پردازش:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
فرمتMIME TypeReaderنیاز به فایل موقت
WAVaudio/wavWaveFileReader❌ خیر
MP3audio/mpegMp3FileReader❌ خیر
M4Aaudio/m4aMediaFoundationReader✅ بله
OGGaudio/oggMediaFoundationReader✅ بله
WebMaudio/webmMediaFoundationReader✅ بله
MP4 Audioaudio/mp4MediaFoundationReader✅ بله
AACaudio/aacMediaFoundationReader✅ بله
+ +
+ ✅ نتیجه نهایی: +
    +
  • 🎯 خطای کامپایل MediaFoundationReader(stream) کاملاً رفع شد
  • +
  • 📦 پشتیبانی از تمام فرمت‌های صوتی رایج (WAV, MP3, M4A, OGG, WebM, MP4, AAC)
  • +
  • 🔄 تبدیل خودکار به فرمت استاندارد Whisper (16kHz Mono 16-bit PCM)
  • +
  • 🗑️ مدیریت صحیح فایل‌های موقت و جلوگیری از نشت منابع
  • +
  • ⚡ بهینه‌سازی: فایل‌های WAV/MP3 بدون فایل موقت پردازش می‌شوند
  • +
  • 🛡️ مدیریت خطا در تمام مراحل با cleanup تضمین‌شده
  • +
+
+ +
+ ⚠️ نکته مهم برای Production: +

اگر سرور شما Linux است، MediaFoundationReader کار نمی‌کند. در این صورت باید از ffmpeg به صورت process خارجی استفاده کنید یا از کتابخانه‌های جایگزین مانند NAudio.Lame و NVorbis بهره ببرید.

+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261004_101945.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261004_101945.txt new file mode 100644 index 0000000..014c9ee --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261004_101945.txt @@ -0,0 +1,6380 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using xAiApi.Providers.Extractors; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // + services.AddSingleton(); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiApi.Models; +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + + /// + /// OCR Configuration ... + /// + public XAiOCRDescriptor OCR { get; set; } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + + // + public const string XAiApiIntroductionPromptName = "Introduction"; + public const string XAiApiContentExtractionPromptName = "ContentExtraction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultVisionModelName = "Qwen"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; +using System.Linq; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Content Extraction ... + /// + /// Extract Files Content ... + /// + /// + /// + /// + /// + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + public async Task> ExtractContent( + [FromForm] IFormFileCollection files, + [FromForm] string extractors = null, + CancellationToken cancellationToken = default + ) { + // + try + { + // + // Validate ... + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + string[] extractorArr = null; + if (!extractors.IsNullOrEmpty()) + { + extractorArr = [.. extractors.ParseListString()]; + } + + // + var result = await aiService.ExtractContent( + files: files, + extractors: extractorArr, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Net.Http; +using xAiApi.Constants; +using xAiModels.Models; +using System.Threading; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using System.ClientModel.Primitives; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + + /// + /// Provide HttpClient for Common Usages ... + /// + /// + /// + public static HttpClient GetHttpClient(this string source) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = source.IsNullOrEmpty() + ? null + : new Uri(source), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Instance and Retrieve Chat Client ... + /// + /// + /// + public static IChatClient GetClient(this XAiModelDescriptor source) + { + // + // Try to Initialize LLm ... + var model = source.LLM; + var apiKey = source.ApiKey; + var url = new Uri(source.Url); + var httpClient = source.Url.GetHttpClient(); + + // + IChatClient result = null; + switch (source.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + /// + Task ExtractContent( + IFormFileCollection files, + string[] extractors = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Extract Files Content ... + /// + /// + /// + /// + /// + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + Task> ExtractContent( + [FromForm] IFormFileCollection files, + [FromForm] string extractors = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Base Service for Extracting File Content ... + /// + public interface IXFileContentExtractorBase + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } + + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + /// + /// + /// + bool CanExtract( + string mimeType, + string[] extractors = null + ); + + /// + /// Extract text content from file stream ... + /// + /// + /// + /// + /// + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ); + + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXVisionFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXVisionFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003121152_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003121152_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003121152_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Models\XAiOCRDescriptor.cs + +using Tesseract; + +namespace xAiApi.Models +{ + public class XAiOCRDescriptor + { + /// + /// Provides Trained Data Path ... + /// + public string DataPath { get; set; } + + /// + /// The Engine for Doing OCR ... + /// + public EngineMode EngineMode { get; set; } + + /// + /// Enable OCR Fallback using Model ... + /// + public bool EnableOcrFallback { get; set; } + + /// + /// Default OCR Language ... + /// + public string DefaultLanguage { get; set; } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(descriptor.Url.GetHttpClient()) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = Descriptor.GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = Descriptor.GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public virtual async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public virtual async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public virtual async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public virtual async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public virtual async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public virtual async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + /// + public async Task ExtractContent( + IFormFileCollection files, + string[] extractors = null, + CancellationToken cancellationToken = default + ) + { + // + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var extractionResults = new List(); + foreach (var file in files) + { + // + if (file.IsNull() || file.Length == 0) + { + continue; + } + + // + var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty; + if (!fileContentExtractor.CanExtract(mimeType, extractors)) + { + continue; + } + + // + try + { + // + using var stream = file.OpenReadStream(); + + // + var fr = await fileContentExtractor.ExtractRichAsync( + fileStream: stream, + mimeType: mimeType, + extractors: extractors, + fileName: file.FileName, + cancellationToken: cancellationToken + ); + + // + if (fr.IsValid()) + { + extractionResults.Add(fr); + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + continue; + } + } + + // + var result = string.Empty; + if (extractionResults.HasChild()) + { + // + extractionResults.ForEach(er => + { + result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----"; + }); + } + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = Descriptor.GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = Descriptor.GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.EMMA; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor() : this( + language: "fa", + whisperModelPath: "Models/ggml-base.bin" + ) + { } + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList _extractors = []; + + public XFileContentExtractor( + IEnumerable availableExtractors + ) + { + // + _extractors = [.. availableExtractors + .Where(e => e.GetType() != typeof(XFileContentExtractor))]; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + /// + /// + /// + public bool CanExtract( + string mimeType, + string[] extractors = null + ) + { + // + return GetExtractors(extractors) + .Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + /// + /// + /// + /// + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ) + { + // + var extractor = GetExtractors(extractors) + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ) + { + // + var extractor = GetExtractors(extractors) + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract Provided Extractors ... + /// + /// + /// + private IList GetExtractors(string[] extractors = null) + { + // + var result = new List(); + + // + if (extractors.IsNull() || !extractors.HasChild()) + { + // + result = [.. _extractors]; + } + else + { + // + result = [.. _extractors + .Where(e => extractors + .Any(ex => e.GetType().FullName.Contains(ex)))]; + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using xAiApi.Configurations; +using xCommons.Extensions; +using xExceptions.Constants; +using Tesseract; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// AI Api Configuration ... + /// + private readonly XAiApiConfiguration configuration; + + public XImageFileContentExtractor( + XAiApiConfiguration configuration + ) + { + // + if (configuration.IsNullOrDefault() || + configuration.OCR.IsNullOrDefault() || + configuration.OCR.DataPath.IsNullOrEmpty() || + configuration.OCR.DefaultLanguage.IsNullOrEmpty()) + { + XException.InvalidConfiguration.Throw(); + } + + // + this.configuration = configuration; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // + // OCR Fallback ... + if (configuration.OCR.EnableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + return await Task.Run(() => + { + // + // Validate Path ... + if (!Directory.Exists(configuration.OCR.DataPath)) + { + // + throw new DirectoryNotFoundException( + $"TessData directory not found at: {configuration.OCR.DataPath}. " + + "Please download traineddata files from https://github.com/tesseract-ocr/tessdata" + ); + } + + // + using var engine = new TesseractEngine( + datapath: configuration.OCR.DataPath, + language: configuration.OCR.DefaultLanguage, + engineMode: configuration.OCR.EngineMode + ); + + // + using var pix = Pix.LoadFromMemory(imageBytes); + + // + cancellationToken.ThrowIfCancellationRequested(); + + // + using var page = engine.Process(pix); + + // + var text = page.GetText(); + + // + text = text?.Trim(); + + // + return text ?? string.Empty; + }, cancellationToken); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XVisionFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from files using Vision Model ... + /// + public class XVisionFileContentExtractor : IXVisionFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/bmp", + "application/pdf", + ]; + + // + private readonly string prompt; + private readonly XAiModelDescriptor descriptor; + + public XVisionFileContentExtractor( + XAiApiConfiguration configuration, + string promptName = XAiApiConstants.XAiApiContentExtractionPromptName, + string modelName = XAiApiConstants.XAiDefaultVisionModelName + ) + { + // + // Retrieve Descriptoir ... + if (modelName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + descriptor = configuration.GetModel(modelName); + + // + // Retrieve Extraction Prompt ... + if (promptName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + prompt = configuration.GetPrompt( + name: promptName, + @params: null + ); + + // + // Validate Requirements ... + if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken); + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + var messages = new[] + { + // + new ChatMessage(ChatRole.User, new[] + { + new DataContent(imageBytes, mimeType) + }) + }; + + // + var extractedContent = await RequestOCRAsync( + messages: messages, + cancellationToken: cancellationToken + ); + if (!extractedContent.IsNullOrEmpty()) + { + result.Text = extractedContent.Trim(); + } + else + { + result.ErrorMessage = "there is not any Extracted Content ..."; + } + } + catch (Exception ex) + { + result.ErrorMessage = $"Error in Vision Extraction: {ex.Message} ..."; + } + + // + return result; + } + + // + #region OCR ... + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + public virtual async Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = descriptor.GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var response = await client.GetResponseAsync( + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = descriptor.GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: messages + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + #endregion + } +} diff --git a/xAiApi b/xAiApi index 70c2d5a..e6e5369 160000 --- a/xAiApi +++ b/xAiApi @@ -1 +1 @@ -Subproject commit 70c2d5a649bd2fbda347e1a89dff8f95d6017237 +Subproject commit e6e5369b95624f4832cff69df84d561b61d75ce6