📑 فهرست مطالب
🔍 گام ۱: تحلیل ریشه خطا
در کتابخانه 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/OGGWhisper.net- برای Speech-to-Text
🗑️ مدیریت فایل موقت
- فایلهای موقت در
Path.GetTempPath() - در بلوک
finallyحذف میشوند - حتی در صورت خطا پاکسازی انجام میشود
🎯 فرمت هدف Whisper
- Sample Rate: 16000 Hz
- Channels: 1 (Mono)
- Bits Per Sample: 16
- Encoding: PCM
جدول فرمتها و روش پردازش:
| فرمت | MIME Type | Reader | نیاز به فایل موقت |
|---|---|---|---|
| WAV | audio/wav |
WaveFileReader |
❌ خیر |
| MP3 | audio/mpeg |
Mp3FileReader |
❌ خیر |
| M4A | audio/m4a |
MediaFoundationReader |
✅ بله |
| OGG | audio/ogg |
MediaFoundationReader |
✅ بله |
| WebM | audio/webm |
MediaFoundationReader |
✅ بله |
| MP4 Audio | audio/mp4 |
MediaFoundationReader |
✅ بله |
| AAC | audio/aac |
MediaFoundationReader |
✅ بله |
✅ نتیجه نهایی:
- 🎯 خطای کامپایل
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 بهره ببرید.