Files
xSaherelmWorkspace/Documents/Docs/Developer/15_AudioFileContentExtractor_1.html
T
2026-10-04 11:42:43 +03:30

712 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>تکمیل سرویس XAudioFileContentExtractor - فن آوران ساحر علم</title>
<style>
:root {
--primary: #1e3a8a;
--secondary: #3b82f6;
--accent: #f59e0b;
--success: #10b981;
--danger: #ef4444;
--warning: #f97316;
--bg-light: #f8fafc;
--bg-code: #1e293b;
--text-dark: #0f172a;
--text-muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Tahoma', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 100%);
color: var(--text-dark);
line-height: 1.8;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 { font-size: 2.1em; margin-bottom: 10px; }
.header .subtitle { font-size: 1.1em; opacity: 0.95; }
.meta-bar {
display: flex;
justify-content: space-between;
background: var(--bg-light);
padding: 15px 30px;
border-bottom: 2px solid var(--border);
flex-wrap: wrap;
gap: 15px;
}
.meta-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; color: var(--text-muted); }
.meta-item strong { color: var(--primary); }
.content { padding: 40px; }
.section {
margin-bottom: 35px;
padding: 25px;
background: var(--bg-light);
border-radius: 12px;
border-right: 5px solid var(--secondary);
}
.section h2 {
color: var(--primary);
font-size: 1.5em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--border);
}
.section h3 { color: var(--secondary); font-size: 1.2em; margin: 20px 0 12px; }
pre {
background: var(--bg-code);
color: #e2e8f0;
padding: 18px;
border-radius: 8px;
overflow-x: auto;
direction: ltr;
text-align: left;
font-family: 'Consolas', monospace;
font-size: 0.85em;
margin: 15px 0;
border-right: 4px solid var(--accent);
}
code {
background: #fef3c7;
color: #92400e;
padding: 2px 8px;
border-radius: 4px;
font-family: 'Consolas', monospace;
font-size: 0.9em;
direction: ltr;
display: inline-block;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: white;
border-radius: 8px;
overflow: hidden;
}
th { background: var(--primary); color: white; padding: 12px; text-align: right; }
td { padding: 12px; border-bottom: 1px solid var(--border); }
tr:hover { background: var(--bg-light); }
.alert { padding: 15px 20px; border-radius: 8px; margin: 15px 0; border-right: 4px solid; }
.alert-info { background: #dbeafe; border-color: var(--secondary); color: #1e40af; }
.alert-success { background: #d1fae5; border-color: var(--success); color: #065f46; }
.alert-warning { background: #fef3c7; border-color: var(--accent); color: #92400e; }
.alert-danger { background: #fee2e2; border-color: var(--danger); color: #991b1b; }
.footer { background: var(--primary); color: white; padding: 25px; text-align: center; }
.toc { background: white; padding: 20px; border-radius: 10px; margin-bottom: 25px; border: 2px solid var(--border); }
.toc h3 { color: var(--primary); margin-bottom: 15px; }
.toc ol { padding-right: 25px; }
.toc li { padding: 6px 0; }
.toc a { color: var(--secondary); text-decoration: none; }
.file-change { background: #f0f9ff; border-right: 4px solid var(--secondary); padding: 15px; margin: 10px 0; border-radius: 8px; }
.file-change .path { font-family: 'Consolas', monospace; color: var(--primary); font-weight: bold; direction: ltr; display: inline-block; }
.badge-modify { background: var(--warning); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; margin-right: 8px; }
.badge-new { background: var(--success); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; margin-right: 8px; }
.arch-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin: 20px 0; }
.arch-card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); border-top: 4px solid var(--secondary); }
.arch-card h4 { color: var(--primary); margin-bottom: 12px; }
.arch-card ul { list-style: none; padding-right: 0; }
.arch-card li { padding: 6px 0; padding-right: 20px; position: relative; }
.arch-card li::before { content: '▸'; position: absolute; right: 0; color: var(--accent); font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎙️ تکمیل سرویس XAudioFileContentExtractor</h1>
<div class="subtitle">پیاده‌سازی کامل تبدیل صوت به متن با Whisper و NAudio</div>
</div>
<div class="meta-bar">
<div class="meta-item">👨‍💻 <strong>توسعه‌دهنده:</strong> هادی خزاعی اصل</div>
<div class="meta-item">🏢 <strong>شرکت:</strong> فن آوران ساحر علم</div>
<div class="meta-item">📅 <strong>تاریخ:</strong> یکشنبه ۱۳ مهر ۱۴۰۵</div>
<div class="meta-item">📦 <strong>پروژه:</strong> xAiApi</div>
</div>
<div class="content">
<div class="toc">
<h3>📑 فهرست مطالب</h3>
<ol>
<li><a href="#analysis">تحلیل وضعیت فعلی</a></li>
<li><a href="#packages">گام ۱: نصب پکیج‌های NuGet</a></li>
<li><a href="#model">گام ۲: دانلود مدل Whisper</a></li>
<li><a href="#config">گام ۳: پیکربندی appsettings.json</a></li>
<li><a href="#code">گام ۴: کد کامل XAudioFileContentExtractor</a></li>
<li><a href="#di">گام ۵: ثبت در Startup.cs</a></li>
<li><a href="#tips">گام ۶: نکات و عیب‌یابی</a></li>
</ol>
</div>
<!-- Section 1: Analysis -->
<div class="section" id="analysis">
<h2>🔍 گام ۱: تحلیل وضعیت فعلی</h2>
<p>با بررسی آخرین نسخه کد <code>XAudioFileContentExtractor</code>، موارد زیر شناسایی شد:</p>
<table>
<tr>
<th>بخش</th>
<th>وضعیت</th>
<th>توضیح</th>
</tr>
<tr>
<td><code>SupportedMimeTypes</code></td>
<td>✅ کامل</td>
<td>پشتیبانی از MP3, WAV, OGG, M4A, WebM</td>
</tr>
<tr>
<td><code>CanExtract()</code></td>
<td>✅ کامل</td>
<td>تشخیص صحیح MIME Type</td>
</tr>
<tr>
<td><code>ExtractAsync()</code></td>
<td>✅ کامل</td>
<td>Delegate به ExtractRichAsync</td>
</tr>
<tr>
<td><code>ExtractRichAsync()</code></td>
<td>⚠️ نیمه‌کاره</td>
<td>ساختار اصلی وجود دارد ولی نیاز به تکمیل دارد</td>
</tr>
<tr>
<td><code>ConvertToWavAsync()</code></td>
<td>❌ خالی (TODO)</td>
<td>فقط placeholder است و باید پیاده‌سازی شود</td>
</tr>
<tr>
<td><code>ILogger</code></td>
<td>❌ موجود نیست</td>
<td>نیاز به تزریق Logger برای ثبت وقایع</td>
</tr>
</table>
</div>
<!-- Section 2: Packages -->
<div class="section" id="packages">
<h2>📦 گام ۲: نصب پکیج‌های NuGet</h2>
<pre># پکیج اصلی Whisper برای Speech-to-Text
Install-Package Whisper.net
# Runtime مورد نیاز Whisper (برای CPU)
Install-Package Whisper.net.Runtime
# کتابخانه NAudio برای تبدیل فرمت صوتی
Install-Package NAudio</pre>
<div class="alert alert-info">
<strong>💡 نکته:</strong> اگر از GPU NVIDIA استفاده می‌کنید، به جای <code>Whisper.net.Runtime</code> پکیج <code>Whisper.net.Runtime.Cuda</code> را نصب کنید تا سرعت پردازش چندین برابر شود.
</div>
</div>
<!-- Section 3: Model -->
<div class="section" id="model">
<h2>📥 گام ۳: دانلود مدل Whisper</h2>
<p>مدل‌های Whisper در اندازه‌های مختلف موجود هستند. برای زبان فارسی، مدل <code>base</code> یا <code>small</code> پیشنهاد می‌شود:</p>
<table>
<tr>
<th>مدل</th>
<th>حجم</th>
<th>دقت فارسی</th>
<th>سرعت</th>
<th>VRAM مورد نیاز</th>
</tr>
<tr>
<td><code>ggml-tiny.bin</code></td>
<td>~75 MB</td>
<td>⭐⭐</td>
<td>⭐⭐⭐⭐⭐</td>
<td>~1 GB</td>
</tr>
<tr>
<td><code>ggml-base.bin</code></td>
<td>~142 MB</td>
<td>⭐⭐⭐</td>
<td>⭐⭐⭐⭐</td>
<td>~1 GB</td>
</tr>
<tr>
<td><code>ggml-small.bin</code></td>
<td>~466 MB</td>
<td>⭐⭐⭐⭐</td>
<td>⭐⭐⭐</td>
<td>~2 GB</td>
</tr>
<tr>
<td><code>ggml-medium.bin</code></td>
<td>~1.5 GB</td>
<td>⭐⭐⭐⭐⭐</td>
<td>⭐⭐</td>
<td>~5 GB</td>
</tr>
</table>
<p>دانلود از:</p>
<pre># لینک مستقیم دانلود مدل base:
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin
# قرار دادن در مسیر:
xAiApi/Models/ggml-base.bin</pre>
</div>
<!-- Section 4: Config -->
<div class="section" id="config">
<h2>⚙️ گام ۴: پیکربندی appsettings.json</h2>
<pre>{
"AiApiConfiguration": {
"FileExtraction": {
"Audio": {
"WhisperModelPath": "Models/ggml-base.bin",
"DefaultLanguage": "fa"
}
}
}
}</pre>
</div>
<!-- Section 5: Code -->
<div class="section" id="code">
<h2>🛠️ گام ۵: کد کامل XAudioFileContentExtractor</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Providers/Extractors/XAudioFileContentExtractor.cs</span>
</div>
<pre>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
{
/// &lt;summary&gt;
/// Extracts text content from Audio files using Whisper ...
/// &lt;/summary&gt;
public class XAudioFileContentExtractor : IXAudioFileContentExtractor
{
/// &lt;summary&gt;
/// Supported MIME Types ...
/// &lt;/summary&gt;
private static readonly string[] SupportedMimeTypes =
[
"audio/mpeg",
"audio/mp3",
"audio/wav",
"audio/wave",
"audio/ogg",
"audio/m4a",
"audio/mp4",
"audio/webm"
];
/// &lt;summary&gt;
/// Language for Whisper STT ...
/// &lt;/summary&gt;
private readonly string language;
/// &lt;summary&gt;
/// Path to Whisper GGML model file ...
/// &lt;/summary&gt;
private readonly string whisperModelPath;
/// &lt;summary&gt;
/// Logger ...
/// &lt;/summary&gt;
private readonly ILogger&lt;XAudioFileContentExtractor&gt; logger;
/// &lt;summary&gt;
/// Default Constructor ...
/// &lt;/summary&gt;
public XAudioFileContentExtractor()
: this(
logger: null,
whisperModelPath: "Models/ggml-base.bin",
language: "fa"
)
{ }
/// &lt;summary&gt;
/// Constructor with configuration ...
/// &lt;/summary&gt;
public XAudioFileContentExtractor(
ILogger&lt;XAudioFileContentExtractor&gt; logger,
string whisperModelPath = "Models/ggml-base.bin",
string language = "fa"
)
{
this.logger = logger;
this.language = language;
this.whisperModelPath = whisperModelPath;
}
/// &lt;summary&gt;
/// Check if this extractor supports the specified MIME type ...
/// &lt;/summary&gt;
public bool CanExtract(string mimeType)
{
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// &lt;summary&gt;
/// Extract text content from file stream ...
/// &lt;/summary&gt;
public async Task&lt;string&gt; ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
var result = await ExtractRichAsync(
fileStream,
"audio",
mimeType,
cancellationToken
);
return result.Text;
}
/// &lt;summary&gt;
/// Extract content from stream as Rich Result ...
/// &lt;/summary&gt;
public async Task&lt;XFileExtractionResult&gt; 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;
}
/// &lt;summary&gt;
/// تبدیل فرمت صوتی به WAV 16kHz 16-bit Mono ...
/// فرمت استاندارد مورد نیاز موتور Whisper
/// &lt;/summary&gt;
private async Task&lt;Stream&gt; ConvertToWavAsync(
Stream inputStream,
string mimeType,
CancellationToken cancellationToken
)
{
return await Task.Run(() =&gt;
{
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 &gt; 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)) &gt; 0)
{
writer.Write(buffer, 0, bytesRead);
}
}
// ساخت MemoryStream جدید از داده‌های نهایی
// (چون WaveFileWriter پس از Dispose، stream را می‌بندد)
var resultStream = new MemoryStream(tempStream.ToArray());
tempStream.Dispose();
return resultStream;
}
finally
{
reader?.Dispose();
}
}, cancellationToken);
}
/// &lt;summary&gt;
/// ایجاد WaveStream Reader مناسب بر اساس نوع فایل صوتی ...
/// &lt;/summary&gt;
private WaveStream CreateWaveReader(Stream stream, string mimeType)
{
return mimeType?.ToLowerInvariant() switch
{
"audio/mpeg" or "audio/mp3"
=&gt; new Mp3FileReader(stream),
"audio/wav" or "audio/wave"
=&gt; new WaveFileReader(stream),
// برای سایر فرمت‌ها، ابتدا به عنوان MP3 تلاش می‌کنیم
// در صورت خطا، باید از FFmpeg استفاده شود
_ =&gt; new Mp3FileReader(stream)
};
}
}
}</pre>
</div>
<!-- Section 6: DI -->
<div class="section" id="di">
<h2>🔌 گام ۶: ثبت در Startup.cs</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Startup.cs</span>
</div>
<pre>public void ConfigureServices(IServiceCollection services)
{
// ... (سایر ثبت‌ها)
// ✅ ثبت XAudioFileContentExtractor با پیکربندی
services.AddSingleton&lt;IXFileContentExtractorBase&gt;(sp =&gt;
{
var logger = sp.GetRequiredService&lt;ILogger&lt;XAudioFileContentExtractor&gt;&gt;();
var configuration = sp.GetRequiredService&lt;IConfiguration&gt;();
var modelPath = configuration.GetValue&lt;string&gt;(
"AiApiConfiguration:FileExtraction:Audio:WhisperModelPath"
) ?? "Models/ggml-base.bin";
var language = configuration.GetValue&lt;string&gt;(
"AiApiConfiguration:FileExtraction:Audio:DefaultLanguage"
) ?? "fa";
return new XAudioFileContentExtractor(
logger: logger,
whisperModelPath: modelPath,
language: language
);
});
// ... (سایر ثبت‌ها)
}</pre>
</div>
<!-- Section 7: Tips -->
<div class="section" id="tips">
<h2>💡 گام ۷: نکات و عیب‌یابی</h2>
<h3>۷.۱. مشکلات رایج:</h3>
<table>
<tr>
<th>مشکل</th>
<th>علت</th>
<th>راه‌حل</th>
</tr>
<tr>
<td><code>FileNotFoundException</code></td>
<td>فایل مدل Whisper یافت نشد</td>
<td>دانلود مدل و قرار دادن در مسیر <code>Models/ggml-base.bin</code></td>
</tr>
<tr>
<td><code>InvalidOperationException</code> در NAudio</td>
<td>فرمت صوتی پشتیبانی نمی‌شود</td>
<td>نصب <code>MediaFoundation</code> یا استفاده از FFmpeg برای فرمت‌های OGG/M4A</td>
</tr>
<tr>
<td>دقت پایین STT فارسی</td>
<td>مدل base برای فارسی محدود است</td>
<td>استفاده از مدل <code>ggml-small.bin</code> یا <code>ggml-medium.bin</code></td>
</tr>
<tr>
<td>کندی پردازش</td>
<td>اجرا روی CPU</td>
<td>نصب <code>Whisper.net.Runtime.Cuda</code> برای GPU</td>
</tr>
<tr>
<td><code>OutOfMemoryException</code></td>
<td>فایل صوتی بسیار بزرگ</td>
<td>محدود کردن حجم فایل ورودی یا تقسیم به بخش‌های کوچکتر</td>
</tr>
</table>
<h3>۷.۲. پشتیبانی از فرمت‌های OGG و M4A:</h3>
<p>کتابخانه NAudio به صورت پیش‌فرض از فرمت‌های <code>OGG</code> و <code>M4A</code> پشتیبانی نمی‌کند. برای این فرمت‌ها دو راه‌حل وجود دارد:</p>
<div class="arch-grid">
<div class="arch-card">
<h4>راه‌حل ۱: استفاده از FFmpeg</h4>
<ul>
<li>نصب FFmpeg روی سرور</li>
<li>تبدیل فرمت با Process.Start</li>
<li>پشتیبانی از تمام فرمت‌ها</li>
</ul>
</div>
<div class="arch-card">
<h4>راه‌حل ۲: NAudio + MediaFoundation</h4>
<ul>
<li>فقط در Windows کار می‌کند</li>
<li>پشتیبانی از M4A و برخی فرمت‌ها</li>
<li>بدون نیاز به نصب اضافی</li>
</ul>
</div>
</div>
<h3>۷.۳. ساختار پوشه نهایی:</h3>
<pre>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</pre>
<div class="alert alert-success">
<strong>✅ خلاصه تغییرات:</strong>
<ul style="padding-right: 25px; margin-top: 10px;">
<li>🎙️ <strong>STT کامل:</strong> تبدیل صوت به متن با Whisper</li>
<li>🔄 <strong>تبدیل فرمت:</strong> تبدیل خودکار MP3/WAV به 16kHz Mono با NAudio</li>
<li>🌐 <strong>پشتیبانی فارسی:</strong> زبان پیش‌فرض فارسی</li>
<li>🛡️ <strong>مدیریت خطا:</strong> بررسی وجود مدل و مدیریت استثناها</li>
<li>📝 <strong>Logging:</strong> ثبت موفقیت/شکست با جزئیات</li>
<li>⚡ <strong>بهینه:</strong> اجرای تبدیل فرمت در Thread جداگانه</li>
<li>🔄 <strong>قابل لغو:</strong> پشتیبانی از CancellationToken</li>
</ul>
</div>
</div>
</div>
<div class="footer">
<p><strong>👨‍💻 توسعه‌دهنده:</strong> هادی خزاعی اصل</p>
<p><strong>🏢 شرکت:</strong> فن آوران ساحر علم</p>
<p><strong>📅 تاریخ:</strong> یکشنبه ۱۳ مهر ۱۴۰۵</p>
<p style="margin-top: 15px; opacity: 0.8; font-size: 0.9em;">
🎙️ مستند فنی تکمیل سرویس XAudioFileContentExtractor - تمامی حقوق محفوظ است
</p>
</div>
</div>
</body>
</html>