Created
May 28, 2025 16:57
-
-
Save qgustavor/b3e37e8978628c53b616670305e23036 to your computer and use it in GitHub Desktop.
JumpCutter for tldv.io: Speeds up silent parts in tldv.io meetings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name JumpCutter for tldv.io | |
// @namespace Violentmonkey Scripts | |
// @match https://tldv.io/app/meetings/* | |
// @grant none | |
// @version 1.3 | |
// @author qgustavor | |
// @description Speeds up silent parts in tldv.io meetings | |
// @require https://cdn.jsdelivr.net/npm/[email protected] | |
// ==/UserScript== | |
(function () { | |
const scriptName = 'JUMPCUTTER' | |
const config = { | |
threshold: 0.01, | |
silentSpeed: 4, | |
talkingSpeed: 1.1, | |
silentDebounceLimit: 100, | |
talkingDebounceLimit: 0, | |
volumeGain: 1, | |
delay: 0, | |
showGUI: true | |
} | |
window[scriptName] = config | |
let lastVideo = null | |
let audioCtx, sourceNode, gainNode, delayNode, scriptProcessor | |
let isTalking = true | |
let counter = 0 | |
let gui = null | |
function initGUI () { | |
if (gui) { | |
gui.destroy() | |
} | |
gui = new lil.GUI({ | |
title: 'JumpCutter Settings', | |
width: 280 | |
}) | |
gui.close() | |
// Audio detection settings | |
const settingsFolder = gui.addFolder('Settings') | |
settingsFolder.add(config, 'threshold', 0.001, 0.1, 0.001) | |
.name('Volume Threshold') | |
.onChange(value => { | |
console.log(`[${scriptName}] Threshold changed to:`, value) | |
}) | |
// Speed settings | |
settingsFolder.add(config, 'talkingSpeed', 0.5, 3, 0.1) | |
.name('Talking Speed') | |
.onChange(value => { | |
console.log(`[${scriptName}] Talking speed changed to:`, value) | |
}) | |
settingsFolder.add(config, 'silentSpeed', 1, 16, 0.5) | |
.name('Silent Speed') | |
.onChange(value => { | |
console.log(`[${scriptName}] Silent speed changed to:`, value) | |
}) | |
// Debounce settings | |
settingsFolder.add(config, 'talkingDebounceLimit', 0, 500, 1) | |
.name('Talking Debounce') | |
.onChange(value => { | |
console.log(`[${scriptName}] Talking debounce changed to:`, value) | |
}) | |
settingsFolder.add(config, 'silentDebounceLimit', 0, 500, 1) | |
.name('Silent Debounce') | |
.onChange(value => { | |
console.log(`[${scriptName}] Silent debounce changed to:`, value) | |
}) | |
// Audio processing settings | |
settingsFolder.add(config, 'volumeGain', 0, 5, 0.1) | |
.name('Volume Gain') | |
.onChange(value => { | |
console.log(`[${scriptName}] Volume gain changed to:`, value) | |
}) | |
// Control buttons | |
settingsFolder.add({ | |
reset: () => { | |
Object.assign(config, { | |
threshold: 0.01, | |
silentSpeed: 4, | |
talkingSpeed: 1.1, | |
silentDebounceLimit: 100, | |
talkingDebounceLimit: 0, | |
volumeGain: 1, | |
delay: 0 | |
}) | |
gui.destroy() | |
initGUI() | |
console.log(`[${scriptName}] Settings reset to defaults`) | |
} | |
}, 'reset').name('Reset to Defaults') | |
// Status display | |
const statusFolder = gui.addFolder('Status') | |
const statusObj = { | |
currentMode: 'Initializing...', | |
currentSpeed: '1.0x', | |
audioLevel: '0.000' | |
} | |
const modeController = statusFolder.add(statusObj, 'currentMode').name('Mode').listen() | |
const speedController = statusFolder.add(statusObj, 'currentSpeed').name('Current Speed').listen() | |
const audioController = statusFolder.add(statusObj, 'audioLevel').name('Audio Level').listen() | |
// Update status in the audio processing loop | |
window[scriptName + '_updateStatus'] = (mode, speed, audioLevel) => { | |
statusObj.currentMode = mode | |
statusObj.currentSpeed = speed + 'x' | |
statusObj.audioLevel = audioLevel.toFixed(3) | |
} | |
// Expand important folders by default | |
speedFolder.open() | |
statusFolder.open() | |
console.log(`[${scriptName}] GUI initialized`) | |
} | |
function setupVideo (video) { | |
cleanup() | |
lastVideo = video | |
audioCtx = new AudioContext() | |
sourceNode = audioCtx.createMediaElementSource(video) | |
delayNode = audioCtx.createDelay(30) | |
gainNode = audioCtx.createGain() | |
scriptProcessor = audioCtx.createScriptProcessor(256, 1, 1) | |
sourceNode.connect(delayNode) | |
delayNode.connect(gainNode) | |
gainNode.connect(audioCtx.destination) | |
sourceNode.connect(scriptProcessor) | |
scriptProcessor.connect(audioCtx.destination) | |
scriptProcessor.onaudioprocess = evt => { | |
const { threshold, silentSpeed, talkingSpeed, silentDebounceLimit, talkingDebounceLimit, volumeGain, delay } = config | |
if (video.paused) { | |
counter = -100 | |
if (video.playbackRate !== talkingSpeed) { | |
video.playbackRate = talkingSpeed | |
} | |
// Update status | |
if (window[scriptName + '_updateStatus']) { | |
window[scriptName + '_updateStatus']('Paused', video.playbackRate, 0) | |
} | |
return | |
} | |
const input = evt.inputBuffer.getChannelData(0) | |
let avg = 0 | |
for (let i = 0; i < input.length; i++) avg += Math.abs(input[i]) | |
avg /= input.length | |
isTalking = avg > threshold | |
const targetSpeed = isTalking ? talkingSpeed : silentSpeed | |
if (video.playbackRate !== targetSpeed) counter++ | |
else if (isTalking) counter = 0 | |
const limit = isTalking ? talkingDebounceLimit : silentDebounceLimit | |
if (counter > limit) { | |
console.log('Speed change:', targetSpeed) | |
video.playbackRate = targetSpeed | |
counter = 0 | |
} | |
gainNode.gain.value = volumeGain | |
delayNode.delayTime.value = delay | |
// Update status | |
if (window[scriptName + '_updateStatus']) { | |
const mode = isTalking ? 'Talking' : 'Silent' | |
window[scriptName + '_updateStatus'](mode, video.playbackRate, avg) | |
} | |
} | |
console.log(`[${scriptName}] Initialized on new video`) | |
} | |
function cleanup () { | |
try { | |
scriptProcessor?.disconnect() | |
sourceNode?.disconnect() | |
gainNode?.disconnect() | |
delayNode?.disconnect() | |
audioCtx?.close() | |
} catch (e) { | |
console.warn(`[${scriptName}] Cleanup error`, e) | |
} | |
} | |
const observer = new MutationObserver(() => { | |
const video = document.querySelector('video') | |
if (video && video !== lastVideo) { | |
setupVideo(video) | |
} | |
}) | |
observer.observe(document.body, { childList: true, subtree: true }) | |
// Initialize video detection | |
function initVideo () { | |
const video = document.querySelector('video') | |
if (video) { | |
setupVideo(video) | |
} | |
} | |
// Wait for lil-gui to be available, then initialize | |
function waitForGUI () { | |
if (typeof lil !== 'undefined' && lil.GUI) { | |
initGUI() | |
initVideo() | |
console.log(`[${scriptName}] Script loaded`) | |
} else { | |
setTimeout(waitForGUI, 100) | |
} | |
} | |
waitForGUI() | |
// Cleanup on page unload | |
window.addEventListener('beforeunload', () => { | |
cleanup() | |
if (gui) { | |
gui.destroy() | |
} | |
}) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment