कंटेंट पर जाएं

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

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

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

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

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

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

सारांश

  • रियल-टाइम स्पीच टू टेक्स्ट सिस्टम बनाने के लिए आर्किटेक्चर को बारीकी से ट्यून करना जरूरी है, ताकि पूरी पाइपलाइन में लेटेंसी कम रहे।
  • ज़्यादातर पाइपलाइनों के लिए WebSocket सही डिफ़ॉल्ट है, हालांकि WebRTC अधिक जटिल होने के बावजूद कई फायदे देता है।
  • Voice Activity Detection हैंड्स-फ़्री सेगमेंटेशन संभालता है, जबकि मैन्युअल कमिट आपके ऐप्लिकेशन को टर्न खत्म होने पर ओवरराइड देता है।
  • पार्शियल अस्थायी होते हैं और फ़ाइनल कमिट किए जाते हैं, इसलिए इन्हें अलग तरह से रेंडर करना चाहिए। 
  • करीब 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 आपको दो पूरक तरीके देता है:

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

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

साइलेंस थ्रेशोल्ड में वास्तविक समझौता होता है; कोई एक मान हर जगह सही नहीं है:

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

यहां कोई वैश्विक कॉन्स्टेंट काम नहीं करता; थ्रेशोल्ड को इंटरैक्शन के अनुसार ट्यून करें:

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

इन सुझावों से आप प्रभावी एंड-पॉइंटिंग सिस्टम बना सकते हैं और रियल-टाइम स्पीच टू टेक्स्ट के करीब पहुंच सकते हैं।

इन-स्ट्रीम फीचर्स: भाषा पहचान और स्पीकर डायराइज़ेशन

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

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

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

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

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

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

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

8kHz पर mu-law टेलीफ़ोनी फ़ॉर्मैट है। यदि आप 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 key गुप्त है और क्लाइंट-साइड कोड में कभी नहीं आनी चाहिए। key सर्वर पर रहती है। यदि ब्राउज़र को सीधे रिकग्नाइज़र से बात करनी हो, तो सर्वर साइड से कम समय के लिए मान्य, सिंगल-यूज़ टोकन बनाएं और API key के बजाय क्लाइंट को वही दें।

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

क्लाइंट आपके सर्वर से 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 key सर्वर पर रखता है, बाइनरी 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 key और 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;
}

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

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

रियल-टाइम स्पीच टू टेक्स्ट पाने के तरीके का सार 

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

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

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

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

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

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

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

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

रियल-टाइम स्पीच टू टेक्स्ट लेटेंसी: अक्सर पूछे जाने वाले सवाल

संबंधित लेख

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