Files
ERP/bale stt report/bale_stt_master_crawler_blueprint.md
T

30 KiB
Raw Blame History

سند جامع، تحلیلی و عملیاتی کرالر تبدیل صوت‌به‌متن (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)

این دیاگرام نشان می‌دهد ویس ارسالی کاربر چگونه در خط‌لوله موازی پردازش شده و کاندید بله بدون ایجاد وقفه در سیستم ثبت استعلام می‌نشیند:

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)

برای جلوگیری از فرآیند پرریسک و پیچیده «آپلود مستقیم فایل از اکانت شخصی»، از الگوی بات رله استفاده می‌شود:

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)

این فلو تضمین می‌کند که حتی در بدترین سناریوها (تغییر پروتکل بله، بن شدن اکانت یا قطعی سرور بله)، سیستم اصلی تدارکات دچار حتی یک ثانیه قطعی نخواهد شد:

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)

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

  • قرارداد ارتباطی: تابع _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

  • متغیرهای محیطی با پیشوند PROCUREMENT_BOT_STT_PROVIDER_N_* (که N اعداد ۱ تا ۳ است) خوانده می‌شوند.
  • یک پرووایدر تنها زمانی فعال می‌شود که هر دو فیلد BASE_URL و API_KEY مقداردهی شده باشند. بنابراین با خالی کردن این دو فیلد، پرووایدر بله فوراً از مدار خارج می‌شود (Rollback آنی).

۲. مشخصات قطعی کشف‌شده از پروتکل بله

در بررسی و راستی‌آزمایی مخازن PyPI روی پکیج bale-sdk (نسخه 0.1.1)، متد و ساختار فیلدهای ترنسکریپشن دقیقاً استخراج شده است:

  • فضای نام و سرویس: bale.turing.v1.AI
  • متد باینری: GetTranscript
  • اسکیمای درخواست (GetTranscriptRequest):
    message GetTranscriptRequest {
        Media voice = 1;        // آبجکت فایل صدا (شناسه و خصوصیات باینری)
        OutPeer outPeer = 2;    // مقصد چت (پیر چت شخصی یا بات رله)
        MessageId messageId = 3;// شناسه پیام ثبت‌شده در چت
    }
    
  • اسکیمای پاسخ (GetTranscriptResponse):
    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)

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)

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)

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

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

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 بات اصلی

اتصال به بات اصلی بدون دستکاری حتی ۱ خط از کدهای پایتون بات انجام می‌پذیرد:

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.