feat(bot): add strict whitelist auth bypass, multi-STT consensus pipeline, and applicant logging
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
# سند جامع، تحلیلی و عملیاتی کرالر تبدیل صوتبهمتن (STT) بله
|
||||
> **نسخه:** ۳.۰ (جامع، تجمیعی و نهایی)
|
||||
> **مخاطب اول:** مدیر پروژه (تصمیمگیری کلان، ماتریسهای مقایسهای، جریانهای داده و فلوهای اجرایی)
|
||||
> **مخاطب دوم:** ایجنت هوش مصنوعی مجری (شناخت پروژه، معماری فنی، ساختار پروتکل، کدهای مرجع و چکلیست راهاندازی)
|
||||
> **وضعیت نیازمندیها:** محدودیت حریم خصوصی موقتاً برداشته شده؛ صحت و کیفیت بالای موتور بله در استعلامهای تدارکاتی توسط کاربر اثبات شده؛ وجود دکمه تبدیل متن در وب (`web.bale.ai`) تأیید شده؛ رویکرد Playwright به عنوان **فالبک آخر (Last Resort)** و رویکرد RPC به عنوان **اولویت اول** تعیین شده است؛ هیچ تخمین زمانی تقویمی وجود ندارد چون تمامی پیادهسازیها توسط ایجنت انجام میشود.
|
||||
|
||||
---
|
||||
|
||||
# بخش اول: برای مدیر پروژه (دید کلان، مقایسهها و فلوهای تصویری)
|
||||
|
||||
## ۱. صورتمسئله و دلایل توجیهی پروژه
|
||||
|
||||
در سامانه استعلام هوشمند تدارکات، کاربران پیامهای صوتی (ویس) حاوی درخواستهای پیچیده خرید (شامل نام کالا، برند، مشخصات فنی، مقادیر، واحدها و اصطلاحات بازار) ارسال میکنند.
|
||||
بات تدارکات برای رونویسی این پیامها از یک **خطلوله چندمدله (Multi-Provider STT Pipeline)** استفاده میکند؛ به این معنا که ویس همزمان به چند سرویس تبدیل صوت ارسال شده و متنهای دریافتشده در اختیار یک مدل تحلیلی هوش مصنوعی (DeepSeek NLU) قرار میگیرد تا با ایجاد اجماع (Consensus)، خطاهای واژگانی برطرف و ساختار استاندارد استعلام استخراج گردد.
|
||||
|
||||
### چرا کرال کردن موتور بله ارزش بالایی دارد؟
|
||||
1. **کیفیت اثباتشده در آزمون واقعی:** تستهای تجربی روی ویسهای واقعی استعلام تدارکات نشان داد که موتور تشخیص گفتار بله تسلط بسیار بالایی بر محاورات عامیانه فارسی، اصطلاحات بازاری و لهجهها دارد.
|
||||
2. **فقدان متد در رابط رسمی (Bot API):** در مستندات رسمی بله (`docs.bale.ai`)، شیء `Voice` ارسالی به رباتها فاقد فیلد ترنسکریپت بوده و تنها مشخصاتی نظیر `file_id` را برمیگرداند؛ بنابراین دسترسی به این موتور صرفاً از طریق شبیهسازی نشست کاربری امکانپذیر است.
|
||||
3. **تأیید وجود دکمه در وب بله:** بررسی محیط `web.bale.ai` اثبات کرد که قابلیت تبدیل ویس به متن در کلاینت تحت وب بله فعال است؛ این یافته امکان کپچر ترافیک و مهندسی معکوس و نیز استفاده از وب به عنوان پشتیبان را تضمین میکند.
|
||||
4. **دستور مدیریت پروژه در چینش اولویتها:**
|
||||
* **اولویت اول:** ارتباط مستقیم در لایه شبکه و پروتکل کاربری (RPC / WebSocket) به دلیل مصرف ناچیز منابع (کمتر از ۲۰ مگابایت رم) و سرعت استخراج بالا.
|
||||
* **فالبک آخر (Last Resort):** کرال کردن بصری صفحه چت با Playwright صرفاً در شرایطی که ارتباط شبکهای به بنبست برسد.
|
||||
|
||||
---
|
||||
|
||||
## ۲. ماتریس مقایسهای جامع ۸ راهکار موجود
|
||||
|
||||
برای ایجاد دید همهجانبه، تمامی راهکارهای مطرح در اکوسیستم فنی در جدول زیر کالبدشکافی شدهاند (بدون ستونهای زائد زمانی؛ با تمرکز بر رفتار سیستمی):
|
||||
|
||||
| # | راهکار فنی | نحوه عملکرد | مصرف حافظه RAM | خلوص متن خروجی | پایداری در برابر تغییرات | ریسک عملیاتی | اولویت در پروژه ما |
|
||||
| :---: | :--- | :--- | :---: | :---: | :---: | :---: | :---: |
|
||||
| **۱** | **کلاینت شبکه و RPC بله (Web RPC / WS)** | فراخوانی مستقیم متد کشفشده `GetTranscript` در کلاینت کاربری پایتون | کمتر از ۲۰ مگابایت | متن خالص گفتار | متوسط (وابسته به ثبات متد) | احتمال نیاز به تمدید نشست (OTP) | ⭐⭐⭐ **اولویت اول و اصلی** |
|
||||
| **۲** | **اتوماسیون Playwright در `web.bale.ai`** | مرورگر بدون سربرگ (Headless)، کلیک روی دکمه و خواندن متن از DOM | ۳۵۰ تا ۵۰۰ مگابایت | متن خالص گفتار | بالا (مقاوم به پکتهای شبکه) | مصرف رم و سربار رندر المانها | ⚠️ **فالبک نهایی (Last Resort)** |
|
||||
| **۳** | **وبسرویسهای تجاری ایرانی (فارسآوا / آوانگار)** | اتصال رسمی به API شرکتهای پردازش صوت بومی (عامراندیش، پارتآی و...) | ناچیز (سمت سرور ثالث) | متن خالص گفتار | بسیار بالا (SLA تجاری) | ایجاد هزینه اشتراک ریالی برای دقایق صوت | ⭐⭐ **پیشنهاد به عنوان کاندید موازی** |
|
||||
| **۴** | **مدل متنباز خودمیزبان (Faster-Whisper)** | استقرار مدل `whisper-v3-turbo` با واژگان تدارکات روی سرور داخلی | ۱ تا ۳ گیگابایت | متن خالص گفتار | پایدار مطلق (بدون وابستگی) | نیاز به پردازنده گرافیکی (GPU) مناسب | ⭐⭐ **پرووایدر پشتیبان و مستقل** |
|
||||
| **۵** | **وبسرویسهای واسط غیررسمی (MajidAPI و...)** | ارسال لینک ویس به پراکسیهای عمومی رایگان | ناچیز (سمت سرور ثالث) | وابسته به موتور واسط | بسیار پایین و ناپایدار | فقدان SLA، احتمال قطعی ناگهانی و نشت صوت | ❌ **رد شده (غیرقابل اتکا)** |
|
||||
| **۶** | **چت هوش مصنوعی بله (اوراکل)** | ارسال ویس به ربات رسمی هوش مصنوعی درون بله | کمتر از ۲۰ مگابایت | **ناخالص** (متن + تحلیل LLM) | بسیار پایین | پاسخ تفسیری، تاخیر ۵ تا ۱۵ ثانیهای | ❌ **رد شده (تخریب اجماع)** |
|
||||
| **۷** | **شبیهساز اندروید (Appium / UIAutomator)** | اجرای اپلیکیشن اندروید بله روی ماشین مجازی و اسکرپ متن | بیش از ۲ گیگابایت | متن خالص گفتار | شکننده | سربار بسیار سنگین CPU/RAM سرور | ❌ **رد شده** |
|
||||
| **۸** | **سرویسهای ابری خارجی (OpenAI / Chirp 2)** | ارسال فایل به API رسمی Whisper یا Google Chirp | ناچیز (سمت سرور ثالث) | متن خالص گفتار | بسیار بالا | وابستگی به ارز و ریسک تحریم/اختلال شبکه | ⭐ **پرووایدر کمکی در صورت دسترسی** |
|
||||
|
||||
---
|
||||
|
||||
## ۳. فلوهای جامع تصویری (Architecture & Sequence Flows)
|
||||
|
||||
### فلو ۱: جریان کلان پردازش ویس در سیستم تدارکات (End-to-End System Flow)
|
||||
این دیاگرام نشان میدهد ویس ارسالی کاربر چگونه در خطلوله موازی پردازش شده و کاندید بله بدون ایجاد وقفه در سیستم ثبت استعلام مینشیند:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor User as کاربر تدارکات
|
||||
participant MainBot as بات اصلی تدارکات (Bale Bot API)
|
||||
participant STTPipeline as موتور چندمدله (bot/app/stt.py)
|
||||
participant BaleAdapter as میکروسرویس آداپتور بله (bale-stt:8080)
|
||||
participant OtherProviders as سایر پرووایدرها (OpenAI / محلی)
|
||||
participant ConsensusNLU as مدل استخراج هوشمند (DeepSeek NLU)
|
||||
|
||||
User->>MainBot: ارسال پیام صوتی استعلام خرید
|
||||
MainBot->>STTPipeline: دریافت ویس و تبدیل فرمت به MP3 (16kHz Mono)
|
||||
|
||||
par استعلام موازی از پرووایدرهای فعال
|
||||
STTPipeline->>BaleAdapter: POST /v1/audio/transcriptions (بایتهای ویس)
|
||||
STTPipeline->>OtherProviders: POST /v1/audio/transcriptions
|
||||
end
|
||||
|
||||
critical پردازش در کانتینر ایزوله کرالر بله
|
||||
BaleAdapter->>BaleAdapter: کنترل صف ترتیبی و اعمال تاخیر رفتاری (Jitter)
|
||||
BaleAdapter->>BaleAdapter: ارسال صوت به اکانت کرالر و فراخوانی متد GetTranscript
|
||||
BaleAdapter-->>BaleAdapter: دریافت متن پیادهشده و حذف پیام اسکرچ
|
||||
end
|
||||
|
||||
BaleAdapter-->>STTPipeline: بازگشت متن کاندید بله {"text": "..."}
|
||||
OtherProviders-->>STTPipeline: بازگشت متن سایر کاندیدها
|
||||
|
||||
STTPipeline->>ConsensusNLU: ارسال تمام کاندیدها به مدل اجماعگیر
|
||||
ConsensusNLU-->>MainBot: استخراج اقلام کالا، برند، مشخصات و تعداد
|
||||
MainBot-->>User: ارسال پیشفاکتور استعلام یا تاییدیه به کاربر
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### فلو ۲: جریان رله صوتی در کرالر (Relay Bot Pattern Flow)
|
||||
برای جلوگیری از فرآیند پرریسک و پیچیده «آپلود مستقیم فایل از اکانت شخصی»، از الگوی **بات رله** استفاده میشود:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Adapter as هسته آداپتور FastAPI
|
||||
participant RelayBot as بات رله اختصاصی (Bale Bot API)
|
||||
participant BaleServer as سرورهای مرکزی بله
|
||||
participant CrawlerUser as اکانت کاربری کرالر (WebSocket Client)
|
||||
|
||||
Adapter->>Adapter: دریافت فایل صوتی از خطلوله بات
|
||||
Adapter->>RelayBot: فراخوانی sendVoice(chat_id=کرالر, voice=audio, caption=nonce)
|
||||
RelayBot->>BaleServer: ارسال ویس با مجوز رسمی بات
|
||||
BaleServer-->>CrawlerUser: دریافت رویداد پیام صوتی جدید (دارای Media و MessageId و OutPeer)
|
||||
|
||||
CrawlerUser->>BaleServer: فراخوانی مستقیم RPC: GetTranscript(Media, OutPeer, MessageId)
|
||||
BaleServer-->>CrawlerUser: بازگشت پاسخ (mustWait / downloadSource)
|
||||
|
||||
opt در صورت فعال بودن mustWait
|
||||
CrawlerUser->>CrawlerUser: پولینگ سبک یا انتظار دریافت رویداد تکمیلی
|
||||
end
|
||||
|
||||
CrawlerUser->>BaleServer: فراخوانی DeleteMessage (حذف پیام برای حفظ بهداشت اکانت)
|
||||
CrawlerUser-->>Adapter: تحویل متن خالص استخراجشده
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### فلو ۳: فلو تابآوری و عدم توقف بات (Fail-Safe & Graceful Degradation)
|
||||
این فلو تضمین میکند که حتی در بدترین سناریوها (تغییر پروتکل بله، بن شدن اکانت یا قطعی سرور بله)، سیستم اصلی تدارکات دچار حتی یک ثانیه قطعی نخواهد شد:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start["کاربر ویس میفرستد"] --> Multi["اجرای همزمان transcribe_multi() در stt.py"]
|
||||
|
||||
Multi --> CallBale["ارسال درخواست به آداپتور بله"]
|
||||
Multi --> CallOthers["ارسال درخواست به سایر پرووایدرها"]
|
||||
|
||||
CallBale --> CheckBale{"آیا خطایی رخ داد؟ (بن/تغییر پروتکل/تایماوت)"}
|
||||
|
||||
CheckBale -- "بله" --> Resp503["آداپتور کد HTTP 503 برمیگرداند"]
|
||||
Resp503 --> LogWarn["تابع _call_single_stt هشدار ثبت کرده و None بازمیگرداند"]
|
||||
LogWarn --> IgnoreBale["کاندید بله بدون اختلال از لیست اجماع حذف میشود"]
|
||||
|
||||
CheckBale -- "خیر" --> Resp200["آداپتور متن رونویسیشده را تحویل میدهد"]
|
||||
Resp200 --> AddBale["متن بله به لیست کاندیدها اضافه میشود"]
|
||||
|
||||
CallOthers --> RespOthers["متنهای سایر پرووایدرها دریافت میشوند"]
|
||||
|
||||
IgnoreBale --> NLU["اجماع NLU روی سایر متنهای موجود انجام میشود"]
|
||||
AddBale --> NLU
|
||||
RespOthers --> NLU
|
||||
|
||||
NLU --> Done["استعلام با موفقیت ثبت و پاسخ به کاربر داده میشود"]
|
||||
|
||||
subgraph Rollback["عملیات قطع دائمی (در صورت لزوم)"]
|
||||
Env["خالی کردن متغیرهای PROCUREMENT_BOT_STT_PROVIDER_3_*"]
|
||||
ZeroDowntime["سیستم بدون ریاستارت یا دیپلوی مجدد به کار عادی ادامه میدهد"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### فلو ۴: سلسلهمراتب گزینش موتور در آداپتور (Engine Fallback Hierarchy)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
StartEngine["درخواست STT وارد آداپتور میشود"] --> ModeCheck{"بررسی تنظیم متغیر BALE_STT_ENGINE"}
|
||||
|
||||
ModeCheck -- "rpc (پیشفرض)" --> TryRPC["اجرای موتور E1: متد باینری GetTranscript"]
|
||||
TryRPC --> CheckRPC{"آیا رونویسی با موفقیت انجام شد؟"}
|
||||
|
||||
CheckRPC -- "بله" --> ReturnText["بازگرداندن متن (HTTP 200)"]
|
||||
CheckRPC -- "شکست در پروتکل شبکه" --> CheckFallback{"آیا فالبک خودکار مجاز است؟"}
|
||||
|
||||
CheckFallback -- "بله" --> TryPW["سوئیچ به موتور E2: اتوماسیون Playwright (فالبک آخر)"]
|
||||
TryPW --> CheckPW{"آیا متن از وب استخراج شد؟"}
|
||||
CheckPW -- "بله" --> ReturnText
|
||||
CheckPW -- "خیر" --> Fail503["بازگرداندن خطای HTTP 503 (Fail-Soft)"]
|
||||
|
||||
CheckFallback -- "خیر" --> Fail503
|
||||
ModeCheck -- "playwright" --> TryPW
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# بخش دوم: برای ایجنت هوش مصنوعی مجری (Technical Execution Specification)
|
||||
|
||||
## ۱. شناخت ساختار کدهای موجود در پروژه
|
||||
|
||||
ایجنت باید بداند بات تدارکات فعلی در فایلهای زیر پیادهسازی شده و قراردادهای آن به چه صورت است:
|
||||
|
||||
### ۱.۱ فایل [bot/app/stt.py](file:///c:/Users/Reza/Desktop/v1.5/bot/app/stt.py)
|
||||
* **قرارداد ارتباطی:** تابع `_call_single_stt` ریکوئست `POST` به اندپوینت `{base_url}/audio/transcriptions` با هدر `Authorization: Bearer {api_key}` ارسال میکند.
|
||||
* **فایل ارسالی:** فرم چندبخشی (Multipart) شامل کلید `file` با باینری صوتی MP3 (16kHz Mono)، `model` و `language="fa"`.
|
||||
* **مدیریت پاسخ:**
|
||||
* در صورت دریافت کد `200` و وجود کلید `text` غیرخالی، مقدار به عنوان کاندید معتبر ثبت میشود.
|
||||
* در صورت دریافت هر کد خطایی (مانند `503`) یا بروز استثنا، صرفاً لاگ هشدار ثبت شده و مقدار `None` بازمیگردد.
|
||||
* **تابع `transcribe_multi`:** تمام تسکها را با `asyncio.gather(..., return_exceptions=True)` اجرا میکند؛ یعنی شکست یک پرووایدر هرگز کل فرآیند را متوقف نمیکند.
|
||||
|
||||
### ۱.۲ فایل [bot/app/config.py](file:///c:/Users/Reza/Desktop/v1.5/bot/app/config.py)
|
||||
* متغیرهای محیطی با پیشوند `PROCUREMENT_BOT_STT_PROVIDER_N_*` (که N اعداد ۱ تا ۳ است) خوانده میشوند.
|
||||
* یک پرووایدر تنها زمانی فعال میشود که **هر دو فیلد `BASE_URL` و `API_KEY` مقداردهی شده باشند**. بنابراین با خالی کردن این دو فیلد، پرووایدر بله فوراً از مدار خارج میشود (Rollback آنی).
|
||||
|
||||
---
|
||||
|
||||
## ۲. مشخصات قطعی کشفشده از پروتکل بله
|
||||
|
||||
در بررسی و راستیآزمایی مخازن PyPI روی پکیج `bale-sdk` (نسخه 0.1.1)، متد و ساختار فیلدهای ترنسکریپشن دقیقاً استخراج شده است:
|
||||
|
||||
* **فضای نام و سرویس:** `bale.turing.v1.AI`
|
||||
* **متد باینری:** `GetTranscript`
|
||||
* **اسکیمای درخواست (`GetTranscriptRequest`):**
|
||||
```protobuf
|
||||
message GetTranscriptRequest {
|
||||
Media voice = 1; // آبجکت فایل صدا (شناسه و خصوصیات باینری)
|
||||
OutPeer outPeer = 2; // مقصد چت (پیر چت شخصی یا بات رله)
|
||||
MessageId messageId = 3;// شناسه پیام ثبتشده در چت
|
||||
}
|
||||
```
|
||||
* **اسکیمای پاسخ (`GetTranscriptResponse`):**
|
||||
```protobuf
|
||||
message GetTranscriptResponse {
|
||||
bool mustWait = 1; // فلگ بولی نشاندهنده در صف بودن پردازش سرور
|
||||
DownloadSource downloadSource = 2; // منبع یا لینک بافر متنی خروجی
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۳. ارزیابی کتابخانههای پایتون و استراتژی Clean-Room
|
||||
|
||||
| نام پکیج | نسخه در PyPI | مجوز حقوقی | وضعیت استفاده فنی در پروژه ما |
|
||||
| :--- | :---: | :---: | :--- |
|
||||
| **`bale-sdk`** | 0.1.1 | اختصاصی (All Rights Reserved) | **استفاده به عنوان مرجع پروتکل؛** به دلیل مجوز اختصاصی، کدهای آن کپی مستقیم نمیشوند بلکه ساختار پیامها بر اساس کپچر بازسازی میگردد. |
|
||||
| **`autobale`** | 1.0.0 | **MIT** (متنباز) | **مناسب برای هسته ارتباطی؛** فورک فعال و بازنویسیشده از `aiobale` که بر بستر WebSocket و Dispatcher ناهمگام کار میکند. |
|
||||
| **`aiobale-py`** | 0.3.9 | عمومی | دارای لایه وبسوکت و هندلرهای باینری مبتنی بر `blackboxprotobuf` (`bbpb`). |
|
||||
| **`bbpb` / `blackboxprotobuf`** | آخرین نسخه | MIT | ابزار دیکود و انکود فریمهای باینری پروتوباف بدون نیاز به کامپایل فایلهای `.proto`. |
|
||||
|
||||
---
|
||||
|
||||
## ۴. ورودیهای انسانی مورد نیاز ایجنت (Agent Inputs)
|
||||
|
||||
ایجنت قبل از راهاندازی، این ۵ مورد را از انسان دریافت میکند:
|
||||
1. `BALE_PHONE`: شماره سیمکارت شرکتی اختصاصی جهت ورود به اکانت کاربری.
|
||||
2. `BALE_OTP_CODE`: کد پیامکی ۶ رقمی یکبارمصرف در زمان اجرای اسکریپت ورود اولیه.
|
||||
3. `BALE_RELAY_BOT_TOKEN`: توکن بات رله اختصاصی که از BotFather بله دریافت شده است.
|
||||
4. `BALE_STT_INTERNAL_KEY`: کلید امنیتی تصادفی برای هدر `Authorization: Bearer <KEY>`.
|
||||
5. `CRAWLER_USER_ID`: شناسه عددی کاربری اکانت کرالر (جهت ارسال پیام توسط بات رله).
|
||||
|
||||
---
|
||||
|
||||
## ۵. کدهای مرجع میکروسرویس آداپتور (`bale-stt`)
|
||||
|
||||
### ۵.۱ ساختار پروژه ایجنت
|
||||
```
|
||||
bale-stt/
|
||||
├── adapter/
|
||||
│ ├── main.py # میکروسرویس FastAPI سازگار با قرارداد OpenAI
|
||||
│ ├── counters.py # مدیریت سقف روزانه با دیتابیس پایدار SQLite
|
||||
│ ├── config.py # تنظیمات محیطی بدون مقدار پیشفرض خطرناک
|
||||
│ ├── engines/
|
||||
│ │ ├── base.py # اینترفیس استاندارد موتورها
|
||||
│ │ ├── rpc.py # موتور اولویت ۱: ارتباط RPC و الگوی بات رله
|
||||
│ │ └── playwright.py # موتور فالبک آخر: اتوماسیون صفحه وب بله
|
||||
│ ├── Dockerfile
|
||||
│ └── requirements.txt
|
||||
├── docker-compose.stt.yml
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
### ۵.۲ مدیریت سقف روزانه (`adapter/counters.py`)
|
||||
```python
|
||||
import os
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
from datetime import date
|
||||
|
||||
DB_PATH = os.getenv("BALE_STT_DB", "/app/state/usage.db")
|
||||
|
||||
def _init_db() -> sqlite3.Connection:
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
con = sqlite3.connect(DB_PATH, timeout=5)
|
||||
con.execute("CREATE TABLE IF NOT EXISTS usage (day TEXT PRIMARY KEY, count INTEGER NOT NULL)")
|
||||
return con
|
||||
|
||||
def get_today_count() -> int:
|
||||
with closing(_init_db()) as con:
|
||||
row = con.execute("SELECT count FROM usage WHERE day = ?", (date.today().isoformat(),)).fetchone()
|
||||
return row[0] if row else 0
|
||||
|
||||
def increment_today_count() -> None:
|
||||
with closing(_init_db()) as con, con:
|
||||
con.execute(
|
||||
"INSERT INTO usage(day, count) VALUES (?, 1) "
|
||||
"ON CONFLICT(day) DO UPDATE SET count = count + 1",
|
||||
(date.today().isoformat(),),
|
||||
)
|
||||
```
|
||||
|
||||
### ۵.۳ کد اصلی آداپتور (`adapter/main.py`)
|
||||
```python
|
||||
import os
|
||||
import secrets
|
||||
import asyncio
|
||||
import logging
|
||||
from fastapi import FastAPI, File, Form, Header, HTTPException, UploadFile
|
||||
|
||||
from counters import get_today_count, increment_today_count
|
||||
from engines.base import get_engine
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("bale-stt-adapter")
|
||||
|
||||
app = FastAPI(title="Bale STT OpenAI Adapter")
|
||||
|
||||
INTERNAL_KEY = os.environ["BALE_STT_INTERNAL_KEY"]
|
||||
DAILY_CAP = int(os.getenv("BALE_STT_DAILY_CAP", "200"))
|
||||
TIMEOUT_SECONDS = float(os.getenv("BALE_STT_TIMEOUT", "25.0"))
|
||||
|
||||
# قفل همزمانی برای پردازش تکاکانت کاربری
|
||||
account_lock = asyncio.Lock()
|
||||
engine = get_engine()
|
||||
|
||||
@app.get("/healthz")
|
||||
def health_check():
|
||||
return {
|
||||
"status": "ok",
|
||||
"engine": engine.name,
|
||||
"used_today": get_today_count(),
|
||||
"cap": DAILY_CAP
|
||||
}
|
||||
|
||||
@app.post("/v1/audio/transcriptions")
|
||||
async def transcribe(
|
||||
file: UploadFile = File(...),
|
||||
model: str = Form("bale-stt"),
|
||||
language: str = Form("fa"),
|
||||
authorization: str = Header("")
|
||||
):
|
||||
# ۱. اعتبارسنجی امن کلید احراز هویت (مقاوم به Timing Attack)
|
||||
expected_header = f"Bearer {INTERNAL_KEY}"
|
||||
if not secrets.compare_digest(authorization.encode(), expected_header.encode()):
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
# ۲. کنترل سقف مصرف مجاز روزانه
|
||||
if get_today_count() >= DAILY_CAP:
|
||||
log.warning("Daily STT capacity reached (%d)", DAILY_CAP)
|
||||
raise HTTPException(status_code=429, detail="Daily quota exhausted")
|
||||
|
||||
audio_bytes = await file.read()
|
||||
if not audio_bytes:
|
||||
raise HTTPException(status_code=400, detail="Empty audio payload")
|
||||
|
||||
# ۳. ورود به صف ترتیبی با فاصله زمانی تصادفی انسانی
|
||||
async with account_lock:
|
||||
try:
|
||||
# اعمال Jitter بین ۱ تا ۲.۵ ثانیه
|
||||
await asyncio.sleep(1.0 + (secrets.randbelow(15) / 10.0))
|
||||
|
||||
# فراخوانی موتور فعال با سقف زمانی مجاز
|
||||
text = await asyncio.wait_for(
|
||||
engine.transcribe(audio_bytes, filename=file.filename or "voice.mp3"),
|
||||
timeout=TIMEOUT_SECONDS
|
||||
)
|
||||
|
||||
cleaned_text = (text or "").strip()
|
||||
if not cleaned_text:
|
||||
raise RuntimeError("Received empty transcript from Bale")
|
||||
|
||||
increment_today_count()
|
||||
log.info("Transcription successful (length=%d)", len(cleaned_text))
|
||||
return {"text": cleaned_text}
|
||||
|
||||
except Exception as exc:
|
||||
log.exception("Transcription failed in engine %s: %s", engine.name, exc)
|
||||
# پرتاب 503 حیاتی است: باعث میشود bot/app/stt.py کاندید را نادیده بگیرد
|
||||
raise HTTPException(status_code=503, detail="Bale STT Service Temporarily Unavailable")
|
||||
```
|
||||
|
||||
### ۵.۴ موتور RPC با الگوی بات رله (`adapter/engines/rpc.py`)
|
||||
```python
|
||||
import os
|
||||
import uuid
|
||||
import asyncio
|
||||
import httpx
|
||||
from engines.base import BaseEngine
|
||||
|
||||
class RpcRelayEngine(BaseEngine):
|
||||
name = "rpc"
|
||||
|
||||
def __init__(self):
|
||||
self.bot_token = os.environ["BALE_RELAY_BOT_TOKEN"]
|
||||
self.crawler_user_id = int(os.environ["BALE_CRAWLER_USER_ID"])
|
||||
self.api_url = f"https://tapi.bale.ai/bot{self.bot_token}"
|
||||
|
||||
async def transcribe(self, audio: bytes, filename: str) -> str:
|
||||
nonce = str(uuid.uuid4())[:8]
|
||||
|
||||
# ۱. ارسال ویس با Bot API رسمی به چت اکانت کرالر
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(
|
||||
f"{self.api_url}/sendVoice",
|
||||
data={"chat_id": self.crawler_user_id, "caption": nonce},
|
||||
files={"voice": ("voice.ogg", audio, "audio/ogg")}
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"Relay bot sendVoice failed: {resp.text}")
|
||||
|
||||
sent_message_id = resp.json()["result"]["message_id"]
|
||||
|
||||
try:
|
||||
# ۲. دریافت رویداد پیام در کلاینت کاربری و فراخوانی GetTranscript
|
||||
from driver.bale_client import get_client
|
||||
bale_client = await get_client()
|
||||
|
||||
transcript_text = await bale_client.call_get_transcript(
|
||||
message_id=sent_message_id,
|
||||
nonce=nonce,
|
||||
timeout=18.0
|
||||
)
|
||||
return transcript_text
|
||||
|
||||
finally:
|
||||
# ۳. پاکسازی بهداشتی: حذف پیام از چت
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
await client.post(
|
||||
f"{self.api_url}/deleteMessage",
|
||||
json={"chat_id": self.crawler_user_id, "message_id": sent_message_id}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۶. تنظیمات داکر و یکپارچهسازی با بات اصلی
|
||||
|
||||
### ۶.۱ فایل `adapter/Dockerfile`
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
ENV TZ=Asia/Tehran PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
```
|
||||
|
||||
### ۶.۲ فایل `docker-compose.stt.yml`
|
||||
```yaml
|
||||
services:
|
||||
bale-stt:
|
||||
build:
|
||||
context: ./adapter
|
||||
dockerfile: Dockerfile
|
||||
container_name: bale-stt-adapter
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Tehran
|
||||
- BALE_STT_ENGINE=rpc
|
||||
- BALE_STT_INTERNAL_KEY=${BALE_STT_INTERNAL_KEY}
|
||||
- BALE_STT_DAILY_CAP=200
|
||||
- BALE_RELAY_BOT_TOKEN=${BALE_RELAY_BOT_TOKEN}
|
||||
- BALE_CRAWLER_USER_ID=${BALE_CRAWLER_USER_ID}
|
||||
volumes:
|
||||
- bale_session_data:/app/session
|
||||
- bale_usage_data:/app/state
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/healthz')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
bale_session_data:
|
||||
bale_usage_data:
|
||||
|
||||
networks:
|
||||
bot_network:
|
||||
external: true
|
||||
```
|
||||
|
||||
### ۶.۳ تنظیمات متغیرهای محیطی در فایل `.env` بات اصلی
|
||||
اتصال به بات اصلی **بدون دستکاری حتی ۱ خط از کدهای پایتون بات** انجام میپذیرد:
|
||||
```env
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_NAME=bale
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_BASE_URL=http://bale-stt:8080/v1
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_API_KEY=${BALE_STT_INTERNAL_KEY}
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_MODEL=bale-stt
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_LANGUAGE=fa
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_TIMEOUT=30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۷. چکلیست گامبهگام پیادهسازی خودکار توسط ایجنت
|
||||
|
||||
ایجنت مجری برای به سرانجام رساندن پروژه، این چکلیست عملیاتی را بدون معطلی و به ترتیب اجرا میکند:
|
||||
|
||||
* [ ] **گام ۱ (لاگین کاربری و ذخیره سشن):** اجرای اسکریپت CLI برای ثبت شماره `BALE_PHONE`، دریافت کد `BALE_OTP_CODE` از انسان، صدور توکن پایدار JWT و ذخیره در مسیر `/app/session/session.json`.
|
||||
* [ ] **گام ۲ (تست اتصال بات رله):** تست ارسال یک صوت آزمایشی با `sendVoice` از بات رله به `BALE_CRAWLER_USER_ID` و اطمینان از دریافت وبسوکت در کلاینت کرالر.
|
||||
* [ ] **گام ۳ (اعتبارسنجی فراخوانی `GetTranscript`):** پاس دادن `Media` و `MessageId` به متد باینری و دریافت رشته متن فارسی.
|
||||
* [ ] **گام ۴ (بیلد کانتینر آداپتور FastAPI):** بیلد ایمیج داکر و اجرای محلی روی پورت 8080.
|
||||
* [ ] **گام ۵ (تست تطابق با قرارداد OpenAI):** ارسال ریکوئست تستی با `curl` به `/v1/audio/transcriptions` و اعتبارسنجی پاسخ `{"text": "..."}`.
|
||||
* [ ] **گام ۶ (تست سناریوی خطا):** قطع عمدی اتصال بله و حصول اطمینان از بازگشت کد `HTTP 503` (جهت تضمین عدم توقف بات اصلی).
|
||||
* [ ] **گام ۷ (فعالسازی در بات تدارکات):** قرار دادن مقادیر در `.env` بات اصلی.
|
||||
* [ ] **گام ۸ (تست سراسری E2E):** ارسال پیام صوتی استعلام قیمت در پیامرسان بله و مشاهده لاگهای `bot.stt` مبنی بر رونویسی موفق کاندید بله و اجماع در مدل DeepSeek NLU.
|
||||
Reference in New Issue
Block a user