[{"data":1,"prerenderedAt":1973},["ShallowReactive",2],{"work-\u002Fwork\u002Flive-translation-cobrowsing":3},{"id":4,"title":5,"body":6,"date":1957,"description":1958,"extension":1959,"externalUrl":1960,"featured":202,"kind":1961,"meta":1962,"navigation":202,"path":1963,"seo":1964,"stem":1965,"tags":1966,"__hash__":1972},"work\u002Fwork\u002Flive-translation-cobrowsing.md","Real-time speech translation, streamed into a live browser session",{"type":7,"value":8,"toc":1948},"minimark",[9,13,25,28,35,40,43,66,69,75,79,87,93,101,555,558,789,800,1020,1024,1027,1038,1274,1283,1287,1290,1301,1307,1316,1676,1683,1687,1690,1888,1892,1934,1938,1941,1944],[10,11,5],"h1",{"id":12},"real-time-speech-translation-streamed-into-a-live-browser-session",[14,15,16,17,24],"p",{},"Built on ",[18,19,23],"a",{"href":20,"rel":21},"https:\u002F\u002Fwww.webfuse.com",[22],"nofollow","Webfuse",", Surfly's co-browsing platform. It puts a live, multilingual caption overlay inside a shared browser session: everyone speaks their own language, everyone sees a single running feed translated into the language they picked. We use it internally during prospect calls when the room doesn't share a language, and it ships to customers who need real-time translation inside their own co-browsing sessions.",[14,26,27],{},"The interesting parts aren't \"call a translation API.\" They're the three that make it feel live and multiplayer: streaming transcription, a partial-vs-final split, and using the co-browsing channel itself to make one overlay appear for everyone.",[14,29,30],{},[31,32],"img",{"alt":33,"src":34},"The shared captions feed: every participant's speech, transcribed and translated into French in real time","\u002Fimages\u002Flive-translation-cobrowsing\u002F1.webp",[36,37,39],"h2",{"id":38},"how-the-pieces-fit","How the pieces fit",[14,41,42],{},"The extension runs across three contexts, each with one job:",[44,45,46,54,60],"ul",{},[47,48,49,53],"li",{},[50,51,52],"strong",{},"The popup"," is the engine. It captures the mic, runs streaming transcription, translates finished utterances, owns the language picker, and is a detached, draggable, minimisable floating widget.",[47,55,56,59],{},[50,57,58],{},"The background"," is the hub. It holds the shared caption store, broadcasts your captions to the other participants, merges theirs back in, and fans everything out to the overlay.",[47,61,62,65],{},[50,63,64],{},"The content script"," is the overlay. It injects a closed Shadow-DOM widget into the page and renders the caption cards.",[14,67,68],{},"That separation is what keeps each piece small. The engine never touches the DOM; the overlay never touches the network.",[14,70,71],{},[31,72],{"alt":73,"src":74},"The control popup: pick a speaker and a target language, then start listening","\u002Fimages\u002Flive-translation-cobrowsing\u002F2.webp",[36,76,78],{"id":77},"streaming-not-batch","Streaming, not batch",[14,80,81,82,86],{},"The captions appear ",[83,84,85],"em",{},"as you speak",", not after you stop. That rules out the usual \"record a clip, POST it, wait\" approach. Instead the mic is streamed to OpenAI's Realtime transcription endpoint over a WebSocket.",[14,88,89],{},[31,90],{"alt":91,"src":92},"Listening: a live waveform, a running session timer, and a green status","\u002Fimages\u002Flive-translation-cobrowsing\u002F3.webp",[14,94,95,96,100],{},"Capturing raw audio means an ",[97,98,99],"code",{},"AudioWorklet",". I inline it as a Blob URL so there's no extra file to ship in the extension, downsample to 24 kHz mono to match the API's PCM16 format, and stream every frame:",[102,103,108],"pre",{"className":104,"code":105,"language":106,"meta":107,"style":107},"language-ts shiki shiki-themes material-theme-lighter github-light github-dark","\u002F\u002F Inline AudioWorklet - no separate file to bundle\nconst WORKLET_CODE = `\n  class PcmProcessor extends AudioWorkletProcessor {\n    process(inputs) {\n      const input = inputs[0]?.[0];\n      if (input) this.port.postMessage(input);\n      return true;\n    }\n  }\n  registerProcessor(\"pcm-processor\", PcmProcessor);\n`;\n\n\u002F\u002F 24 kHz mono to match OpenAI's expected PCM16 format\nthis.audioContext = new AudioContext({ sampleRate: 24000 });\nconst blob = new Blob([WORKLET_CODE], { type: \"application\u002Fjavascript\" });\nawait this.audioContext.audioWorklet.addModule(URL.createObjectURL(blob));\n\nthis.workletNode.port.onmessage = (e) => {\n  if (this.ws?.readyState !== WebSocket.OPEN) return;\n  const pcm16 = float32ToInt16(e.data as Float32Array);\n  this.ws.send(JSON.stringify({\n    type: \"input_audio_buffer.append\",\n    audio: arrayBufferToBase64(pcm16.buffer),\n  }));\n};\n","ts","",[97,109,110,119,138,145,151,157,163,169,175,181,187,197,204,210,259,309,349,354,391,431,465,495,513,538,549],{"__ignoreMap":107},[111,112,115],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"sutJx","\u002F\u002F Inline AudioWorklet - no separate file to bundle\n",[111,120,122,126,130,134],{"class":113,"line":121},2,[111,123,125],{"class":124},"sbsja","const",[111,127,129],{"class":128},"s_hVV"," WORKLET_CODE",[111,131,133],{"class":132},"smGrS"," =",[111,135,137],{"class":136},"sjJ54"," `\n",[111,139,141],{"class":113,"line":140},3,[111,142,144],{"class":143},"s_sjI","  class PcmProcessor extends AudioWorkletProcessor {\n",[111,146,148],{"class":113,"line":147},4,[111,149,150],{"class":143},"    process(inputs) {\n",[111,152,154],{"class":113,"line":153},5,[111,155,156],{"class":143},"      const input = inputs[0]?.[0];\n",[111,158,160],{"class":113,"line":159},6,[111,161,162],{"class":143},"      if (input) this.port.postMessage(input);\n",[111,164,166],{"class":113,"line":165},7,[111,167,168],{"class":143},"      return true;\n",[111,170,172],{"class":113,"line":171},8,[111,173,174],{"class":143},"    }\n",[111,176,178],{"class":113,"line":177},9,[111,179,180],{"class":143},"  }\n",[111,182,184],{"class":113,"line":183},10,[111,185,186],{"class":143},"  registerProcessor(\"pcm-processor\", PcmProcessor);\n",[111,188,190,193],{"class":113,"line":189},11,[111,191,192],{"class":136},"`",[111,194,196],{"class":195},"sP7_E",";\n",[111,198,200],{"class":113,"line":199},12,[111,201,203],{"emptyLinePlaceholder":202},true,"\n",[111,205,207],{"class":113,"line":206},13,[111,208,209],{"class":117},"\u002F\u002F 24 kHz mono to match OpenAI's expected PCM16 format\n",[111,211,213,217,220,224,227,230,234,237,240,244,247,251,254,257],{"class":113,"line":212},14,[111,214,216],{"class":215},"s39Yj","this",[111,218,219],{"class":195},".",[111,221,223],{"class":222},"su5hD","audioContext ",[111,225,226],{"class":132},"=",[111,228,229],{"class":132}," new",[111,231,233],{"class":232},"sGLFI"," AudioContext",[111,235,236],{"class":222},"(",[111,238,239],{"class":195},"{",[111,241,243],{"class":242},"skxfh"," sampleRate",[111,245,246],{"class":195},":",[111,248,250],{"class":249},"srdBf"," 24000",[111,252,253],{"class":195}," }",[111,255,256],{"class":222},")",[111,258,196],{"class":195},[111,260,262,264,267,269,271,274,277,280,283,286,289,292,294,297,300,303,305,307],{"class":113,"line":261},15,[111,263,125],{"class":124},[111,265,266],{"class":128}," blob",[111,268,133],{"class":132},[111,270,229],{"class":132},[111,272,273],{"class":232}," Blob",[111,275,276],{"class":222},"([",[111,278,279],{"class":128},"WORKLET_CODE",[111,281,282],{"class":222},"]",[111,284,285],{"class":195},",",[111,287,288],{"class":195}," {",[111,290,291],{"class":242}," type",[111,293,246],{"class":195},[111,295,296],{"class":136}," \"",[111,298,299],{"class":143},"application\u002Fjavascript",[111,301,302],{"class":136},"\"",[111,304,253],{"class":195},[111,306,256],{"class":222},[111,308,196],{"class":195},[111,310,312,316,319,321,324,326,329,331,334,336,339,341,344,347],{"class":113,"line":311},16,[111,313,315],{"class":314},"sVHd0","await",[111,317,318],{"class":215}," this",[111,320,219],{"class":195},[111,322,323],{"class":222},"audioContext",[111,325,219],{"class":195},[111,327,328],{"class":222},"audioWorklet",[111,330,219],{"class":195},[111,332,333],{"class":232},"addModule",[111,335,236],{"class":222},[111,337,338],{"class":128},"URL",[111,340,219],{"class":195},[111,342,343],{"class":232},"createObjectURL",[111,345,346],{"class":222},"(blob))",[111,348,196],{"class":195},[111,350,352],{"class":113,"line":351},17,[111,353,203],{"emptyLinePlaceholder":202},[111,355,357,359,361,364,366,369,371,374,376,379,383,385,388],{"class":113,"line":356},18,[111,358,216],{"class":215},[111,360,219],{"class":195},[111,362,363],{"class":222},"workletNode",[111,365,219],{"class":195},[111,367,368],{"class":222},"port",[111,370,219],{"class":195},[111,372,373],{"class":232},"onmessage",[111,375,133],{"class":132},[111,377,378],{"class":195}," (",[111,380,382],{"class":381},"s99_P","e",[111,384,256],{"class":195},[111,386,387],{"class":124}," =>",[111,389,390],{"class":195}," {\n",[111,392,394,397,399,401,403,406,409,412,415,418,420,423,426,429],{"class":113,"line":393},19,[111,395,396],{"class":314},"  if",[111,398,378],{"class":242},[111,400,216],{"class":215},[111,402,219],{"class":195},[111,404,405],{"class":222},"ws",[111,407,408],{"class":195},"?.",[111,410,411],{"class":222},"readyState",[111,413,414],{"class":132}," !==",[111,416,417],{"class":222}," WebSocket",[111,419,219],{"class":195},[111,421,422],{"class":128},"OPEN",[111,424,425],{"class":242},") ",[111,427,428],{"class":314},"return",[111,430,196],{"class":195},[111,432,434,437,440,442,445,447,449,451,454,457,461,463],{"class":113,"line":433},20,[111,435,436],{"class":124},"  const",[111,438,439],{"class":128}," pcm16",[111,441,133],{"class":132},[111,443,444],{"class":232}," float32ToInt16",[111,446,236],{"class":242},[111,448,382],{"class":222},[111,450,219],{"class":195},[111,452,453],{"class":222},"data",[111,455,456],{"class":314}," as",[111,458,460],{"class":459},"sbgvK"," Float32Array",[111,462,256],{"class":242},[111,464,196],{"class":195},[111,466,468,471,473,475,477,480,482,485,487,490,492],{"class":113,"line":467},21,[111,469,470],{"class":215},"  this",[111,472,219],{"class":195},[111,474,405],{"class":222},[111,476,219],{"class":195},[111,478,479],{"class":232},"send",[111,481,236],{"class":242},[111,483,484],{"class":128},"JSON",[111,486,219],{"class":195},[111,488,489],{"class":232},"stringify",[111,491,236],{"class":242},[111,493,494],{"class":195},"{\n",[111,496,498,501,503,505,508,510],{"class":113,"line":497},22,[111,499,500],{"class":242},"    type",[111,502,246],{"class":195},[111,504,296],{"class":136},[111,506,507],{"class":143},"input_audio_buffer.append",[111,509,302],{"class":136},[111,511,512],{"class":195},",\n",[111,514,516,519,521,524,526,529,531,534,536],{"class":113,"line":515},23,[111,517,518],{"class":242},"    audio",[111,520,246],{"class":195},[111,522,523],{"class":232}," arrayBufferToBase64",[111,525,236],{"class":242},[111,527,528],{"class":222},"pcm16",[111,530,219],{"class":195},[111,532,533],{"class":222},"buffer",[111,535,256],{"class":242},[111,537,512],{"class":195},[111,539,541,544,547],{"class":113,"line":540},24,[111,542,543],{"class":195},"  }",[111,545,546],{"class":242},"))",[111,548,196],{"class":195},[111,550,552],{"class":113,"line":551},25,[111,553,554],{"class":195},"};\n",[14,556,557],{},"The other half is letting the server decide where one utterance ends and the next begins. I hand turn detection to server-side voice-activity detection, tuned so it cuts on a natural pause rather than mid-sentence:",[102,559,561],{"className":104,"code":560,"language":106,"meta":107,"style":107},"this.ws.send(JSON.stringify({\n  type: \"session.update\",\n  session: {\n    type: \"transcription\",\n    audio: {\n      input: {\n        format: { type: \"audio\u002Fpcm\", rate: 24000 },\n        transcription: { model: \"gpt-4o-transcribe\" },\n        turn_detection: {\n          type: \"server_vad\",\n          threshold: 0.5,\n          silence_duration_ms: 500,\n          prefix_padding_ms: 300,\n        },\n      },\n    },\n  },\n}));\n",[97,562,563,587,603,612,627,635,644,676,699,708,724,736,748,760,765,770,775,780],{"__ignoreMap":107},[111,564,565,567,569,571,573,575,577,579,581,583,585],{"class":113,"line":114},[111,566,216],{"class":215},[111,568,219],{"class":195},[111,570,405],{"class":222},[111,572,219],{"class":195},[111,574,479],{"class":232},[111,576,236],{"class":222},[111,578,484],{"class":128},[111,580,219],{"class":195},[111,582,489],{"class":232},[111,584,236],{"class":222},[111,586,494],{"class":195},[111,588,589,592,594,596,599,601],{"class":113,"line":121},[111,590,591],{"class":242},"  type",[111,593,246],{"class":195},[111,595,296],{"class":136},[111,597,598],{"class":143},"session.update",[111,600,302],{"class":136},[111,602,512],{"class":195},[111,604,605,608,610],{"class":113,"line":140},[111,606,607],{"class":242},"  session",[111,609,246],{"class":195},[111,611,390],{"class":195},[111,613,614,616,618,620,623,625],{"class":113,"line":147},[111,615,500],{"class":242},[111,617,246],{"class":195},[111,619,296],{"class":136},[111,621,622],{"class":143},"transcription",[111,624,302],{"class":136},[111,626,512],{"class":195},[111,628,629,631,633],{"class":113,"line":153},[111,630,518],{"class":242},[111,632,246],{"class":195},[111,634,390],{"class":195},[111,636,637,640,642],{"class":113,"line":159},[111,638,639],{"class":242},"      input",[111,641,246],{"class":195},[111,643,390],{"class":195},[111,645,646,649,651,653,655,657,659,662,664,666,669,671,673],{"class":113,"line":165},[111,647,648],{"class":242},"        format",[111,650,246],{"class":195},[111,652,288],{"class":195},[111,654,291],{"class":242},[111,656,246],{"class":195},[111,658,296],{"class":136},[111,660,661],{"class":143},"audio\u002Fpcm",[111,663,302],{"class":136},[111,665,285],{"class":195},[111,667,668],{"class":242}," rate",[111,670,246],{"class":195},[111,672,250],{"class":249},[111,674,675],{"class":195}," },\n",[111,677,678,681,683,685,688,690,692,695,697],{"class":113,"line":171},[111,679,680],{"class":242},"        transcription",[111,682,246],{"class":195},[111,684,288],{"class":195},[111,686,687],{"class":242}," model",[111,689,246],{"class":195},[111,691,296],{"class":136},[111,693,694],{"class":143},"gpt-4o-transcribe",[111,696,302],{"class":136},[111,698,675],{"class":195},[111,700,701,704,706],{"class":113,"line":177},[111,702,703],{"class":242},"        turn_detection",[111,705,246],{"class":195},[111,707,390],{"class":195},[111,709,710,713,715,717,720,722],{"class":113,"line":183},[111,711,712],{"class":242},"          type",[111,714,246],{"class":195},[111,716,296],{"class":136},[111,718,719],{"class":143},"server_vad",[111,721,302],{"class":136},[111,723,512],{"class":195},[111,725,726,729,731,734],{"class":113,"line":189},[111,727,728],{"class":242},"          threshold",[111,730,246],{"class":195},[111,732,733],{"class":249}," 0.5",[111,735,512],{"class":195},[111,737,738,741,743,746],{"class":113,"line":199},[111,739,740],{"class":242},"          silence_duration_ms",[111,742,246],{"class":195},[111,744,745],{"class":249}," 500",[111,747,512],{"class":195},[111,749,750,753,755,758],{"class":113,"line":206},[111,751,752],{"class":242},"          prefix_padding_ms",[111,754,246],{"class":195},[111,756,757],{"class":249}," 300",[111,759,512],{"class":195},[111,761,762],{"class":113,"line":212},[111,763,764],{"class":195},"        },\n",[111,766,767],{"class":113,"line":261},[111,768,769],{"class":195},"      },\n",[111,771,772],{"class":113,"line":311},[111,773,774],{"class":195},"    },\n",[111,776,777],{"class":113,"line":351},[111,778,779],{"class":195},"  },\n",[111,781,782,785,787],{"class":113,"line":356},[111,783,784],{"class":195},"}",[111,786,546],{"class":222},[111,788,196],{"class":195},[14,790,791,792,795,796,799],{},"Transcripts then arrive as a stream of deltas followed by a completion. One detail that earned its place: the API has shipped deltas as ",[83,793,794],{},"incremental"," chunks in some revisions and ",[83,797,798],{},"cumulative"," strings in others. Rather than trust one shape, the accumulator handles both:",[102,801,803],{"className":104,"code":802,"language":106,"meta":107,"style":107},"if (data.type   === \"conversation.item.input_audio_transcription.delta\") {\n  const itemId = data.item_id;\n  const chunk = data.delta || \"\";\n  \u002F\u002F If the delta already includes the prior text, take it as-is;\n  \u002F\u002F otherwise append. Survives both API delta formats.\n  const prior = this.partials.get(itemId) || \"\";\n  const next = chunk.startsWith(prior) ? chunk : prior + chunk;\n  this.partials.set(itemId, next);\n  this.onDelta({ itemId, partial: next });\n}\n",[97,804,805,832,851,875,880,885,920,960,985,1015],{"__ignoreMap":107},[111,806,807,810,813,815,818,821,823,826,828,830],{"class":113,"line":114},[111,808,809],{"class":314},"if",[111,811,812],{"class":222}," (data",[111,814,219],{"class":195},[111,816,817],{"class":222},"type   ",[111,819,820],{"class":132},"===",[111,822,296],{"class":136},[111,824,825],{"class":143},"conversation.item.input_audio_transcription.delta",[111,827,302],{"class":136},[111,829,425],{"class":222},[111,831,494],{"class":195},[111,833,834,836,839,841,844,846,849],{"class":113,"line":121},[111,835,436],{"class":124},[111,837,838],{"class":128}," itemId",[111,840,133],{"class":132},[111,842,843],{"class":222}," data",[111,845,219],{"class":195},[111,847,848],{"class":222},"item_id",[111,850,196],{"class":195},[111,852,853,855,858,860,862,864,867,870,873],{"class":113,"line":140},[111,854,436],{"class":124},[111,856,857],{"class":128}," chunk",[111,859,133],{"class":132},[111,861,843],{"class":222},[111,863,219],{"class":195},[111,865,866],{"class":222},"delta",[111,868,869],{"class":132}," ||",[111,871,872],{"class":136}," \"\"",[111,874,196],{"class":195},[111,876,877],{"class":113,"line":147},[111,878,879],{"class":117},"  \u002F\u002F If the delta already includes the prior text, take it as-is;\n",[111,881,882],{"class":113,"line":153},[111,883,884],{"class":117},"  \u002F\u002F otherwise append. Survives both API delta formats.\n",[111,886,887,889,892,894,896,898,901,903,906,908,911,913,916,918],{"class":113,"line":159},[111,888,436],{"class":124},[111,890,891],{"class":128}," prior",[111,893,133],{"class":132},[111,895,318],{"class":215},[111,897,219],{"class":195},[111,899,900],{"class":222},"partials",[111,902,219],{"class":195},[111,904,905],{"class":232},"get",[111,907,236],{"class":242},[111,909,910],{"class":222},"itemId",[111,912,425],{"class":242},[111,914,915],{"class":132},"||",[111,917,872],{"class":136},[111,919,196],{"class":195},[111,921,922,924,927,929,931,933,936,938,941,943,946,948,951,953,956,958],{"class":113,"line":165},[111,923,436],{"class":124},[111,925,926],{"class":128}," next",[111,928,133],{"class":132},[111,930,857],{"class":222},[111,932,219],{"class":195},[111,934,935],{"class":232},"startsWith",[111,937,236],{"class":242},[111,939,940],{"class":222},"prior",[111,942,425],{"class":242},[111,944,945],{"class":132},"?",[111,947,857],{"class":222},[111,949,950],{"class":132}," :",[111,952,891],{"class":222},[111,954,955],{"class":132}," +",[111,957,857],{"class":222},[111,959,196],{"class":195},[111,961,962,964,966,968,970,973,975,977,979,981,983],{"class":113,"line":171},[111,963,470],{"class":215},[111,965,219],{"class":195},[111,967,900],{"class":222},[111,969,219],{"class":195},[111,971,972],{"class":232},"set",[111,974,236],{"class":242},[111,976,910],{"class":222},[111,978,285],{"class":195},[111,980,926],{"class":222},[111,982,256],{"class":242},[111,984,196],{"class":195},[111,986,987,989,991,994,996,998,1000,1002,1005,1007,1009,1011,1013],{"class":113,"line":177},[111,988,470],{"class":215},[111,990,219],{"class":195},[111,992,993],{"class":232},"onDelta",[111,995,236],{"class":242},[111,997,239],{"class":195},[111,999,838],{"class":222},[111,1001,285],{"class":195},[111,1003,1004],{"class":242}," partial",[111,1006,246],{"class":195},[111,1008,926],{"class":222},[111,1010,253],{"class":195},[111,1012,256],{"class":242},[111,1014,196],{"class":195},[111,1016,1017],{"class":113,"line":183},[111,1018,1019],{"class":195},"}\n",[36,1021,1023],{"id":1022},"partials-feel-instant-translation-waits","Partials feel instant; translation waits",[14,1025,1026],{},"A live transcript is a stream of partial guesses that sharpen as you keep talking. If I translated every partial, I'd hammer the translation API dozens of times per sentence and watch the text flicker as each guess got re-translated.",[14,1028,1029,1030,1033,1034,1037],{},"So the two signals are split. ",[50,1031,1032],{},"Partials"," surface the source text immediately - that's what makes the row appear in real time, with a blinking cursor and a \"Speaking\" pill. ",[50,1035,1036],{},"Translation is deferred"," until the utterance is final:",[102,1039,1041],{"className":104,"code":1040,"language":106,"meta":107,"style":107},"new RealtimeTranscriber(\n  \u002F\u002F FINAL: translate once the utterance is complete, then broadcast it\n  async ({ transcript, itemId }) => {\n    const translated = await translateText(transcript, currentTarget.name);\n    browser.runtime.sendMessage({\n      type: \"lt-caption-out\", itemId, original: transcript, translated, \u002F* ... *\u002F\n    });\n  },\n  \u002F\u002F PARTIAL: surface the source text instantly. No translation yet -\n  \u002F\u002F translating every delta would spam the API and flicker the UI.\n  ({ itemId, partial }) => {\n    browser.runtime.sendMessage({\n      type: \"lt-partial-out\", itemId, original: partial, \u002F* ... *\u002F\n    });\n  },\n);\n",[97,1042,1043,1054,1059,1081,1116,1135,1171,1180,1184,1189,1194,1211,1227,1256,1264,1268],{"__ignoreMap":107},[111,1044,1045,1048,1051],{"class":113,"line":114},[111,1046,1047],{"class":132},"new",[111,1049,1050],{"class":232}," RealtimeTranscriber",[111,1052,1053],{"class":222},"(\n",[111,1055,1056],{"class":113,"line":121},[111,1057,1058],{"class":117},"  \u002F\u002F FINAL: translate once the utterance is complete, then broadcast it\n",[111,1060,1061,1064,1067,1070,1072,1074,1077,1079],{"class":113,"line":140},[111,1062,1063],{"class":124},"  async",[111,1065,1066],{"class":195}," ({",[111,1068,1069],{"class":381}," transcript",[111,1071,285],{"class":195},[111,1073,838],{"class":381},[111,1075,1076],{"class":195}," })",[111,1078,387],{"class":124},[111,1080,390],{"class":195},[111,1082,1083,1086,1089,1091,1094,1097,1099,1102,1104,1107,1109,1112,1114],{"class":113,"line":147},[111,1084,1085],{"class":124},"    const",[111,1087,1088],{"class":128}," translated",[111,1090,133],{"class":132},[111,1092,1093],{"class":314}," await",[111,1095,1096],{"class":232}," translateText",[111,1098,236],{"class":242},[111,1100,1101],{"class":222},"transcript",[111,1103,285],{"class":195},[111,1105,1106],{"class":222}," currentTarget",[111,1108,219],{"class":195},[111,1110,1111],{"class":222},"name",[111,1113,256],{"class":242},[111,1115,196],{"class":195},[111,1117,1118,1121,1123,1126,1128,1131,1133],{"class":113,"line":153},[111,1119,1120],{"class":222},"    browser",[111,1122,219],{"class":195},[111,1124,1125],{"class":222},"runtime",[111,1127,219],{"class":195},[111,1129,1130],{"class":232},"sendMessage",[111,1132,236],{"class":242},[111,1134,494],{"class":195},[111,1136,1137,1140,1142,1144,1147,1149,1151,1153,1155,1158,1160,1162,1164,1166,1168],{"class":113,"line":159},[111,1138,1139],{"class":242},"      type",[111,1141,246],{"class":195},[111,1143,296],{"class":136},[111,1145,1146],{"class":143},"lt-caption-out",[111,1148,302],{"class":136},[111,1150,285],{"class":195},[111,1152,838],{"class":222},[111,1154,285],{"class":195},[111,1156,1157],{"class":242}," original",[111,1159,246],{"class":195},[111,1161,1069],{"class":222},[111,1163,285],{"class":195},[111,1165,1088],{"class":222},[111,1167,285],{"class":195},[111,1169,1170],{"class":117}," \u002F* ... *\u002F\n",[111,1172,1173,1176,1178],{"class":113,"line":165},[111,1174,1175],{"class":195},"    }",[111,1177,256],{"class":242},[111,1179,196],{"class":195},[111,1181,1182],{"class":113,"line":171},[111,1183,779],{"class":195},[111,1185,1186],{"class":113,"line":177},[111,1187,1188],{"class":117},"  \u002F\u002F PARTIAL: surface the source text instantly. No translation yet -\n",[111,1190,1191],{"class":113,"line":183},[111,1192,1193],{"class":117},"  \u002F\u002F translating every delta would spam the API and flicker the UI.\n",[111,1195,1196,1199,1201,1203,1205,1207,1209],{"class":113,"line":189},[111,1197,1198],{"class":195},"  ({",[111,1200,838],{"class":381},[111,1202,285],{"class":195},[111,1204,1004],{"class":381},[111,1206,1076],{"class":195},[111,1208,387],{"class":124},[111,1210,390],{"class":195},[111,1212,1213,1215,1217,1219,1221,1223,1225],{"class":113,"line":199},[111,1214,1120],{"class":222},[111,1216,219],{"class":195},[111,1218,1125],{"class":222},[111,1220,219],{"class":195},[111,1222,1130],{"class":232},[111,1224,236],{"class":242},[111,1226,494],{"class":195},[111,1228,1229,1231,1233,1235,1238,1240,1242,1244,1246,1248,1250,1252,1254],{"class":113,"line":206},[111,1230,1139],{"class":242},[111,1232,246],{"class":195},[111,1234,296],{"class":136},[111,1236,1237],{"class":143},"lt-partial-out",[111,1239,302],{"class":136},[111,1241,285],{"class":195},[111,1243,838],{"class":222},[111,1245,285],{"class":195},[111,1247,1157],{"class":242},[111,1249,246],{"class":195},[111,1251,1004],{"class":222},[111,1253,285],{"class":195},[111,1255,1170],{"class":117},[111,1257,1258,1260,1262],{"class":113,"line":212},[111,1259,1175],{"class":195},[111,1261,256],{"class":242},[111,1263,196],{"class":195},[111,1265,1266],{"class":113,"line":261},[111,1267,779],{"class":195},[111,1269,1270,1272],{"class":113,"line":311},[111,1271,256],{"class":222},[111,1273,196],{"class":195},[14,1275,1276,1277,1282],{},"The glue is a ",[50,1278,1279,1280],{},"stable per-utterance ",[97,1281,910],{}," - the same id rides every partial and the final. The overlay uses it to update the in-progress card in place instead of appending new ones, so a row sharpens and then settles rather than stuttering down the feed.",[36,1284,1286],{"id":1285},"the-multiplayer-trick","The multiplayer trick",[14,1288,1289],{},"This is the part I'm proudest of, because it leans on what Webfuse already is.",[14,1291,1292,1293,1296,1297,1300],{},"Webfuse streams a single proxied page to every participant. The content script injects the caption overlay into ",[83,1294,1295],{},"that"," page - so even though one participant's browser renders it, the overlay is part of the shared session and ",[50,1298,1299],{},"everyone sees it for free",". No second channel, no per-client rendering. From the source:",[1302,1303,1304],"blockquote",{},[14,1305,1306],{},"The content script renders the overlay... but the DOM it injects is part of the proxied page and is streamed to all participants - so the overlay is visible to everyone.",[14,1308,1309,1310,1313,1314,246],{},"The captions themselves are shared over the co-browsing data channel. Each participant transcribes ",[83,1311,1312],{},"their own"," mic locally and broadcasts the result; everyone merges the broadcasts into one ordered feed, keyed by that same ",[97,1315,910],{},[102,1317,1319],{"className":104,"code":1318,"language":106,"meta":107,"style":107},"\u002F\u002F Broadcast my caption to the whole session\nbrowser.webfuseSession.broadcastMessage(JSON.stringify({\n  type: \"live-translator\",\n  kind: \"partial\",            \u002F\u002F or \"final\"\n  itemId,                     \u002F\u002F stable per-utterance id\n  participantName: selfName,\n  original: message.original,\n  targetName, targetShort, targetColor,\n}));\n\n\u002F\u002F ...and merge everyone else's into the shared store\nbrowser.webfuseSession.on.addListener((payload) => {\n  \u002F\u002F Webfuse payloads arrive as a bare string OR { message }. Handle both.\n  const raw = typeof payload === \"string\" ? payload : payload?.message;\n  const msg = JSON.parse(raw);\n  if (msg?.type !== \"live-translator\") return;\n  pushCaption({\n    itemId: msg.itemId,\n    participantName: msg.participantName,\n    partial: msg.kind === \"partial\",\n    \u002F* ... *\u002F\n  });\n});\n",[97,1320,1321,1326,1353,1368,1387,1397,1409,1426,1443,1451,1455,1460,1491,1496,1537,1563,1591,1600,1615,1631,1655,1660,1668],{"__ignoreMap":107},[111,1322,1323],{"class":113,"line":114},[111,1324,1325],{"class":117},"\u002F\u002F Broadcast my caption to the whole session\n",[111,1327,1328,1331,1333,1336,1338,1341,1343,1345,1347,1349,1351],{"class":113,"line":121},[111,1329,1330],{"class":222},"browser",[111,1332,219],{"class":195},[111,1334,1335],{"class":222},"webfuseSession",[111,1337,219],{"class":195},[111,1339,1340],{"class":232},"broadcastMessage",[111,1342,236],{"class":222},[111,1344,484],{"class":128},[111,1346,219],{"class":195},[111,1348,489],{"class":232},[111,1350,236],{"class":222},[111,1352,494],{"class":195},[111,1354,1355,1357,1359,1361,1364,1366],{"class":113,"line":140},[111,1356,591],{"class":242},[111,1358,246],{"class":195},[111,1360,296],{"class":136},[111,1362,1363],{"class":143},"live-translator",[111,1365,302],{"class":136},[111,1367,512],{"class":195},[111,1369,1370,1373,1375,1377,1380,1382,1384],{"class":113,"line":147},[111,1371,1372],{"class":242},"  kind",[111,1374,246],{"class":195},[111,1376,296],{"class":136},[111,1378,1379],{"class":143},"partial",[111,1381,302],{"class":136},[111,1383,285],{"class":195},[111,1385,1386],{"class":117},"            \u002F\u002F or \"final\"\n",[111,1388,1389,1392,1394],{"class":113,"line":153},[111,1390,1391],{"class":222},"  itemId",[111,1393,285],{"class":195},[111,1395,1396],{"class":117},"                     \u002F\u002F stable per-utterance id\n",[111,1398,1399,1402,1404,1407],{"class":113,"line":159},[111,1400,1401],{"class":242},"  participantName",[111,1403,246],{"class":195},[111,1405,1406],{"class":222}," selfName",[111,1408,512],{"class":195},[111,1410,1411,1414,1416,1419,1421,1424],{"class":113,"line":165},[111,1412,1413],{"class":242},"  original",[111,1415,246],{"class":195},[111,1417,1418],{"class":222}," message",[111,1420,219],{"class":195},[111,1422,1423],{"class":222},"original",[111,1425,512],{"class":195},[111,1427,1428,1431,1433,1436,1438,1441],{"class":113,"line":171},[111,1429,1430],{"class":222},"  targetName",[111,1432,285],{"class":195},[111,1434,1435],{"class":222}," targetShort",[111,1437,285],{"class":195},[111,1439,1440],{"class":222}," targetColor",[111,1442,512],{"class":195},[111,1444,1445,1447,1449],{"class":113,"line":177},[111,1446,784],{"class":195},[111,1448,546],{"class":222},[111,1450,196],{"class":195},[111,1452,1453],{"class":113,"line":183},[111,1454,203],{"emptyLinePlaceholder":202},[111,1456,1457],{"class":113,"line":189},[111,1458,1459],{"class":117},"\u002F\u002F ...and merge everyone else's into the shared store\n",[111,1461,1462,1464,1466,1468,1470,1473,1475,1478,1480,1482,1485,1487,1489],{"class":113,"line":199},[111,1463,1330],{"class":222},[111,1465,219],{"class":195},[111,1467,1335],{"class":222},[111,1469,219],{"class":195},[111,1471,1472],{"class":222},"on",[111,1474,219],{"class":195},[111,1476,1477],{"class":232},"addListener",[111,1479,236],{"class":222},[111,1481,236],{"class":195},[111,1483,1484],{"class":381},"payload",[111,1486,256],{"class":195},[111,1488,387],{"class":124},[111,1490,390],{"class":195},[111,1492,1493],{"class":113,"line":206},[111,1494,1495],{"class":117},"  \u002F\u002F Webfuse payloads arrive as a bare string OR { message }. Handle both.\n",[111,1497,1498,1500,1503,1505,1508,1511,1514,1516,1519,1521,1524,1526,1528,1530,1532,1535],{"class":113,"line":212},[111,1499,436],{"class":124},[111,1501,1502],{"class":128}," raw",[111,1504,133],{"class":132},[111,1506,1507],{"class":132}," typeof",[111,1509,1510],{"class":222}," payload",[111,1512,1513],{"class":132}," ===",[111,1515,296],{"class":136},[111,1517,1518],{"class":143},"string",[111,1520,302],{"class":136},[111,1522,1523],{"class":132}," ?",[111,1525,1510],{"class":222},[111,1527,950],{"class":132},[111,1529,1510],{"class":222},[111,1531,408],{"class":195},[111,1533,1534],{"class":222},"message",[111,1536,196],{"class":195},[111,1538,1539,1541,1544,1546,1549,1551,1554,1556,1559,1561],{"class":113,"line":261},[111,1540,436],{"class":124},[111,1542,1543],{"class":128}," msg",[111,1545,133],{"class":132},[111,1547,1548],{"class":128}," JSON",[111,1550,219],{"class":195},[111,1552,1553],{"class":232},"parse",[111,1555,236],{"class":242},[111,1557,1558],{"class":222},"raw",[111,1560,256],{"class":242},[111,1562,196],{"class":195},[111,1564,1565,1567,1569,1572,1574,1577,1579,1581,1583,1585,1587,1589],{"class":113,"line":311},[111,1566,396],{"class":314},[111,1568,378],{"class":242},[111,1570,1571],{"class":222},"msg",[111,1573,408],{"class":195},[111,1575,1576],{"class":222},"type",[111,1578,414],{"class":132},[111,1580,296],{"class":136},[111,1582,1363],{"class":143},[111,1584,302],{"class":136},[111,1586,425],{"class":242},[111,1588,428],{"class":314},[111,1590,196],{"class":195},[111,1592,1593,1596,1598],{"class":113,"line":351},[111,1594,1595],{"class":232},"  pushCaption",[111,1597,236],{"class":242},[111,1599,494],{"class":195},[111,1601,1602,1605,1607,1609,1611,1613],{"class":113,"line":356},[111,1603,1604],{"class":242},"    itemId",[111,1606,246],{"class":195},[111,1608,1543],{"class":222},[111,1610,219],{"class":195},[111,1612,910],{"class":222},[111,1614,512],{"class":195},[111,1616,1617,1620,1622,1624,1626,1629],{"class":113,"line":393},[111,1618,1619],{"class":242},"    participantName",[111,1621,246],{"class":195},[111,1623,1543],{"class":222},[111,1625,219],{"class":195},[111,1627,1628],{"class":222},"participantName",[111,1630,512],{"class":195},[111,1632,1633,1636,1638,1640,1642,1645,1647,1649,1651,1653],{"class":113,"line":433},[111,1634,1635],{"class":242},"    partial",[111,1637,246],{"class":195},[111,1639,1543],{"class":222},[111,1641,219],{"class":195},[111,1643,1644],{"class":222},"kind",[111,1646,1513],{"class":132},[111,1648,296],{"class":136},[111,1650,1379],{"class":143},[111,1652,302],{"class":136},[111,1654,512],{"class":195},[111,1656,1657],{"class":113,"line":467},[111,1658,1659],{"class":117},"    \u002F* ... *\u002F\n",[111,1661,1662,1664,1666],{"class":113,"line":497},[111,1663,543],{"class":195},[111,1665,256],{"class":242},[111,1667,196],{"class":195},[111,1669,1670,1672,1674],{"class":113,"line":515},[111,1671,784],{"class":195},[111,1673,256],{"class":222},[111,1675,196],{"class":195},[14,1677,1678,1679,1682],{},"Because every entry carries its own language metadata, ",[50,1680,1681],{},"each participant translates into the language they chose"," - your captions land in your language, mine in mine, all in the same feed, each tagged with the speaker and a per-language colour.",[36,1684,1686],{"id":1685},"no-keys-in-the-client","No keys in the client",[14,1688,1689],{},"The extension never holds the OpenAI key. A small backend mints a short-lived ephemeral key, and the realtime socket authenticates with that instead - so nothing long-lived ever reaches the browser, and translation is proxied the same way:",[102,1691,1693],{"className":104,"code":1692,"language":106,"meta":107,"style":107},"const res = await fetch(`${env.BACKEND_URL}\u002Fapi\u002Fopenai\u002Fephemeral-key`, {\n  method: \"POST\",\n  headers: { Authorization: `Bearer ${env.API_TOKEN}` },\n  body: JSON.stringify({ session: { type: \"transcription\", \u002F* ... *\u002F } }),\n});\nconst data = await res.json();\nawait transcriber.connect(data.client_secret.value);\n",[97,1694,1695,1733,1749,1784,1831,1839,1861],{"__ignoreMap":107},[111,1696,1697,1699,1702,1704,1706,1709,1711,1714,1717,1719,1722,1724,1727,1729,1731],{"class":113,"line":114},[111,1698,125],{"class":124},[111,1700,1701],{"class":128}," res",[111,1703,133],{"class":132},[111,1705,1093],{"class":314},[111,1707,1708],{"class":232}," fetch",[111,1710,236],{"class":222},[111,1712,1713],{"class":136},"`${",[111,1715,1716],{"class":222},"env",[111,1718,219],{"class":136},[111,1720,1721],{"class":128},"BACKEND_URL",[111,1723,784],{"class":136},[111,1725,1726],{"class":143},"\u002Fapi\u002Fopenai\u002Fephemeral-key",[111,1728,192],{"class":136},[111,1730,285],{"class":195},[111,1732,390],{"class":195},[111,1734,1735,1738,1740,1742,1745,1747],{"class":113,"line":121},[111,1736,1737],{"class":242},"  method",[111,1739,246],{"class":195},[111,1741,296],{"class":136},[111,1743,1744],{"class":143},"POST",[111,1746,302],{"class":136},[111,1748,512],{"class":195},[111,1750,1751,1754,1756,1758,1761,1763,1766,1769,1772,1774,1776,1779,1782],{"class":113,"line":140},[111,1752,1753],{"class":242},"  headers",[111,1755,246],{"class":195},[111,1757,288],{"class":195},[111,1759,1760],{"class":242}," Authorization",[111,1762,246],{"class":195},[111,1764,1765],{"class":136}," `",[111,1767,1768],{"class":143},"Bearer ",[111,1770,1771],{"class":136},"${",[111,1773,1716],{"class":222},[111,1775,219],{"class":136},[111,1777,1778],{"class":128},"API_TOKEN",[111,1780,1781],{"class":136},"}`",[111,1783,675],{"class":195},[111,1785,1786,1789,1791,1793,1795,1797,1799,1801,1804,1806,1808,1810,1812,1814,1816,1818,1820,1823,1825,1827,1829],{"class":113,"line":147},[111,1787,1788],{"class":242},"  body",[111,1790,246],{"class":195},[111,1792,1548],{"class":128},[111,1794,219],{"class":195},[111,1796,489],{"class":232},[111,1798,236],{"class":222},[111,1800,239],{"class":195},[111,1802,1803],{"class":242}," session",[111,1805,246],{"class":195},[111,1807,288],{"class":195},[111,1809,291],{"class":242},[111,1811,246],{"class":195},[111,1813,296],{"class":136},[111,1815,622],{"class":143},[111,1817,302],{"class":136},[111,1819,285],{"class":195},[111,1821,1822],{"class":117}," \u002F* ... *\u002F",[111,1824,253],{"class":195},[111,1826,253],{"class":195},[111,1828,256],{"class":222},[111,1830,512],{"class":195},[111,1832,1833,1835,1837],{"class":113,"line":153},[111,1834,784],{"class":195},[111,1836,256],{"class":222},[111,1838,196],{"class":195},[111,1840,1841,1843,1845,1847,1849,1851,1853,1856,1859],{"class":113,"line":159},[111,1842,125],{"class":124},[111,1844,843],{"class":128},[111,1846,133],{"class":132},[111,1848,1093],{"class":314},[111,1850,1701],{"class":222},[111,1852,219],{"class":195},[111,1854,1855],{"class":232},"json",[111,1857,1858],{"class":222},"()",[111,1860,196],{"class":195},[111,1862,1863,1865,1868,1870,1873,1876,1878,1881,1883,1886],{"class":113,"line":165},[111,1864,315],{"class":314},[111,1866,1867],{"class":222}," transcriber",[111,1869,219],{"class":195},[111,1871,1872],{"class":232},"connect",[111,1874,1875],{"class":222},"(data",[111,1877,219],{"class":195},[111,1879,1880],{"class":222},"client_secret",[111,1882,219],{"class":195},[111,1884,1885],{"class":222},"value)",[111,1887,196],{"class":195},[36,1889,1891],{"id":1890},"the-details-that-make-it-feel-finished","The details that make it feel finished",[44,1893,1894,1904,1910,1928],{},[47,1895,1896,1899,1900,1903],{},[50,1897,1898],{},"Closed Shadow DOM."," The overlay lives in a closed shadow root at max ",[97,1901,1902],{},"z-index",", so the host page's styles can't leak in and the overlay can't leak out - it looks identical on any site.",[47,1905,1906,1909],{},[50,1907,1908],{},"In-place updates."," Partial to final mutates the existing card's text nodes rather than swapping the element, which keeps the typing cursor and entry animation smooth.",[47,1911,1912,1915,1916,1919,1920,1923,1924,1927],{},[50,1913,1914],{},"A detached, persistent popup"," that survives navigation, auto-fits its chrome to its content with a ",[97,1917,1918],{},"ResizeObserver",", and drags via ",[97,1921,1922],{},"screenX\u002FY"," because it lives in an iframe where ",[97,1925,1926],{},"clientX\u002FY"," would be wrong.",[47,1929,1930,1933],{},[50,1931,1932],{},"Defensive everywhere it talks to something it doesn't own"," - the two API delta formats, the two Webfuse payload shapes. The fun stops being fun in production when an upstream quietly changes shape, so I assume both.",[36,1935,1937],{"id":1936},"why-it-matters","Why it matters",[14,1939,1940],{},"It turns a language barrier on a live call into a non-event. Internally, a multilingual prospect call just works - everyone reads along in their own language while the conversation flows. Externally, it's a capability customers can drop into their own co-browsing sessions.",[14,1942,1943],{},"The thing I'd underline: the leverage came from treating the platform as part of the architecture. The hard problem - \"how do I get one synchronised, multilingual overlay in front of N people\" - mostly dissolved once I used Webfuse's proxied DOM and data channel as the transport, instead of building a parallel one beside it.",[1945,1946,1947],"style",{},"html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":107,"searchDepth":121,"depth":121,"links":1949},[1950,1951,1952,1953,1954,1955,1956],{"id":38,"depth":121,"text":39},{"id":77,"depth":121,"text":78},{"id":1022,"depth":121,"text":1023},{"id":1285,"depth":121,"text":1286},{"id":1685,"depth":121,"text":1686},{"id":1890,"depth":121,"text":1891},{"id":1936,"depth":121,"text":1937},"2026-06-10","A live, multilingual caption overlay for Webfuse co-browsing sessions - streaming transcription over WebSockets, per-participant target languages, and a shared feed every participant sees. Used internally for multilingual prospect calls and shipped to customers.","md",null,"case-study",{},"\u002Fwork\u002Flive-translation-cobrowsing",{"title":5,"description":1958},"work\u002Flive-translation-cobrowsing",[23,1967,1968,1969,1970,1971],"OpenAI Realtime","WebSockets","Web Audio","TypeScript","Voice AI","EhnfuClABa0zQvgk6DCxBc89in5DTaFW4IdE5zVxGq0",1781635014771]