Skip to content

Instantly share code, notes, and snippets.

@qgustavor
Created May 28, 2025 16:57
Show Gist options
  • Save qgustavor/b3e37e8978628c53b616670305e23036 to your computer and use it in GitHub Desktop.
Save qgustavor/b3e37e8978628c53b616670305e23036 to your computer and use it in GitHub Desktop.
JumpCutter for tldv.io: Speeds up silent parts in tldv.io meetings
// ==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