कॉन्टेंट पर जाएं

रीयल-टाइम स्पीच टू टेक्स्ट 200ms से कम में: एक आर्किटेक्चर गाइड

प्रकाशित
आखिरी बार अपडेट किया गया

सुनेंइस आर्टिकल को सुनें

रीयल-टाइम स्पीच टू टेक्स्ट (STT) ऑडियो को उसी समय ट्रांसक्राइब करता है जब कोई बोलता है, और कुछ ही सौ मिलीसेकंड में उनके शब्द टेक्स्ट में बदल देता है। लेकिन STT की लेटेंसी कम रखना सिर्फ मॉडल का नहीं, आर्किटेक्चर का भी सवाल है। इंजीनियर्स को ट्रांसपोर्ट, चंकिंग, एंड-पॉइंटिंग और कैप्चर पाथ—हर स्टेप—की प्लानिंग करनी होती है, क्योंकि हर एक लेटेंसी बढ़ा सकता है। इनमें से किसी भी हिस्से में गड़बड़ी आपके 200ms के बजट को बिगाड़ सकती है।

यह गाइड आपको एक प्रैक्टिकल सिस्टम देता है जिससे आप ट्रांसपोर्ट लेयर से लेकर ऊपर तक रीयल-टाइम स्पीच टू टेक्स्ट पाइपलाइन्स बना सकते हैं। हम फोकस करेंगे Scribe v2 रीयलटाइम पर, जो लगभग 150ms मॉडल लेटेंसी में पार्टियल ट्रांसक्रिप्शन देता है, 90+ भाषाओं को सपोर्ट करता है, PCM (8kHz-48kHz) और mu-law ऑडियो लेता है, और वॉइस एक्टिविटी डिटेक्शन के साथ-साथ मैन्युअल कमिट कंट्रोल भी देता है ताकि सेगमेंट फाइनल किया जा सके।

हम देखेंगे कि ऑडियो सर्वर तक कैसे पहुंचता है, हाइपोथेसिस कैसे फाइनल टेक्स्ट में बदलता है, इन-स्ट्रीम फीचर्स का क्या असर पड़ता है, और ऑडियो को सही तरीके से कैसे कैप्चर और फॉरवर्ड करें।

संक्षिप्त में

  • रीयल-टाइम स्पीच टू टेक्स्ट सिस्टम बनाना आर्किटेक्चर को बारीकी से ट्यून करने की मांग करता है, ताकि पूरी पाइपलाइन में लेटेंसी कम रहे।
  • ज्यादातर पाइपलाइन्स के लिए WebSocket सही डिफॉल्ट है, हालांकि WebRTC भी कई फायदे देता है लेकिन थोड़ा जटिल है।
  • वॉइस एक्टिविटी डिटेक्शन हैंड्स-फ्री सेगमेंटेशन संभालता है, और मैन्युअल कमिट आपके ऐप को ओवरराइड का ऑप्शन देता है जब उसे पता हो कि टर्न खत्म हो गया है।
  • पार्टियल अस्थायी होते हैं और फाइनल कमिटेड, इसलिए इन्हें अलग तरीके से दिखाएं।
  • करीब 100ms के छोटे PCM चंक्स से पहले पार्टियल की लेटेंसी कम होती है।

रीयल-टाइम स्पीच टू टेक्स्ट के लिए WebSocket बनाम WebRTC

कोई भी ट्रांसक्रिप्शन शुरू होने से पहले, ऑडियो को सोर्स से रिकग्नाइज़र तक पहुंचना होता है। जो चैनल आप चुनते हैं, वही आगे की पूरी लेटेंसी का बेस सेट करता है। ऑडियो को ट्रांसक्रिप्शन लेयर तक पहुंचाने के दो अच्छे विकल्प हैं।

WebSocket एक लंबा चलने वाला, ऑर्डर में, भरोसेमंद, बायडायरेक्शनल चैनल है जो TCP पर चलता है। आप कनेक्शन खोलते हैं, बाइनरी ऑडियो फ्रेम्स भेजते हैं, और ट्रांसक्रिप्ट इवेंट्स पढ़ते हैं। यह क्लाइंट और सर्वर दोनों पर सीधा है, HTTPS को सपोर्ट करने वाले कॉर्पोरेट प्रॉक्सी और फायरवॉल्स को पार कर जाता है, और हर ब्राउज़र व सर्वर रनटाइम इसे सपोर्ट करता है।

WebSocket की लिमिट यह है कि यह TCP पर चलता है। अगर कोई पैकेट खो जाए, तो TCP उसे फिर से भेजता है और बाद के डेटा को रोक लेता है जब तक गैप न भर जाए। अच्छी नेटवर्क कंडीशन में यह दिखता नहीं, लेकिन पैकेट लॉस में ऑडियो रुक जाता है और फिर एक साथ आ जाता है।

WebRTC रीयल-टाइम मीडिया के लिए बना है। यह मीडिया को UDP (SRTP के जरिए) पर भेजता है, जिससे खोया पैकेट स्ट्रीम को नहीं रोकता; पाइपलाइन चलती रहती है। इसमें जिटर बफर है जो पैकेट के आने के समय में फर्क को संभालता है, ICE/STUN/TURN से NAT ट्रैवर्सल करता है ताकि राउटर के पीछे के पीयर्स कनेक्ट हो सकें, और इसमें अपना ऑडियो कैप्चर व एनकोडिंग सिस्टम है।

आमतौर पर आपको उन क्लाइंट्स के लिए TURN सर्वर चाहिए जो डायरेक्ट कनेक्ट नहीं कर सकते, और सर्वर साइड को मीडिया स्ट्रीम को टर्मिनेट करना पड़ता है, बाइट स्ट्रीम पढ़ने की बजाय।

संक्षेप में तुलना:

WebSocket
Transport
TCP (reliable, ordered)
Behavior under packet loss
Head-of-line blocking, bursty recovery
Jitter handling
Your responsibility
NAT traversal
Not needed (client-initiated)
Browser support
Universal, trivial
Server complexity
Low
WebRTC
Transport
UDP/SRTP (real-time, loss-tolerant)
Behavior under packet loss
Graceful degradation
Jitter handling
Built-in jitter buffer
NAT traversal
Requires ICE/STUN/TURN
Browser support
Universal, but more API surface
Server complexity
High (media server or SFU)

ज्यादातर मामलों में WebSocket सही विकल्प है। इसे तब इस्तेमाल करें जब आपके क्लाइंट्स की कनेक्टिविटी ठीक हो और आप कैप्चर पाथ कंट्रोल करते हों: सर्वर-टू-सर्वर पाइपलाइन्स, डेस्कटॉप ऐप्स, ब्राउज़र ऐप्स ब्रॉडबैंड पर, और ज्यादातर कॉन्टैक्ट-सेंटर बैकएंड्स जहां ऑडियो पहले से आपके सर्वर पर पहुंचता है।

WebRTC चुनें जब आप सीधे कंज्यूमर डिवाइस से, अनस्टेबल मोबाइल नेटवर्क पर कैप्चर कर रहे हों, जब आप पहले से टू-वे ऑडियो के लिए WebRTC स्टैक चला रहे हों (जैसे वॉइस एजेंट जो जवाब भी देता है), या जब कम लॉस और रीयल-टाइम बिहेवियर इम्प्लीमेंटेशन सिंप्लिसिटी से ज्यादा जरूरी हो।

इस गाइड के बाकी हिस्से में हम रिकग्नाइज़र कनेक्शन के लिए WebSocket ट्रांसपोर्ट का इस्तेमाल करेंगे, क्योंकि इससे हर स्टेप साफ दिखता है और ज्यादातर टीम्स के लिए यही सही शुरुआत है। इसमें कुछ भी WebSocket के लिए खास नहीं है, आप बाद में WebRTC मीडिया लेग जोड़ सकते हैं, सर्वर पर ऑडियो को PCM में डिकोड कर सकते हैं, और वही चंक्स पाइपलाइन में भेज सकते हैं।

पार्टियल और फाइनल ट्रांसक्रिप्ट्स: अंतरिम नतीजों की समझ

एक रीयल-टाइम रिकग्नाइज़र पूरी लाइन का इंतजार नहीं करता। वह लगातार अनुमान भेजता है जो जैसे-जैसे ऑडियो आता है, और पक्का होते जाते हैं। इन दोनों स्टेट्स का फर्क समझना जरूरी है—यही फर्क एक जिंदा ट्रांसक्रिप्ट और एक टूटे हुए ट्रांसक्रिप्ट में होता है।

पार्टियल (इंटरिम) हाइपोथेसिस मॉडल का अब तक मिले ऑडियो पर बेस्ट अनुमान होता है। पार्टियल डिजाइन से ही अस्थिर होते हैं। जैसे-जैसे और ऑडियो आता है, मॉडल पहले के शब्द बदल सकता है: "I want to" बाद में "I want two tickets" बन सकता है। ये जल्दी आते हैं (~150ms लेटेंसी इसी का आंकड़ा है) और इन्हें ओवरराइट करने के लिए ही बनाया गया है।

फाइनल हाइपोथेसिस एक कमिटेड सेगमेंट होता है जो अब नहीं बदलेगा। एक बार सेगमेंट फाइनल हो गया, रिकग्नाइज़र आगे बढ़ जाता है, और अगली हाइपोथेसिस बाद के ऑडियो के लिए होती है। फाइनल्स वही हैं जिन्हें आप सेव करते हैं, LLM को भेजते हैं, या ट्रांसक्रिप्ट के रूप में स्टोर करते हैं।

पार्टियल और फाइनल का फर्क तीन चीजों को तय करता है, जिन्हें आप गलत कर सकते हैं अगर फर्क न समझें:

  • यूज़र एक्सपीरियंस: पार्टियल दिखाने से ट्रांसक्रिप्ट लाइव लगता है: यूज़र को बोलते ही शब्द दिखते हैं, जिससे उन्हें भरोसा होता है कि माइक्रोफोन काम कर रहा है और सिस्टम सुन रहा है।
  • एंड-पॉइंटिंग: पार्टियल से आपको स्पीच एक्टिविटी का लगातार सिग्नल मिलता है। VAD के साथ मिलाकर, आप तय कर सकते हैं कि स्पीकर कब रुका है।
  • डाउनस्ट्रीम टाइमिंग: एक वॉइस एजेंट पाइपलाइन स्टेप्स हैं: ऑडियो इन, फिर स्पीच टू टेक्स्ट, फिर एक LLM, फिर

पार्टियल और फाइनल को अलग तरीके से दिखाएं। एक सिंपल तरीका है कि एक "करंट लाइन" को लेटेस्ट पार्टियल से बांधें और फाइनल आते ही उसे ट्रांसक्रिप्ट में जोड़ दें:

type TranscriptState = {
  committed: string[]; // finalized segments, never rewritten
  current: string;     // latest partial, overwritten on each update
};

const onPartial = (s: TranscriptState, text: string): TranscriptState =>
  ({ ...s, current: text });

const onFinal = (s: TranscriptState, text: string): TranscriptState =>
  ({ committed: [...s.committed, text], current: "" });

दिखाने में, कमिटेड टेक्स्ट को सेटेल्ड दिखाएं और करंट को हल्के या इटैलिक स्टाइल में, ताकि यूज़र समझ सके कि यह बदल सकता है।

एंड-पॉइंटिंग और वॉइस एक्टिविटी डिटेक्शन (VAD)

क्या कहा गया, यह जानना आधी लड़ाई है। रिकग्नाइज़र को यह भी जानना होता है कि कब एक विचार खत्म हुआ। यही तय करता है कि सेगमेंट कब फाइनल करें और एजेंट में सिस्टम कब जवाब देना शुरू करे।

एंड-पॉइंटिंग का मतलब है तय करना कि बोलना खत्म हो गया है। जल्दी फाइनल करने से यूज़र की बात बीच में कट सकती है। देर से फाइनल करने से यूज़र के रुकने के बाद एजेंट चुप रहता है।

Scribe v2 Realtime आपको दो कंप्लीमेंटरी तरीके देता है:

  • वॉइस एक्टिविटी डिटेक्शन साइलेंस के आधार पर ऑडियो को सेगमेंट करता है: रिकग्नाइज़र पहचानता है कि कब स्पीच के बाद लगातार साइलेंस आता है और उस बाउंड्री पर सेगमेंट को ऑटोमेटिकली फाइनल कर देता है। VAD कन्वर्सेशनल इंटरफेस के लिए सही डिफॉल्ट है क्योंकि यह नेचुरल स्पीच रिदम के साथ खुद को एडजस्ट करता है, आपको टाइमिंग ट्रैक करने की जरूरत नहीं पड़ती।
  • मैन्युअल कमिट कंट्रोल: मैन्युअल कमिट कंट्रोल से आपका ऐप खुद तय कर सकता है कि करंट सेगमेंट कब फाइनल करना है, साइलेंस से अलग। आप कमिट सिग्नल भेजते हैं, रिकग्नाइज़र करंट सेगमेंट बंद करता है और फाइनल भेजता है। यह तब सही है जब आपके ऐप को पहले से पता हो कि टर्न खत्म है: जैसे पुश-टू-टॉक बटन छोड़ना, "सेंड" एक्शन या कोई बाहरी टर्न-टेकिंग पॉलिसी।

दोनों को साथ में इस्तेमाल किया जा सकता है। एक आम वॉइस एजेंट VAD से हैंड्स-फ्री ऑपरेशन करता है और मैन्युअल कमिट को ओवरराइड के रूप में देता है, ताकि सोचने के लिए रुका यूज़र कट न जाए, लेकिन बटन दबाने वाला यूज़र तुरंत फाइनल पा सके।

साइलेंस थ्रेशोल्ड एक असली ट्रेडऑफ है, इसका कोई एक सही मान नहीं है:

  • छोटा एंड-ऑफ-स्पीच टाइमआउट (जैसे ~200-400ms साइलेंस के बाद फाइनलाइजेशन) सिस्टम को रिस्पॉन्सिव बनाता है। लेकिन इससे वे यूज़र कट सकते हैं जो नेचुरल पॉज़ लेते हैं, जिससे एक विचार कई सेगमेंट्स में बंट सकता है और एजेंट जल्दी जवाब दे सकता है।
  • लंबा टाइमआउट (जैसे ~800-1200ms) नेचुरल पॉज़ को सहन करता है और पूरी बात को एक साथ रखता है, लेकिन सिस्टम के रिएक्शन में थोड़ा लैग आ सकता है।

यहां कोई ग्लोबल कॉन्स्टेंट नहीं है; थ्रेशोल्ड को इंटरैक्शन के हिसाब से ट्यून करें:

  • डिक्टेशन और नोट-टेकिंग में यूज़र बीच में सोचते हैं, तो लंबा पॉज़ चलता है। लंबे टाइमआउट रखें और VAD पर भरोसा करें।
  • कमांड-एंड-कंट्रोल और ट्रांजैक्शनल एजेंट्स में छोटे टाइमआउट और मैन्युअल कमिट फायदेमंद हैं, क्योंकि टर्न छोटे और तेज होते हैं।
  • मल्टीलिंगुअल या नॉन-नेटिव स्पीकर्स ज्यादा पॉज़ लेते हैं, तो फाइनलाइजेशन से पहले ज्यादा साइलेंस का बजट रखें।

इन टिप्स से आप एक असरदार एंड-पॉइंटिंग सिस्टम बना सकते हैं और रीयल-टाइम स्पीच टू टेक्स्ट की ओर बढ़ सकते हैं।

इन-स्ट्रीम फीचर्स: लैंग्वेज डिटेक्शन और स्पीकर डायराइजेशन

स्ट्रीमिंग रिकग्निशन सिर्फ शब्द ही नहीं बना सकता। लेकिन हर एक्स्ट्रा सिग्नल लेटेंसी और स्टेबिलिटी को प्रभावित करता है। सिंपल रूल है—सिर्फ वही ऑन करें जो लाइव एक्सपीरियंस के लिए जरूरी हो, बाकी को बैच प्रोसेस में डालें।

ऑटोमैटिक लैंग्वेज रिकग्निशन Scribe v2 Realtime को 90+ सपोर्टेड भाषाओं में से बोली जा रही भाषा पहचानने देता है, आपको पहले से बताने की जरूरत नहीं। इसकी कीमत यह है कि मॉडल को भरोसेमंद पहचान के लिए थोड़ा ऑडियो चाहिए, तो स्ट्रीम के शुरुआती पार्टियल कम स्थिर हो सकते हैं। अगर आपको भाषा पहले से पता है, तो उसे बताने से शुरुआती पार्टियल ज्यादा स्थिर मिलते हैं।

स्पीकर डायराइजेशन अलग-अलग स्पीकर्स की स्पीच को पहचानता है, कौन क्या बोल रहा है। बैच ट्रांसक्रिप्शन में यह आसान है क्योंकि मॉडल पूरी फाइल देखता है। स्ट्रीमिंग में यह मुश्किल है: रिकग्नाइज़र को अब तक के ऑडियो से ही स्पीकर लेबल देना होता है, और शुरू में दिया गया लेबल बाद में बदलना पड़ सकता है। स्ट्रीमिंग स्पीकर लेबल्स को वैसे ही ट्रीट करें जैसे पार्टियल टेक्स्ट को: फाइनलाइजेशन तक प्रोविजनल।

वर्ड-लेवल टाइमिंग और एंटिटी कॉन्टेक्स्ट भी यही लॉजिक फॉलो करते हैं। जितना ज्यादा पर-टोकन मेटाडेटा मांगेंगे, मॉडल और वायर दोनों पर लोड बढ़ेगा। ज्यादातर रीयल-टाइम UI के लिए आपको सिर्फ टेक्स्ट और सेगमेंट बाउंड्री लाइव चाहिए, बाकी फाइन-ग्रेन्ड मेटाडेटा Scribe v2 के बैच प्रोसेस में डाल सकते हैं।

स्ट्रीमिंग के लिए ऑडियो फॉर्मेट्स: PCM और mu-law

ट्रांसपोर्ट और रिकग्निशन लॉजिक पर सबसे ज्यादा ध्यान जाता है, लेकिन असल में कई बग नीचे की लेयर में होते हैं—कैसे आप ऑडियो को एनकोड और चंक करते हैं। फॉर्मेट और चंक साइज सही रखना स्पीच टू टेक्स्ट लेटेंसी कम करने का सबसे आसान तरीका है।

PCM (लिनियर, 16-बिट साइन, लिटिल-एंडियन) तब इस्तेमाल करें जब आप कैप्चर कंट्रोल करते हैं। ज्यादा सैंपल रेट में ज्यादा डिटेल आती है: 16kHz स्पीच रिकग्निशन के लिए स्टैंडर्ड है और आमतौर पर काफी है; 8kHz टेलीफोन-ग्रेड है और हाई-फ्रीक्वेंसी कंटेंट खो देता है। सोर्स के हिसाब से रेट चुनें। 8kHz टेलीफोनी ऑडियो को 48kHz में अपसैंपल करने का कोई फायदा नहीं, क्योंकि उसमें उतनी जानकारी नहीं है।

Mu-law 8kHz टेलीफोनी फॉर्मेट है। अगर आप Twilio जैसे प्रोवाइडर से कॉल ले रहे हैं, तो ऑडियो 8kHz mu-law में आता है, और आपको उसी फॉर्मेट में फॉरवर्ड करना चाहिए, दो बार ट्रांसकोड करने की बजाय। सोर्स फॉर्मेट मैच करने से रिसैंपलिंग आर्टिफैक्ट्स और बेकार कन्वर्जन स्टेप बचता है।

चंक साइजिंग सबसे डायरेक्ट तरीके से लेटेंसी को प्रभावित करता है। आप ऑडियो चंक्स में भेजते हैं, और रिकग्नाइज़र चंक्स आते ही पार्टियल बनाता है। छोटे चंक्स का मतलब ज्यादा बार अपडेट और पहले पार्टियल तक कम लेटेंसी; बड़े चंक्स का मतलब कम मैसेज और हर इंफरेंस में थोड़ा ज्यादा कॉन्टेक्स्ट। प्रैक्टिकल रेंज है 20-250ms ऑडियो प्रति चंक। उदाहरण के लिए, 16kHz मोनो 16-बिट PCM में एक सेकंड का ऑडियो 32,000 बाइट्स होता है, तो 100ms चंक करीब 3,200 बाइट्स का होगा।

ब्राउज़र में माइक्रोफोन इनपुट कैप्चर करना

ब्राउज़र में सही टूल है Web Audio API और AudioWorklet। वर्कलेट ऑडियो रेंडरिंग थ्रेड पर चलता है, छोटे फ्रेम्स में ऑडियो लेता है, और पुराने ScriptProcessorNode की तरह मेन-थ्रेड जंक से प्रभावित नहीं होता। इसका काम है ब्राउज़र के फ्लोट सैंपल्स को 16-बिट PCM में बदलना और मेन थ्रेड को देना, जो WebSocket पर भेजता है।

वर्कलेट प्रोसेसर का कोर है फ्लोट-टू-PCM कन्वर्जन:

// pcm-worklet.ts - registered via audioContext.audioWorklet.addModule()
class PCMWorklet extends AudioWorkletProcessor {
  process(inputs: Float32Array[][]) {
    const channel = inputs[0]?.[0]; // mono; Float32, range [-1, 1]
    if (!channel) return true;
    const pcm = new Int16Array(channel.length);
    for (let i = 0; i < channel.length; i++) {
      const s = Math.max(-1, Math.min(1, channel[i]));
      pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
    }
    // Transfer the buffer to the main thread without copying.
    this.port.postMessage(pcm.buffer, [pcm.buffer]);
    return true;
  }
}
registerProcessor("pcm-worklet", PCMWorklet);

कोड में पाइपलाइन

पाइपलाइन में तीन हिस्से हैं: एक ब्राउज़र क्लाइंट जो माइक्रोफोन कैप्चर करता है और PCM को सर्वर पर स्ट्रीम करता है, एक Node सर्वर जो ऑडियो Scribe v2 Realtime को भेजता है और ट्रांसक्रिप्ट वापस लाता है, और एक स्क्रिप्टेबल क्लाइंट जो फाइल या टेलीफोनी ब्रिज से PCM स्ट्रीम करता है।

सर्वर डायरेक्टली रिकग्नाइज़र को ब्राउज़र से एक्सपोज़ नहीं करता, इसकी एक बड़ी वजह है: आपका ElevenLabs API की सीक्रेट है और कभी भी क्लाइंट-साइड कोड में नहीं आना चाहिए। सर्वर की पर रखिए। अगर आपको ब्राउज़र से डायरेक्ट रिकग्नाइज़र से बात करनी है, तो सर्वर-साइड एक शॉर्ट-लिव्ड सिंगल-यूज़ टोकन बनाएं और क्लाइंट को API की की जगह वही दें।

ब्राउज़र क्लाइंट

क्लाइंट आपके सर्वर से WebSocket खोलता है, ऊपर बताए वर्कलेट से माइक्रोफोन कैप्चर करता है, और हर PCM फ्रेम को बनते ही फॉरवर्ड करता है। इनकमिंग इवेंट्स (जो सर्वर पहले ही { type, text } में नॉर्मलाइज़ कर चुका है) ऊपर बताए पार्टियल/फाइनल स्टेट को ड्राइव करते हैं:

// client.ts - runs in the browser. ws is an open WebSocket to your server.
const audioContext = new AudioContext({ sampleRate: 16000 });
await audioContext.audioWorklet.addModule("pcm-worklet.js");

const mediaStream = await navigator.mediaDevices.getUserMedia({
  audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
});

const source = audioContext.createMediaStreamSource(mediaStream);
const worklet = new AudioWorkletNode(audioContext, "pcm-worklet");

// Forward each PCM frame to the server the moment it is produced.
worklet.port.onmessage = (e: MessageEvent<ArrayBuffer>) => {
  if (ws.readyState === WebSocket.OPEN) ws.send(e.data);
};
source.connect(worklet);

// Manual commit: tell the server to finalize the current segment.
const commit = () => ws.send(JSON.stringify({ type: "commit" }));

सर्वर रिले

सर्वर हर क्लाइंट के लिए एक रिकग्नाइज़र कनेक्शन खोलता है, API की सर्वर पर रखता है, बाइनरी PCM सीधे फॉरवर्ड करता है, और रिकग्नाइज़र इवेंट्स को { type, text } के स्टेबल फॉर्मेट में नॉर्मलाइज़ करता है जिसे क्लाइंट इस्तेमाल करता है:

// server.ts - Node, using the `ws` library. ELEVENLABS_API_KEY and the
// recognizer URL come from the environment; see the Speech to Text reference
// for the exact path and query parameters.
import { WebSocketServer, WebSocket } from "ws";

new WebSocketServer({ port: 8080 }).on("connection", (client) => {
  // The API key stays on the server, never on the wire to the browser.
  const recognizer = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
    headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
  });

  // Browser -> recognizer: forward binary PCM, translate control messages.
  client.on("message", (data, isBinary) => {
    if (recognizer.readyState !== WebSocket.OPEN) return;
    if (isBinary) recognizer.send(data); // raw PCM bytes
    else if (JSON.parse(data.toString()).type === "commit")
      recognizer.send(sendCommit());
  });

  // Recognizer -> browser: normalize events into a stable shape.
  recognizer.on("message", (raw) => {
    const event = parseRecognizerEvent(raw.toString());
    if (event && client.readyState === WebSocket.OPEN)
      client.send(JSON.stringify(event));
  });

  // ... open handshake, queueing pre-open audio, and teardown on close/error
});

हर एंडपॉइंट-स्पेसिफिक चीज नीचे दिए दो एडॉप्टर फंक्शन्स में सीमित है। फील्ड नेम्स को स्पीच टू टेक्स्ट रेफरेंस से बिल्कुल वैसे ही बदलें; बाकी पाइपलाइन नहीं बदलती:

// The single place that knows the recognizer's wire format.
const sendCommit = (): string => JSON.stringify({ type: "commit" });

type NormalizedEvent =
  | { type: "partial"; text: string }
  | { type: "final"; text: string }
  | { type: "vad"; speaking: boolean };

function parseRecognizerEvent(raw: string): NormalizedEvent | null {
  const msg = JSON.parse(raw);
  if (msg.is_final === true || msg.type === "final")
    return { type: "final", text: msg.text ?? "" };
  if (msg.type === "vad") return { type: "vad", speaking: !!msg.speaking };
  if (typeof msg.text === "string")
    return { type: "partial", text: msg.text };
  return null;
}

स्क्रिप्टेबल बैकएंड क्लाइंट

बैकएंड पाइपलाइन्स और नीचे दिए बेंचमार्क के लिए, वही रिकग्नाइज़र कनेक्शन बिना ब्राउज़र के भी चलता है: किसी भी सोर्स से PCM पढ़ें, उसे रीयल-टाइम चंक कैडेंस पर भेजें, और इवेंट्स वापस पढ़ें। API की और URL एनवायरनमेंट से आते हैं, जैसे सर्वर पर।

// stream-stt.ts - pace ~100ms chunks at real time, then commit the tail.
const SAMPLE_RATE = 16000, CHUNK_MS = 100;
const CHUNK_BYTES = (SAMPLE_RATE * 2 * CHUNK_MS) / 1000; // 3200 bytes
const ws = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
  headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
});

// Send: walk the PCM buffer in 100ms chunks, sleeping between to mimic a
// live source. For audio that already arrives in real time, drop the sleep.
async function sendAudio(pcm: Buffer) {
  for (let off = 0; off < pcm.length; off += CHUNK_BYTES) {
    ws.send(pcm.subarray(off, off + CHUNK_BYTES));
    await new Promise((r) => setTimeout(r, CHUNK_MS));
  }
  ws.send(JSON.stringify({ type: "commit" })); // finalize the trailing segment
}

// Receive: print partials in place, append finals.
ws.on("message", (raw) => {
  const e = parseRecognizerEvent(raw.toString());
  if (e?.type === "final") console.log(`[final]   ${e.text}`);
  else if (e?.type === "partial") process.stdout.write(`[partial] ${e.text}\r`);
});

स्पीच टू टेक्स्ट लेटेंसी और वर्ड एरर रेट की बेंचमार्किंग

लेटेंसी और वर्ड एरर रेट दोनों स्पीकर, भाषा, ऑडियो की क्वालिटी, ऑडियो की लंबाई, आपके नेटवर्क पाथ और हर सर्विस के करंट लोड पर निर्भर करते हैं।

एक लैपटॉप से एक शहर में मापा गया रिजल्ट आपके प्रोडक्शन फ्लीट के लिए लागू नहीं होता। बेंचमार्किंग उसी इंफ्रास्ट्रक्चर से करें जो प्रोडक्शन जैसा हो, उसी तरह के ऑडियो पर, और एक नंबर की बजाय रेंज और डिस्ट्रीब्यूशन रिपोर्ट करें।

सिर्फ वही लेटेंसी और एक्युरेसी मायने रखती है जो आप अपने ऑडियो और प्रोडक्शन जैसी इंफ्रास्ट्रक्चर पर मापते हैं। यहां स्पीच टू टेक्स्ट लेटेंसी बेंचमार्किंग का गाइड है।

स्पीच टू टेक्स्ट लेटेंसी के लिए क्या मापें

नीचे वे मुख्य मेट्रिक्स हैं जिन्हें आपको रीयल-टाइम स्पीच टू टेक्स्ट लेटेंसी बेंचमार्किंग में मापना चाहिए:

  • पहले पार्टियल तक का समय: पहला ऑडियो चंक भेजने से लेकर पहला नॉन-एम्प्टी पार्टियल मिलने तक।
  • पार्टियल-से-फाइनल लैग: एक उच्चारण के आखिरी ऑडियो चंक से फाइनल हाइपोथेसिस तक।
  • वर्ड एरर रेट (WER): फाइनल ट्रांसक्रिप्ट पर ह्यूमन रेफरेंस के मुकाबले WER, सभी सिस्टम्स में एक ही तरह से गिना जाता है।
  • स्टेबिलिटी चर्न: फाइनलाइजेशन से पहले कितने पार्टियल दोबारा लिखे गए। यह मापता है कि लाइव UI कितना बदलता दिखेगा।

कंट्रोल्स

अनरिलाएबल डेटा से बचने के लिए, आपको अपने एक्सपेरिमेंट में कुछ कंट्रोल्स लागू करने चाहिए ताकि सब कुछ एक जैसा रहे।

यहां वे मुख्य कंट्रोल्स हैं जो स्पीच टू टेक्स्ट लेटेंसी बेंचमार्किंग में इस्तेमाल करें:

  • एक जैसा ऑडियो: हर सिस्टम को वही फाइल्स, वही सैंपल रेट और वही एनकोडिंग दें।
  • एक जैसी स्पीड: हर सिस्टम को एक ही रीयल-टाइम चंक कैडेंस (जैसे 100ms चंक्स) पर स्ट्रीम करें।
  • रिपीट और डिस्ट्रीब्यूशन रिपोर्ट करें: हर फाइल को दिन में कई बार चलाएं; मीडियन और टेल (p50/p95) रिपोर्ट करें।
  • एक जैसे रेफरेंस और स्कोरिंग: WER गिनने से पहले टेक्स्ट को एक ही तरह (केसिंग, पंक्चुएशन, नंबर) नॉर्मलाइज़ करें।
  • रीजन और नेटवर्क बताएं: बताएं कि हार्नेस कहां चला और हर प्रोवाइडर तक पाथ क्या था।

इन सब चीजों को एक जैसा रखकर आप ज्यादा सटीक मेट्रिक्स पाएंगे।

हार्नेस का ढांचा

मेजरमेंट कोर एक प्रोवाइडर एडॉप्टर लेता है और टाइम-टू-फर्स्ट-पार्टियल, फाइनलाइजेशन लैग और पार्टियल चर्न रिकॉर्ड करता है:

// benchmark.ts - measurement core; one StreamFn adapter per provider.
type StreamFn = (
  audioPath: string,
  onEvent: (kind: "partial" | "final", text: string) => void,
  result: RunResult
) => Promise<void>; // adapter sets result.lastChunkSentAt on the final chunk

interface RunResult {
  firstPartialMs?: number;
  finalLagMs?: number;
  hypothesis: string;
  partialEdits: number;
  lastChunkSentAt: number;
  startedAt: number;
}

async function measure(streamFn: StreamFn, audioPath: string): Promise<RunResult> {
  const result: RunResult = {
    hypothesis: "", partialEdits: 0, lastChunkSentAt: 0,
    startedAt: performance.now(),
  };
  let prevPartial = "";

  await streamFn(audioPath, (kind, text) => {
    const now = performance.now();
    if (kind === "partial") {
      if (text && result.firstPartialMs === undefined)
        result.firstPartialMs = now - result.startedAt;
      if (text !== prevPartial) { result.partialEdits++; prevPartial = text; }
    } else { // final
      result.hypothesis = result.hypothesis ? `${result.hypothesis} ${text}` : text;
      if (result.lastChunkSentAt)
        result.finalLagMs = now - result.lastChunkSentAt;
    }
  }, result);

  return result;
}

वर्ड एरर रेट नॉर्मलाइज़्ड टेक्स्ट पर स्टैंडर्ड टोकन-लेवल लेवेनस्टीन डिस्टेंस है। रेफरेंस और हाइपोथेसिस दोनों पर लोअरकेस और पंक्चुएशन हटाएं, नहीं तो आप अपने नॉर्मलाइज़र को मापेंगे, मॉडल को नहीं। इस माप को एक लूप में डालें जो हर फाइल को हर प्रोवाइडर पर ~10 बार चलाए और मीडियन टाइम-टू-फर्स्ट-पार्टियल और मीडियन WER (p50/p95) रिपोर्ट करे, क्योंकि एक सैंपल नेटवर्क वेरिएंस से प्रभावित हो सकता है।

इसे चलाने के लिए आपको दो चीजें देनी होंगी। पहले, हर सिस्टम के लिए एक StreamFn एडॉप्टर लिखें। ऊपर दिया स्क्रिप्टेबल क्लाइंट पहले से एक है, बाकी के लिए भी वही (audioPath, onEvent, result) कॉन्ट्रैक्ट फॉलो करें और result.lastChunkSentAt सेट करें जब फाइनल ऑडियो चंक भेजा जाए। दूसरा, अपनी ऑडियो फाइल्स और रेफरेंस लोड करें और उन पर मेजर्स चलाएं। इसे उसी मशीन से चलाएं जो आपके डिप्लॉयमेंट जैसी हो, उसी तरह के ऑडियो पर, और आपको एक दोहराने योग्य तुलना मिलेगी।

कैसे आप रीयल-टाइम स्पीच टू टेक्स्ट पा सकते हैं—सारांश

इस आर्टिकल में हमने कई आर्किटेक्चरल बदलाव कवर किए, जिससे आप अपने सिस्टम को बेहतर बनाकर रीयल-टाइम स्पीच टू टेक्स्ट के करीब जा सकते हैं।

एक प्रोडक्शन रीयल-टाइम STT सिस्टम कुछ अहम फैसलों पर निर्भर करता है:

  • ट्रांसपोर्ट: सिंप्लिसिटी और कंट्रोल्ड नेटवर्क्स के लिए WebSocket चुनें, और जब लॉस टॉलरेंस चाहिए या कंज्यूमर डिवाइस से कैप्चर कर रहे हों तो WebRTC।
  • पार्टियल और फाइनल: पार्टियल को प्रोविजनल और फाइनल को कमिटेड मानें, और इन्हें अलग दिखाएं ताकि यूज़र को लाइव टेक्स्ट पर भरोसा हो।
  • एंड-पॉइंटिंग: हैंड्स-फ्री सेगमेंटेशन के लिए VAD, ओवरराइड के लिए मैन्युअल कमिट, और साइलेंस थ्रेशोल्ड को इंटरैक्शन के हिसाब से ट्यून करें, न कि किसी एक मान पर।
  • इन-स्ट्रीम फीचर्स: इन-स्ट्रीम फीचर्स सिर्फ वहीं ऑन करें जहां लाइव एक्सपीरियंस को जरूरत हो, बाकी को Scribe v2 के बैच प्रोसेस में डालें।
  • ऑडियो फॉर्मेट: छोटे PCM फ्रेम्स में कैप्चर करें, ~100ms के चंक्स भेजें, और टेलीफोनी के लिए सोर्स फॉर्मेट मैच करें।
  • बेंचमार्किंग: एक्युरेसी बनाम लेटेंसी को अपने ऑडियो और टारगेट मेट्रिक के हिसाब से एम्पिरिकली सेट करें।
  • API सुरक्षा: अपनी API की सर्वर पर रखें, या डायरेक्ट क्लाइंट कनेक्शन के लिए सिंगल-यूज़ टोकन बनाएं।

अगर आप देखना चाहते हैं कि वॉइस एजेंट में लेटेंसी कैसे ऑप्टिमाइज़ करें, तो हमने आपके लिए एक गाइड भी लिखा है।

Scribe v2 Realtime के साथ रीयल-टाइम स्पीच टू टेक्स्ट सिस्टम बनाएं

Scribe v2 Realtime लगभग 150ms मॉडल लेटेंसी में पार्टियल देता है। आपके यूज़र्स को वही लेटेंसी मिलेगी या ज्यादा, यह आपके आर्किटेक्चर पर निर्भर करता है, जिसे आप कंट्रोल करते हैं। इस आर्टिकल में बताए गए स्ट्रैटेजीज़ से आप अपनी पाइपलाइन आर्किटेक्चर को बेहतर बना सकते हैं, लेटेंसी घटा सकते हैं और कस्टमर एक्सपीरियंस सुधार सकते हैं।

और गहराई से जानने के लिए देखें स्पीच टू टेक्स्ट की क्षमताएं का ओवरव्यू, हमारे मॉडल्स रेफरेंस में पूरी फीचर और भाषा लिस्ट पढ़ें, और रीयल-टाइम प्रोडक्ट पेजेज़ देखें: रीयल-टाइम स्पीच टू टेक्स्ट API और रीयल-टाइम स्पीच टू टेक्स्ट.

जब आप तैयार हों, मुफ़्त में ElevenLabs अकाउंट बनाएं और आज ही अपनी पहली ट्रांसक्रिप्ट स्ट्रीम करें।

रीयल-टाइम स्पीच टू टेक्स्ट लेटेंसी FAQ

संबंधित लेख

उच्चतम गुणवत्ता वाले AI ऑडियो के साथ बनाएं