Greasy Fork is available in English.
Plays a pleasant double-chime when Gemini finishes generating, only if the tab is not in focus.
// ==UserScript==
// @name Gemini Completion Sound
// @namespace http://tampermonkey.net/
// @version 2025-02-19
// @description Plays a pleasant double-chime when Gemini finishes generating, only if the tab is not in focus.
// @author Vincent Nahn
// @homepage https://vincentnahn.com
// @match https://gemini.google.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let isGenerating = false;
// Function to generate a pleasant, bell-like double chime
function playNotificationSound() {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Helper function to play a single note with a smooth fade-out (envelope)
function playNote(frequency, startTime, duration) {
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
// 'sine' waves are the smoothest sounding
oscillator.type = 'sine';
oscillator.frequency.value = frequency;
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
// Create a pleasant volume envelope (no harsh clicks)
gainNode.gain.setValueAtTime(0, startTime);
// Quick fade-in to 15% volume
gainNode.gain.linearRampToValueAtTime(0.15, startTime + 0.05);
// Smooth, lingering fade-out
gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
oscillator.start(startTime);
oscillator.stop(startTime + duration);
}
const now = audioCtx.currentTime;
// Play an ascending major third (C5 to E5) for a happy, "task complete" feel
playNote(523.25, now, 0.8); // First note (C5)
playNote(659.25, now + 0.15, 1.0); // Second note (E5) comes in slightly after
}
const observer = new MutationObserver(() => {
const stopButton = document.querySelector('[aria-label*="Stop"]');
if (stopButton && !isGenerating) {
isGenerating = true;
}
if (isGenerating && !stopButton) {
isGenerating = false;
if (!document.hasFocus()) {
playNotificationSound();
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();