feat(bot): add strict whitelist auth bypass, multi-STT consensus pipeline, and applicant logging
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
# پلیبوک اجرایی: افزودن موتور STT بله به خط لوله چندمدلهی بات تدارکات
|
||||
|
||||
> **نسخه:** ۱.۰
|
||||
> **مخاطب:** ایجنت هوش مصنوعی مجری + تیم فنی + مدیر محصول
|
||||
> **ماهیت سند:** سند تصمیممحور — هر تست به یک «دروازهی تصمیم (Gate)» میرسد و هر دروازه، جدول «وضعیت مشاهدهشده ← اقدام» دارد. ایجنت باید فقط بر اساس وضعیتهای واقعیِ مشاهدهشده در همان لحظه، شاخهی درست را انتخاب کند.
|
||||
> **هدف نهایی:** افزودن ترنسکریپشن موتور بله (STT سمت سرور بله) بهعنوان یکی از کاندیداهای خط لولهی Multi-Provider STT فعلیِ بات، بدون هیچ تغییر ساختاری در بات.
|
||||
|
||||
---
|
||||
|
||||
## ۰. قواعد الزامآور اجرا (برای ایجنت — قبل از هر اقدام بخوانید)
|
||||
|
||||
1. **مبنای گیت فقط `production` است.** سایر برنچها (از جمله `prod-v7` که کد بات روی آن است) متعلق به جریان کاری خودشاناند و **نباید** توسط ایجنت دستکاری، مرج یا پوش شوند. تنها مهندس ارشد شرکت مرج میکند. **هیچ پوشی بدون تأیید صریح مالک انجام نشود.**
|
||||
2. **درخواستهای شبکه به بله باید کمحجم و انساننما باشند.** هشدار جدی: سازندگان کتابخانههای مهندسیمعکوس بله تأکید کردهاند که «استفادهی زیاد از POST gRPCِ غیر-احراز (پیام/مدیا/حضور) میتواند فلگ، rate-limit یا بن موقت ایجاد کند». پس: ترجیح کانال WebSocket، ترتیبی نه موازی، تأخیر انسانی، سقف روزانه.
|
||||
3. **حسابها:** فقط شمارهی اختصاصی شرکت (Bale ئرنر) — هرگز شمارهی شخصی. حساب فقط برای این کار باشد.
|
||||
4. **حریم خصوصی:** صدای کاربران تدارکات روی سرورهای بله پردازش میشود (همین حالا هم ویس از بله میآید)؛ اما اگر ایجنت صدا را مجدداً به یک چت آپلود میکند (مسیر R2/R3)، باید بلافاصله پس از ترنسکرایب پیام را حذف کند و آدرسی از صدا نگه ندارد.
|
||||
5. **هیچ سکرتی (توکن بات، توکن کاربر، session_file، کلید LLM) نباید در گیت کامیت شود.** سکرتهای موجود در `docker-compose.yml` (توکن بات و کلید DeepSeek بهعنوان پیشفرض) باید در فرصت مناسب rotate و به `${VAR:?…}` تبدیل شوند.
|
||||
6. **معیار موفقیت، سرعت نیست — دقت است.** بدون عبور از دروازهی دقت (G5) هیچ ساخت/استقراری مجاز نیست.
|
||||
|
||||
---
|
||||
|
||||
## ۱. واقعیتهای تأییدشده (پایهی همهی تصمیمها)
|
||||
|
||||
### ۱.۱ — سیستم فعلی: بات و خط لولهی STT
|
||||
|
||||
| مؤلفه | مسیر | توضیح |
|
||||
|---|---|---|
|
||||
| خط لوله STT | `bot/app/stt.py` | `transcribe_multi()` صدای MP3 (خروجی ffmpeg: mono 16kHz) را **به موازات** به همهی پرووایدرها میفرستد؛ هر پرووایدر یک `POST {base_url}/audio/transcriptions` (multipart، فیلد `file`، بههمراه `model` و `language=fa`) است و خروجی `{"text": "..."}` انتظار دارد. **fail-soft:** خطای هر پرووایدر فقط آن را حذف میکند (`return None`)؛ بقیه ادامه میدهند. |
|
||||
| پیکربندی پرووایدرها | `bot/app/config.py` | `STT_PROVIDER_1..3_NAME/BASE_URL/API_KEY/MODEL/LANGUAGE` + `STT_PROVIDERS_JSON` + fallback قدیمی `STT_*`. نکته: یک پرووایدر فقط وقتی فعال میشود که **هم `BASE_URL` و هم `API_KEY`** پر باشند. |
|
||||
| مصرف خروجیها | `bot/app/nlu.py` | همهی ترنسکریپتهای کاندید به DeepSeek میروند و خروجی نهایی `consensus_text` + `intent` + `patch` (اقلام استعلام) است. **یعنی افزودن بله = یک کاندید بیشتر در ورودی consensus.** |
|
||||
| مسیر ویس در بات | `bot/app/handlers/__init__.py` | ویس → دانلود (`getFile`) → `transcribe_multi` → متن بهعنوان ورودی NLU. |
|
||||
|
||||
**نتیجهی معماری (کلیدی):** افزودن هر مسیر STT = ساخت **یک سرویس adapter با قرارداد OpenAI-compatible** + پر کردن `STT_PROVIDER_3_*`. **صفر تغییر در کد بات.** (این نقطهی اتصال رسمی و پایدار است.)
|
||||
|
||||
### ۱.۲ — شواهد تجربی Bot API (تستشده — تکرارپذیر)
|
||||
|
||||
روی توکن بات فعلی (`procurement_assistant_bot`)، متدهای مشکوک STT تست شدند:
|
||||
|
||||
```
|
||||
POST https://tapi.bale.ai/bot<TOKEN>/transcribeVoice → {"ok":false,"error_code":404,"description":"Bad Request: handler not found"}
|
||||
POST .../getTranscription → (همان 404)
|
||||
POST .../transcribe → (همان 404)
|
||||
POST .../speechToText → (همان 404)
|
||||
POST .../voiceToText → (همان 404)
|
||||
POST .../getMe (کنترل) → {"ok":true,"result":{"username":"procurement_assistant_bot",...}}
|
||||
```
|
||||
|
||||
مستندات رسمی (`docs.bale.ai`) هم هیچ متد یا فیلد ترنسکریپشن برای آبجکت Voice/Audio ندارند.
|
||||
**نتیجه: Bot API بله قطعاً STT ندارد. مسیر «فیلد مخفی» فقط با تست T1 (بخش ۳) قابل رد شدن نهایی است.**
|
||||
|
||||
### ۱.۳ — یافتههای گزارشهای ایجنت شرکت (خلاصهی راستیآزماییشده)
|
||||
|
||||
| یافته | وضعیت راستیآزمایی |
|
||||
|---|---|
|
||||
| کلاینت وب بله روی **gRPC-Web + Protocol Buffers** (`@bufbuild/protobuf`) است | **منطقی و همراستا:** کتابخانههای مستقل مهندسیمعکوس بله هم همین را تأیید میکنند؛ باید در T2 مجدداً مشاهده شود. |
|
||||
| گیتویها: `https://maviz-ws.bale.ai` و `wss://maviz-ws.bale.ai/ws/?uid={user_id}` (+ بتا/نکست) | در وب ایندکس نشده (طبیعی) — **باید در T2 با چشم مشاهده شود.** |
|
||||
| ماژولهای `SpeechTranscript` و `AIChat` در باندل | سرنخ معتبر — **باید در T2/T3 تأیید و دیکود شود.** |
|
||||
| «روش دوم غیرعملی است چون باید کل .protoها بازسازی شوند» | **نادرست/بیشبرآورد:** با دیکود جنریک Protobuf و استخراج توصیفگر از خود باندل (T3) یا با استفاده از aiobale-py (T4)، این کار در حد «افزودن یک متد» کوچک میشود. |
|
||||
| ماتریس تأخیر («روش دوم زیر ۱ ثانیه») | **گمراهکننده:** گلوگاه، استنتاج ASR روی سرور بله است؛ عدد واقعی فقط از T2/T5 میآید. |
|
||||
|
||||
### ۱.۴ — اکوسیستم مهندسی معکوس موجود (مهم — دارایی راهبردی)
|
||||
|
||||
- **`aiobale-py`** (v0.3.9، فعال): فریمورک async برای بله روی **WebSocket + Protobuf**، الهامگرفته از aiogram؛ ~۸۰ متد RPC (پیام، فایل، گروه، مخاطبین…)، ورود تلفنی با OTP، `session_file`، توکن JWT، rate-limiter داخلی، docs و مخزن عمومی:
|
||||
- PyPI: `pip install aiobale-py` (ایمپورت: `aiobale`)
|
||||
- GitHub: `github.com/aminmadaniofficial/aiobale` — مستندات: `aminmadaniofficial.github.io/aiobale/`
|
||||
- **متد ترنسکرایب ندارد** → باید همان RPC کشفشده در T2 به آن افزوده شود (زیرساخت لاگین/WS/کدک آماده است).
|
||||
- نسخهی اولیهی `Enalite/aiobale` (GitHub الان 404؛ نام PyPI توسط یک پکیج خالی اشغال شده) → **فقط به فورک فعال `aiobale-py` اتکا کنید.**
|
||||
- پروژهی جانبی مرتبط: `bale-mcp-server` (MCP برای بله با aiobale-py) — برای الگوگیری نحوهی استفاده.
|
||||
- سرویس ثالث STT: [majidapi.ir](https://majidapi.ir) (`/ai/stt?url=...&token=...`) — مسیر جایگزین R7 (بخش ۵).
|
||||
|
||||
### ۱.۵ — وضعیت برنچها (قواعد همکاری)
|
||||
|
||||
| برنچ | وضعیت | مجاز برای ایجنت؟ |
|
||||
|---|---|---|
|
||||
| `production` (`253c922`) | مبنای رسمی کار | ✅ فقط خواندن/مبنا |
|
||||
| `prod-v7` (کد بات/ایجنت، `638076d`) | کار مالک این سند؛ منتظر تست نهایی و مرج توسط مهندس ارشد | ⛔ دستکاری/پوش ممنوع |
|
||||
| `prod-v8/9/10`، `pm`، `pm_2` و بقیه | برنچهای سایر اعضا؛ در حال تست | ⛔ هیچ دخالتی |
|
||||
|
||||
---
|
||||
|
||||
## ۲. نقشهی وضعیتها (State Machine) — نمای کلی مسیر
|
||||
|
||||
```
|
||||
S0 (شروع)
|
||||
│
|
||||
├─T1─> G1 ──(فیلد ترنسکریپشن پیدا شد)───────────────► R1: پایان! (بدون کرالر) ✅
|
||||
│ └─(پیدا نشد)──► T2
|
||||
│
|
||||
├─T2─> G2 ──(RPC ساده/دیکودپذیر)──► T3+T4 ─> G3 ──(موفق)──► T5+ G4 ─► T7 ─> G5
|
||||
│ ├─(فقط روی WS)──────────► همان T3+T4 (aiobale مسیر WS است) ▲
|
||||
│ ├─(رمزنگاری/امضای خاص)──► R4 (Playwright) یا R3 ─► T5/T7
|
||||
│ └─(نیاز به اثر انگشت مرورگر)─► R4 ─► T5/T7
|
||||
│
|
||||
├─(T5/T7)─> G5 ──(بهبود دقت کافی)──► G6 (ToS) ──(سبز)──► BUILD (R2/R3/R4) ─► G7 (سواک) ─► استقرار کامل
|
||||
│ └─(بهبود ناکافی)──► STOP: همان پرووایدرهای فعلی ⛔
|
||||
│ └─(قرمز)──► STOP یا فقط R4 در حد محدود ⛔
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۳. فاز A — تستهای کشف (T1…T9)
|
||||
|
||||
> برای هر تست: هدف، پیشنیاز، مراحل، خروجی، معیار قبولی، و حالتهای ممکن. ایجنت بعد از هر تست **حتماً** به جدول دروازهی مربوطه (بخش ۴) مراجعه کند.
|
||||
|
||||
### T1 — تست payload خام ویس (۳۰ دقیقه)
|
||||
|
||||
**هدف:** رد یا تأیید نهایی وجود فیلد ترنسکریپشن مخفی در پیامهای Bot API.
|
||||
**پیشنیاز:** یک ربات تستی **مستقل** (از BotFather بله بسازید؛ از ربات فعلی استفاده نکنید چون در حال long-poll است و مصرفکنندهی دوم تعارض ایجاد میکند). یک حساب بله برای فرستادن ویس.
|
||||
**مراحل:**
|
||||
1. اسکریپت مینیمال پایتون: `getUpdates` بدون offset + چاپ JSON کامل هر آپدیت در فایل.
|
||||
2. یک ویس فارسی واقعی (۵–۱۰ ثانیه) به ربات تستی بفرستید.
|
||||
3. JSON کامل ویس را ذخیره کنید؛ کلیدهای آبجکت `voice`/`audio`/`message` را لیست کنید.
|
||||
4. `getFile` را هم صدا بزنید و فیلدهای پاسخ را ضبط کنید.
|
||||
**معیار قبولی/حالتها:**
|
||||
- **حالت A:** هر فیلدی مثل `transcription`/`transcript`/`speech`/`text` روی ویس وجود دارد → **پایان پروژهی کرالر (R1)**؛ فقط به بات وصل کنید.
|
||||
- **حالت B:** خبری نیست (قویترین احتمال طبق مستندات) → برو به T2.
|
||||
|
||||
### T2 — کپچر ترافیک کلاینت وب (نیمروز) — قاطعترین تست
|
||||
|
||||
**هدف:** کشف «دقیق» مکانیزم ترنسکرایب: نام RPC، ترابری (unary/WS)، قالب فریم، هدر احراز، و **زمان واقعی ASR**.
|
||||
**پیشنیاز:** حساب سوختهی اختصاصی (سیمکارت شرکت)، مرورگر Chromium، ابزار دیکود Protobuf: `protoscope` یا `blackboxprotobuf` (نصب با pip/go)، و اختیاری mitmproxy.
|
||||
**مراحل:**
|
||||
1. ورود به `web.bale.ai`؛ DevTools → Network با گزینهی Preserve log؛ فیلتر `WS` و `Fetch/XHR` را فعال نگه دارید.
|
||||
2. یک ویس در یک چت باز کنید و دکمهی «تبدیل به متن / متن پیاده» را بزنید.
|
||||
3. **همهی درخواستهای همان لحظه** را ضبط کنید:
|
||||
- اگر `POST` روی هاستی مثل `maviz-ws.bale.ai` با `content-type: application/grpc-web+proto` (یا `grpc-web-text`) ظاهر شد → همین RPC است.
|
||||
- اگر قاب باینری روی WebSocket `wss://…/ws/?uid=…` رفت → RPC روی WS است.
|
||||
4. برای هر نمونه ذخیره کنید: `req.bin`، `resp.bin`، هدرهای مهم (نام هدر توکن: `authorization`/`x-auth`/…)، URL کامل، و **زمان پاسخ از تب Timing** (همین = تأخیر واقعی ASR بله).
|
||||
5. ۵ تا ۱۰ ویس متنوع (کوتاه/بلند/با لهجه) تکرار کنید تا پایداری اسکیما معلوم شود؛ در `samples/` ذخیره و در `findings.md` خلاصه کنید.
|
||||
6. دیکود آفلاین: به یاد داشته باشید پیامهای gRPC-Web یک **پیشوند ۵ بایتی** دارند (۱ بایت فلگ + ۴ بایت طول) و سپس Protobuf میآید → ابتدا پیشوند را بردارید، سپس:
|
||||
`protoc --decode_raw < resp_payload.bin` یا `protoscope resp_payload.bin`
|
||||
7. اگر آرایهی WS دیده شد: فریمها را از DevTools کپی و با protoscope دیکود کنید؛ به دنبال پیام معادل `SpeechTranscript` بگردید.
|
||||
**خروجی:** `capture/findings.md` شامل: نام دقیق RPC، ترابری، framing، هدر احراز، تأخیر واقعی، و نقشهی مقدماتی فیلدها (کدام فیلد = message_id؟ file_id؟ متن؟).
|
||||
|
||||
### T3 — استخراج اسکیما (۱–۲ روز)
|
||||
|
||||
**هدف:** تبدیل کپچرها به یک اسکیمای قابل استفاده (و ترجیحاً `.proto`).
|
||||
**روشها (به ترتیب):**
|
||||
1. **از خود باندل:** باندل وب بله (`/static/js/index.*.js` و در صورت وجود `.js.map`) را از قبل دارید؛ پروژههای ساختهشده با `@bufbuild/protobuf` معمولاً **`FileDescriptorProto` باینری را داخل JS جاسازی میکنند**. روش عملی: جستوجوی بایتهای توصیفگر، استخراج و سپس:
|
||||
`protoc --decode=google.protobuf.FileDescriptorProto google/protobuf/descriptor.proto < desc.bin`
|
||||
(یا دیکود همان بایتها با پایتون + `grpcio-tools`). اگر sourcemap موجود بود، مستقیمتر است.
|
||||
2. **از نمونهها:** با چند نمونهی req/resp دیکودشده با `protoc --decode_raw`، همبستگی فیلدها را استخراج کنید (فیلد شمارهی متن جواب، message_id درخواست و…).
|
||||
3. **مسیر موازی (اپ اندروید):** APK رسمی بله (از سایت رسمی) + `jadx`؛ جستوجوی `"SpeechTranscript"`؛ در اندروید معمولاً شمارهی فیلدها واضحترند.
|
||||
**معیار قبولی:** توان ساخت درخواست معتبر و خواندن پاسخ — حتی بهصورت «فیلد ۱ = message_id، فیلد ۲ = متن». داشتن `.proto` کامل لازم نیست.
|
||||
|
||||
### T4 — اسپایک aiobale-py (۱ روز)
|
||||
|
||||
**هدف:** اثبات امکان اجرای ترنسکرایب با یک اکانت کاربری، بدون مرورگر.
|
||||
**پیشنیاز:** venv ایزوله (**نه** محیط بات)، حساب سوخته، اسکیمای T3.
|
||||
**مراحل:**
|
||||
1. `pip install aiobale-py` → ورود: `Client(session_file="bale.session", phone_number="…")` و عبور از OTP (دروازهی انسانی).
|
||||
2. کاوش لایهی کدک: در `site-packages/aiobale/…` ببینید متدها چطور تعریف میشوند (احتمالاً مپینگ متد→پیام Protobuf). **یک متد سفارشی برای RPC ترنسکریپت کشفشده اضافه کنید.**
|
||||
3. **کشف مکانیزم دقیق:** آیا RPC ترنسکرایب `message_id` میخواهد (یعنی صدا باید از قبل «پیامی در بله» باشد) یا `file_id`/بایت خام قبول میکند؟ هر سه فرض را تست کنید:
|
||||
- (الف) آپلود ویس به یک **چت اسکرچ** (چت با خود یا گروه خصوصی اختصاصی) → گرفتن `message_id` → ترنسکرایب → **حذف پیام**.
|
||||
- (ب) `upload_file` (در aiobale موجود است) → ترنسکرایب با فایل.
|
||||
- (ج) اگر RPC ورودی بایت میگیرد → سادهترین (بدون پیام واسط).
|
||||
4. تست صحت: متن برگشتی را با متنی که وب بله برای **همان ویس** نشان میدهد مقایسه کنید (باید یکسان/نزدیک باشد).
|
||||
5. تست پایداری نشست: ریاستارت پروسه و استفادهی مجدد از session file؛ ثبت زمان انقضای توکن.
|
||||
**حالتها:**
|
||||
- **موفق کامل** → مسیر R2 برای ساخت adapter انتخاب میشود.
|
||||
- **نیمهموفق** (login/WS میشود ولی RPC دیکود نمیشود) → T3 را عمیقتر + کپچر بیشتر؛ در غیر این صورت R3/R4.
|
||||
- **ناموفق** (چالش احراز، بن، نبود قابلیت افزودن متد) → R3 و R4.
|
||||
|
||||
### T-extra — تست «AI Chat بهعنوان اوراکل» (نیمروز، موازی با T2/T4)
|
||||
|
||||
**هدف:** اگر چت هوش مصنوعی بله ویس را با متن پیادهشده پاسخ میدهد، **حتی نیازی به RPC جدید نیست**: با متدهای پیامرسانیِ *موجودِ* aiobale، ویس را به چت AI بفرستید و متن را از پاسخ استخراج کنید.
|
||||
**مراحل:** با اکانت سوخته در وب بله، یک ویس به چت AI بفرستید؛ دقیقاً ببینید آیا متن پیادهشده در پاسخ میآید و با چه قالبی. سپس همان را با aiobale بازسازی کنید.
|
||||
**معیار:** اگر متن پیاده با قالب قابلاعتماد (ثابت) برگردد → مسیر R3 فعال میشود (ارزانتر از R2).
|
||||
|
||||
### T5 — اندازهگیری تأخیر/پایداری واقعی (۱–۲ روز)
|
||||
|
||||
**هدف:** جایگزینی اعداد ساختگی گزارش با اعداد واقعی.
|
||||
**مراحل:** ۵۰–۱۰۰ ویس واقعی از مسیر انتخابی (ترتیبی، با فاصلهی انسانی ۱–۳ ثانیه)؛ ثبت p50/p95، نرخ خطا، انواع خطا، و هر نشانهی rate-limit.
|
||||
**معیار پذیرش پیشنهادی:** نرخ موفقیت ≥ ۹۵٪ و p95 ≤ ۸ ثانیه (برای UX بات). در غیر این صورت G4 قرمز میشود.
|
||||
|
||||
### T7 — بنچمارک دقت (۱ روز) — **دروازهی کسبوکاری، قبل از هر ساخت**
|
||||
|
||||
**هدف:** آیا افزودن بله واقعاً خروجی نهایی را بهتر میکند؟
|
||||
**مراحل:**
|
||||
1. ۲۰–۳۰ ویس **واقعی تدارکاتی** جمع کنید (با اطلاع فرستندگان؛ صداها را بلندمدت نگه ندارید).
|
||||
2. برای هر ویس سه خروجی بگیرید: (الف) وضعیت فعلی (whisper/سایر + consensus فعلی)، (ب) فقط متن بله، (ج) consensus با اضافهشدن کاندید بله.
|
||||
3. سه معیار بسنجید: WER متن، دقت استخراج موجودیتها (نام کالا/برند/تعداد/واحد در JSON نهایی)، و تعداد «موارد سخت» که با بله درست شدهاند.
|
||||
**معیار پیشنهادی:** اگر کاهش خطای استخراج ≥ ~۱۵٪ نسبی **یا** حل ≥ ۲ مورد سختِ لهجهای → پروژه توجیه دارد؛ در غیر اینصورت **STOP** (همان خط لولهی فعلی کافی است).
|
||||
|
||||
### T6 + T9 — سواک بن و ماندگاری نشست (۱–۲ هفته، پسیو)
|
||||
|
||||
استفادهی کمحجم روزانه (مثلاً ≤ ۱۰۰ ترنسکرایب در روز) از IP شرکت؛ ثبت: تعداد ورود مجدد لازم، چالشهای امنیتی، هشدارها، عمر واقعی توکن. خروجی: سقف عملیاتی + runbook ورود مجدد.
|
||||
|
||||
### T8 — بازبینی حقوقی/ToS (موازی — **دروازهی انسانی**)
|
||||
|
||||
ایجنت فقط شواهد را جمع میکند؛ تصمیم Go/No-Go توسط مدیریت مستند میشود. معیار: سند تصمیم مکتوب.
|
||||
|
||||
---
|
||||
|
||||
## ۴. دروازههای تصمیم (جدولهای وضعیت ← اقدام)
|
||||
|
||||
### G1 (بعد از T1)
|
||||
| وضعیت مشاهدهشده | اقدام |
|
||||
|---|---|
|
||||
| فیلد ترنسکریپشن روی ویس هست | **پایان:** R1 — مستقیم به بات وصل کن (بدون هیچ کرالری) |
|
||||
| نیست | برو T2 |
|
||||
|
||||
### G2 (بعد از T2) — مهمترین دروازهی فنی
|
||||
| وضعیت مشاهدهشده | معنا | اقدام |
|
||||
|---|---|---|
|
||||
| RPC از نوع **unary POST gRPC-Web** با پروتوباف دیکودپذیر | بهترین حالت | برو T3+T4 → R2 |
|
||||
| RPC **فقط روی WebSocket** | حالت مطلوب دوم (aiobale دقیقاً WS است) | برو T3+T4 → R2 (از کانال WS؛ **POST gRPC زیاد نزن**) |
|
||||
| درخواست **رمزنگاری/امضای اختصاصی** دارد (فراتر از پروتوباف) | هزینهی نگهداری بالا | R4 (Playwright) یا R3 بهعنوان مسیر اصلی؛ R2 فقط اگر … شکست |
|
||||
| ترنسکرایب نیازمند **اثر انگشت/رفتار مرورگر** است | R2 غیرممکن | R4 |
|
||||
| ترنسکرایب **message_id روی پیامی موجود** میخواهد | نیازمند «آپلود به چت اسکرچ» | R2 با فلوی «آپلود→ترنسکرایب→حذف» (بخش ۵.۲) |
|
||||
|
||||
### G3 (بعد از T3/T4)
|
||||
| وضعیت | اقدام |
|
||||
|---|---|
|
||||
| ساخت درخواست + خواندن پاسخ ممکن شد | برو T5 → T7 |
|
||||
| فقط یکی از دو جهت ممکن است | کپچر بیشتری بگیر (T2)؛ اگر نشد → R3/R4 |
|
||||
| شکست کامل | R3 (اگر T-extra موفق بود) وگرنه R4؛ اگر هر دو هم نشد → STOP |
|
||||
|
||||
### G4 (بعد از T5)
|
||||
| وضعیت | اقدام |
|
||||
|---|---|
|
||||
| نرخ موفقیت ≥ ۹۵٪ و p95 ≤ ۸s | ادامه |
|
||||
| rate-limit مشاهده شد | کاهش نرخ + backoff نمایی + سقف روزانه؛ مجدداً بسنج |
|
||||
| بن/چالش امنیتی | توقف فوری؛ رفتن به R4 با اکانت جدید، یا STOP |
|
||||
|
||||
### G5 (بعد از T7) — دروازهی کسبوکار
|
||||
| وضعیت | اقدام |
|
||||
|---|---|
|
||||
| بهبود دقت مطابق آستانه | ادامه به G6 |
|
||||
| بهبود ناکافی | **STOP** — همان پرووایدرها؛ یافتهها را مستند کن |
|
||||
|
||||
### G6 (ToS) / G7 (سواک)
|
||||
| وضعیت | اقدام |
|
||||
|---|---|
|
||||
| ToS سبز + سواک سالم | استقرار کامل (بخش ۶) |
|
||||
| ToS قرمز | STOP یا فقط R4 با حجم محدود (طبق تصمیم مدیریت) |
|
||||
| سواک ناسالم | rollback به پرووایدرهای قبلی (حذف envها) و بازگشت به G4 |
|
||||
|
||||
---
|
||||
|
||||
## ۵. فاز B — پیادهسازی (فقط پس از عبور از دروازهها)
|
||||
|
||||
### ۵.۱ — قرارداد مشترک adapter (برای همهی مسیرها یکسان)
|
||||
|
||||
سرویس `bale-stt` (کانتینر داخلی، **بدون پورت عمومی**):
|
||||
- `POST /v1/audio/transcriptions` — multipart، فیلد `file` (فایل MP3)، `model`، `language=fa` → پاسخ `{"text": "..."}` (دقیقاً قرارداد OpenAI که بات انتظار دارد).
|
||||
- هدر `Authorization: Bearer <INTERNAL_KEY>` را بپذیرد (فقط کلید داخلی، نه توکن بله).
|
||||
- `GET /healthz` برای سلامت.
|
||||
- موتور داخلی قابلسوییچ با env: `BALE_STT_ENGINE=grpc|aichat|playwright` → همهی مسیرها همزیستی دارند و تعویض موتور بدون تغییر بات.
|
||||
|
||||
### ۵.۲ — R2: مهندسی معکوس (مسیر پیشنهادی اصلی)
|
||||
|
||||
معماری: `bot → adapter → aiobale-py (اکانت سوخته) → WS/gRPC بله → متن`
|
||||
نکات پیادهسازی:
|
||||
- اکانت: `session_file` روی یک **داکر ولوم**؛ هرگز در گیت. `BALE_PHONE` و سکرتها در env.
|
||||
- اگر RPC به message_id نیاز دارد: فلوی «آپلود ویس به چت اسکرچ → ترنسکرایب → **حذف پیام**» — با `send_voice` و `delete_message` موجود در aiobale.
|
||||
- ترتیبی اجرا کنید (قفل asyncio)، فاصلهی ۱–۳ ثانیه، سقف روزانه قابلتنظیم (`BALE_STT_DAILY_CAP`).
|
||||
- مدیریت انقضای توکن: تشخیص خطای احراز → علامتگذاری «نیازمند OTP» + هشدار به ادمین (دروازهی انسانی).
|
||||
- fail-soft مضاعف: adapter در هر خطا `{"text": ""}` (یا 5xx کنترلشده) برگرداند تا بات فقط این کاندید را حذف کند.
|
||||
|
||||
### ۵.۳ — R3: اوراکل AI Chat
|
||||
همان adapter، اما داخلش: ارسال ویس به چت AI (با aiobale) → استخراج متن از قالب پاسخ → بازگرداندن. سبکترین پیادهسازی؛ وابسته به پایداری قالب پاسخ.
|
||||
|
||||
### ۵.۴ — R4: Playwright (فالبک)
|
||||
کانتینر جدا با Chromium headless + پروفایل پایدار (`userDataDir` روی ولوم)، یک worker تکنمونه با صف؛ مراحل: ورود یکباره (OTP انسانی) → برای هر درخواست: آپلود ویس به یک چت اسکرچ → کلیک «تبدیل به متن» → انتظار روی سلکتور متن → استخراج → حذف پیام. RAM/زمان بیشتر؛ مقاومتر به تغییر پروتکل.
|
||||
|
||||
### ۵.۵ — ادغام با بات (بدون تغییر کد)
|
||||
در `docker-compose.yml` (نسخهی تیم/سرور، **نه** برنچ دیگران) سرویس `bale-stt` اضافه و در سرویس `bot` این 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=<INTERNAL_KEY>
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_MODEL=bale-stt
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_LANGUAGE=fa
|
||||
```
|
||||
(یادآوری از کد: پرووایدر فقط با پر بودن همزمان BASE_URL و API_KEY فعال میشود.)
|
||||
|
||||
### ۵.۶ — سکرتها و امنیت
|
||||
| سکرت | محل | نکته |
|
||||
|---|---|---|
|
||||
| توکن بات بله | env سرور | **موجود در compose کامیتشده → rotate** |
|
||||
| کلید DeepSeek | env سرور | **همینطور rotate** |
|
||||
| `session_file` اکانت سوخته | داکر ولوم | هرگز گیت؛ مالک مشخص |
|
||||
| `INTERNAL_KEY` adapter | env | فقط بین bot و adapter |
|
||||
| `DRAFT_TOKEN_SECRET` بات | env | پیشفرض فعلی در کد ضعیف است → ست شود |
|
||||
|
||||
### ۵.۷ — مقیاس
|
||||
فاز اول: تکنمونه، ترتیبی، سقف روزانه. توسعه فقط بعد از G7 و فقط با شواهد پایداری.
|
||||
|
||||
---
|
||||
|
||||
## ۶. پذیرش، استقرار، پایش، بازگشت
|
||||
|
||||
**تستهای پذیرش (اجباری):**
|
||||
1. `curl -F "file=@voice.mp3" -F model=bale-stt http://localhost:8080/v1/audio/transcriptions` → `{"text": "<متن فارسی درست>"}`.
|
||||
2. بات: ارسال ویس واقعی → پاسخ استخراجشدهی صحیح.
|
||||
3. **fail-soft:** توقف کانتینر `bale-stt` → بات همچنان ویس را (با پرووایدرهای دیگر) پردازش کند ✓.
|
||||
4. حریم خصوصی: بررسی کنید هیچ فایل صوتی روی adapter باقی نمیماند و پیامهای اسکرچ حذف شدهاند.
|
||||
|
||||
**استقرار فازبندیشده:** dev → سرور با حجم کم (۱ هفته) → کامل.
|
||||
**پایش:** شمارش درخواست، نرخ موفقیت، هیستوگرام تأخیر، انواع خطا، وضعیت نشست. هشدار اگر نرخ خطا در ۳۰ دقیقه > ۲۰٪.
|
||||
**بازگشت (Rollback):** حذف `STT_PROVIDER_3_*` از env بات + توقف کانتینر adapter — بات فوراً به وضعیت قبل برمیگردد (بدون deploy مجدد).
|
||||
|
||||
---
|
||||
|
||||
## ۷. رجیستر ریسک
|
||||
|
||||
| ریسک | احتمال | اثر | مقابله |
|
||||
|---|---|---|---|
|
||||
| بن/rate-limit اکانت | متوسط | توقف مسیر | حجم کم، WS-first، backoff، اکانت اختصاصی قابلتعویض، G4 |
|
||||
| تغییر پروتکل بله | متوسط | قطع سرویس | pایش نسخهی باندل (hash)، آلرت خطا، فالبک R4، سوییچ موتور با env |
|
||||
| انقضای نشست/نیاز OTP | بالا (دورهای) | توقف موقت | ولوم session + آلرت + runbook ورود (انسانی) |
|
||||
| دقت ناکافی نسبت به هزینه | نامعلوم | هزینهی بیفایده | **G5 قبل از ساخت** |
|
||||
| ریسک حقوقی/ToS | نامعلوم | توقف اجباری | **T8/G6 — تصمیم مدیریت** |
|
||||
| حریم خصوصی صدا | کم | اعتباری | حذف فوری پیامها/فایلها؛ عدم نگهداری |
|
||||
| وابستگی به فرد نگهدارنده | متوسط | رکود | مستندسازی در همین پلیبوک + مالک رسمی |
|
||||
|
||||
---
|
||||
|
||||
## ۸. دروازههای انسانی (فقط انسان میتواند)
|
||||
|
||||
1. **OTP ورود اکانت** (بار اول و هر بار انقضا).
|
||||
2. **تصمیم ToS/حقوقی (G6).**
|
||||
3. **جمعآوری نمونههای واقعی ویس برای T7** (با رضایت).
|
||||
4. **تأیید نهایی استقرار** و تأیید مرج برنچ توسط مهندس ارشد.
|
||||
|
||||
---
|
||||
|
||||
## ضمیمه A — دستورات probe (تکرارپذیر)
|
||||
|
||||
```bash
|
||||
TOKEN="<BOT_TOKEN>"
|
||||
for M in transcribeVoice getTranscription transcribe speechToText voiceToText; do
|
||||
curl -s -X POST "https://tapi.bale.ai/bot$TOKEN/$M" -H "Content-Type: application/json" -d '{}'; echo
|
||||
done
|
||||
curl -s -X POST "https://tapi.bale.ai/bot$TOKEN/getMe" -H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
خروجی مورد انتظار: پنج 404 «handler not found» و یک getMe سالم. (همین تست را قبل از هر تصمیم مجدداً اجرا کنید — شاید بله متد اضافه کرده باشد.)
|
||||
|
||||
## ضمیمه B — اسکلت adapter (قابل استفاده برای هر سه موتور)
|
||||
|
||||
```python
|
||||
# bale_stt/adapter.py — قرارداد OpenAI-compatible برای بات
|
||||
import os
|
||||
from fastapi import FastAPI, File, Form, Header, HTTPException, UploadFile
|
||||
|
||||
app = FastAPI()
|
||||
INTERNAL_KEY = os.environ["BALE_STT_INTERNAL_KEY"]
|
||||
ENGINE = os.environ.get("BALE_STT_ENGINE", "grpc") # grpc | aichat | playwright
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz():
|
||||
return {"status": "ok", "engine": ENGINE}
|
||||
|
||||
@app.post("/v1/audio/transcriptions")
|
||||
async def transcribe(file: UploadFile = File(...), model: str = Form("bale-stt"),
|
||||
language: str = Form("fa"), authorization: str = Header("")):
|
||||
if authorization != f"Bearer {INTERNAL_KEY}":
|
||||
raise HTTPException(status_code=401)
|
||||
audio = await file.read()
|
||||
if ENGINE == "grpc":
|
||||
text = await grpc_transcribe(audio) # ← aiobale-py + RPC کشفشده
|
||||
elif ENGINE == "aichat":
|
||||
text = await aichat_transcribe(audio) # ← ارسال به چت AI و استخراج متن
|
||||
else:
|
||||
text = await playwright_transcribe(audio)
|
||||
return {"text": text or ""}
|
||||
# نکته: هرگز استثنا را بیرون نده؛ در خطا {"text": ""} برگردان تا بات fail-soft بماند.
|
||||
```
|
||||
|
||||
## ضمیمه C — منابع
|
||||
|
||||
- aiobale-py: [PyPI](https://pypi.org/project/aiobale-py/) · [GitHub](https://github.com/aminmadaniofficial/aiobale) · [مستندات](https://aminmadaniofficial.github.io/aiobale/)
|
||||
- مستندات رسمی بات بله: [docs.bale.ai](https://docs.bale.ai)
|
||||
- سرویس ثالث STT: [majidapi.ir](https://majidapi.ir)
|
||||
- گزارشهای ایجنت شرکت: `bale_stt_integration_report.md` و `bale_stt_integration_report_2.md` (کنار همین فایل)
|
||||
|
||||
## ضمیمه D — چکلیست سریع ایجنت (TL;DR)
|
||||
|
||||
1. [ ] قواعد بخش ۰ را خواندم.
|
||||
2. [ ] T1 (payload ویس) → G1.
|
||||
3. [ ] T2 (کپچر) → G2 → مسیر (R2 / R3 / R4) انتخاب شد.
|
||||
4. [ ] T3+T4 (اسکیما + اسپایک) → G3.
|
||||
5. [ ] T-extra (اوراکل AI Chat) اگر مسیر R3 مدنظر است.
|
||||
6. [ ] T5 (تأخیر واقعی) → G4.
|
||||
7. [ ] T7 (دقت) → G5 — **بدون این، ساخت ممنوع.**
|
||||
8. [ ] T8 (ToS — انسانی) → G6.
|
||||
9. [ ] ساخت adapter (۵.۱) + ادغام env (۵.۵) + تستهای پذیرش (بخش ۶).
|
||||
10. [ ] سواک (T6/T9) → G7 → استقرار کامل. هر شکست ⇒ جدول همان دروازه ⇒ اقدام جایگزین یا STOP.
|
||||
@@ -0,0 +1,256 @@
|
||||
# گزارش جامع: اعتبارسنجی گزارشها و راهکار کرال موتور STT بله
|
||||
|
||||
> **تاریخ:** ۱۵ مهر ۱۴۰۵
|
||||
> **موضوع:** افزودن متن تولیدشده توسط قابلیت «تبدیل ویس به متن» پیامرسان بله بهعنوان یک کاندید در خطلوله چندمدله STT بات تدارکات
|
||||
> **ورودیها:** سه سند ارائهشده (تحلیل جامع، پلیبوک اجرایی، تحلیل مقایسهای) + راستیآزمایی مستقل وب
|
||||
> **محدودیت:** `integration_report` شماره ۱ و ۲ در دسترس من نبود و فقط از روی نقلقولهای همین اسناد قضاوت شدهاند. دستورهای کشف (بخش ۴) در محیط من اجرا نشدهاند، چون شبکه sandbox بسته بود.
|
||||
|
||||
---
|
||||
|
||||
## ۱. خلاصه مدیریتی
|
||||
|
||||
1. **Bot API رسمی بله STT ندارد.** همه اسناد در این مورد درست میگویند.
|
||||
2. **تناقض اصلی** (مهندسی معکوس «غیرعملی است» در برابر «ساده است») هیچکدام کاملاً درست نیست. مسیر شدنی است، ولی «افزودن یک متد» نیست و نیاز به یک فاز کشف دارد.
|
||||
3. **دو فرض اثباتنشده** کل مسیر را تحت تأثیر قرار میدهد:
|
||||
- دکمه «تبدیل به متن» روی **وب** بله وجود دارد (هر سه سند فرض کردهاند، هیچ منبعی تأیید نکرده).
|
||||
- پکیج `aiobale-py` نسخه 0.3.9 فعال است (پیدا نشد؛ به بخش ۲ مراجعه کنید).
|
||||
4. **کشف جدید:** پکیج `bale-sdk` با ۶۰۷ RPC تایپشده و پروتوهای بازیابیشده. احتمال وجود RPC ترنسکریپشن در آن بالاست. ولی مجوز آن اختصاصی است.
|
||||
5. **بزرگترین صرفهجویی:** آزمون دقت را **قبل از ساخت** و بهصورت **دستی** روی ۳۰ ویس واقعی انجام دهید. اگر بله از خطلوله فعلی بهتر نبود، پروژه لازم نیست.
|
||||
6. **ریسک حقوقی/ToS** (استفاده خودکار از حساب کاربری) تصمیم مدیریتی است و باید قبل از استقرار روشن شود.
|
||||
|
||||
---
|
||||
|
||||
## ۲. اعتبارسنجی گزارشها
|
||||
|
||||
### ۲.۱ جدول ادعاها
|
||||
|
||||
| ادعا | نتیجه | توضیح |
|
||||
|---|---|---|
|
||||
| Bot API رسمی STT ندارد | ✅ تأیید | پروبها همگی ۴۰۴ دادهاند و تایپهای SDKها برای Voice فقط شناسه فایل و مدتزمان دارند. **تذکر:** ۴۰۴ برای نامهای حدسی چیزی درباره فیلد مخفی در payload نمیگوید؛ تست T1 (ویس به یک بات تستی و خواندن JSON کامل) هنوز لازم است (۳۰ دقیقه). |
|
||||
| بله قابلیت تبدیل ویس به متن دارد | ✅ با شرط | نسخه جدید بله این قابلیت را برای اندروید و iOS منتشر کرده. وجود دکمه در **وب** تأییدنشده است و ریسک اصلی مسیر Playwright همین است. |
|
||||
| مهندسی معکوس «غیرعملی» است (گزارش ۲) | ⚠️ بدبینانه | کلاینتهای متنباز مهندسیمعکوسشده وجود دارند. اما استدلال «Protobuf خودتوصیف است» نادرست است: wire format بدون `.proto` فقط تا حدی (شماره فیلد و نوع خام، بدون نام و معنا) دیکود میشود. |
|
||||
| مهندسی معکوس «در حد افزودن یک متد» است (پلیبوک) | ⚠️ خوشبینانه | کشف RPC، نگاشت فیلدها و پایداری نشست کار واقعی است. تخمین «نیمروز» برای T2 ریسکی است. |
|
||||
| `aiobale-py` v0.3.9 فعال است و نام `aiobale` را پکیج خالی گرفته | ❌ تأییدنشده / احتمالاً غلط | `aiobale-py` را پیدا نکردم. `aiobale` یک کتابخانه واقعی است (MIT، مستندات `docs.aiobale.ir`، نسخههای 0.1.x). فورکهایی مثل Autobale میگویند نسخه اصلی نگهداری نمیشود و باگ دارد. قبل از وابستگی به هر نام، `pip index versions` را اجرا کنید. |
|
||||
| `bale-sdk` (کشف جدید) | 🔎 مهم | از کلاینت وب بله مهندسی معکوس شده؛ ۶۰۷ RPC در ۵۲ سرویس، پروتوهای بازیابیشده، احراز با JWT یا OTP؛ نسخه 0.1.1 (ژوئن ۲۰۲۶). **مجوز اختصاصی و «همه حقوق محفوظ»**. |
|
||||
| تأخیر «زیر ۱ ثانیه» | ❌ رد | همه موافقند. اعداد ۱–۴ ثانیه (R2) و ۴–۸ ثانیه (Playwright) هم **اندازهگیری نشدهاند**؛ فقط T5 عدد واقعی میدهد. |
|
||||
| AI Chat بله ویس را «پیاده» میکند (R3) | ⚠️ تأییدنشده و نامطلوب | حتی اگر درست باشد، خروجی «متن + تفسیر مدل» است و برای consensus مناسب نیست. با کنار گذاشتن موافقم. |
|
||||
| RAM ۳۵۰–۵۰۰ مگابایت برای Playwright | ✅ معقول | برای بات تککاربره قابلتحمل است. |
|
||||
| Playwright در ۱–۲ روز | ⚠️ خوشبینانه | وابسته به وجود دکمه وب، چالشهای ورود و پایداری سلکتورها. |
|
||||
| ساختار Gate / State-machine پلیبوک | ✅ قوی | بهترین بخش اسناد. ایراد: دروازه دقت (T7) بعد از ساخت آمده، در حالی که دستی و قبل از ساخت شدنی است. |
|
||||
| ریسک بن، ToS و حریم خصوصی | ✅ درست و حیاتی | فقط پلیبوک به آن پرداخته است. |
|
||||
| ترجیح WebSocket به POST gRPC (پلیبوک) | ✅ منطقی | کلاینت رسمی بله POST gRPC را عمدتاً برای چند متد احراز هویت استفاده میکند؛ استفاده زیاد از آن غیرعادی به نظر میرسد. |
|
||||
| «Playwright اول» (سند مقایسهای) در برابر «R2 اول» (پلیبوک) | ⚠️ هر دو مشروط | بدون نتیجه کشف روز اول قابل تصمیم نیست. |
|
||||
|
||||
### ۲.۲ ایرادهای کد adapter در سند اول
|
||||
|
||||
- کلید پیشفرض ضعیف (`bale_secure_token`) و مقایسه کلید با `!=` بهجای مقایسه ثابتزمان.
|
||||
- بلعیدن همه استثناها بدون لاگ.
|
||||
- بازگرداندن ۲۰۰ با متن خالی: ممکن است بات آن را یک کاندید معتبر (خالی) در consensus بشمارد. باید در `bot/app/stt.py` بررسی شود که پاسخ ۵xx و متن خالی چطور مدیریت میشوند. توصیه: در خطا ۵۰۳ برگردد.
|
||||
- نبود قفل ترتیبی، سقف روزانه و تأخیر تصادفی در خود adapter.
|
||||
|
||||
### ۲.۳ نکات فنی اصلاحشده
|
||||
|
||||
- فریم gRPC-Web پیشوند ۵ بایتی دارد (۱ بایت فلگ + ۴ بایت طول). این برای **POST gRPC-Web** درست است؛ اینکه فریمهای WebSocket بله هم همان قالب را دارند فرض اثباتنشدهای است و باید در T2 دیده شود.
|
||||
- هاستها و نام ماژولهایی مثل `maviz-ws.bale.ai` و `SpeechTranscript` از تحلیل باندل آمدهاند و در وب ایندکس نشدهاند. باید با چشم در کپچر تأیید شوند.
|
||||
|
||||
---
|
||||
|
||||
## ۳. راهکارها
|
||||
|
||||
| راهکار | توضیح | رم | خلوص متن | ریسک | حکم |
|
||||
|---|---|---|---|---|---|
|
||||
| **A. مسیر رسمی** | درخواست API یا همکاری از بله | — | — | — | هزینه صفر؛ قبل از هر کار غیررسمی یک ایمیل بزنید |
|
||||
| **B. RPC مستقیم** | کلاینت کاربری روی WebSocket با `bale-sdk` یا `aiobale` + RPC ترنسکریپت | ≈ ۲۰ MB | خالص | متوسط (تغییر پروتکل، بن) | **بهترین نتیجه فنی**، مشروط به کشف RPC |
|
||||
| **C. Playwright** | Chromium headless روی `web.bale.ai` با پروفایل پایدار | ۳۵۰–۵۰۰ MB | خالص | کم–متوسط | مقاوم، **فقط اگر دکمه وب موجود باشد** |
|
||||
| **D. AI Chat** | ویس به چت هوش مصنوعی بله و استخراج از پاسخ | ≈ ۲۰ MB | ❌ متن + تفسیر | بالا | کنار گذاشته شود |
|
||||
| **E. سرویس ثالث** | پروکسیهای STT با ادعای بله | — | نامشخص | حریم خصوصی/پایداری | کنار گذاشته شود |
|
||||
| **F. اندروید امولاتور** | اپ بله + uiautomator | زیاد | خالص | بالا (شکننده) | آخرین چاره اگر B و C ممکن نشد |
|
||||
|
||||
---
|
||||
|
||||
## ۴. مسیر دقیق اجرا
|
||||
|
||||
### گام ۰: آزمون دقت دستی (نیمروز، بدون کد)
|
||||
|
||||
۳۰ ویس واقعی تدارکاتی (با رضایت فرستندگان؛ صداها را بلندمدت نگه ندارید) را در اپ بله دستی به متن تبدیل کنید و سه حالت را مقایسه کنید:
|
||||
|
||||
1. خروجی فعلی بات (whisper/سایرین + consensus)
|
||||
2. فقط متن بله
|
||||
3. consensus با اضافهشدن بله
|
||||
|
||||
سه معیار: WER متن، دقت استخراج اقلام (نام کالا، برند، تعداد، واحد) و تعداد موارد سخت لهجهای که با بله درست شدهاند.
|
||||
|
||||
**معیار پیشنهادی:** کاهش نسبی خطای استخراج حدود ۱۵٪ یا بیشتر، یا حل حداقل ۲ مورد سخت. در غیر این صورت **STOP**.
|
||||
|
||||
### گام ۱: کشف سریع (حدود ۳۰ دقیقه)
|
||||
|
||||
```bash
|
||||
python -m venv .v && . .v/bin/activate
|
||||
pip index versions aiobale-py # وجود یا عدم وجود را بررسی کنید
|
||||
pip download bale-sdk --no-deps --no-binary :all: && tar xzf bale_sdk-*.tar.gz
|
||||
grep -rniE "transcri|speech|asr|stt" bale_sdk-*/proto bale_sdk-*/docs | head -50
|
||||
```
|
||||
|
||||
همچنین در پایتون `from bale import ALL_RPCS` را با همان کلیدواژهها فیلتر کنید.
|
||||
|
||||
همزمان **T1**: یک بات تستی مستقل بسازید، یک ویس بفرستید و JSON کامل آپدیت را بخوانید. اگر فیلد ترنسکریپشن وجود داشت، پروژه کرالر لازم نیست.
|
||||
|
||||
### گام ۲: وب و کپچر (نیمروز تا یک روز)
|
||||
|
||||
1. با **یک حساب اختصاصی شرکت** روی `web.bale.ai` وارد شوید و بررسی کنید دکمه «تبدیل به متن» روی ویس هست یا نه (روی ویس دریافتی و ارسالی).
|
||||
2. DevTools تب Network را با Preserve log و فیلتر WS و Fetch/XHR ضبط کنید و هنگام کلیک روی دکمه ذخیره کنید:
|
||||
- URL کامل و ترابری (unary یا WS)
|
||||
- هدر احراز
|
||||
- `req.bin` و `resp.bin`
|
||||
- زمان پاسخ (همان تأخیر واقعی ASR)
|
||||
3. ۵ تا ۱۰ ویس متنوع (کوتاه، بلند، لهجهدار) را تکرار کنید و در `findings.md` خلاصه کنید.
|
||||
4. دیکود آفلاین: پیشوند ۵ بایتی را بردارید، سپس `protoc --decode_raw` یا `protoscope`.
|
||||
|
||||
### گام ۳: انتخاب مسیر
|
||||
|
||||
| وضعیت | مسیر |
|
||||
|---|---|
|
||||
| RPC در `bale-sdk` یا کپچر پیدا شد | **B** |
|
||||
| RPC پیدا نشد ولی دکمه وب هست | **C** |
|
||||
| هیچکدام | **F** یا توقف |
|
||||
| ترنسکرایب نیاز به امضا یا اثر انگشت مرورگر دارد | **C** |
|
||||
|
||||
**الگوی پیشنهادی برای B:** بات خودتان با `sendVoice(file_id)` ویس را برای حساب کرالر بفرستد (کرالر یکبار بات را `/start` کرده). اینطور نیازی به دانلود و آپلود مجدد نیست و `message_id` بومی در اختیار کرالر است. پس از دریافت متن، پیام حذف شود. اگر RPC به `message_id` نیاز نداشت، مستقیم فایل یا بایت داده شود.
|
||||
|
||||
**مجوز:** `aiobale` مجوز MIT دارد ولی نگهداری نمیشود (فورکها: Autobale و BaleClient). `bale-sdk` مجوز اختصاصی دارد؛ برای استفاده تجاری از نویسنده اجازه بگیرید.
|
||||
|
||||
### گام ۴: ساخت adapter سختشده
|
||||
|
||||
قرارداد OpenAI-compatible (همان `POST /v1/audio/transcriptions` که بات انتظار دارد) و موتور قابلسوییچ با `BALE_STT_ENGINE=grpc|playwright`. پیشنهاد کد:
|
||||
|
||||
```python
|
||||
import os, secrets, asyncio, logging
|
||||
from fastapi import FastAPI, File, Form, Header, HTTPException, UploadFile
|
||||
|
||||
log = logging.getLogger("bale-stt")
|
||||
app = FastAPI()
|
||||
KEY = os.environ["BALE_STT_INTERNAL_KEY"] # بدون مقدار پیشفرض
|
||||
DAILY_CAP = int(os.environ.get("BALE_STT_DAILY_CAP", "100"))
|
||||
lock = asyncio.Lock() # اجرای ترتیبی
|
||||
count_today = 0 # در عمل با ریست روزانه
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.post("/v1/audio/transcriptions")
|
||||
async def transcribe(file: UploadFile = File(...), model: str = Form("bale-stt"),
|
||||
language: str = Form("fa"), authorization: str = Header("")):
|
||||
if not secrets.compare_digest(authorization, f"Bearer {KEY}"):
|
||||
raise HTTPException(401)
|
||||
if count_today >= DAILY_CAP:
|
||||
raise HTTPException(429)
|
||||
async with lock:
|
||||
try:
|
||||
text = await run_engine(await file.read()) # + تأخیر تصادفی ۱ تا ۳ ثانیه
|
||||
except Exception:
|
||||
log.exception("bale stt failed")
|
||||
raise HTTPException(503) # تا بات فقط همین کاندید را حذف کند
|
||||
if not text:
|
||||
raise HTTPException(503)
|
||||
return {"text": text}
|
||||
```
|
||||
|
||||
اتصال به بات (بدون تغییر کد):
|
||||
|
||||
```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=<INTERNAL_KEY>
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_MODEL=bale-stt
|
||||
PROCUREMENT_BOT_STT_PROVIDER_3_LANGUAGE=fa
|
||||
```
|
||||
|
||||
> پرووایدر فقط وقتی فعال میشود که هم `BASE_URL` و هم `API_KEY` پر باشند.
|
||||
|
||||
### گام ۵: عملیات و ایمنی
|
||||
|
||||
- فقط **شماره اختصاصی شرکت**؛ هرگز شماره شخصی.
|
||||
- `session_file` یا پروفایل مرورگر روی ولوم داکر و هرگز در گیت.
|
||||
- **OTP تنها گام انسانی** است. در ابطال نشست: هشدار به ادمین و هدایت ترافیک به سایر پرووایدرها.
|
||||
- اجرای ترتیبی، فاصله تصادفی ۱ تا ۳ ثانیه، سقف روزانه، و ترجیح WebSocket به POST gRPC.
|
||||
- حذف فوری پیام اسکرچ پس از استخراج و عدم نگهداری فایل صوتی روی دیسک.
|
||||
- پایش: نرخ موفقیت، p50 و p95 تأخیر، انواع خطا، وضعیت نشست. هشدار اگر نرخ خطا در ۳۰ دقیقه بیش از ۲۰٪ شد.
|
||||
- **Rollback:** خالی کردن `STT_PROVIDER_3_*` در env بات و توقف کانتینر. بات فوراً به وضعیت قبل برمیگردد.
|
||||
- سکرتهایی که در `docker-compose.yml` کامیت شدهاند (توکن بات، کلید DeepSeek) باید rotate شوند و `DRAFT_TOKEN_SECRET` پیشفرض ضعیف را عوض کنید.
|
||||
- روی برنچهای دیگران (`prod-v7` و غیره) دخالت نکنید و بدون تأیید مالک چیزی پوش نشود.
|
||||
|
||||
### گام ۶: اندازهگیری و سواک
|
||||
|
||||
- **T5:** ۵۰ تا ۱۰۰ ویس ترتیبی با فاصله انسانی؛ پذیرش پیشنهادی: موفقیت ≥ ۹۵٪ و p95 ≤ ۸ ثانیه.
|
||||
- **سواک یکهفتهای** با حجم کم (حدود ۱۰۰ ترنسکرایب در روز): ثبت تعداد ورود مجدد، چالشهای امنیتی و عمر واقعی توکن.
|
||||
|
||||
---
|
||||
|
||||
## ۵. دروازههای تصمیم (خلاصه)
|
||||
|
||||
| دروازه | شرط عبور | در صورت شکست |
|
||||
|---|---|---|
|
||||
| G0 دقت دستی | بهبود ≈ ۱۵٪ یا ۲ مورد سخت | **STOP** |
|
||||
| G1 (T1) | فیلد ترنسکریپشن نبود | اگر بود: پایان، مستقیم وصل شود |
|
||||
| G2 کشف | RPC یا دکمه وب پیدا شد | F یا توقف |
|
||||
| G3 پروتوتایپ | درخواست ساخته و پاسخ خوانده شد | سوییچ به C |
|
||||
| G4 نرخ و پایداری | ≥ ۹۵٪ موفقیت، p95 ≤ ۸ ثانیه | کاهش نرخ و backoff؛ در صورت بن: توقف |
|
||||
| G6 ToS | تصمیم مکتوب مدیریت | **STOP** یا حجم بسیار محدود |
|
||||
| G7 سواک | نشست سالم یک هفته | rollback و بازگشت به G4 |
|
||||
|
||||
---
|
||||
|
||||
## ۶. زمانبندی واقعبینانه
|
||||
|
||||
| مرحله | زمان |
|
||||
|---|---|
|
||||
| گام ۰ و ۱ (دقت دستی + کشف سریع) | نیمروز |
|
||||
| گام ۲ (وب و کپچر) | ۱ روز |
|
||||
| ساخت B یا C | ۲ تا ۴ روز |
|
||||
| T5 و سواک | ۱ هفته |
|
||||
|
||||
---
|
||||
|
||||
## ۷. رجیستر ریسک
|
||||
|
||||
| ریسک | احتمال | اثر | مقابله |
|
||||
|---|---|---|---|
|
||||
| بن یا rate-limit حساب | متوسط | توقف مسیر | حجم کم، WS-first، سقف روزانه، حساب اختصاصی قابلتعویض |
|
||||
| تغییر پروتکل یا UI بله | متوسط | قطع سرویس | هشدار خطا، فالبک، سوییچ موتور با env، rollback با env |
|
||||
| انقضای نشست و نیاز به OTP | بالا (دورهای) | توقف موقت | ولوم نشست + هشدار + runbook ورود انسانی |
|
||||
| دکمه وب وجود ندارد | نامعلوم | ناممکنشدن C | بررسی در گام ۲ قبل از ساخت |
|
||||
| دقت ناکافی نسبت به هزینه | نامعلوم | هزینه بیفایده | G0 قبل از ساخت |
|
||||
| ToS / مسائل حقوقی | نامعلوم | توقف اجباری | تصمیم مکتوب مدیریت (G6)؛ ایمیل به بله (A) |
|
||||
| مجوز اختصاصی `bale-sdk` | بالا | ممنوعیت استفاده | اجازه از نویسنده یا استفاده از `aiobale` (MIT) |
|
||||
| حریم خصوصی صدا | کم | اعتباری | حذف فوری پیامها، عدم نگهداری فایل |
|
||||
| وابستگی به یک فرد نگهدارنده | متوسط | رکود | مستندسازی و مالک رسمی |
|
||||
|
||||
---
|
||||
|
||||
## ۸. دروازههای انسانی
|
||||
|
||||
1. OTP ورود حساب (بار اول و هر انقضا).
|
||||
2. تصمیم ToS / حقوقی.
|
||||
3. جمعآوری نمونه ویس واقعی با رضایت.
|
||||
4. تأیید نهایی استقرار و مرج توسط مهندس ارشد.
|
||||
|
||||
---
|
||||
|
||||
## ۹. توصیه نهایی
|
||||
|
||||
1. **همین هفته:** گام ۰ (دقت دستی) + ایمیل به بله + گام ۱ (کشف سریع و T1).
|
||||
2. **اگر دقت توجیه داشت:** گام ۲ و انتخاب B یا C بر اساس شواهد.
|
||||
3. **معماری:** adapter با قرارداد OpenAI و موتور قابلسوییچ؛ بات دستنخورده میماند.
|
||||
4. **تصمیم مدیریتی** درباره ToS قبل از استقرار.
|
||||
5. اگر هر کدام از G0 یا G6 شکست خورد، **توقف** و ادامه با پرووایدرهای فعلی.
|
||||
|
||||
---
|
||||
|
||||
## منابع
|
||||
|
||||
- aiobale (PyPI): https://pypi.org/project/aiobale
|
||||
- bale-sdk (PyPI): https://pypi.org/project/bale-sdk/
|
||||
- Autobale (فورک aiobale): https://pypi.org/project/autobale/
|
||||
- خبر انتشار نسخه بله با قابلیت تبدیل پیام صوتی به متن (خبرگزاری آنا): https://ana.ir/fa/news/994908
|
||||
- مستندات رسمی بات بله: https://docs.bale.ai
|
||||
@@ -0,0 +1,159 @@
|
||||
# گزارش فنی و امکانسنجی: ادغام قابلیت صوت به متن (STT) بله در سامانه تدارکات هوشمند
|
||||
|
||||
> [!NOTE]
|
||||
> **مخاطب گزارش:** تیم فنی، معماران سیستم و مدیران محصول
|
||||
> **موضوع:** بررسی، مقایسه دو رویکرد فنی و نقشه راه پیادهسازی اتصال به موتور تبدیل ویس به متن پیامرسان «بله»
|
||||
> **هدف نهایی:** افزودن بله به عنوان یکی از مدلهای کاندید در خط لوله STT چندمدله (Multi-Provider STT) جهت افزایش دقت استخراج اقلام تدارکات
|
||||
|
||||
---
|
||||
|
||||
## ۱. خلاصه مدیریتی (Executive Summary)
|
||||
|
||||
سامانه هوشمند تدارکات در حال حاضر مجهز به موتور پردازش موازی صوت (**Multi-Provider STT**) است که قادر است فایلهای صوتی دریافتی از کاربران را همزمان به چندین ارائهدهنده (نظیر Whisper، Soniox یا سرویسهای ابری) ارسال کرده و از طریق مدل تحلیلی DeepSeek یک خروجی یکپارچه و دقیق (**Consensus Text**) به همراه اقلام استعلام استخراج کند.
|
||||
|
||||
برای افزایش هرچه بیشتر دقت در زبان فارسی و لهجههای محلی، استفاده از **موتور صوتبهمتن بومی پیامرسان بله** به عنوان یکی از مدلهای کاندید مطرح شده است. از آنجا که پلتفرم توسعهدهندگان بله (Bale Bot API) متد مستقیمی برای فراخوانی API ترنسکرایب ارائه نمیدهد، دو روش اجرایی اصلی برای دستیابی به این موتور وجود دارد:
|
||||
1. **روش اول (اتوماسیون رابط کاربری وب / Headless Browser):** شبیهسازی رفتار کاربر در مرورگر و کلیک روی دکمه تبدیل وویس.
|
||||
2. **روش دوم (مهندسی معکوس پروتکل کلاینت / Direct Client API):** شبیهسازی درخواستهای شبکه (WebSocket/HTTP) کلاینت بله بدون نیاز به مرورگر.
|
||||
|
||||
---
|
||||
|
||||
## ۲. پسزمینه فنی زیرساخت بله (Technical Context)
|
||||
|
||||
| بخش | وضعیت دسترسی | توضیحات |
|
||||
| :--- | :--- | :--- |
|
||||
| **Bale Bot API (رسمی)** | فاقد متد STT | فقط امکان دانلود فایل صوتی خام (`voice` / `audio`) با `file_id` را فراهم میکند. |
|
||||
| **کلاینتهای کاربری بله (Web / Android)** | دارای موتور فعال STT | روی هر ویس یک دکمه تبدیل صوت به متن وجود دارد که با فشرده شدن آن، صوت روی سرورهای بله به متن تبدیل میشود. |
|
||||
| **دستیار هوش مصنوعی بله** | پشتیبانی از ورودی صوتی | کاربر با ارسال ویس به چت هوش مصنوعی بله، متن پیادهشده به همراه پاسخ تحلیلی را دریافت میکند. |
|
||||
|
||||
**نتیجه:** برای بهرهبرداری از این قابلیت، بات تدارکات باید در لایه ارتباطی خود، یک نشست کاربری (User Session) در بله داشته باشد تا بتواند خدمات تبدیل صوت کلاینت را فراخوانی کند.
|
||||
|
||||
---
|
||||
|
||||
## ۳. روش اول: اتوماسیون رابط کاربری وب (Headless Browser Scraping)
|
||||
|
||||
در این رویکرد، یک مرورگر کنترلشده بدون واسط گرافیکی (مانند Chromium تحت فریمورک **Playwright**) در یک کانتینر اختصاصی داکر مستقر شده و در نسخه وب بله (`web.bale.ai`) فعال نگه داشته میشود.
|
||||
|
||||
### معماری جریان داده (Architecture Flow)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Bot as Procurement Bot
|
||||
participant Worker as Browser Worker (Playwright)
|
||||
participant BaleWeb as web.bale.ai (Chromium)
|
||||
participant BaleServer as سرورهای پردازش صوت بله
|
||||
|
||||
Bot->>Worker: ارسال فایل صوتی (voice.mp3)
|
||||
Worker->>BaleWeb: بارگذاری فایل در چت کلاینت
|
||||
BaleWeb->>BaleServer: ارسال صوت و درخواست تبدیل
|
||||
BaleServer-->>BaleWeb: بازگرداندن متن پیادهشده به صفحه
|
||||
Worker->>BaleWeb: استخراج متن از DOM (.transcription-text)
|
||||
Worker-->>Bot: بازگرداندن متن خروجی به خط لوله STT
|
||||
```
|
||||
|
||||
### مؤلفههای پیادهسازی:
|
||||
1. **مدیریت نشست پایدار (Persistent Profile):**
|
||||
- پوشه پروفایل مرورگر (`userDataDir`) بر روی یک داکر ولوم ماندگار (`/data/bale_browser_session`) قرار میگیرد.
|
||||
- ورود اولیه فقط یکبار توسط تیم فنی با شماره تلفن اختصاصی و کد OTP پیامکی انجام میشود و کوکیها و توکنهای LocalStorage/IndexedDB برای همیشه حفظ میگردند.
|
||||
2. **انتظار هوشمند بر اساس رخدادهای DOM:**
|
||||
- استفاده از سلکتورهای مقاوم با `waitForSelector` یا `MutationObserver` روی باکس متن ترنسکرایب.
|
||||
3. **مزایا و معایب:**
|
||||
- 🟢 **تضمین تطابق با سرور:** چون مرورگر رسمی در حال اجرا است، هیچگونه ریسک ناسازگاری پکتهای شبکه وجود ندارد.
|
||||
- 🟢 **پایداری در برابر تغییرات داخلی API:** حتی اگر بله فریمورک پکتهای وبسوکت خود را عوض کند، تا زمانی که دکمه در UI وجود دارد سیستم کار میکند.
|
||||
- 🔴 **مصرف بالای منابع:** هر نمونه Chromium حدود ۳۰۰ تا ۵۰۰ مگابایت رم اشغال میکند.
|
||||
- 🔴 **تاخیر زمانی (Latency):** فرآیند رندر صفحه، شبیهسازی آپلود و رویداد کلیک حدود ۳ تا ۶ ثانیه زمان میبرد.
|
||||
|
||||
---
|
||||
|
||||
## ۴. روش دوم: مهندسی معکوس شبکه و پروتکل کلاینت (Direct Client API)
|
||||
|
||||
در این روش، بدون استفاده از مرورگر سنگین، کلاینت پایتونی سبک ما مستقیماً همان درخواستهای سوکت یا HTTP کلاینت رسمی بله را بازسازی و فراخوانی میکند.
|
||||
|
||||
### معماری جریان داده (Architecture Flow)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Bot as Procurement Bot
|
||||
participant Adapter as Bale Client Protocol Adapter (Python)
|
||||
participant BaleCore as هسته سرور بله (WebSocket / RPC)
|
||||
|
||||
Bot->>Adapter: ارسال بایتهای فایل صوتی
|
||||
Adapter->>BaleCore: فراخوانی مستقیم متد Transcribe (از طریق سوکت یا HTTP)
|
||||
BaleCore-->>Adapter: پاسخ مستقیم باینری/JSON متن
|
||||
Adapter-->>Bot: متن نهایی در کمتر از ۱ ثانیه
|
||||
```
|
||||
|
||||
### مؤلفههای پیادهسازی:
|
||||
1. **آنالیز ترافیک شبکه (Traffic Inspection):**
|
||||
- تحلیل تب Network مرورگر در `web.bale.ai`:
|
||||
- استخراج مسیر اتصال (غالباً `wss://api.bale.ai/v1/users/websocket` یا اندپوینتهای REST).
|
||||
- استخراج هدرهای نشست از `localStorage` کلاینت وب (مانند `access_token`, `device_id`, `session_key`).
|
||||
- کشف پکت یا درخواست RPC مسئول تبدیل صوت (شامل شناسه پیام `message_id` و شناسه فایل صوتی `file_id`).
|
||||
2. **کلاینت ناهمگام پایتون (`httpx` / `websockets`):**
|
||||
- اتصال دائمی و سبک به زیرساخت پیامرسانی بله.
|
||||
- ارسال مستقیم درخواست بدون بارگذاری فایلهای گرافیکی، جاوااسکریپت و CSS مرورگر.
|
||||
3. **مزایا و معایب:**
|
||||
- 🟢 **سرعت خیرهکننده (Ultra-low Latency):** پاسخگویی معمولاً در **کمتر از ۱ ثانیه** انجام میشود.
|
||||
- 🟢 **مصرف ناچیز منابع (Lightweight):** مصرف حافظه رم زیر ۱۰ مگابایت.
|
||||
- 🟢 **ظرفیت پردازش همزمان بالا (High Concurrency):** امکان ارسال همزمان دهها درخواست بدون افت پرفورمنس.
|
||||
- 🔴 **حساسیت به پروتکل:** در صورت بازنویسی پروتکل یا فیلدهای باینری کلاینت بله، ممکن است نیاز به بهروزرسانی پکتها باشد.
|
||||
|
||||
---
|
||||
|
||||
## ۵. ماتریس مقایسه جامع فنی (Technical Decision Matrix)
|
||||
|
||||
| معیار ارزیابی | روش اول: اتوماسیون وب (Playwright) | روش دوم: مهندسی معکوس پروتکل کلاینت | اهمیت برای سیستم |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| **تاخیر پاسخدهی (Latency)** | ۳ تا ۶ ثانیه | **زیر ۱ ثانیه** ⚡️ | بسیار بالا (تاثیر مستقیم روی UX وویس) |
|
||||
| **مصرف حافظه RAM** | ۳۵۰ الی ۵۰۰ مگابایت | **کمتر از ۱۵ مگابایت** 🪶 | متوسط به بالا |
|
||||
| **هزینه پردازشی CPU** | متوسط تا بالا (رندرینگ) | بسیار پایین (درخواست شبکه) | متوسط |
|
||||
| **پیچیدگی پیادهسازی اولیه** | کم (طی چند ساعت با سلکتور) | متوسط (نیازمند اسنیف و دیکود پکت) | موقت |
|
||||
| **پایداری نشست کاربری (Session)** | ذخیره در پوشه کاربری مرورگر | ذخیره توکن در متغیر محیطی / دیتابیس | بالا |
|
||||
| **ریسک تغییرات توسط بله** | کم (مقاوم در برابر تغییر پروتکل) | نیازمند پایش در تغییر نسخههای ماژور | متوسط |
|
||||
| **قابلیت مقیاسپذیری (Scalability)** | محدود (محدودیت منابع مرورگر) | نامحدود (ایسنک و چندنخی) | بالا |
|
||||
|
||||
---
|
||||
|
||||
## ۶. نحوه اتصال به پایپلاین STT چندمدله فعلی
|
||||
|
||||
سامانه تدارکات در حال حاضر قابلیت دریافت چند کاندیدا را دارد. با اتصال بله، خروجی به شکل زیر در پردازش قرار میگیرد:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Voice[وویس دریافتی کاربر از بله] --> FFMPEG[تبدیل یکباره به MP3]
|
||||
FFMPEG --> S1[مدل ۱: Whisper-Large]
|
||||
FFMPEG --> S2[مدل ۲: Soniox / OpenRouter]
|
||||
FFMPEG --> S3[مدل ۳: موتور صوت هوش مصنوعی بله]
|
||||
|
||||
S1 --> Aggregator[تجمیع نتایج کاندیداها]
|
||||
S2 --> Aggregator
|
||||
S3 --> Aggregator
|
||||
|
||||
Aggregator --> DeepSeek[تحلیلگر هوشمند DeepSeek]
|
||||
DeepSeek --> Final[متن اصلاحشده Consensus Text + استخراج اقلام استعلام JSON]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۷. نقشه راه پیشنهادی دو مرحلهای برای تیم توسعه (Recommended Roadmap)
|
||||
|
||||
> [!TIP]
|
||||
> **استراتژی برد-برد (Fast Win + High Performance):**
|
||||
> برای اینکه از سریعترین حالت ممکن با کمترین ریسک بهرهمند شویم، فرآیند اجرای فازبندیشده زیر پیشنهاد میشود:
|
||||
|
||||
### فاز ۱: ممیزی و اکتشاف ترافیک شبکه (کمتر از ۱ روز کاری)
|
||||
1. باز کردن `web.bale.ai` با تب Network و کلیک روی دکمه تبدیل ویس.
|
||||
2. تعیین ماهیت درخواست:
|
||||
- اگر درخواست یک **HTTP POST ساده** یا یک فریم متنی وبسوکت با Authorization Header باشد:
|
||||
👉 **روش دوم بلافاصله پیادهسازی میشود** و به بالاترین سرعت و بهرهوری ممکن میرسیم.
|
||||
- اگر پروتکل شامل امضاها و فریمهای رمزگذاریشده پیچیده باشد:
|
||||
👉 به سراغ اجرای فاز ۲ (روش اول) میرویم.
|
||||
|
||||
### فاز ۲: استقرار سرویس اتوماسیون (Fallback تضمینشده)
|
||||
1. ساخت کانتینر داکر سبک `playwright-worker` در کنار کانتینر `procurement-bot`.
|
||||
2. اتصال امن این کانتینر از طریق یک اندپوینت داخلی ساده:
|
||||
`POST http://bale-stt-worker:8080/transcribe`
|
||||
3. قرار گرفتن این سرویس به عنوان مدل سوم در متغیر محیطی:
|
||||
`PROCUREMENT_BOT_STT_PROVIDER_3_BASE_URL=http://bale-stt-worker:8080/v1`
|
||||
|
||||
---
|
||||
*تهیه شده جهت ارائه به تیم فنی و مدیران سیستم تدارکات هوشمند.*
|
||||
@@ -0,0 +1,137 @@
|
||||
# گزارش فنی و امکانسنجی: ادغام قابلیت صوت به متن (STT) بله در سامانه تدارکات هوشمند
|
||||
|
||||
> [!NOTE]
|
||||
> **مخاطب گزارش:** تیم فنی، معماران سیستم و مدیران محصول
|
||||
> **موضوع:** بررسی، مقایسه دو رویکرد فنی و نقشه راه پیادهسازی اتصال به موتور تبدیل ویس به متن پیامرسان «بله»
|
||||
> **هدف نهایی:** افزودن بله به عنوان یکی از مدلهای کاندید در خط لوله STT چندمدله (Multi-Provider STT) جهت افزایش دقت استخراج اقلام تدارکات
|
||||
|
||||
---
|
||||
|
||||
## ۱. خلاصه مدیریتی (Executive Summary)
|
||||
|
||||
سامانه هوشمند تدارکات در حال حاضر مجهز به موتور پردازش موازی صوت (**Multi-Provider STT**) است که قادر است فایلهای صوتی دریافتی از کاربران را همزمان به چندین ارائهدهنده (نظیر Whisper، Soniox یا سرویسهای ابری) ارسال کرده و از طریق مدل تحلیلی DeepSeek یک خروجی یکپارچه و دقیق (**Consensus Text**) به همراه اقلام استعلام استخراج کند.
|
||||
|
||||
برای افزایش هرچه بیشتر دقت در زبان فارسی و لهجههای محلی، استفاده از **موتور صوتبهمتن بومی پیامرسان بله** به عنوان یکی از مدلهای کاندید مطرح شده است. از آنجا که پلتفرم توسعهدهندگان بله (Bale Bot API) متد مستقیمی برای فراخوانی API ترنسکرایب ارائه نمیدهد، دو روش اجرایی اصلی برای دستیابی به این موتور وجود دارد:
|
||||
1. **روش اول (اتوماسیون رابط کاربری وب / Headless Browser):** شبیهسازی رفتار کاربر در مرورگر و کلیک روی دکمه تبدیل وویس.
|
||||
2. **روش دوم (مهندسی معکوس پروتکل کلاینت / Direct Client API):** شبیهسازی درخواستهای شبکه (WebSocket/HTTP) کلاینت بله بدون نیاز به مرورگر.
|
||||
|
||||
---
|
||||
|
||||
## ۲. پسزمینه فنی زیرساخت بله (Technical Context)
|
||||
|
||||
| بخش | وضعیت دسترسی | توضیحات |
|
||||
| :--- | :--- | :--- |
|
||||
| **Bale Bot API (رسمی)** | فاقد متد STT | فقط امکان دانلود فایل صوتی خام (`voice` / `audio`) با `file_id` را فراهم میکند. |
|
||||
| **کلاینتهای کاربری بله (Web / Android)** | دارای موتور فعال STT | روی هر ویس یک دکمه تبدیل صوت به متن وجود دارد که با فشرده شدن آن، صوت روی سرورهای بله به متن تبدیل میشود. |
|
||||
| **دستیار هوش مصنوعی بله** | پشتیبانی از ورودی صوتی | کاربر با ارسال ویس به چت هوش مصنوعی بله، متن پیادهشده به همراه پاسخ تحلیلی را دریافت میکند. |
|
||||
|
||||
**نتیجه:** برای بهرهبرداری از این قابلیت، بات تدارکات باید در لایه ارتباطی خود، یک نشست کاربری (User Session) در بله داشته باشد تا بتواند خدمات تبدیل صوت کلاینت را فراخوانی کند.
|
||||
|
||||
---
|
||||
|
||||
## ۳. روش اول: اتوماسیون رابط کاربری وب (Headless Browser Scraping)
|
||||
|
||||
در این رویکرد، یک مرورگر کنترلشده بدون واسط گرافیکی (مانند Chromium تحت فریمورک **Playwright**) در یک کانتینر اختصاصی داکر مستقر شده و در نسخه وب بله (`web.bale.ai`) فعال نگه داشته میشود.
|
||||
|
||||
### معماری جریان داده (Architecture Flow)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Bot as Procurement Bot
|
||||
participant Worker as Browser Worker (Playwright)
|
||||
participant BaleWeb as web.bale.ai (Chromium)
|
||||
participant BaleServer as سرورهای پردازش صوت بله
|
||||
|
||||
Bot->>Worker: ارسال فایل صوتی (voice.mp3)
|
||||
Worker->>BaleWeb: بارگذاری فایل در چت کلاینت
|
||||
BaleWeb->>BaleServer: ارسال صوت و درخواست تبدیل
|
||||
BaleServer-->>BaleWeb: بازگرداندن متن پیادهشده به صفحه
|
||||
Worker->>BaleWeb: استخراج متن از DOM (.transcription-text)
|
||||
Worker-->>Bot: بازگرداندن متن خروجی به خط لوله STT
|
||||
```
|
||||
|
||||
### مؤلفههای پیادهسازی:
|
||||
1. **مدیریت نشست پایدار (Persistent Profile):**
|
||||
- پوشه پروفایل مرورگر (`userDataDir`) بر روی یک داکر ولوم ماندگار (`/data/bale_browser_session`) قرار میگیرد.
|
||||
- ورود اولیه فقط یکبار توسط تیم فنی با شماره تلفن اختصاصی و کد OTP پیامکی انجام میشود و کوکیها و توکنهای LocalStorage/IndexedDB برای همیشه حفظ میگردند.
|
||||
2. **انتظار هوشمند بر اساس رخدادهای DOM:**
|
||||
- استفاده از سلکتورهای مقاوم با `waitForSelector` یا `MutationObserver` روی باکس متن ترنسکرایب.
|
||||
3. **مزایا و معایب:**
|
||||
- 🟢 **تضمین تطابق با سرور:** چون مرورگر رسمی در حال اجرا است، هیچگونه ریسک ناسازگاری پکتهای شبکه وجود ندارد.
|
||||
- 🟢 **پایداری در برابر تغییرات داخلی API:** حتی اگر بله فریمورک پکتهای وبسوکت خود را عوض کند، تا زمانی که دکمه در UI وجود دارد سیستم کار میکند.
|
||||
- 🔴 **مصرف بالای منابع:** هر نمونه Chromium حدود ۳۰۰ تا ۵۰۰ مگابایت رم اشغال میکند.
|
||||
- 🔴 **تاخیر زمانی (Latency):** فرآیند رندر صفحه، شبیهسازی آپلود و رویداد کلیک حدود ۳ تا ۶ ثانیه زمان میبرد.
|
||||
|
||||
---
|
||||
|
||||
## ۴. روش دوم: مهندسی معکوس شبکه و پروتکل کلاینت (Direct Client Protocol / gRPC-Web)
|
||||
|
||||
در این روش، بدون استفاده از مرورگر سنگین، تلاش میشود تا کلاینت پایتونی مستقیماً همان درخواستهای شبکه کلاینت رسمی بله را شبیهسازی و فراخوانی کند.
|
||||
|
||||
### یافتههای واقعی حاصل از بازرسی و تحلیل سورسکد لایو وب بله (`web.bale.ai`):
|
||||
تیم ما اسکریپتهای باندل تولیدی بله (`/static/js/index.4aefbadad3.js` و بستههای وابسته) را مستقیماً تحلیل کرد که نتایج فنی مهم زیر حاصل شد:
|
||||
1. **پروتکل زیرساختی:** بله از APIهای متنی ساده (مانند REST یا JSON) استفاده **نمیکند**؛ بلکه کل ارتباطات کلاینت وب بر پایه **gRPC-Web همراه با Protocol Buffers دودویی (`@bufbuild/protobuf`)** پیادهسازی شده است.
|
||||
2. **آدرسهای Gateway سرورهای بله:**
|
||||
- سرور gRPC پروداکشن: `https://maviz-ws.bale.ai`
|
||||
- اتصال وبسوکت پروداکشن: `wss://maviz-ws.bale.ai/ws/?uid={user_id}`
|
||||
- محیطهای آزمایشی: `beta-ws.bale.ai` و `next-ws.bale.ai`
|
||||
3. **شناسهها و فیچرهای کشفشده در کد:**
|
||||
- شناسه ماژول پیادهسازی صوت: `u.SpeechTranscript = "SpeechTranscript"`
|
||||
- ماژول چت هوش مصنوعی: `u.AIChat = "AIChat"` با روت صفحه `/${L.AI_CHAT}`
|
||||
- دامنههای مربوطه: `domain_chat_ai: "app.all.domain_chat_ai"` و `chat_ai_ttl`
|
||||
|
||||
### ارزیابی فنی و چالشهای روش دوم:
|
||||
- **پیچیدگی بالای پیادهسازی:** به دلیل استفاده از پکتهای فشرده دودویی Protobuf، برای فراخوانی مستقیم در پایتون باید فایلهای تعاریف اینترفیس (`.proto`) بله به صورت کامل مهندسی معکوس و دیکود شوند و فریمهای ۵ بایتی استاندارد gRPC-Web در کلاینت پایتون کدگذاری گردند.
|
||||
- **ریسک شکنندگی در تغییر اسکیما:** با کوچکترین تغییر در شماره فیلدهای Protobuf یا متدهای سرور بله در نسخههای جدید، کلاینت مهندسی معکوسشده از کار میافتد.
|
||||
|
||||
---
|
||||
|
||||
## ۵. ماتریس مقایسه جامع فنی (Technical Decision Matrix)
|
||||
|
||||
| معیار ارزیابی | روش اول: اتوماسیون وب (Playwright) | روش دوم: مهندسی معکوس پروتکل کلاینت | اهمیت برای سیستم |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| **تاخیر پاسخدهی (Latency)** | ۳ تا ۶ ثانیه | **زیر ۱ ثانیه** ⚡️ | بسیار بالا (تاثیر مستقیم روی UX وویس) |
|
||||
| **مصرف حافظه RAM** | ۳۵۰ الی ۵۰۰ مگابایت | **کمتر از ۱۵ مگابایت** 🪶 | متوسط به بالا |
|
||||
| **هزینه پردازشی CPU** | متوسط تا بالا (رندرینگ) | بسیار پایین (درخواست شبکه) | متوسط |
|
||||
| **پیچیدگی پیادهسازی اولیه** | کم (طی چند ساعت با سلکتور) | متوسط (نیازمند اسنیف و دیکود پکت) | موقت |
|
||||
| **پایداری نشست کاربری (Session)** | ذخیره در پوشه کاربری مرورگر | ذخیره توکن در متغیر محیطی / دیتابیس | بالا |
|
||||
| **ریسک تغییرات توسط بله** | کم (مقاوم در برابر تغییر پروتکل) | نیازمند پایش در تغییر نسخههای ماژور | متوسط |
|
||||
| **قابلیت مقیاسپذیری (Scalability)** | محدود (محدودیت منابع مرورگر) | نامحدود (ایسنک و چندنخی) | بالا |
|
||||
|
||||
---
|
||||
|
||||
## ۶. نحوه اتصال به پایپلاین STT چندمدله فعلی
|
||||
|
||||
سامانه تدارکات در حال حاضر قابلیت دریافت چند کاندیدا را دارد. با اتصال بله، خروجی به شکل زیر در پردازش قرار میگیرد:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Voice[وویس دریافتی کاربر از بله] --> FFMPEG[تبدیل یکباره به MP3]
|
||||
FFMPEG --> S1[مدل ۱: Whisper-Large]
|
||||
FFMPEG --> S2[مدل ۲: Soniox / OpenRouter]
|
||||
FFMPEG --> S3[مدل ۳: موتور صوت هوش مصنوعی بله]
|
||||
|
||||
S1 --> Aggregator[تجمیع نتایج کاندیداها]
|
||||
S2 --> Aggregator
|
||||
S3 --> Aggregator
|
||||
|
||||
Aggregator --> DeepSeek[تحلیلگر هوشمند DeepSeek]
|
||||
DeepSeek --> Final[متن اصلاحشده Consensus Text + استخراج اقلام استعلام JSON]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۷. نقشه راه پیشنهادی دو مرحلهای برای تیم توسعه (Recommended Roadmap)
|
||||
|
||||
> [!TIP]
|
||||
> **نتیجه تست و تحلیل تجربی ترافیک:**
|
||||
> بر اساس آنالیز دقیق سورسکد لایو `web.bale.ai` که انجام شد، ماهیت درخواستهای بله مشخص گردید: ارتباطات به صورت فریمهای دودویی **Protobuf + gRPC-Web** است. بنابراین مهندسی معکوس پکتها نیازمند استخراج و بازسازی مستمر فایلهای `.proto` اختصاصی بله است که نگهداری آن را بسیار شکننده میکند.
|
||||
>
|
||||
> **نتیجهگیری و پیشنهاد نهایی:**
|
||||
> مناسبترین، مطمئنترین و پایدارترین گزینه برای استقرار در محصول، **روش اول (اتوماسیون مرورگر با Playwright Worker)** است:
|
||||
> ۱. مرورگر به صورت طبیعی از وبسوکت و پروتکل Protobuf بله پشتیبانی میکند و نیازی به رمزگشایی پکتها نیست.
|
||||
> ۲. با یک سشن پایدار در داکر، وویس در صفحه پیاده شده و متن خروجی به شکل مطمئن به خط لوله Multi-STT تحویل داده میشود.
|
||||
> ۳. پیادهسازی آن در قالب یک کانتینر ایزوله (`bale-stt-worker`) با یک اندپوینت سبک `POST /transcribe` سریعاً انجام میپذیرد.
|
||||
|
||||
---
|
||||
*تهیه شده جهت ارائه به تیم فنی و مدیران سیستم تدارکات هوشمند.*
|
||||
@@ -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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,432 @@
|
||||
# گزارش جامع: ارزیابی پروژههای متنباز و سرویسهای شخص ثالث، و ترکیب بهینه برای کرالر STT بله
|
||||
|
||||
> **تاریخ:** ۱۶ مهر ۱۴۰۵ (8 Oct 2026)
|
||||
> **ورودیها:** دو گزارش مدیر پروژه
|
||||
> - **گزارش A:** «ارزیابی تحلیلی راهکارهای استخراج صوتبهمتن در بله: سرویسهای شخص ثالث، کتابخانهها، پروژههای متنباز و جایگزینها»
|
||||
> - **گزارش B:** `bale_stt_executive_roadmap_report3.md` (سند عملیاتی برای مدیر پروژه و ایجنت مجری)
|
||||
>
|
||||
> **دامنه (طبق درخواست):** حریم خصوصی و امنیت در این گزارش بررسی نشده و بعداً تصمیمگیری میشود. فقط فهرست کامل راهکارها، ارزیابی، الگوبرداری از متنباز و بهترین ترکیب ارائه شده است.
|
||||
>
|
||||
> **روش کار:** هر دو گزارش را خواندم و ادعاهای قابل راستیآزمایی را با منابع عمومی (PyPI، مستندات رسمی بله، arXiv، Hugging Face، GitHub) تطبیق دادم. هر ادعا یکی از سه وضعیت دارد:
|
||||
> ✅ **تأیید شد** | 🟡 **تأییدنشده / فقط استنباط** | ❌ **با شواهد در تناقض است**
|
||||
>
|
||||
> **محدودیت مهم:** شبکه sandbox من بسته است؛ پس نتوانستم پکیجها را نصب کنم یا پروتوها را بخوانم. هر جا بگویم «تأییدنشده»، یعنی در منابع عمومی قابل مشاهده برای من نبود، نه لزوماً اینکه غلط باشد.
|
||||
|
||||
---
|
||||
|
||||
## ۱. خلاصه اجرایی
|
||||
|
||||
### پاسخ کوتاه به چهار سؤال شما
|
||||
|
||||
| سؤال | پاسخ کوتاه |
|
||||
|---|---|
|
||||
| **نظر درباره گزارش A (متنباز و شخص ثالث)** | تحلیل خوبی است و دستهبندی ۸ راهکار درست است. ولی چند ادعای مهم آن تأییدنشده یا غلط است: نام و وضعیت پکیج `aiobale-py`، وجود متد `GetTranscript` (فقط ادعا شده، من نتوانستم ببینم)، و برتری «مدل متنباز» بر سرویسهای تجاری. دادههای مستقل خلاف این برتری را نشان میدهد (بخش ۶). |
|
||||
| **آیا دیگران همین مشکل را داشتهاند و چطور حل کردهاند؟** | **هیچ پروژه عمومیای که دقیقاً خروجی STT خود بله را استخراج کند پیدا نکردم.** همان الگوهای نزدیک وجود دارد: کلاینتهای کاربری مهندسیمعکوسشده (`aiobale`، `bale-sdk`، فورکها)، سلفباتها و اسکرپرهای مبتنی بر Selenium/Playwright، و کسبوکارهایی که سراغ ASR مستقل رفتهاند (بخش ۳). |
|
||||
| **اگر الگوبرداری یا شخص ثالث بخواهیم، چه کنیم؟** | از متنباز، لایههای انتقال و نشست و ابزار دیکود پروتوباف را بردارید، نه خود «ترنسکرایب» را. از شخص ثالث، چند ASR را بهعنوان کاندید موازی کنار بله بگذارید (بخش ۵ و ۶). |
|
||||
| **بهترین ترکیب؟** | **«بله + یک ASR تجاری فارسی + یک مدل خودمیزبان با واژگان تخصصی + LLM اجماعگیر با کاتالوگ کالا»**، با کرالر بله بهصورت adapter قابلسوییچ. جزئیات در بخش ۷. |
|
||||
|
||||
### سه یافتهای که تصمیم شما را تغییر میدهد
|
||||
|
||||
1. **«نیازی به آپلود ویس نیست.»** بات خودتان با `sendVoice(file_id)` میتواند همان ویس کاربر را برای حساب کرالر بفرستد. اینطور فایل را کرالر دانلود و آپلود نمیکند. این طراحی از گزارش B سادهتر و کمریسکتر است (بخش ۷.۲).
|
||||
2. **داده بنچمارک مستقل نشان میدهد ASR تجاری فارسی حدود ۱۴ امتیاز WER از بهترین متنباز جلوتر است** (مقاله PSRB، بخش ۶.۲). این با ادعای گزارش A که «متنباز پایدارترین راهکار» است، همخوان نیست. البته روی داده عمومی بوده، نه واژگان تدارکات شما.
|
||||
3. **کد مرجع گزارش B شبهکد است، نه کد اجراشدنی.** توابعی مثل `client.send_voice` و `client.raw_call` در مستندات عمومی `bale-sdk` وجود ندارند (بخش ۲.۲).
|
||||
|
||||
---
|
||||
|
||||
## ۲. ارزیابی دو گزارش مدیر پروژه
|
||||
|
||||
### ۲.۱ گزارش A: متنباز و شخص ثالث
|
||||
|
||||
**نقاط قوت**
|
||||
- پوشش گسترده: ۸ راهکار در یک ماتریس مقایسه (تأخیر، رم، خلوص متن، پایداری، هزینه).
|
||||
- تفکیک درست «چه چیزی را از بله بگیریم» و «چه چیزی را مستقل بسازیم».
|
||||
- پیشنهاد معماری دومسیره (بله + مسیر مستقل) درست و عملی است.
|
||||
- معرفی `bale-sdk` و توجه به مجوز اختصاصی آن (نکتهای که گزارشهای قبلی کمرنگ داشتند).
|
||||
- اشاره درست به اینکه Playwright روی وب بله الگوی جاافتادهای است.
|
||||
|
||||
**نقاط ضعف**
|
||||
- چند ادعای کلیدی بدون منبع است و در بررسی من تأیید نشد (جدول زیر).
|
||||
- مقایسه دقت متنباز با تجاری فقط بر اساس WER خودگزارشی مدلها روی Common Voice است. این عدد روی ویسهای واقعی و محاورهای معنی ندارد.
|
||||
- ارائهدهندگان تجاری مهم فارسی (Avanegar/Partai، Aipa) و Google Chirp 2 اصلاً در گزارش نیستند.
|
||||
- تحلیل خطای شخص ثالث ناقص است (مثلاً قیمتها بدون تاریخ و منبع).
|
||||
|
||||
### جدول راستیآزمایی ادعاهای گزارش A
|
||||
|
||||
| ادعا | وضعیت | یافته من |
|
||||
|---|---|---|
|
||||
| Bot API رسمی STT ندارد | ✅ | مستندات رسمی `docs.bale.ai`: شیء `Voice` فقط `file_id` و `file_unique_id` دارد و هیچ متدی برای ترنسکریپت نیست. **اصلاح:** گزارش میگوید «حجم و مدتزمان» هم هست؛ در مستندات رسمی نیست (SDKهای شخص ثالث فیلد `duration` را اضافه کردهاند). |
|
||||
| `bale-sdk` از `mtp1376`، نسخه 0.1.1، ۶۰۷ RPC در ۵۲ سرویس، پوشه `proto/` | ✅ | صفحه PyPI (27 Jun 2026) دقیقاً همین را میگوید. |
|
||||
| `bale-sdk` مجوز اختصاصی «All Rights Reserved» دارد | ✅ | تأیید شد. خود README هم میگوید استفاده فقط با حسابها و دادههایی که مجاز هستید و مطابق ToS بله باشد. |
|
||||
| سرویس `bale.turing.v1.AI` و متد `GetTranscript` با فیلدهای ۱ تا ۳ | 🟡 | **نتوانستم از منبع عمومی تأیید کنم.** فایلهای `docs/API_COVERAGE.md` و `docs/API_FA.md` در صفحه PyPI لینک شدهاند ولی برای من باز نشد. این مهمترین ادعای هر دو گزارش است و باید در ۳۰ دقیقه اول خودتان تأیید شود (بخش ۹). |
|
||||
| `aiobale-py` توسط امین مدنی، نسخه 0.3.x، متکی بر `blackboxprotobuf` | 🟡 | `aiobale-py` را پیدا نکردم. آنچه پیدا کردم: `aiobale` (Enalite، MIT، آخرین نسخه 0.1.5 در Aug 2025). توضیح خودش میگوید «از صفر و بدون فایل proto ساخته شده». |
|
||||
| `aiobale` اولیه رها شده | ✅ | آخرین انتشار Aug 2025؛ فورکها (`autobale`، `BaleClient`) میگویند نگهداری نمیشود و باگ دارد. |
|
||||
| `autobale` توسط علی عمادی، نسخه 1.0.0، MIT | ✅ | تأیید شد (مخزن `AliEmadii/autobale`). |
|
||||
| `BaleClient` | — | در گزارش نیست، ولی وجود دارد: فورک دیگری از aiobale (`Foad-KhAb/baleclient`، Python 3.11+). |
|
||||
| `bale-mcp-server` روی `aiobale-py` | 🟡/❌ | پروژهای به نام `bale-mcp` پیدا کردم که **با توکن بات رسمی** و `tapi.bale.ai` کار میکند، نه کلاینت کاربری. شاید پروژه دیگری باشد. |
|
||||
| `bale-chat-exporter` با نشست کلاینت | 🟡 | اسکرپر Playwright + BeautifulSoup برای Bale Web پیدا کردم (خروجی اکسل). همپروژهبودن با آنچه گزارش میگوید تأییدنشده است، ولی **الگوی Playwright روی وب بله اثباتشده است.** |
|
||||
| `Bale-Self-Bot` در GitLab چابکان | 🟡 | آن را پیدا نکردم. ولی `baleself` (PyPI، `zellias/bale-self-bot`) یک سلفبات مبتنی بر **Selenium** است و الگو را تأیید میکند. |
|
||||
| MajidAPI یک پراکسی روی موتورهای عمومی است | 🟡 | endpoint را در منابع قابل جستوجو پیدا نکردم. «پراکسی روی موتور عمومی» استنباط گزارش است، نه چیزی که ثابت شده باشد. هرچه باشد، SLA ندارد. |
|
||||
| فارسآوا (عامراندیش): ۱۰ هزار ساعت داده، نصب On-Premise، ساعتی ۴۵–۶۰ هزار تومان | 🟡 | قیمت و جزئیات را مستقل تأیید نکردم؛ قیمتها زماندارند. قبل از تصمیم، استعلام رسمی بگیرید. |
|
||||
| آیاو تایپ دقت «بالای ۹۸٪» | 🟡 | ادعای بازاریابی است. روی ویس تدارکات خودتان اندازه بگیرید. |
|
||||
| `hezarai/whisper-small-fa` روی CPU زیر ۲ ثانیه | 🟡 | محتمل برای ویس کوتاه، ولی مدل small روی فارسی محاورهای ضعیف است (بخش ۶). |
|
||||
| `nezamisafa/whisper-v3-turbo-persian-v1.0` با WER ۲۹.۹٪ | 🟡 | این مدل را نتوانستم باز کنم. مدل مشابه `vhdm/whisper-v3-turbo-persian-v1.1` WER 14.07٪ میدهد ولی روی **دادگان خودش**؛ اعداد WER بین دادگانهای مختلف قابل مقایسه نیستند. |
|
||||
| Faster-Whisper تا ۴ برابر سریعتر | ✅ (تا حد دانستههای من) | ادعای رسمی خود پروژه است. در مقاله PSRB هم Faster-Whisper هالوسینیشن کمتری از Whisper خام داشت. |
|
||||
| «مدلهای متنباز پایدارترین راهکار فنیاند» | ❌ | با داده PSRB سازگار نیست: ASR تجاری فارسی و Chirp 2 بهطور محسوس دقیقترند (بخش ۶.۲). متنباز «مستقلترین» است، نه لزوماً «دقیقترین». |
|
||||
|
||||
### ۲.۲ گزارش B: نقشه راه اجرایی
|
||||
|
||||
**نقاط قوت**
|
||||
- دیاگرامهای توالی و جریان تابآوری خوب و قابلتحویل به مدیر پروژهاند.
|
||||
- اصلاحات قبلی اعمال شده است: `compare_digest`، سقف روزانه، قفل ترتیبی، ۵۰۳ در خطا و متن خالی.
|
||||
- ورودیهای انسانی روشناند و «تأیید دکمه روی وب» بهعنوان ورودی گذاشته شده است.
|
||||
- Rollback با خالیکردن env درست است.
|
||||
|
||||
**ایرادها و ریسکهای اجرایی**
|
||||
|
||||
| مورد | توضیح |
|
||||
|---|---|
|
||||
| **کد مرجع شبهکد است** | `client.send_voice(...)`، `client.raw_call(service=..., method=...)` و `sent_msg.voice.to_media()` در README عمومی `bale-sdk` نیست. جدول متدهای سطحبالای آن ارسال رسانه ندارد. مدل دسترسی RPC خام آن اینطور است: «namespace برابر snake_case بخش آخر نام سرویس». یعنی `bale.messaging.v2.Messaging` → `client.messaging`. پس فراخوانی احتمالی `bale.turing.v1.AI` باید چیزی شبیه `client.ai.GetTranscript(...)` باشد (**استنباط من**). |
|
||||
| **آپلود ویس** | کارِ آپلود فایل و ساخت پیام صوتی (Media) را سند فرض گرفته ولی طراحی نکرده. این بخش از خود RPC ترنسکریپت سختتر است. راه سادهتر در بخش ۷.۲ آمده. |
|
||||
| **پاسخ `GetTranscript`** | طبق خود سند، پاسخ `bool mustWait` و یک `DownloadSource` دارد، نه رشته متن. یعنی احتمالاً متن باید از یک لینک/منبع دانلود خوانده شود. مثال کد این را مدیریت نمیکند. |
|
||||
| **کلید پیشفرض** | `os.environ.get("BALE_STT_INTERNAL_KEY", "bale_internal_secret")` هنوز مقدار پیشفرض دارد؛ بدون env هم بالا میآید. با `os.environ[...]` خطا بدهد. |
|
||||
| **شمارنده روزانه** | متغیر درونحافظه است: با ریست کانتینر صفر میشود و ریست روزانه هم ندارد (پس بعد از رسیدن به سقف تا ریاستارت بعدی میماند). |
|
||||
| **`.env` نمونه** | `API_KEY=bale_internal_secret` یعنی همان مقدار پیشفرض ضعیف. |
|
||||
| **مسیرهای محلی** | لینکها به `file:///c:/Users/Reza/Desktop/...` اشاره دارند. در اشتراک سند باید پاک شوند. |
|
||||
| **«بدون وقفه»** | ادعای «بدون حتی ۱ ثانیه قطعی» فقط وقتی درست است که `stt.py` در مواجهه با ۵۰۳ همان پرووایدر را بیخطا رد کند. این را باید در کد بات بررسی کرد، نه از روی سند. |
|
||||
|
||||
---
|
||||
|
||||
## ۳. آیا دیگران هم همین مشکل را داشتهاند؟ و چطور حل کردهاند؟
|
||||
|
||||
### ۳.۱ نتیجه کلی
|
||||
|
||||
**پروژه عمومیِ مشخصی که خروجی STT خود بله را برای یک بات استخراج کند پیدا نکردم** (گزارش A هم همین را میگوید). آنچه وجود دارد الگوهای مجاور است:
|
||||
|
||||
| الگو | نمونههای تأییدشده | درسی که برای ما دارد |
|
||||
|---|---|---|
|
||||
| **A. بات رسمی + ASR مستقل** | باتهای بله روی Webhook (مثلاً `hamedcode/bale-ai-bot` روی Cloudflare Workers)، باتهای مشتریمداری با LLM | رایجترین راه. بله فقط کانال پیام است؛ صوت دانلود و به موتور دیگر داده میشود. |
|
||||
| **B. کلاینت کاربری مهندسیمعکوسشده** | `aiobale` (MIT)، فورکهای `autobale` و `BaleClient`، `bale-sdk` (اختصاصی، ۶۰۷ RPC) | نشان میدهد نگهداری نشست کاربری و تماس با RPCهای داخلی شدنی است، ولی پروژهها تازهاند و بعضی رها میشوند. |
|
||||
| **C. اتوماسیون مرورگر** | `baleself` (Selenium)، اسکرپر چت Bale Web با Playwright | روی وب بله جاافتاده است، ولی معمولاً برای کارهای ساده مثل پاسخ خودکار و بکآپ. |
|
||||
| **D. سوءاستفاده خلاقانه از زیرساخت** | `BaleVPN` (تونل روی تماس صوتی بله) | نشان میدهد جامعه تا کجا از لایههای داخلی استفاده میکند؛ برای STT به کار ما نمیآید. |
|
||||
| **E. ابزارهای مدیریتی روی Bot API** | `bale-mcp`، SDKهای PHP/Node/Python/Swift/C# | فقط Bot API؛ بدون هیچ راهی برای رسیدن به ترنسکریپت. |
|
||||
|
||||
### ۳.۲ برداشت از همین شواهد
|
||||
|
||||
1. **راه مشترک جامعه این است که STT را بیرون از بله انجام میدهند.** هیچکس STT بله را «کرال» نکرده یا اگر کرده منتشر نشده است. پس در هر حال ریسک بالاتر و شواهد کمتری برای مسیر شماست.
|
||||
2. **زیرساخت لازم برای کرالر وجود دارد** (انتقال WebSocket، OTP، نشست، ارسال و دانلود فایل)؛ پس این پروژه «ساخت از صفر» نیست.
|
||||
3. **پایداری کتابخانهها ضعیف است** (aiobale رها شد، `bale-sdk` نسخه 0.1.1 است). هر معماری باید فرض کند اجزای متنباز ممکن است از کار بیفتند.
|
||||
4. **تنها چیزی که کل شرطبندی را توجیه میکند، دقت بله روی ویسهای شماست.** بدون آزمون دستی (بخش ۸، گام ۰) نباید ساخت شروع شود.
|
||||
|
||||
---
|
||||
|
||||
## ۴. کاتالوگ کامل راهکارها
|
||||
|
||||
برای هر راهکار: چه کار میکند، مزیت، عیب، تلاش (تخمین من)، وضعیت تأیید.
|
||||
|
||||
### گروه ۱: استفاده از STT خود بله
|
||||
|
||||
| # | راهکار | توضیح | مزایا | معایب | تلاش | وضعیت |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **S1** | **مسیر رسمی/تجاری با بله** | ایمیل یا تماس با تیم توسعهدهندگان/بیزینس بله برای API رسمی STT یا قرارداد | پایدار، بدون ریسک بن | احتمال موافقت کم؛ زمانبر | ۱ ایمیل | هیچ مستند عمومیای برای وجود چنین API نیست |
|
||||
| **S2** | **RPC مستقیم با `bale-sdk`** | استفاده از پروتوها و کلاینت آماده، فراخوانی `GetTranscript` | سریعترین مسیر فنی؛ پروتو آماده | **مجوز اختصاصی**؛ نسخه 0.1.1؛ ادعای RPC تأییدنشده | ۱–۲ روز | 🟡 |
|
||||
| **S3** | **RPC مستقیم با `aiobale` / `autobale` / `BaleClient` (MIT) + پروتوی خودساخته** | انتقال و نشست و آپلود از کتابخانه MIT؛ تعریف `GetTranscript` را از کپچر ترافیک خودتان در `web.bale.ai` بسازید | بدون مشکل مجوز؛ قابلنگهداری | کشف RPC و تعریف پروتو کار میبرد؛ کتابخانهها کمنگهداریاند | ۳–۵ روز | ✅ ابزارها؛ 🟡 RPC |
|
||||
| **S4** | **Playwright روی `web.bale.ai`** | کرومیوم headless با پروفایل پایدار؛ ارسال ویس، کلیک دکمه، خواندن DOM | مقاوم در برابر تغییر پروتکل؛ الگوی اثباتشده | ۳۵۰–۵۰۰MB رم؛ شکننده در برابر تغییر UI؛ وابسته به وجود دکمه در وب | ۱–۲ روز | ✅ الگو؛ 🟡 دکمه وب |
|
||||
| **S5** | **ترکیب «مرورگر برای ورود، پروتکل برای کار»** | Playwright فقط ورود/تمدید توکن (JWT) را انجام میدهد؛ کار اصلی را کلاینت WebSocket سبک میکند | از سبکبودن S3 و پایداری ورود S4 بهره میبرد | پیچیدگی دو لایه | ۴–۶ روز | پیشنهاد من |
|
||||
| **S6** | **شبیهساز اندروید + uiautomator** | اپ بله در شبیهساز، خواندن متن از رابط | مستقل از وب | رم >2GB؛ شکننده؛ سختترین نگهداری | ۵+ روز | آخرین چاره |
|
||||
| **S7** | **چت هوش مصنوعی بله (Oracle)** | ارسال ویس به بات هوش مصنوعی بله و گرفتن پاسخ | بدون کشف RPC | خروجی «متن + تفسیر»؛ تأخیر ۵–۱۵ ثانیه؛ اینکه ویس را پیاده میکند هم تأییدنشده | ۱ روز | 🟡 |
|
||||
|
||||
### گروه ۲: سرویسهای شخص ثالث
|
||||
|
||||
| # | راهکار | توضیح | مزایا | معایب | تلاش | وضعیت |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **T1** | **پراکسیهای غیررسمی (MajidAPI و مشابه)** | فراخوانی REST با لینک ویس | چند ساعت | بدون SLA؛ موتور نامشخص؛ ورودی URL (ویس باید لینک عمومی شود) | ساعتی | 🟡 |
|
||||
| **T2** | **ASR تجاری فارسی** | **Avanegar/Partai** (API: `api.ivira.ai/partai/speechRecognition`)، **Aipa**، عامراندیش (فارسآوا)، IOType | دقت بالا روی فارسی؛ پشتیبانی؛ گاهی On-Premise | هزینه ریالی؛ قرارداد | ۱ روز (adapter) | ✅ Avanegar/Aipa در بنچمارک؛ 🟡 قیمتها |
|
||||
| **T3** | **ASR ابری بینالمللی** | Google **Chirp 2**، Azure Speech، OpenAI Whisper API، Groq Whisper | Chirp 2 تقریباً همسطح بهترین تجاری فارسی | هزینه ارزی؛ محدودیت دسترسی؛ بستهبه شرایط شبکه | ۱ روز | ✅ Chirp/Azure در بنچمارک |
|
||||
|
||||
### گروه ۳: مدلهای متنباز خودمیزبان
|
||||
|
||||
| # | راهکار | توضیح | مزایا | معایب | تلاش | وضعیت |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **O1** | **Faster-Whisper large-v3 (+ `initial_prompt`/hotwords واژگان تدارکات)** | سرور OpenAI-compatible داخلی | مستقل؛ قابلشخصیسازی با واژگان؛ بهترین متنباز در بنچمارک | WER ≈ 34٪ روی فارسی واقعی؛ نیاز به GPU برای سرعت | ۱ روز | ✅ |
|
||||
| **O2** | **Whisper large-v3-turbo فاینتیون فارسی** (مثل `vhdm/whisper-v3-turbo-persian-v1.1`) | مدل فاینتیونشده روی دادگان فارسی | سریعتر؛ احتمالاً دقیقتر روی گفتار تمیز | ارقام خودگزارشی و روی دادگان متفاوت؛ باید روی داده شما سنجید | ۱ روز | 🟡 |
|
||||
| **O3** | **NeMo FastConformer فارسی (NVIDIA)** | `stt_fa_fastconformer_hybrid_large` | مدل مستقل از Whisper؛ دیکودر CTC | در بنچمارک WER ≈ 45٪ (ضعیفتر از Whisper) | ۲ روز | ✅ |
|
||||
| **O4** | **Vosk فارسی (`vosk-model-fa-0.5`)** | Kaldi/DNN-HMM سبک، CPU | بسیار سبک؛ آفلاین | WER ≈ 45٪ | ساعتی | ✅ |
|
||||
| **O5** | **SLPL wav2vec2 فارسی (دانشگاه شریف)** | XLS-R فاینتیونشده | مناسب برای تحقیق و ensemble | WER ≈ 47٪ | ۲ روز | ✅ |
|
||||
| **O6** | **Seamless M4T v2** | مدل چندزبانه | — | احتمال بازنویسی/هالوسینیشن (Interpretation Error در مقاله PSRB) | ۲ روز | ✅ مشکل |
|
||||
|
||||
### گروه ۴: لایههای بالاتر (نه جایگزین، بلکه تقویتکننده)
|
||||
|
||||
| # | راهکار | توضیح |
|
||||
|---|---|---|
|
||||
| **L1** | **LLM اجماعگیر با کاتالوگ** | کاندیدها + فهرست کالا/برند/واحد دیتابیس تدارکات را به LLM بدهید تا خروجی ساختاریافته و تصحیحشده بسازد. (مقاله PSRB هم میگوید LLMها خطاهای جزئی ASR را خوب اصلاح میکنند.) |
|
||||
| **L2** | **نرمالسازی فارسی** | یکسانسازی «ی/ک»، ارقام، نیمفاصله (ZWNJ) و اعداد حرفی. PSRB نشان میدهد مرزبندی کلمه و نیمفاصله WER را بالا میبرد. |
|
||||
| **L3** | **فاینتیون روی داده خودتان** | ویسهای واقعی + متن نهایی تأییدشده توسط کاربر → LoRA روی Whisper. بلندمدتترین سرمایهگذاری برای استقلال از بله. |
|
||||
| **L4** | **مسیریابی بر اساس اعتماد** | اگر کاندیدها در اعداد/واحدها اختلاف دارند، از کاربر تأیید بگیرید (UX)، نه حدس بزنید. |
|
||||
|
||||
---
|
||||
|
||||
## ۵. اگر از پروژههای متنباز الگوبرداری کنیم: چه چیزی را میتوان برداشت؟
|
||||
|
||||
| مؤلفه | منبع | مجوز | چه چیزی بردارید | ملاحظات |
|
||||
|---|---|---|---|---|
|
||||
| انتقال WebSocket، ورود با OTP، ذخیره نشست، Dispatcher | `aiobale` / `autobale` / `BaleClient` | MIT ✅ | همه چیز؛ حتی میتوانید فورک کنید | `aiobale` رها شده؛ `autobale` فعالتر ادعا میشود. نسخه را قفل کنید |
|
||||
| ارسال/دانلود فایل و رسانه | همان کتابخانهها (گزارش A: حدود ۸۰ متد) | MIT | آپلود و ساخت پیام صوتی | اگر ایده بخش ۷.۲ را بپذیرید (بات `sendVoice` بفرستد) اصلاً لازم نیست |
|
||||
| **تعریف RPC و پروتوها** | `bale-sdk` (`proto/`) | **اختصاصی ❌** | **فقط بهعنوان مرجع و با اجازه نویسنده**؛ کپی مستقیم ممنوع | راه تمیز: از کپی خود کپچر `web.bale.ai` پروتو بسازید. ابزارها: `bbpb`/`blackboxprotobuf`، `protoscope`، `protoc --decode_raw` |
|
||||
| دیکود بدون `.proto` | `blackboxprotobuf` (NCC Group؛ پکیج رسمی PyPI با نام `bbpb`) | MIT | تحلیل payloadهای ناشناخته | wire format خودتوصیف نیست؛ نام فیلد را باید خودتان حدس بزنید |
|
||||
| الگوی DOM و صفحهخوانی بله | اسکرپر Playwright چت (GitHub) و `baleself` (Selenium) | بررسی مجوز هر مخزن | سلکتورها، انتظار برای بارگذاری، اسکرول | سلکتورها با هر آپدیت UI میشکنند |
|
||||
| سرور OpenAI-compatible برای Whisper | Faster-Whisper، پروژههای سرور مبتنی بر آن | معمولاً MIT/Apache | سرور داخلی با همان `/v1/audio/transcriptions` | همان قرارداد adapter بات؛ تغییر کد لازم نیست |
|
||||
| نرمالسازی و ارزیابی فارسی | مقاله و دادگان PSRB (`PartAI/PSRB` در Hugging Face)، `hazm` | بررسی | معیار SW-WER، فهرست خطاهای رایج فارسی | دادگان عمومی فقط زیرمجموعه است |
|
||||
|
||||
### قواعد پیشنهادی الگوبرداری
|
||||
1. **تمیزکاری حقوقی:** منطق را از متنباز MIT بردارید و تعریف RPC را از کپچر خودتان تولید کنید (clean-room).
|
||||
2. **لایهبندی:** adapter فقط قرارداد OpenAI را بشناسد؛ هیچ import از `bale*` بیرون از ماژول `driver/` نباشد.
|
||||
3. **قفل نسخه و آینه:** نسخه دقیق کتابخانهها را pin و کپی داخلی نگه دارید (رهاشدن مخزن، مثل `aiobale`، اتفاق افتاده است).
|
||||
4. **تست قرارداد:** با یک «ویس طلایی» شبانه بررسی کنید پاسخ هنوز ساختار قبلی را دارد.
|
||||
|
||||
---
|
||||
|
||||
## ۶. اگر شخص ثالث بخواهیم: چه کار کنیم؟
|
||||
|
||||
### ۶.۱ سادهترین کار: هر سرویس یک «پرووایدر» در `stt.py`
|
||||
|
||||
بات شما اکنون پرووایدرهای شمارهدار با قرارداد OpenAI دارد. پس هر شخص ثالث را یا مستقیم (اگر OpenAI-compatible است) یا با یک adapter کوچک اضافه کنید. مزیت: چند ASR موازی، اجماع LLM، و حذف خودکار پرووایدر خراب (۵۰۳).
|
||||
|
||||
### ۶.۲ داده مستقل برای انتخاب: بنچمارک PSRB (مقاله arXiv:2505.21230)
|
||||
|
||||
مقاله «PSRB: A Comprehensive Benchmark for Evaluating Persian ASR Systems» (مرکز تحقیقات Part AI، May 2025) ده سیستم را روی گفتار واقعی (۵۸٪ خودانگیخته، ۷۱٪ غیررسمی، ۵۳٪ نویزی/تلفنی) سنجیده است.
|
||||
|
||||
| مدل | نوع | CER | WER | SW-WER |
|
||||
|---|---|---|---|---|
|
||||
| **Avanegar** | تجاری فارسی | **8.75** | **19.30** | **15.68** |
|
||||
| **Google Chirp v2** | ابری چندزبانه | 9.05 | 19.92 | 15.99 |
|
||||
| **Aipa** | تجاری فارسی | 10.43 | 24.64 | 19.83 |
|
||||
| Faster-Whisper large-v3 | متنباز | 13.72 | 33.93 | 26.77 |
|
||||
| Azure | ابری چندزبانه | 15.78 | 33.94 | 28.06 |
|
||||
| Seamless M4T v2 | متنباز | 22.30 | 38.85 | 34.76 |
|
||||
| Whisper large-v3 | متنباز | 18.92 | 41.49 | 36.97 |
|
||||
| Vosk fa 0.5 | متنباز | 23.96 | 44.62 | 39.41 |
|
||||
| NeMo FastConformer fa | متنباز | 19.42 | 44.85 | 37.58 |
|
||||
| SLPL wav2vec2 | متنباز | 19.06 | 46.74 | 39.16 |
|
||||
|
||||
**نکات خواندن این جدول:**
|
||||
- **ASR تجاری فارسی (و Chirp 2) تقریباً ۱۴ امتیاز WER از بهترین متنباز جلوتر است.** این یعنی ادعای «متنباز پایدارترین راهکار» در گزارش A دقیق نیست. متنباز مستقلتر است، نه دقیقتر.
|
||||
- **سوگیری احتمالی:** نویسندگان از Part AI هستند و از آدرس API در پاورقی مقاله، Avanegar ظاهراً محصول همان شرکت است. پس عدد Avanegar را «بالاترین حد خوشبینانه» بدانید.
|
||||
- **دادهها عمومیاند، نه واژگان تدارکات.** نام کالا، برند، واحد و اعداد در محاوره سختترین بخشاند و فقط آزمون روی ویسهای خودتان تصمیمگیر است.
|
||||
- مقاله میگوید خطا در **گفتار غیررسمی و لهجههای محلی** (مثلاً مشهدی، یزدی) بیشتر است و **Whisper/Seamless هالوسینیشن و بازنویسی** دارند. هر دو برای متن تدارکات خطرناکاند، چون عدد یا واحد اشتباه معنی سفارش را عوض میکند.
|
||||
- ترکیب «مرتبه اول: دو سیستم متفاوت» معمولاً از «یک سیستم قوی» پایدارتر است. اجماع LLM هم برای همین است.
|
||||
|
||||
### ۶.۳ ارزیابی شخص ثالث: روش پیشنهادی (بدون وابستگی به ادعاهای فروشنده)
|
||||
|
||||
1. **مجموعه طلایی:** ۱۰۰ ویس واقعی (حداقل ۳۰) با رونوشت انسانی و جدول کالا/تعداد/واحد.
|
||||
2. **اجرای همه کاندیدها** روی همان ویسها: بله (دستی در گام ۰)، Avanegar، Aipa، Chirp 2، Faster-Whisper، مدل فاینتیون فارسی.
|
||||
3. **سه معیار:** CER (برای فارسی مناسبتر از WER)، WER و مهمتر **F1 استخراج اقلام** (نام کالا، تعداد، واحد).
|
||||
4. **انتخاب بر اساس «بهبود اجماع»،** نه دقت تکی: هر کاندید را به مجموعه فعلی اضافه کنید و ببینید خطای نهایی استخراج اقلام چقدر کم میشود.
|
||||
5. **هزینه:** هزینه به ازای هر ویس ۱۰–۳۰ثانیهای در ماه برای حجم واقعی شما.
|
||||
|
||||
### ۶.۴ چه سرویسی را کجا بگذاریم؟ (پیشنهاد مقدماتی)
|
||||
|
||||
| نقش | کاندید | چرا |
|
||||
|---|---|---|
|
||||
| پرووایدر ۱ (تجاری فارسی) | Avanegar یا Aipa | بهترین در بنچمارک فارسی و لهجهها |
|
||||
| پرووایدر ۲ (ابری) | Chirp 2 (اگر دسترسی ممکن است) | تقریباً همسطح؛ مکمل متفاوت |
|
||||
| پرووایدر ۳ (خودمیزبان) | Faster-Whisper large-v3 + واژگان تدارکات | مستقل؛ قابل بهبود با داده خودتان |
|
||||
| پرووایدر ۴ (بله) | کرالر بله (S3/S4/S5) | تنها کاندیدی که تفاوت واقعی (آموزشدیده روی دادههای متفاوت) دارد؛ ارزش آن را گام ۰ تعیین میکند |
|
||||
| پشتیبان سبک | Vosk | فقط برای اینکه همیشه یک خروجی داشته باشیم |
|
||||
|
||||
---
|
||||
|
||||
## ۷. بهترین ترکیب برای کرالر شما
|
||||
|
||||
### ۷.۱ اصول طراحی
|
||||
|
||||
1. **کرالر بله «یک کاندید» است، نه ستون فقرات.** اگر خراب شد، سیستم باید بدون آن ادامه دهد.
|
||||
2. **حداقل دو کاندید غیر بله همیشه فعال** باشند تا اجماع بدون بله هم معنی داشته باشد.
|
||||
3. **تصمیم بر اساس داده خودتان**، نه ادعای مقاله و فروشنده.
|
||||
4. **هر چیز بله پشت یک قرارداد ثابت (OpenAI-compatible)** باشد تا موتور داخلی عوض شود و بات دست نخورد.
|
||||
5. **داده را جمع کنید** تا در بلندمدت از بله مستقل شوید (L3).
|
||||
|
||||
### ۷.۲ معماری پیشنهادی
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[کاربر: ویس] --> BOT[بات تدارکات - Bot API رسمی]
|
||||
BOT --> PRE[پیشپردازش: 16kHz mono، VAD، مدت]
|
||||
|
||||
PRE --> P1[پرووایدر 1: ASR تجاری فارسی]
|
||||
PRE --> P2[پرووایدر 2: Chirp 2 / ابری]
|
||||
PRE --> P3[پرووایدر 3: Faster-Whisper + واژگان]
|
||||
PRE --> RELAY[رله ویس به حساب کرالر: sendVoice با file_id]
|
||||
|
||||
RELAY --> CR[کرالر بله: adapter]
|
||||
CR -->|انتخاب موتور با env| E1[موتور RPC: GetTranscript]
|
||||
CR -->|فالبک| E2[موتور Playwright]
|
||||
CR --> DEL[حذف پیام رله]
|
||||
|
||||
P1 --> NORM[نرمالسازی فارسی: ZWNJ، ارقام، یی/کاف]
|
||||
P2 --> NORM
|
||||
P3 --> NORM
|
||||
E1 --> NORM
|
||||
E2 --> NORM
|
||||
|
||||
NORM --> LLM[LLM اجماعگیر + کاتالوگ کالا/برند/واحد]
|
||||
LLM --> CONF{اختلاف در عدد/واحد؟}
|
||||
CONF -- خیر --> OUT[ثبت استعلام]
|
||||
CONF -- بله --> ASK[از کاربر تأیید بگیر]
|
||||
ASK --> OUT
|
||||
OUT --> DATA[(ذخیره ویس + متن تأییدشده برای فاینتیون)]
|
||||
```
|
||||
|
||||
### ۷.۳ ایده کلیدی: «رله» بهجای آپلود
|
||||
|
||||
گزارش B فرض میکند کرالر ویس را به **Saved Messages** خودش آپلود میکند. راه سادهتر و سالمتر:
|
||||
|
||||
1. حساب کرالر یکبار به بات شما `/start` میزند (گفتگوی خصوصی بات↔کرالر).
|
||||
2. بات با Bot API رسمی `sendVoice(chat_id=<id حساب کرالر>, voice=<file_id همان ویس کاربر>)` میفرستد. طبق مستندات رسمی، ارسال با `file_id` بدون آپلود مجدد و بدون محدودیت است. (`file_id` مخصوص همان بات است؛ اینجا همان بات میفرستد، پس مشکلی نیست.)
|
||||
3. حساب کرالر (کلاینت کاربری) آپدیت پیام جدید را میگیرد؛ **شیء Media ویس، `OutPeer` و `message_id` همه همانجاست.** دقیقاً سه ورودی `GetTranscript` (Media voice، OutPeer، MessageId).
|
||||
4. کرالر `GetTranscript` را صدا میزند، متن را میگیرد.
|
||||
5. پیام با `deleteMessage` رسمی بات (برای پیام خروجی در گفتگوی خصوصی مجاز است، تا ۴۸ ساعت) یا از سمت کرالر حذف میشود.
|
||||
|
||||
**مزایا:** بدون RPC آپلود؛ بدون ساخت `Media` از صفر؛ بدون الگوی شبهانسانی «آپلود فایل» روی حساب؛ ID پیام و peer واقعیاند.
|
||||
**ریسک:** اینکه RPC روی ویس **دریافتی** هم کار کند (در اپ، دکمه روی ویس دریافتی هم هست؛ ولی تأییدش با آزمون واقعی لازم است).
|
||||
|
||||
### ۷.۴ لایهبندی موتور کرالر (به ترتیب اولویت)
|
||||
|
||||
| اولویت | موتور | شرط | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۱ | **E1: RPC مستقیم** (transport از `aiobale`/`autobale`، پروتوی خودساخته) | RPC تأیید شد | سبک (<۲۰MB)، متن خالص |
|
||||
| ۲ | **E1+: ورود با مرورگر، کار با پروتکل (S5)** | ورود/تمدید JWT ناپایدار باشد | Playwright فقط برای توکن |
|
||||
| ۳ | **E2: Playwright کامل** | RPC تأیید نشد ولی دکمه وب هست | رم ۳۵۰–۵۰۰MB |
|
||||
| ۴ | شبیهساز اندروید | هیچکدام ممکن نشد | فقط در صورت ارزش بالای بله |
|
||||
|
||||
اگر RPC در باندل وب بله وجود دارد، **به احتمال زیاد دکمه هم در وب هست** (استنباط من: گزارشها میگویند پروتوها از کلاینت وب بازیابی شدهاند). ولی تا خودتان نبینید فرض نکنید.
|
||||
|
||||
### ۷.۵ تقویت اجماع (جایی که بیشترین دقت را میگیرید)
|
||||
|
||||
| اقدام | اثر مورد انتظار | هزینه |
|
||||
|---|---|---|
|
||||
| تزریق **کاتالوگ کالا/برند/واحد** به LLM اجماعگیر | بیشترین کاهش خطای استخراج اقلام | کم |
|
||||
| `initial_prompt`/hotwords واژگان تدارکات در Faster-Whisper | بهبود نام برندها و اصطلاحها | کم |
|
||||
| نرمالسازی فارسی (ZWNJ، ارقام) پیش از اجماع | کاهش اختلافهای ساختگی بین کاندیدها | کم |
|
||||
| قاعده «اختلاف عدد/واحد ⇒ تأیید از کاربر» | حذف سفارش اشتباه | متوسط (UX) |
|
||||
| ثبت ویس + متن تأییدشده → LoRA روی Whisper | استقلال بلندمدت؛ بهبود روی لهجه و واژگان شما | بالا (ولی ارزشمندترین) |
|
||||
| VAD پیش از ASR | کاهش هالوسینیشن Whisper روی سکوت/نویز (پیشنهاد مقاله PSRB) | کم |
|
||||
|
||||
### ۷.۶ سناریوهای تصمیم
|
||||
|
||||
| اگر… | آنگاه… |
|
||||
|---|---|
|
||||
| گام ۰: بله بهتر از مجموعه فعلی نیست | **کرالر را نسازید.** سرمایه را روی ASR تجاری + فاینتیون بگذارید. |
|
||||
| بله کمی بهتر است (< ۱۰٪) | فقط مسیر سبک E1 را بسازید؛ Playwright و شبیهساز را رها کنید. |
|
||||
| بله بهطور چشمگیر بهتر است (> ۱۵٪) | E1 + فالبک E2 + مسیر رسمی S1 را موازی دنبال کنید. |
|
||||
| RPC پیدا نشد و دکمه وب نیست | S6 فقط اگر ارزش بله زیاد باشد؛ در غیر این صورت منصرف شوید. |
|
||||
| بله خوب است ولی ناپایدار | از خروجی بله بهعنوان **برچسبگذار** برای ساخت دادگان فاینتیون استفاده کنید (L3) تا وابستگی کم شود. |
|
||||
|
||||
---
|
||||
|
||||
## ۸. نقشه راه پیشنهادی
|
||||
|
||||
| روز | کار | خروجی | دروازه |
|
||||
|---|---|---|---|
|
||||
| ۰ | **آزمون دستی دقت:** ۳۰ ویس واقعی را در اپ بله به متن تبدیل کنید و با کاندیدهای فعلی مقایسه کنید | جدول CER/WER و F1 اقلام | G0: بهبود معنیدار یا توقف |
|
||||
| ۰ | جستوجوی `GetTranscript` در `bale-sdk`؛ تأیید دکمه روی `web.bale.ai`؛ ایمیل به بله (S1) | یک صفحه یافته | G1: RPC یا دکمه وجود دارد |
|
||||
| ۱ | کپچر ترافیک وب و ساخت پروتوی خودتان | `req.bin`/`resp.bin`، `findings.md` | G2 |
|
||||
| ۲–۳ | پروتوتایپ E1 با رله بات→کرالر | یک ویس → یک متن | G3 |
|
||||
| ۳–۴ | adapter سختشده (۵۰۳، سقف روزانه پایدار، قفل، لاگ) | اتصال به `stt.py` | — |
|
||||
| ۴–۵ | **موازی:** پرووایدرهای شخص ثالث (Avanegar/Aipa/Chirp، Faster-Whisper) | سنجش روی مجموعه طلایی | G4: بهبود اجماع |
|
||||
| ۶–۱۰ | سواک ۵۰–۱۰۰ ویس + مانیتورینگ + مجموعه طلایی شبانه | گزارش پایداری | G5: استقرار |
|
||||
| ماه ۲+ | جمعآوری داده و LoRA | مدل داخلی | کاهش وابستگی |
|
||||
|
||||
**KPIها:** F1 استخراج اقلام، CER، نرخ اختلاف عدد/واحد، نرخ موفقیت کرالر، p95 تأخیر، هزینه به ازای ویس.
|
||||
|
||||
---
|
||||
|
||||
## ۹. ضمیمه: دستورها و نمونهها
|
||||
|
||||
### ۹.۱ کشف سریع (۳۰ دقیقه)
|
||||
|
||||
```bash
|
||||
python -m venv .v && . .v/bin/activate
|
||||
pip index versions aiobale-py # آیا واقعاً وجود دارد؟
|
||||
pip index versions aiobale autobale baleclient bale-sdk
|
||||
|
||||
pip download bale-sdk --no-deps --no-binary :all:
|
||||
tar xzf bale_sdk-*.tar.gz
|
||||
grep -rniE "transcri|turing|speech|asr|stt" bale_sdk-*/proto bale_sdk-*/docs | head -50
|
||||
```
|
||||
|
||||
در پایتون:
|
||||
|
||||
```python
|
||||
from bale import ALL_RPCS
|
||||
for r in ALL_RPCS:
|
||||
s = str(r).lower()
|
||||
if any(k in s for k in ("transcri", "turing", "speech")):
|
||||
print(r)
|
||||
```
|
||||
|
||||
### ۹.۲ ساخت پروتو از کپچر خودتان
|
||||
|
||||
1. `web.bale.ai` را با حساب اختصاصی باز کنید، DevTools → Network (فیلتر WS و Fetch).
|
||||
2. روی ویس «تبدیل به متن» را بزنید؛ درخواست/پاسخ را ذخیره کنید.
|
||||
3. پیشوند ۵ بایتی gRPC-Web را بردارید (برای POST gRPC-Web؛ در WebSocket بله قالب را باید ببینید).
|
||||
4. `protoc --decode_raw < req.bin` یا `protoscope`.
|
||||
|
||||
### ۹.۳ اسکلت رله بات→کرالر (شبهکد؛ نام متدها را با کتابخانه واقعی تطبیق دهید)
|
||||
|
||||
```python
|
||||
# سمت بات (Bot API رسمی)
|
||||
async def relay_voice_to_crawler(voice_file_id: str, crawler_user_id: int) -> int:
|
||||
msg = await bot_api("sendVoice", chat_id=crawler_user_id, voice=voice_file_id)
|
||||
return msg["message_id"]
|
||||
|
||||
# سمت کرالر (کلاینت کاربری) - شبهکد
|
||||
@client.on(NewMessage) # فقط از بات خودمان
|
||||
async def on_voice(event):
|
||||
media, peer, mid = event.media, event.peer, event.id
|
||||
resp = await client.ai.GetTranscript(voice=media, outPeer=peer, messageId=mid) # نامها = استنباط
|
||||
while resp.mustWait:
|
||||
await asyncio.sleep(1.5)
|
||||
resp = await client.ai.GetTranscript(voice=media, outPeer=peer, messageId=mid)
|
||||
text = await read_download_source(resp.downloadSource)
|
||||
await client.delete_messages(peer, [mid])
|
||||
return text
|
||||
```
|
||||
|
||||
### ۹.۴ قرارداد adapter (بدون تغییر در بات)
|
||||
|
||||
- ورودی: `POST /v1/audio/transcriptions` (multipart: `file`, `model`, `language`).
|
||||
- خروجی موفق: `{"text": "..."}`.
|
||||
- هر خطا یا متن خالی: **HTTP 503** (نه ۲۰۰ با متن خالی).
|
||||
- کلید داخلی: از env الزامی، بدون مقدار پیشفرض؛ مقایسه با `secrets.compare_digest`.
|
||||
- سقف روزانه و صف ترتیبی؛ شمارنده را پایدار (Redis/فایل) و روزانه ریستشونده بگیرید.
|
||||
|
||||
---
|
||||
|
||||
## ۱۰. پرسشهای باز (قبل از شروع ساخت پاسخ دهید)
|
||||
|
||||
1. `GetTranscript` واقعاً در `bale-sdk` وجود دارد؟ (۳۰ دقیقه، بخش ۹.۱)
|
||||
2. دکمه «تبدیل به متن» روی `web.bale.ai` برای ویس **دریافتی** هست؟
|
||||
3. خروجی پاسخ: متن مستقیم است یا `DownloadSource` که باید دانلود شود؟
|
||||
4. `stt.py` با ۵۰۳ و متن خالی چه میکند؟
|
||||
5. حجم واقعی ویسهای شما در روز و طول متوسط آنها چقدر است؟ (برای هزینه و سقف)
|
||||
6. آیا میتوانید ۳۰–۱۰۰ ویس واقعی با رونوشت انسانی تهیه کنید؟ (بدون آن هیچ مقایسهای معتبر نیست.)
|
||||
7. آیا دسترسی شبکه به Chirp 2 / ارائهدهندگان خارجی از سرورهای شما ممکن است؟
|
||||
8. نگهداری: چه کسی مالک کرالر است اگر بله پروتکل را عوض کرد؟
|
||||
|
||||
---
|
||||
|
||||
## ۱۱. منابع
|
||||
|
||||
- مستندات رسمی بات بله: https://docs.bale.ai/
|
||||
- `bale-sdk` (PyPI): https://pypi.org/project/bale-sdk/ و https://pypi.org/project/bale-sdk/0.1.1/
|
||||
- `aiobale` (PyPI): https://pypi.org/project/aiobale ، مستندات: https://docs.aiobale.ir
|
||||
- `autobale` (PyPI): https://pypi.org/project/autobale/
|
||||
- `BaleClient` (PyPI): https://pypi.org/project/BaleClient/
|
||||
- `baleself` (Selenium): https://pypi.org/project/baleself/
|
||||
- `bale-mcp` (PyPI): https://pypi.org/project/bale-mcp/
|
||||
- بات هوش مصنوعی بله روی Cloudflare Workers: https://github.com/hamedcode/bale-ai-bot
|
||||
- BaleVPN: https://github.com/2077DevWave/BaleVPN
|
||||
- Blackbox Protobuf (NCC Group): https://www.github.com/nccgroup/blackboxprotobuf
|
||||
- PSRB (Persian ASR benchmark): https://arxiv.org/pdf/2505.21230
|
||||
- مدل فارسی Whisper (نمونه): https://huggingface.co/vhdm/whisper-large-fa-v1
|
||||
- خبر انتشار نسخه بله با قابلیت تبدیل ویس به متن (آنا): https://ana.ir/fa/news/994908
|
||||
@@ -250,3 +250,24 @@ def propose_project_distribution(project_names: list[str], items: list[dict]) ->
|
||||
mat_name = it.get("material_name") or f"قلم {idx + 1}"
|
||||
proposals.append((mat_name, assigned_p))
|
||||
return proposals
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Authorized Whitelist Users (Static Role Configuration)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
AUTHORIZED_USERS: dict[int, dict] = {
|
||||
1461166742: {
|
||||
"user_id": "03091956-99ad-4729-923a-a6aa1c5c13be",
|
||||
"display_name": "مدیر تدارکات",
|
||||
"role": "procurement_manager",
|
||||
"is_admin": True,
|
||||
},
|
||||
1839839922: {
|
||||
"user_id": "03091956-99ad-4729-923a-a6aa1c5c13be",
|
||||
"display_name": "همکار / تست",
|
||||
"role": "procurement_manager",
|
||||
"is_admin": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ async def handle_update(ctx: BotContext, update: dict) -> None:
|
||||
|
||||
|
||||
async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
|
||||
user = await _gate(ctx, chat_id, message)
|
||||
if user is None:
|
||||
return
|
||||
|
||||
text = (message.get("text") or message.get("caption") or "").strip()
|
||||
source = "text"
|
||||
voice_transcripts: list[dict] | None = None
|
||||
@@ -56,10 +60,6 @@ async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
|
||||
return
|
||||
ctx.pending.pop(chat_id) # stale/irrelevant flow → fall through
|
||||
|
||||
user = await _gate(ctx, chat_id, message)
|
||||
if user is None:
|
||||
return # gate either started linking or is mid-linking
|
||||
|
||||
reply_to = message.get("reply_to_message")
|
||||
reply_to_msg_id = reply_to.get("message_id") if reply_to else None
|
||||
|
||||
@@ -272,25 +272,58 @@ async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
|
||||
# helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
UNREGISTERED_FILE = Path(__file__).resolve().parent.parent / "unregistered_users.json"
|
||||
|
||||
|
||||
def _record_unregistered_user(chat_id: int, message: dict) -> None:
|
||||
from_user = message.get("from") or {}
|
||||
record = {
|
||||
"chat_id": chat_id,
|
||||
"username": from_user.get("username"),
|
||||
"first_name": from_user.get("first_name"),
|
||||
"last_name": from_user.get("last_name"),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"text_preview": (message.get("text") or "")[:100],
|
||||
}
|
||||
records = []
|
||||
try:
|
||||
if UNREGISTERED_FILE.exists():
|
||||
content = UNREGISTERED_FILE.read_text(encoding="utf-8").strip()
|
||||
if content:
|
||||
records = json.loads(content)
|
||||
if not isinstance(records, list):
|
||||
records = []
|
||||
except Exception:
|
||||
records = []
|
||||
records.append(record)
|
||||
try:
|
||||
UNREGISTERED_FILE.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
except Exception as e:
|
||||
logger.warning("Could not write to unregistered_users.json: %s", e)
|
||||
|
||||
|
||||
async def _gate(ctx: BotContext, chat_id: int, message: dict) -> LinkedUser | None:
|
||||
"""Link or admin gate. Returns None when the message was consumed by linking."""
|
||||
from . import links
|
||||
"""Whitelist gate (Casdoor bypassed). Unregistered users are blocked and logged."""
|
||||
from app.defaults import AUTHORIZED_USERS
|
||||
|
||||
link = await ctx.links.get(chat_id)
|
||||
if link and link.user_id:
|
||||
return LinkedUser(chat_id, link.user_id, link.display_name, link.role, is_admin=False)
|
||||
if chat_id in ctx.admin_chat_ids:
|
||||
return LinkedUser(chat_id, None, None, None, is_admin=True)
|
||||
# 1. Whitelist check (only registered whitelist IDs)
|
||||
if chat_id in AUTHORIZED_USERS:
|
||||
u = AUTHORIZED_USERS[chat_id]
|
||||
return LinkedUser(
|
||||
chat_id=chat_id,
|
||||
user_id=u.get("user_id"),
|
||||
display_name=u.get("display_name"),
|
||||
role=u.get("role", "procurement_manager"),
|
||||
is_admin=u.get("is_admin", True),
|
||||
)
|
||||
|
||||
flow = ctx.pending.get(chat_id)
|
||||
if flow and flow.kind == "link_password":
|
||||
await links.finish_link(ctx, chat_id, flow, text=None, password=message.get("text") or "")
|
||||
return None
|
||||
if flow and flow.kind == "link_username":
|
||||
await links.finish_username_step(ctx, chat_id, flow, message.get("text") or "")
|
||||
return None
|
||||
await ctx.send(chat_id, LINK_START)
|
||||
ctx.pending.set(chat_id, "link_username", {})
|
||||
# 2. Block unregistered user and record log
|
||||
_record_unregistered_user(chat_id, message)
|
||||
await ctx.send(chat_id, "دسترسی شما هنوز تعریف نشده است. درخواست شما جهت بررسی ثبت شد.")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -29,12 +29,18 @@ async def handle_callback(ctx, cq: dict) -> None:
|
||||
|
||||
|
||||
async def _gate(ctx, chat_id: int) -> LinkedUser | None:
|
||||
link = await ctx.links.get(chat_id)
|
||||
if link and link.user_id:
|
||||
return LinkedUser(chat_id, link.user_id, link.display_name, link.role, is_admin=False)
|
||||
if chat_id in ctx.admin_chat_ids:
|
||||
return LinkedUser(chat_id, None, None, None, is_admin=True)
|
||||
await ctx.send(chat_id, UNLINKED)
|
||||
from app.defaults import AUTHORIZED_USERS
|
||||
|
||||
if chat_id in AUTHORIZED_USERS:
|
||||
u = AUTHORIZED_USERS[chat_id]
|
||||
return LinkedUser(
|
||||
chat_id=chat_id,
|
||||
user_id=u.get("user_id"),
|
||||
display_name=u.get("display_name"),
|
||||
role=u.get("role", "procurement_manager"),
|
||||
is_admin=u.get("is_admin", True),
|
||||
)
|
||||
await ctx.send(chat_id, "دسترسی شما هنوز تعریف نشده است. درخواست شما جهت بررسی ثبت شد.")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -31,12 +31,15 @@ async def handle_command(ctx, chat_id: int, text: str) -> None:
|
||||
|
||||
|
||||
async def _start(ctx, chat_id: int) -> None:
|
||||
from app.defaults import AUTHORIZED_USERS
|
||||
|
||||
await ctx.send(chat_id, WELCOME)
|
||||
if chat_id in AUTHORIZED_USERS or chat_id in ctx.admin_chat_ids:
|
||||
return
|
||||
link = await ctx.links.get(chat_id)
|
||||
if not link or not link.user_id:
|
||||
if chat_id not in ctx.admin_chat_ids:
|
||||
await ctx.send(chat_id, "برای شروع، حساب خود را متصل کن — نام کاربری Casdoor را بفرست:")
|
||||
ctx.pending.set(chat_id, "link_username", {})
|
||||
await ctx.send(chat_id, "برای شروع، حساب خود را متصل کن — نام کاربری Casdoor را بفرست:")
|
||||
ctx.pending.set(chat_id, "link_username", {})
|
||||
|
||||
|
||||
async def _drafts(ctx, chat_id: int) -> None:
|
||||
@@ -71,6 +74,13 @@ async def _cancel(ctx, chat_id: int) -> None:
|
||||
|
||||
|
||||
async def _whoami(ctx, chat_id: int) -> None:
|
||||
from app.defaults import AUTHORIZED_USERS
|
||||
|
||||
if chat_id in AUTHORIZED_USERS:
|
||||
u = AUTHORIZED_USERS[chat_id]
|
||||
role = ROLE_FA.get(u.get("role") or "", u.get("role") or "مدیر تدارکات")
|
||||
await ctx.send(chat_id, f"اتصال (لیست سفید): {esc(u.get('display_name'))} — {role}")
|
||||
return
|
||||
link = await ctx.links.get(chat_id)
|
||||
if chat_id in ctx.admin_chat_ids:
|
||||
await ctx.send(chat_id, "حالت ادمین (بدون اتصال Casdoor).")
|
||||
|
||||
@@ -151,6 +151,13 @@ async def understand(settings: Settings, text: str | list[dict], draft_summary:
|
||||
lines.append(f"- نسخه {idx} ({name}): «{t}»")
|
||||
lines.append("\nبا تلفیق این نسخهها و تصحیح خطاهای شنیداری، پیام را تحلیل کن و consensus_text را به همراه intent و patch بساز.")
|
||||
user_content = "\n".join(lines)
|
||||
elif isinstance(text, str) and ("مدل 1" in text or "مدل ۱" in text) and ("مدل 2" in text or "مدل ۲" in text):
|
||||
user_content = (
|
||||
"پیام صوتی کاربر توسط چند مدل تبدیل گفتار به متن (STT) به صورت زیر پیادهسازی شده است:\n"
|
||||
+ normalize_digits(text)
|
||||
+ "\n\nبا تلفیق این نسخهها و تصحیح خطاهای شنیداری، پیام را تحلیل کن و consensus_text را به همراه intent و patch بساز."
|
||||
)
|
||||
first_text = text
|
||||
else:
|
||||
user_content = normalize_digits(str(text))
|
||||
first_text = str(text)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,77 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from app.config import settings
|
||||
from app.bale.client import BaleClient
|
||||
from app.api.client import ErpClient
|
||||
from app.runtime import BotContext, LinkedUser
|
||||
from app.nlu import understand
|
||||
from app.handlers import quote
|
||||
from app.defaults import AUTHORIZED_USERS
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s — %(message)s")
|
||||
logger = logging.getLogger("send_analysis")
|
||||
|
||||
CHAT_ID = 1461166742
|
||||
|
||||
TRANSCRIPTS = [
|
||||
{
|
||||
"name": "مدل 1 (Whisper)",
|
||||
"text": "الان شبت بخیر می خواستیم که بیست و پنج تا کیسه عرض به خدمت شما که گچ و خاک از مصطفوی بخریم و کنارش هم یه پنجاه تا کیسه سیمان پنجاه کیلویی که اونا تقریبا در می آد پامون دونه ای بیست و پنج میلیون سیمانی ها پامون در می آد حدودا چهل میلیون اینا دونه ای بهمون گفته که می تونین سه قسط بهم بدید کلی پولش ها در طول زمان و یا بیاید یه بار پولش رو امروز بهم بدین یه تخفیف ده درصدی تقریبا براتون لحاظ می کنم عددش رو کم می کنم خریدمون هم عمدتا برای منشیانه و دفتر پیش فاکتورم گفته تا حدودا بیست روزی نامه براتون اوکیه همین",
|
||||
},
|
||||
{
|
||||
"name": "مدل 2 (Vosk)",
|
||||
"text": "سلام شبت بخیر. میخواستیم که 25 تا کیسه عرض به خدمت شما که گچ و خاک از مستفدی بخریم و کنارشم یه 50 تا کیسه سیمان 50 کیلویی که اونا تقریباً در میاد پامون دونهای 25 میلیون، سیمانیا پامون در میاد حدوداً 40 میلیون اینا دونهای. بهمون گفته که میتونین سه قسم بدین کلی پولشا در طول زمان و یا بیاین یه باره پولشو امروز بم بدین یه تخفیف 10 درصدی تقریباً بره دون، لحاظ میکنم عددشو کم میکنم. خریدم اونم اومدن برای منشیانه و دفتر. پیشفاکتورم گفته تا حدود یه 20 روز اینا برادون اوکیه. همین.",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def run():
|
||||
print(f"Connecting to Bale and ERP API for Chat ID: {CHAT_ID}...")
|
||||
bale = BaleClient(settings.BALE_API_BASE, settings.BOT_TOKEN)
|
||||
api = ErpClient(settings.API_BASE_URL, settings.API_TIMEOUT_SECONDS, settings.SERVICE_TOKEN)
|
||||
ctx = BotContext(bale, api)
|
||||
|
||||
user_info = AUTHORIZED_USERS.get(CHAT_ID, {})
|
||||
user = LinkedUser(
|
||||
chat_id=CHAT_ID,
|
||||
user_id=user_info.get("user_id"),
|
||||
display_name=user_info.get("display_name", "مدیر تدارکات"),
|
||||
role=user_info.get("role", "procurement_manager"),
|
||||
is_admin=user_info.get("is_admin", True),
|
||||
)
|
||||
|
||||
print("Sending transcripts to NLU (LLM) for multi-model consensus and entity extraction...")
|
||||
result = await understand(settings, TRANSCRIPTS)
|
||||
print("NLU Result:")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
consensus_text = result.get("consensus_text") or ""
|
||||
patch = result.get("patch") or {}
|
||||
|
||||
intro_msg = (
|
||||
f"🤖 **تحلیل هوشمند فایل صوتی (تجمیع خروجی چند مدل STT):**\n\n"
|
||||
f"📝 **متن استخراجشده و تصحیحشده (Consensus):**\n«{consensus_text}»\n\n"
|
||||
f"📋 پیشنویس استعلام خرید با مشخصات استخراجشده صادر گردید:"
|
||||
)
|
||||
await ctx.send(CHAT_ID, intro_msg)
|
||||
|
||||
# Create and render the draft quote
|
||||
await quote.handle_entities(
|
||||
ctx,
|
||||
CHAT_ID,
|
||||
user,
|
||||
patch,
|
||||
source="voice",
|
||||
)
|
||||
print("Draft quote sent successfully to Bale chat!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run())
|
||||
+47
-11
@@ -24,7 +24,7 @@ try:
|
||||
except ImportError:
|
||||
sys.modules.setdefault("asyncpg", types.ModuleType("asyncpg"))
|
||||
|
||||
CHAT = 100200300
|
||||
CHAT = 1461166742
|
||||
|
||||
_passed = _failed = 0
|
||||
|
||||
@@ -297,8 +297,10 @@ def build_ctx():
|
||||
return ctx, bale, api
|
||||
|
||||
|
||||
def text_update(update_id, text, reply_to=None):
|
||||
msg = {"message_id": update_id, "chat": {"id": CHAT}, "text": text, "from": {"id": CHAT}}
|
||||
def text_update(update_id, text, reply_to=None, chat_id=None, from_user=None):
|
||||
cid = chat_id or CHAT
|
||||
fuser = from_user or {"id": cid, "first_name": "مدیر تدارکات", "username": "proc_manager"}
|
||||
msg = {"message_id": update_id, "chat": {"id": cid}, "text": text, "from": fuser}
|
||||
if reply_to:
|
||||
msg["reply_to_message"] = {"message_id": reply_to}
|
||||
return {"update_id": update_id, "message": msg}
|
||||
@@ -318,12 +320,22 @@ async def main() -> int:
|
||||
_install_fake_stores()
|
||||
from app.drafts import build_quote_request, compute_quote_missing, merge_quote_patch, new_quote_payload
|
||||
import app.handlers as H
|
||||
from app.nlu import _extract_json, normalize_digits
|
||||
from app.nlu import _extract_json, normalize_digits, understand
|
||||
from app.config import settings
|
||||
|
||||
# ---- pure logic -------------------------------------------------------
|
||||
check("digits: ۵۰۰→500", normalize_digits("۵۰۰ کیلو") == "500 کیلو")
|
||||
check("json extract: fenced", (_extract_json('```json\n{"a": 1}\n```') or {}) == {"a": 1})
|
||||
|
||||
# ---- multi-STT understand pipeline support ---------------------------
|
||||
multi_stt_samples = [
|
||||
{"name": "whisper", "text": "25 کیسه گچ از مصطفوی"},
|
||||
{"name": "vosk", "text": "بیست و پنج کیسه گچ از مصطفوی"},
|
||||
]
|
||||
stt_res = await understand(settings, multi_stt_samples)
|
||||
check("multi-stt: pipeline accepted list input", isinstance(stt_res, dict) and "consensus_text" in stt_res)
|
||||
check("multi-stt: consensus text generated", bool(stt_res.get("consensus_text")))
|
||||
|
||||
payload = new_quote_payload()
|
||||
merge_quote_patch(payload, {
|
||||
"items": [{"material_name": "میلگرد A3", "quantity": 500, "unit_price": 45000}],
|
||||
@@ -341,15 +353,39 @@ async def main() -> int:
|
||||
body, problems = build_quote_request(payload)
|
||||
check("build: blocked without supplier", body is None)
|
||||
|
||||
# ---- pipeline: linking gate -------------------------------------------
|
||||
# ---- pipeline: whitelist gate & unregistered blocking -----------------
|
||||
ctx, bale, api = build_ctx()
|
||||
await H.handle_update(ctx, text_update(1, "سلام"))
|
||||
check("gate: unlinked asked username", "نام کاربری" in bale.last_text())
|
||||
UNAUTH_CHAT = 999999999
|
||||
from app.handlers import UNREGISTERED_FILE
|
||||
if UNREGISTERED_FILE.exists():
|
||||
try:
|
||||
UNREGISTERED_FILE.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await H.handle_update(ctx, text_update(2, "mohaghegh"))
|
||||
await H.handle_update(ctx, text_update(3, "secret"))
|
||||
check("link: login called", api.login_calls and api.login_calls[0]["username"] == "mohaghegh")
|
||||
check("link: linked greeting", "اتصال انجام شد" in bale.last_text())
|
||||
# 1. Unregistered user is blocked and recorded
|
||||
await H.handle_update(
|
||||
ctx,
|
||||
text_update(
|
||||
1,
|
||||
"سلام من متقاضی جدید هستم",
|
||||
chat_id=UNAUTH_CHAT,
|
||||
from_user={"id": UNAUTH_CHAT, "first_name": "کاربر متفرقه", "username": "intruder"},
|
||||
),
|
||||
)
|
||||
check("gate: unauthorized user blocked", "دسترسی شما هنوز تعریف نشده است" in bale.last_text())
|
||||
|
||||
import json
|
||||
unreg_records = json.loads(UNREGISTERED_FILE.read_text(encoding="utf-8")) if UNREGISTERED_FILE.exists() else []
|
||||
check(
|
||||
"gate: unregistered logged to JSON file",
|
||||
any(u.get("chat_id") == UNAUTH_CHAT and u.get("first_name") == "کاربر متفرقه" for u in unreg_records),
|
||||
)
|
||||
|
||||
# 2. Whitelisted user (1461166742) bypasses Casdoor completely
|
||||
await H.handle_update(ctx, text_update(2, "/whoami"))
|
||||
check("gate: whitelisted user active without casdoor", "مدیر تدارکات" in bale.last_text())
|
||||
check("gate: no casdoor login prompted", "نام کاربری Casdoor" not in bale.last_text())
|
||||
|
||||
# ---- voice → draft quote ----------------------------------------------
|
||||
NLU_QUEUE.append({"intent": "register_quote", "confidence": 0.9, "patch": {
|
||||
|
||||
Reference in New Issue
Block a user