अगर ट्रांसक्रिप्ट में हर शब्द का टाइमस्टैम्प है, तो लंबी रिकॉर्डिंग में छोटी क्लिप्स के कैंडिडेट ढूँढने के लिए किसी मॉडल की ज़रूरत नहीं है। तय लंबाई की एक विंडो हर शब्द से शुरू करें और उसे इस हिसाब से स्कोर दें कि उसमें कितने शब्द आते हैं। इससे वे हिस्से मिल जाते हैं जहाँ सबसे ज़्यादा बात कही गई है। osFoundry डेस्कटॉप ऐप का वीडियो एडिटर ठीक यही चीज़ अपने Suggest clips बटन में देता है, Rust में (canvas_transcript.rs में find_highlights)। नीचे इसे Python में दोबारा बनाया गया है और 5:07 के एक असली इंटरव्यू पर चलाया गया है, और मापा गया है कि हर नियम क्या करता है, विंडो की लंबाई जवाब को कैसे बदलती है, "सबसे अच्छा पहले" वाला चुनाव कब ऑप्टिमल नहीं रहता, और स्कोरिंग को लीनियर कैसे बनाएँ।
इनपुट
रिकॉर्डिंग अंतरिक्ष यात्री Christina Koch के साथ NASA का एक इंटरव्यू है। उसके Whisper ट्रांसक्रिप्ट में 306.97 सेकंड में 1,007 शब्द हैं, यानी औसतन 3.28 शब्द प्रति सेकंड। इस मोड में Whisper शब्दों को बिना विराम-चिह्न के लौटाता है, और एल्गोरिदम को सिर्फ़ समय वाले फ़ील्ड चाहिए:
{"words": [ {"index": 0, "text": "And", "start_ms": 0, "end_ms": 160}, {"index": 1, "text": "we'll", "start_ms": 160, "end_ms": 280}, {"index": 2, "text": "get", "start_ms": 280, "end_ms": 380}, ... ], "duration_ms": 306970} हर शब्द के लिए एक विंडो
हर शब्द से एक कैंडिडेट विंडो शुरू होती है। कोई शब्द तभी गिना जाता है जब वह विंडो खत्म होने से पहले खत्म हो जाए, और स्कैन उस पहले शब्द पर रुक जाता है जो ऐसा नहीं करता:
def candidates(words, window_ms): out = [] for i, w in enumerate(words): start = w["start_ms"] end = start + window_ms n = 0 for x in words[i:]: if x["end_ms"] > end: break n += 1 if n: out.append((start, end, n)) return out विंडो को किसी तय ग्रिड (जैसे हर सेकंड) की जगह शब्दों से शुरू करने का मतलब है कि कैंडिडेट हमेशा किसी शब्द की शुरुआत पर खुलता है, कभी शब्द के बीच में नहीं। स्कोर को असल में बोले गए समय से नहीं, विंडो की लंबाई से भाग दिया जाता है, इसलिए क्लिप के आखिरी 15 सेकंड में शुरू होने वाली 44 विंडो बिना किसी खास केस के अपने आप कम स्कोर पाती हैं।
इस इंटरव्यू के लिए 1,007 कैंडिडेट बनते हैं। मीडियन में 49 शब्द हैं, सबसे अच्छे में 67।
सबसे अच्छा पहले, बिना ओवरलैप के
कैंडिडेट्स को स्कोर के हिसाब से सॉर्ट किया जाता है, बराबर स्कोर होने पर पोज़िशन से तय होता है ताकि हर रन में शॉर्टलिस्ट एक जैसी आए, और फिर ऊपर से लिए जाते हैं जब तक वे पहले से चुने गए किसी से ओवरलैप न करें:
def find_highlights(words, window_ms=15_000, count=3): ranked = sorted(candidates(words, window_ms), key=lambda c: (-c[2], c[0])) chosen = [] for c in ranked: if len(chosen) >= count: break if any(c[0] < k[1] and k[0] < c[1] for k in chosen): continue # overlaps a better window chosen.append(c) return sorted(chosen) ऐप की सेटिंग्स, यानी 15 सेकंड और तीन क्लिप्स के साथ, Python वर्शन वही तीन रेंज लौटाता है जो ऐप दिखाता है:
| क्लिप | शब्द | शब्द/सेकंड |
|---|---|---|
| 0:45.1 – 1:00.1 | 64 | 4.27 |
| 2:03.4 – 2:18.4 | 67 | 4.47 |
| 2:31.5 – 2:46.5 | 65 | 4.33 |
ज़्यादातर काम ओवरलैप वाला नियम करता है। उसके बिना टॉप तीन होंगे 2:03.4, 2:03.6 और 2:00.2, यानी एक ही हिस्सा तीन बार, सब एक-दूसरे से 3.4 सेकंड के अंदर। तेज़ बोला गया हिस्सा कुछ देर तक तेज़ ही रहता है, इसलिए सबसे अच्छी विंडो एक जगह जमा हो जाती हैं: टॉप 19 कैंडिडेट सभी 1:59.7 और 2:04.4 के बीच शुरू होते हैं। दूसरा और तीसरा चुनाव रैंकिंग में 20वें और 21वें नंबर के कैंडिडेट हैं, और वहाँ तक पहुँचने के लिए 18 बेहतर स्कोर वाली विंडो छोड़ी गईं। 67 पर बराबर दो विंडो (2:03.4 और 2:03.6) ही वह वजह हैं जिसके कारण पोज़िशन से टाई तोड़ना ज़रूरी है।
विंडो की लंबाई जवाब बदल देती है
वही ट्रांसक्रिप्ट दूसरी विंडो लंबाइयों के साथ:
| विंडो | चुनाव (शुरुआत, शब्द/सेकंड) |
|---|---|
| 10 s | 0:51.2 (4.5), 2:08.5 (4.7), 3:15.3 (4.5) |
| 15 s | 0:45.1 (4.27), 2:03.4 (4.47), 2:31.5 (4.33) |
| 30 s | 0:25.1 (3.8), 1:48.5 (3.93), 2:31.5 (3.93) |
| 60 s | 0:00.0 (3.57), 1:23.7 (3.48), 2:31.0 (3.77) |
छोटी विंडो किसी एक तेज़ बौछार को इनाम देती हैं और ऊँचे पीक देती हैं। लंबी विंडो रुकावटों और सवालों को भी औसत में मिला लेती हैं और कुल औसत 3.28 की ओर खिसकती हैं। 2:31 के आसपास का हिस्सा 15 से 60 सेकंड तक हर लंबाई में बचा रहता है, जो इस बात का अच्छा संकेत है कि यह एक लगातार चलने वाला जवाब है, न कि कोई एक तेज़ वाक्य। दो लंबाइयों पर चलाकर ओवरलैप देखना कैंडिडेट्स को एक से ज़्यादा नंबर से रैंक करने का सस्ता तरीका है।
क्या "सबसे अच्छा पहले" ऑप्टिमल है?
सबसे अच्छी विंडो पहले लेना एक greedy चुनाव है। इसे जाँचने के लिए, सटीक जवाब शुरुआती समय से सॉर्ट किए गए कैंडिडेट्स पर एक छोटा dynamic program है: हर कैंडिडेट के लिए, या तो उसे छोड़ दें, या उसे लें और उस पहले कैंडिडेट पर कूदें जो इसके खत्म होने के बाद शुरू होता है।
import bisect, functools def best_total(words, window_ms, count): cands = sorted(candidates(words, window_ms)) starts = [c[0] for c in cands] nxt = [bisect.bisect_left(starts, c[1]) for c in cands] @functools.lru_cache(None) def best(i, k): if k == 0 or i >= len(cands): return 0 return max(best(i + 1, k), cands[i][2] + best(nxt[i], k - 1)) return best(0, count) चुनी गई विंडो में कुल शब्द, greedy बनाम सटीक:
| विंडो × क्लिप्स | सबसे अच्छा पहले | सटीक |
|---|---|---|
| 15 s × 3 | 196 शब्द, 3 क्लिप्स | 196 |
| 15 s × 10 | 595 शब्द, 10 क्लिप्स | 595 |
| 30 s × 5 | 563 शब्द, 5 क्लिप्स | 565 |
| 30 s × 10 | 764 शब्द, 8 क्लिप्स | 989 |
| 60 s × 5 | 849 शब्द, 5 क्लिप्स | 991 |
छोटी विंडो की छोटी शॉर्टलिस्ट के लिए यहाँ greedy कुछ नहीं खोता: 15 सेकंड पर 3, 5 और 10 क्लिप्स के लिए यह सटीक टोटल से मेल खाता रहा। मुश्किल तब आती है जब विंडो पूरी रिकॉर्डिंग भरने लगती हैं। 30 सेकंड की दस विंडो 307 सेकंड में आ जाती हैं, लेकिन "सबसे अच्छा पहले" अपने पहले चुनाव वहीं रखता है जहाँ पीक हैं, उनके बीच बचे गैप 30 सेकंड से छोटे रह जाते हैं, और यह 8 क्लिप्स लौटाता है जबकि सटीक जवाब में 10 आ जाती हैं। यही चीज़ 60 s × 5 पर 142 शब्दों का नुकसान करती है। तो greedy वर्शन "मुझे तीन कैंडिडेट दो" के लिए सही टूल है, और "इस टॉक को चैप्टर में बाँटो" के लिए गलत, जहाँ DP बस कुछ लाइनें ज़्यादा है।
स्कोरिंग को लीनियर बनाएँ
candidates में दोबारा स्कैन हर विंडो में हर शब्द पढ़ता है, इसलिए उसकी लागत विंडो की लंबाई गुणा शब्दों की संख्या के हिसाब से बढ़ती है। क्योंकि शुरू और खत्म दोनों समय सिर्फ़ आगे बढ़ते हैं, विंडो का आखिरी शब्द भी सिर्फ़ आगे बढ़ता है, और एक पॉइंटर काफ़ी है:
def candidates_two_pointer(words, window_ms): out, j = [], 0 for i, w in enumerate(words): j = max(j, i) end = w["start_ms"] + window_ms while j < len(words) and words[j]["end_ms"] <= end: j += 1 if j > i: out.append((w["start_ms"], end, j - i)) return out इस ट्रांसक्रिप्ट पर दोनों वर्शन बिल्कुल एक जैसे कैंडिडेट लौटाते हैं। Apple M1 पर Python 3.9 से टाइम किया गया, 7 रन का मीडियन, सिर्फ़ स्कोरिंग, और 1 घंटे वाला केस इंटरव्यू को 12 बार दोहराकर बनाया गया:
| ट्रांसक्रिप्ट | शब्द | दोबारा स्कैन | two-pointer |
|---|---|---|---|
| 5 min | 1,007 | 2.9 ms | 0.4 ms |
| 1 h | 12,084 | 190.5 ms | 5.4 ms |
ऐप का Rust लूप दोबारा स्कैन वाला वर्शन है। Python में, लंबे पॉडकास्ट पर या एक साथ कई विंडो लंबाइयों के साथ, two-pointer वर्शन ही रखने लायक है। हाँ, यह इस बात पर निर्भर करता है कि खत्म होने के समय क्रम में हों। यहाँ हर शब्द के लिए यह सही रहा, लेकिन कई सोर्स से मिलाए गए ट्रांसक्रिप्ट पर इसे assert कर लेना अच्छा है।
स्कोर क्या नहीं जानता
शब्द प्रति सेकंड यह मापता है कि कितना कहा गया, यह नहीं कि वह सुनने लायक है या नहीं। ऐप के सोर्स में find_highlights के ऊपर वाला कमेंट इसे heuristic कहता है और जानबूझकर एक लाइन खींचता है: हाइलाइट चुनने वाला मॉडल एक अलग फ़ीचर होगा, जिसमें पैसे लगते हैं और key चाहिए, और इस फ़ीचर को चुपचाप अपग्रेड करना एक मुफ़्त ऑपरेशन को ऐसे चार्ज में बदल देगा जिसे यूज़र ने कभी मंज़ूर नहीं किया। ऐप का पैनल भी यूज़र से यही कहता है, "a starting point, not a verdict" (एक शुरुआत, फ़ैसला नहीं)। असल में इसका मतलब है कि तीनों विंडो वह जगह हैं जहाँ से सुनना शुरू करना है, और कट पॉइंट अब भी कान से तय होते हैं।
पूरी स्क्रिप्ट, ऊपर के हर माप के साथ, सिर्फ़ standard library वाला लगभग 120 लाइनों का Python है। osFoundry डेस्कटॉप ऐप में यही फ़ंक्शन वीडियो एडिटर के ट्रांसक्रिप्ट पैनल में Suggest clips बटन के पीछे है, एक ट्रांसक्रिप्शन स्टेप के बगल में जो आपकी अपनी cloud API key पर चलता है। ऐप osFoundry डाउनलोड पेज से मुफ़्त है।


