Skip to content

Instantly share code, notes, and snippets.

@dansleboby
Last active January 27, 2026 15:53
Show Gist options
  • Select an option

  • Save dansleboby/b8dacd07ed09dfcd851f7f42f6594136 to your computer and use it in GitHub Desktop.

Select an option

Save dansleboby/b8dacd07ed09dfcd851f7f42f6594136 to your computer and use it in GitHub Desktop.
Suno.com Aligned Words Fetcher to SRT or LRC file
// ==UserScript==
// @name Suno Aligned Words Fetcher with Auth
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Fetch aligned words with auth and add a button under the image on Suno pages.
// @author Dschibait
// @match https://suno.com/song/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const file_type = "srt"; // lrc ou srt
// Helper function to get the value of a cookie by name and chooses the last item it there are duplicates
function getLastCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length > 1) {
return parts[parts.length - 1].split(';')[0];
}
}
// Helper function to fetch aligned words data with Bearer token
async function fetchAlignedWords(songId, token) {
const apiUrl = `https://studio-api.prod.suno.com/api/gen/${songId}/aligned_lyrics/v2/`;
try {
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data && data.aligned_words) {
console.log('Aligned words:', data.aligned_words);
return data.aligned_words;
} else {
console.error('No aligned words found.');
}
} catch (error) {
console.error('Error fetching aligned words:', error);
}
}
// Function to add a button under the image
function addButton(imageSrc, alignedWords) {
const imageElements = document.querySelectorAll(`img[src*="${imageSrc}"].w-full.h-full`);
console.log(imageSrc, imageElements);
imageElements.forEach(function(imageElement, k) {
console.log(k, imageElement);
if (imageElement) {
const button = document.createElement('button');
button.innerText = 'Download '+file_type;
button.style.marginTop = '10px';
button.style.zIndex = '9999';
button.style.position = 'absolute';
button.style.bottom = '0';
button.style.left = '0';
button.style.right = '0';
button.style.background = 'gray';
button.style.borderRadius = '5px';
button.style.padding = '10px 6px';
button.addEventListener('click', () => {
const srtContent = file_type === 'srt' ? convertToSRT(alignedWords) : convertToLRC(alignedWords);
const blob = new Blob([srtContent], { type: 'text/'+file_type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'aligned_words.'+file_type;
a.click();
URL.revokeObjectURL(url); // Clean up the URL object
});
imageElement.parentNode.appendChild(button);
} else {
console.error('Image not found.');
}
});
}
// Function to convert aligned words to SRT format
function convertToSRT(alignedWords) {
let srtContent = '';
alignedWords.forEach((wordObj, index) => {
const startTime = formatTime(wordObj.start_s);
const endTime = formatTime(wordObj.end_s);
srtContent += `${index + 1}\n`;
srtContent += `${startTime} --> ${endTime}\n`;
srtContent += `${wordObj.word}\n\n`;
});
return srtContent;
}
// Helper function to format time into SRT format (HH:MM:SS,MS)
function formatTime(seconds) {
const date = new Date(0);
date.setMilliseconds(seconds * 1000); // Convert seconds to milliseconds
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const secs = String(date.getUTCSeconds()).padStart(2, '0');
const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
return `${hours}:${minutes}:${secs},${milliseconds}`;
}
// Function to convert aligned words to LRC format
function convertToLRC(alignedWords) {
let lrcContent = '';
alignedWords.forEach(wordObj => {
const time = formatLrcTime(wordObj.start_s);
lrcContent += `${time}${wordObj.word}\n`;
});
return lrcContent;
}
// Helper function to format time into LRC format [mm:ss.xx]
function formatLrcTime(seconds) {
const date = new Date(0);
date.setMilliseconds(seconds * 1000); // Convert seconds to milliseconds
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const secs = String(date.getUTCSeconds()).padStart(2, '0');
const hundredths = String(Math.floor(date.getUTCMilliseconds() / 10)).padStart(2, '0'); // Convert milliseconds to hundredths of a second
return `[${minutes}:${secs}.${hundredths}]`;
}
// Main function to run the script
function main() {
const urlParts = window.location.href.split('/');
const songId = urlParts[urlParts.length - 1]; // Get song ID from URL
const imageSrcPattern = songId;
// Get the token from the cookie
const sessionToken = getLastCookie('__session');
if (!sessionToken) {
console.error('Session token not found in cookies.');
return;
}
// Fetch aligned words and add the button
fetchAlignedWords(songId, sessionToken).then((alignedWords) => {
if (alignedWords) {
addButton(imageSrcPattern, alignedWords);
}
});
}
setTimeout(function() { main(); }, 5000);
})();
@dschibait
Copy link

dschibait commented Dec 11, 2025

run into a problem, where the script tried to fetch stuff to fast from Suno... so here is a fix, works for me with word by word really good, align words has some problems, but we fetch this from Suno, so seems like they change something in that algorithm.
So maybe you have to change some entries in the .srt file.

if someone else likes, feel free to look into this :D i think it could be fixed by merging parts to a minimum XX seconds length or so ... that would remove the 3 word syllables issues i noticed.

// ==UserScript==
// @name         Suno Aligned Words Fetcher with Auth & Cache
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Fetch aligned words with auth, cache responses, and add a button under the image on Suno pages. Handles SPA URL changes globally.
// @author       Dschibait
// @match        https://suno.com/*
// @match        https://www.suno.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const file_type = "srt"; // lrc or srt
    const use_lyrics = true;
    const cache = {}; // Cache responses per songId

    // Helper function to get the value of a cookie by name
    function getCookie(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) return parts.pop().split(';').shift();
    }

    async function fetchAlignedWords(songId, token) {
        if (cache[songId]) {
            console.log(`[Cache Hit] Using cached data for songId: ${songId}`);
            return cache[songId];
        }

        const apiUrl = `https://studio-api.prod.suno.com/api/gen/${songId}/aligned_lyrics/v2/`;

        let attempts = 0;
        const maxAttempts = 10;    // bis zu 10x neu versuchen
        const waitTime = 3000;     // 3 Sekunden Pause pro Versuch

        while (attempts < maxAttempts) {
            attempts++;

            try {
                const response = await fetch(apiUrl, {
                    method: 'GET',
                    headers: {
                        'Authorization': `Bearer ${token}`,
                        'Content-Type': 'application/json'
                    }
                });

                const data = await response.json();
                console.log(`[API Response Attempt ${attempts}]`, data);

                // Erfolgreich?
                if (data && (data.aligned_words || data.aligned_lyrics)) {
                    const result = use_lyrics && file_type === "srt"
                    ? data.aligned_lyrics
                    : data.aligned_words;

                    cache[songId] = result;
                    return result;
                }

                // Nicht verfügbar / Processing läuft noch
                if (data.detail && data.detail.includes("Processing")) {
                    console.warn(`[Suno] Lyrics not ready – new try in ${waitTime/1000}s...`);
                } else {
                    console.warn(`[Suno] no aligned words/lyrics – try again ${attempts}/${maxAttempts}`);
                }

            } catch (error) {
                console.error(`[Suno] error on API-Call (try ${attempts}):`, error);
            }

            // warten und erneut versuchen
            await new Promise(res => setTimeout(res, waitTime));
        }

        console.error("[Suno] maximum tries reached – cannot load lyrics.");
        return null;
    }

    function addButton(alignedWords) {
        const imageElements = document.querySelectorAll('img.object-cover');
        imageElements.forEach((imageElement) => {
            // Remove existing button
            const existingButton = imageElement.parentNode.querySelector('.my-suno-download-button');
            if (existingButton) existingButton.remove();

            const rect = imageElement.getBoundingClientRect();
            if (rect.height > 100) {
                const button = document.createElement('button');
                button.innerText = `Download ${file_type}`;
                button.className = 'my-suno-download-button';
                button.style.marginTop = '10px';
                button.style.zIndex = '9999';
                button.style.position = 'absolute';
                button.style.bottom = '0';
                button.style.left = '0';
                button.style.right = '0';
                button.style.background = '#4CAF50';
                button.style.color = '#fff';
                button.style.border = 'none';
                button.style.borderRadius = '5px';
                button.style.padding = '10px 6px';
                button.style.cursor = 'pointer';

                button.addEventListener('click', () => {
                    const content = file_type === 'srt' ? convertToSRT(alignedWords) : convertToLRC(alignedWords);
                    const blob = new Blob([content], { type: 'text/plain' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `aligned_words.${file_type}`;
                    a.click();
                    URL.revokeObjectURL(url);
                });

                imageElement.parentNode.appendChild(button);
            }
        });
    }

    function convertToSRT(alignedWords) {
        let srtContent = '';
        alignedWords.forEach((wordObj, index) => {
            const startTime = formatTime(wordObj.start_s);
            const endTime = formatTime(wordObj.end_s);
            srtContent += `${index + 1}\n`;
            srtContent += `${startTime} --> ${endTime}\n`;
            srtContent += use_lyrics ? `${wordObj.text.replace(/\[.*?\]/g, '')}\n\n` : `${wordObj.word}\n\n`;
        });
        return srtContent;
    }

    function formatTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000);
        const hours = String(date.getUTCHours()).padStart(2, '0');
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
        return `${hours}:${minutes}:${secs},${milliseconds}`;
    }

    function convertToLRC(alignedWords) {
        let lrcContent = '';
        alignedWords.forEach(wordObj => {
            const time = formatLrcTime(wordObj.start_s);
            lrcContent += `${time}${wordObj.word}\n`;
        });
        return lrcContent;
    }

    function formatLrcTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000);
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const hundredths = String(Math.floor(date.getUTCMilliseconds() / 10)).padStart(2, '0');
        return `[${minutes}:${secs}.${hundredths}]`;
    }

    async function main() {
        const url = window.location.href;
        const match = url.match(/\/song\/([^/?#]+)/);
        const songId = match ? match[1] : null;

        if (!songId) {
            console.log('[Suno Fetcher] Not a song page, skipping.');
            return;
        }

        const sessionToken = getCookie('__session');
        if (!sessionToken) {
            console.error('Session token not found in cookies.');
            return;
        }

        console.log(`[Suno Fetcher] Processing songId: ${songId}`);

        const alignedWords = await fetchAlignedWords(songId, sessionToken);
        if (alignedWords) {
            addButton(alignedWords);
        }
    }

    // Hook SPA navigation
    (function(history) {
        const pushState = history.pushState;
        const replaceState = history.replaceState;

        function fireUrlChange() {
            const event = new Event('urlchange');
            window.dispatchEvent(event);
        }

        history.pushState = function() {
            const result = pushState.apply(this, arguments);
            fireUrlChange();
            return result;
        };

        history.replaceState = function() {
            const result = replaceState.apply(this, arguments);
            fireUrlChange();
            return result;
        };

        window.addEventListener('popstate', fireUrlChange);
    })(window.history);

    // Watch for URL changes
    window.addEventListener('urlchange', () => {
        console.log('[Suno Fetcher] URL changed:', window.location.href);
        setTimeout(main, 1500); // Delay for DOM
    });

    console.log('[Suno Fetcher] Initialized');
    setTimeout(main, 1500);
})();

@fxxu
Copy link

fxxu commented Dec 13, 2025

I don't know what happend, but it doesn't work anymore since yesterday...

@CellularDoor
Copy link

Can confirm what fxxu said. The console says the request for the aligned_lyrics/v2 is unauthorized and the script failed to get the token from cookies. Apparently this is the key. I tried to replace the __session cookie with another cokkies of that kind but no avail.

@CellularDoor
Copy link

I guess I managed to find the culprit. It turned out that Suno probably added a duplicate of a __session cookie so the script was confused about it. The function for getting the cookie with token was modified to use the last option (out of two). Worth noting that Suno can ruin it by shuffling cookies up, I suppose, but for today - it works.

// ==UserScript==
// @name         Suno Aligned Words Fetcher with Auth & Cache
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Fetch aligned words with auth, cache responses, and add a button under the image on Suno pages. Handles SPA URL changes globally.
// @author       Dschibait
// @match        https://suno.com/*
// @match        https://www.suno.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const file_type = "srt"; // lrc or srt
    const use_lyrics = true;
    const cache = {}; // Cache responses per songId

    // Helper function to get the value of a cookie by name and chooses the last item it there are duplicates
    function getLastCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length > 1) {
        return parts[parts.length - 1].split(';')[0];
    }
    }

    async function fetchAlignedWords(songId, token) {
        if (cache[songId]) {
            console.log(`[Cache Hit] Using cached data for songId: ${songId}`);
            return cache[songId];
        }

        const apiUrl = `https://studio-api.prod.suno.com/api/gen/${songId}/aligned_lyrics/v2/`;

        let attempts = 0;
        const maxAttempts = 10;    // bis zu 10x neu versuchen
        const waitTime = 3000;     // 3 Sekunden Pause pro Versuch

        while (attempts < maxAttempts) {
            attempts++;

            try {
                const response = await fetch(apiUrl, {
                    method: 'GET',
                    headers: {
                        'Authorization': `Bearer ${token}`,
                        'Content-Type': 'application/json'
                    }
                });

                const data = await response.json();
                console.log(`[API Response Attempt ${attempts}]`, data);

                // Erfolgreich?
                if (data && (data.aligned_words || data.aligned_lyrics)) {
                    const result = use_lyrics && file_type === "srt"
                    ? data.aligned_lyrics
                    : data.aligned_words;

                    cache[songId] = result;
                    return result;
                }

                // Nicht verfügbar / Processing läuft noch
                if (data.detail && data.detail.includes("Processing")) {
                    console.warn(`[Suno] Lyrics not ready – new try in ${waitTime/1000}s...`);
                } else {
                    console.warn(`[Suno] no aligned words/lyrics – try again ${attempts}/${maxAttempts}`);
                }

            } catch (error) {
                console.error(`[Suno] error on API-Call (try ${attempts}):`, error);
            }

            // warten und erneut versuchen
            await new Promise(res => setTimeout(res, waitTime));
        }

        console.error("[Suno] maximum tries reached – cannot load lyrics.");
        return null;
    }

    function addButton(alignedWords) {
        const imageElements = document.querySelectorAll('img.object-cover');
        imageElements.forEach((imageElement) => {
            // Remove existing button
            const existingButton = imageElement.parentNode.querySelector('.my-suno-download-button');
            if (existingButton) existingButton.remove();

            const rect = imageElement.getBoundingClientRect();
            if (rect.height > 100) {
                const button = document.createElement('button');
                button.innerText = `Download ${file_type}`;
                button.className = 'my-suno-download-button';
                button.style.marginTop = '10px';
                button.style.zIndex = '9999';
                button.style.position = 'absolute';
                button.style.bottom = '0';
                button.style.left = '0';
                button.style.right = '0';
                button.style.background = '#4CAF50';
                button.style.color = '#fff';
                button.style.border = 'none';
                button.style.borderRadius = '5px';
                button.style.padding = '10px 6px';
                button.style.cursor = 'pointer';

                button.addEventListener('click', () => {
                    const content = file_type === 'srt' ? convertToSRT(alignedWords) : convertToLRC(alignedWords);
                    const blob = new Blob([content], { type: 'text/plain' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `aligned_words.${file_type}`;
                    a.click();
                    URL.revokeObjectURL(url);
                });

                imageElement.parentNode.appendChild(button);
            }
        });
    }

    function convertToSRT(alignedWords) {
        let srtContent = '';
        alignedWords.forEach((wordObj, index) => {
            const startTime = formatTime(wordObj.start_s);
            const endTime = formatTime(wordObj.end_s);
            srtContent += `${index + 1}\n`;
            srtContent += `${startTime} --> ${endTime}\n`;
            srtContent += use_lyrics ? `${wordObj.text.replace(/\[.*?\]/g, '')}\n\n` : `${wordObj.word}\n\n`;
        });
        return srtContent;
    }

    function formatTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000);
        const hours = String(date.getUTCHours()).padStart(2, '0');
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
        return `${hours}:${minutes}:${secs},${milliseconds}`;
    }

    function convertToLRC(alignedWords) {
        let lrcContent = '';
        alignedWords.forEach(wordObj => {
            const time = formatLrcTime(wordObj.start_s);
            lrcContent += `${time}${wordObj.word}\n`;
        });
        return lrcContent;
    }

    function formatLrcTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000);
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const hundredths = String(Math.floor(date.getUTCMilliseconds() / 10)).padStart(2, '0');
        return `[${minutes}:${secs}.${hundredths}]`;
    }

    async function main() {
        const url = window.location.href;
        const match = url.match(/\/song\/([^/?#]+)/);
        const songId = match ? match[1] : null;

        if (!songId) {
            console.log('[Suno Fetcher] Not a song page, skipping.');
            return;
        }

        const sessionToken = getLastCookie('__session');
        if (!sessionToken) {
            console.error('Session token not found in cookies.');
            return;
        }

        console.log(`[Suno Fetcher] Processing songId: ${songId}`);

        const alignedWords = await fetchAlignedWords(songId, sessionToken);
        if (alignedWords) {
            addButton(alignedWords);
        }
    }

    // Hook SPA navigation
    (function(history) {
        const pushState = history.pushState;
        const replaceState = history.replaceState;

        function fireUrlChange() {
            const event = new Event('urlchange');
            window.dispatchEvent(event);
        }

        history.pushState = function() {
            const result = pushState.apply(this, arguments);
            fireUrlChange();
            return result;
        };

        history.replaceState = function() {
            const result = replaceState.apply(this, arguments);
            fireUrlChange();
            return result;
        };

        window.addEventListener('popstate', fireUrlChange);
    })(window.history);

    // Watch for URL changes
    window.addEventListener('urlchange', () => {
        console.log('[Suno Fetcher] URL changed:', window.location.href);
        setTimeout(main, 1500); // Delay for DOM
    });

    console.log('[Suno Fetcher] Initialized');
    setTimeout(main, 1500);
})();

@meloni0
Copy link

meloni0 commented Jan 4, 2026

Can confirm that the latest version by @CellularDoor works 👍

I struggle to interpret the extracted timestamps though 🤔
The timestamps in the srt file are way larger than the overall song length. Here an example line

1
00:00:12,925 --> 00:00:20,026

This suggests that a line of 5 words is 8s long.
Where is my misunderstanding?

@CellularDoor
Copy link

CellularDoor commented Jan 5, 2026

@meloni0 yeah, I guess the code I shared in my previous comment was designed for the line by line extraction but Suno keep failing to deliver the correct timecodes of that kind since the August. Here is the word by word version. Probably it will come in handy.

// ==UserScript==
// @name         Suno Aligned Words Fetcher with Auth
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Fetch aligned words with auth and add a button under the image on Suno pages.
// @author       Dschibait
// @match        https://suno.com/song/*
// @grant        none
// ==/UserScript==


(function() {
    'use strict';
    const file_type = "srt"; // lrc ou srt

    // Helper function to get the value of a cookie by name and chooses the last item it there are duplicates
    function getLastCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length > 1) {
        return parts[parts.length - 1].split(';')[0];
    }
    }

    // Helper function to fetch aligned words data with Bearer token
    async function fetchAlignedWords(songId, token) {
        const apiUrl = `https://studio-api.prod.suno.com/api/gen/${songId}/aligned_lyrics/v2/`;
        try {
            const response = await fetch(apiUrl, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${token}`,
                    'Content-Type': 'application/json'
                }
            });
            const data = await response.json();
            if (data && data.aligned_words) {
                console.log('Aligned words:', data.aligned_words);
                return data.aligned_words;
            } else {
                console.error('No aligned words found.');
            }
        } catch (error) {
            console.error('Error fetching aligned words:', error);
        }
    }

    // Function to add a button under the image
    function addButton(imageSrc, alignedWords) {
        const imageElements = document.querySelectorAll(`img[src*="${imageSrc}"].w-full.h-full`);
        console.log(imageSrc, imageElements);

        imageElements.forEach(function(imageElement, k) {
            console.log(k, imageElement);
            if (imageElement) {
                const button = document.createElement('button');
                button.innerText = 'Download '+file_type;
                button.style.marginTop = '10px';
                button.style.zIndex = '9999';
                button.style.position = 'absolute';
                button.style.bottom = '0';
                button.style.left = '0';
                button.style.right = '0';
                button.style.background = 'gray';
                button.style.borderRadius = '5px';
                button.style.padding = '10px 6px';

                button.addEventListener('click', () => {
                    const srtContent = file_type === 'srt' ? convertToSRT(alignedWords) : convertToLRC(alignedWords);
                    const blob = new Blob([srtContent], { type: 'text/'+file_type });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = 'aligned_words.'+file_type;
                    a.click();
                    URL.revokeObjectURL(url); // Clean up the URL object
                });

                imageElement.parentNode.appendChild(button);
            } else {
                console.error('Image not found.');
            }
        });
    }

    // Function to convert aligned words to SRT format
    function convertToSRT(alignedWords) {
        let srtContent = '';
        alignedWords.forEach((wordObj, index) => {
            const startTime = formatTime(wordObj.start_s);
            const endTime = formatTime(wordObj.end_s);
            srtContent += `${index + 1}\n`;
            srtContent += `${startTime} --> ${endTime}\n`;
            srtContent += `${wordObj.word}\n\n`;
        });
        return srtContent;
    }

    // Helper function to format time into SRT format (HH:MM:SS,MS)
    function formatTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000); // Convert seconds to milliseconds
        const hours = String(date.getUTCHours()).padStart(2, '0');
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
        return `${hours}:${minutes}:${secs},${milliseconds}`;
    }

     // Function to convert aligned words to LRC format
    function convertToLRC(alignedWords) {
        let lrcContent = '';
        alignedWords.forEach(wordObj => {
            const time = formatLrcTime(wordObj.start_s);
            lrcContent += `${time}${wordObj.word}\n`;
        });
        return lrcContent;
    }

    // Helper function to format time into LRC format [mm:ss.xx]
    function formatLrcTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000); // Convert seconds to milliseconds
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const hundredths = String(Math.floor(date.getUTCMilliseconds() / 10)).padStart(2, '0'); // Convert milliseconds to hundredths of a second
        return `[${minutes}:${secs}.${hundredths}]`;
    }


    // Main function to run the script
    function main() {
        const urlParts = window.location.href.split('/');
        const songId = urlParts[urlParts.length - 1]; // Get song ID from URL
        const imageSrcPattern = songId;

        // Get the token from the cookie
        const sessionToken = getLastCookie('__session');
        if (!sessionToken) {
            console.error('Session token not found in cookies.');
            return;
        }

        // Fetch aligned words and add the button
        fetchAlignedWords(songId, sessionToken).then((alignedWords) => {
            if (alignedWords) {
                addButton(imageSrcPattern, alignedWords);
            }
        });
    }

    setTimeout(function() { main(); }, 5000);
})();

@dschibait
Copy link

its sad, that suno likes to shuffle things up ... i use the paid version, i didnt see any solution /feature for this - i mean, it would make sense when there is a function to download these somehow in a free or paid version, but so, it seems like suno devs like to troll.

i mean, the stems are crapp, i also tried to get a .srt file by converting it with Whisper... seems like, its a bigger feature request when i see the activity on this thread here.

@sunodevs: maybe dont bringing effort into protection, you could implement that feature...

@CellularDoor
Copy link

CellularDoor commented Jan 9, 2026

It is but at the same time I can't blame them for shuffling things, because they didn't explicitly released this functionality for public usage, maybe they're using this data for their internal needs which we don't know about, while I 100% aggree that SRT feature definitely should be included to a paid plan.

I tried making SRTs using AI tools as well but the main problem is that the AI is struggling to understand the words being sung, especially the names and moreover, when it comes to different languages if you're doing localisations for ad campaings it goes even worse while you can't check fast if the text is correct, so their own SRTs are gold. So while their word by word SRTs are quite precise it is pretty feasible to rearrange the SRT according to your own line structure on your side.

@wtaochange-2025
Copy link

gemini, upload mp3 file, ask to download srt file

@KachouCX
Copy link

Wow! This extension is such a time saver, was losing my mind doing it manually. For some reason I had problems to get it to work with Chrome (developer mode on) but in Firefox it works. Thanks a lot @dschibait and @CellularDoor !

@Kinnar-V
Copy link

Suno.com thumbnail for song layout seem changes, now at the place "Animate" button is appearing. I have attached the screenshot. Any modified script is possible to have? Because of this I think it is not working for me. I am getting indication on the TamperMonkey Addon that is is being able to get access to the suno and script, by having "1" in red background superscripted icon on the Addon.

Screenshot 2026-01-27 161334

@CellularDoor
Copy link

CellularDoor commented Jan 27, 2026

@Kinnar-V try this. This is slightly modified version of @dschibait code.

// ==UserScript==
// @name         Suno Aligned Words Fetcher with Auth & Cache
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Fetch aligned words with auth, cache responses, and add a button under the image on Suno pages. Handles SPA URL changes globally.
// @author       Dschibait
// @match        https://suno.com/*
// @match        https://www.suno.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const file_type = "srt"; // lrc or srt
    const use_lyrics = true;
    const cache = {}; // Cache responses per songId

    // Helper function to get the value of a cookie by name and chooses the last item it there are duplicates
    function getLastCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length > 1) {
        return parts[parts.length - 1].split(';')[0];
    }
    }

    async function fetchAlignedWords(songId, token) {
        if (cache[songId]) {
            console.log(`[Cache Hit] Using cached data for songId: ${songId}`);
            return cache[songId];
        }

        const apiUrl = `https://studio-api.prod.suno.com/api/gen/${songId}/aligned_lyrics/v2/`;

        let attempts = 0;
        const maxAttempts = 10;    // bis zu 10x neu versuchen
        const waitTime = 3000;     // 3 Sekunden Pause pro Versuch

        while (attempts < maxAttempts) {
            attempts++;

            try {
                const response = await fetch(apiUrl, {
                    method: 'GET',
                    headers: {
                        'Authorization': `Bearer ${token}`,
                        'Content-Type': 'application/json'
                    }
                });

                const data = await response.json();
                console.log(`[API Response Attempt ${attempts}]`, data);

                // Erfolgreich?
                if (data && (data.aligned_words || data.aligned_lyrics)) {
                    const result = use_lyrics && file_type === "srt"
                    ? data.aligned_lyrics
                    : data.aligned_words;

                    cache[songId] = result;
                    return result;
                }

                // Nicht verfügbar / Processing läuft noch
                if (data.detail && data.detail.includes("Processing")) {
                    console.warn(`[Suno] Lyrics not ready – new try in ${waitTime/1000}s...`);
                } else {
                    console.warn(`[Suno] no aligned words/lyrics – try again ${attempts}/${maxAttempts}`);
                }

            } catch (error) {
                console.error(`[Suno] error on API-Call (try ${attempts}):`, error);
            }

            // warten und erneut versuchen
            await new Promise(res => setTimeout(res, waitTime));
        }

        console.error("[Suno] maximum tries reached – cannot load lyrics.");
        return null;
    }

    function addButton(alignedWords) {
        const imageElements = document.querySelectorAll('img.object-cover');
        imageElements.forEach((imageElement) => {
            // Remove existing button
            const existingButton = imageElement.parentNode.querySelector('.my-suno-download-button');
            if (existingButton) existingButton.remove();

            const rect = imageElement.getBoundingClientRect();
            if (rect.height > 100) {
                const button = document.createElement('button');
                button.innerText = `Download ${file_type}`;
                button.className = 'my-suno-download-button';
                button.style.marginTop = '10px';
                button.style.zIndex = '9999';
                button.style.position = 'absolute';
                button.style.bottom = '0';
                button.style.left = '0';
                button.style.right = '0';
                button.style.background = '#4CAF50';
                button.style.color = '#fff';
                button.style.border = 'none';
                button.style.borderRadius = '5px';
                button.style.padding = '10px 6px';
                button.style.cursor = 'pointer';

                button.addEventListener('click', () => {
                    const content = file_type === 'srt' ? convertToSRT(alignedWords) : convertToLRC(alignedWords);
                    const blob = new Blob([content], { type: 'text/plain' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `aligned_words.${file_type}`;
                    a.click();
                    URL.revokeObjectURL(url);
                });

                imageElement.parentNode.appendChild(button);
            }
        });
    }

    function convertToSRT(alignedWords) {
        let srtContent = '';
        alignedWords.forEach((wordObj, index) => {
            const startTime = formatTime(wordObj.start_s);
            const endTime = formatTime(wordObj.end_s);
            srtContent += `${index + 1}\n`;
            srtContent += `${startTime} --> ${endTime}\n`;
            srtContent += use_lyrics ? `${wordObj.text.replace(/\[.*?\]/g, '')}\n\n` : `${wordObj.word}\n\n`;
        });
        return srtContent;
    }

    function formatTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000);
        const hours = String(date.getUTCHours()).padStart(2, '0');
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
        return `${hours}:${minutes}:${secs},${milliseconds}`;
    }

    function convertToLRC(alignedWords) {
        let lrcContent = '';
        alignedWords.forEach(wordObj => {
            const time = formatLrcTime(wordObj.start_s);
            lrcContent += `${time}${wordObj.word}\n`;
        });
        return lrcContent;
    }

    function formatLrcTime(seconds) {
        const date = new Date(0);
        date.setMilliseconds(seconds * 1000);
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const secs = String(date.getUTCSeconds()).padStart(2, '0');
        const hundredths = String(Math.floor(date.getUTCMilliseconds() / 10)).padStart(2, '0');
        return `[${minutes}:${secs}.${hundredths}]`;
    }

    async function main() {
        const url = window.location.href;
        const match = url.match(/\/song\/([^/?#]+)/);
        const songId = match ? match[1] : null;

        if (!songId) {
            console.log('[Suno Fetcher] Not a song page, skipping.');
            return;
        }

        const sessionToken = getLastCookie('__session');
        if (!sessionToken) {
            console.error('Session token not found in cookies.');
            return;
        }

        console.log(`[Suno Fetcher] Processing songId: ${songId}`);

        const alignedWords = await fetchAlignedWords(songId, sessionToken);
        if (alignedWords) {
            addButton(alignedWords);
        }
    }

    // Hook SPA navigation
    (function(history) {
        const pushState = history.pushState;
        const replaceState = history.replaceState;

        function fireUrlChange() {
            const event = new Event('urlchange');
            window.dispatchEvent(event);
        }

        history.pushState = function() {
            const result = pushState.apply(this, arguments);
            fireUrlChange();
            return result;
        };

        history.replaceState = function() {
            const result = replaceState.apply(this, arguments);
            fireUrlChange();
            return result;
        };

        window.addEventListener('popstate', fireUrlChange);
    })(window.history);

    // Watch for URL changes
    window.addEventListener('urlchange', () => {
        console.log('[Suno Fetcher] URL changed:', window.location.href);
        setTimeout(main, 1500); // Delay for DOM
    });

    console.log('[Suno Fetcher] Initialized');
    setTimeout(main, 1500);
})();

@Kinnar-V
Copy link

Kinnar-V commented Jan 27, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment