There's another anon adding various QoL features like (You) notification and nested quote posts here, you should check it out. Here's the thread he's operating in if you want to make requests as I'll probably not add anything new here.

Don't copy <style>, </style>, <script>, and </script>, they're just there to look pretty.

This script is meant to:
>Highlight (You)r posts and posts that reply to (You)
>Append (You) to your name and posts you reply to for easier searching
>Unspoiler image thumbnails and highlight spoilered images (thanks to this Anon for the improvement)
>Add a button next to Refresh to allow you to sort posts by ID (credits to this Anon's chatgpt'd script, I modified it to make the process reversible)
>Give an optional way to manually add IDs as (You)
>Pin the navigation/status bar at the bottom and add some more buttons to it (credits to this Anon for the idea)
>Append youtube titles to their links
>Beeps when someone quotes (You)
>Hide "[Unhide post /vyt/_____]" and "[Manage Board] [Moderate Board] [Moderate Thread]"

Steps:

  1. Click the gear icon, which should show this
  2. Click on CSS and copy the CSS section below (don't copy <style> and </style>), then paste it into the CSS field and press Save
  3. Do the same for JS (remember to not copy <script> and </script> and to press Save)
  4. Refresh the page and check CSS and JS in your Settings again to doublecheck, and then you should be good.

CSS

<style>
/* Gives posts that reply to (You) a solid border */
div.innerPost:has(a.you) {
    border: 2px solid #ff0000 !important;
    background-color: rgba(255, 0, 0, 0.05) !important;
}

/* Gives (You)r posts a dashed border (change border to border-left for 4chanX style) */
div.innerPost:has(a.youName) {
    border: 2px dashed #ff8080 !important;
    background-color: rgba(255, 102, 102, 0.05) !important;
}

/* Makes Posts/UIDS/Files centered and pinned to the bottom */
.threadBottom {
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: fixed;
    bottom: 0;
    z-index: 1;
    width: 100%;
    left: 0;
    background: var(--background-color);
    padding: 5px 0;
}

/* Makes [Index] -> [Refresh] centered*/
.innerUtility {
    margin-top: 0;
    font-size: 100%;
}

/* Hover */
.quoteTooltip {
    background-color: var(--background-color);
}

/* Hides "[Unhide post /vyt/_____]", remove this section if you want to keep that */
.unhideButton.glowOnHover {
  display: none !important;
}

/* Removes excess & unselectable (You) on your name */
a.linkName.noEmailName.youName::after {
  display: none;
}

/* Removes excess & unselectable (You) on quotes */
a.quoteLink.you::after {
  display: none;
}

/* Removes "[Manage Board] [Moderate Board] [Moderate Thread]" */
#boardContentLinks {
  display: none !important;
}
</style>

JS

<script>
const manualIds = ['aaaaaa', 'bbbbbb']; // Modify 'aaaaaa' and 'bbbbbb' of your choice to mark them as (You)
const enableQuoteBeep = false; // Set to true to enable beep sound when someone quotes you

const enableUnspoiler = true; // Set to false to disable unspoiling image thumbnails
const enableAnonymousYou = true; // Set to false to disable Anonymous (You) names
const enableQuotelinkYou = true; // Set to false to disable (You) in quote links
const enableBacklinkYou = true; // Set to false to disable (You) in backlinks | Buggy atm, not sure how to fix
const enableYouTubeTitle = true; // Set to false to disable YouTube title fetching

//For the newly pinned navbar from the CSS section
const enableBottomButton = true;
const enableRefreshButton = true;
const enableRemoveIndexButton = false;
const enableRemoveCatalogButton = false;
const enableRemoveArchiveButton = false;

let originalPostCells = [];
let isSorted = false;
let prevLastReplyId = 0;
const titleCache = new Map();

document.addEventListener('DOMContentLoaded', () => {
    addSortButton(); // Adds JUDGE button next to Refresh
    addBottomButton(); // Adds [Bottom] in the bottom bar
    addRefreshButton(); // Adds [Refresh] in the bottom bar
    removeIndexButton(); // Remove [Index] button in the bottom bar
    removeCatalogButton(); // Remove [Catalog] button in the bottom bar
    removeArchiveButton(); // Remove [Archive] button in the bottom bar
    processPosts();
    initQuoteBeepObserver();

    const observer = new MutationObserver((mutations) => {
        let needsProcessing = false;
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1) {
                        if (node.matches('.quoteTooltip')) {
                            processQuoteTooltip(node, myPostIds);
                        } else if (
                            node.matches('.postCell') ||
                            node.matches('.innerPost') ||
                            node.querySelector('.postCell, .innerPost')
                        ) {
                            needsProcessing = true;
                        }
                    }
                });
            }
        });
        if (needsProcessing) {
            processPosts();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
});

let myPostIds = [];

async function processPosts() {
    const manualPostIds = applyManualYouNames(manualIds);

    const myPosts = document.querySelectorAll('.postCell .postInfo .linkName.youName');
    myPostIds = Array.from(myPosts).map(post => post.closest('.postCell')?.id).filter(Boolean);

    const allMyPostIds = [...new Set([...manualPostIds, ...myPostIds])];

    document.querySelectorAll('.postCell, .innerPost').forEach(element => {
        processElement(element, allMyPostIds);
    });
}

function processQuoteTooltip(tooltip, postIds) {
    const quotelinks = tooltip.querySelectorAll('.divMessage a.quoteLink');
    quotelinks.forEach(link => {
        const match = link.getAttribute('href')?.match(/#q?(\d+)/);
        if (match) {
            const linkPostId = match[1];
            if (postIds.includes(linkPostId)) {
                if (!link.classList.contains('you')) {
                    link.classList.add('you');
                    link.textContent = `>>${linkPostId} (You)`;
                }
            }
        }
    });
}

async function processElement(element, myPostIds) {
    // Unspoiler image thumbnails
    if (enableUnspoiler) {
        //var arr = document.querySelectorAll("img[src='/spoiler.png']");
        var arr = document.querySelectorAll("img[src='/vyt/custom.spoiler']");
        arr.forEach(element => {
            const parentLink = element.parentElement;
            if (parentLink && parentLink.href && parentLink.href.includes('/.media/')) {
                const filename = parentLink.href.split('/.media/')[1].split('.')[0];
                element.src = `/.media/t_${filename}`;
                element.style.border = "2px solid #ff66ff";
                element.style.boxShadow = "0 0 10px 3px #ff66ff";
            }
        });
    }

    // Append (You)s for ctrl+f
    // To your name
    if (enableAnonymousYou) {
        const linkName = element.querySelector('.postInfo .linkName.youName');
        if (linkName) {
            linkName.textContent = 'Anonymous (You)';
        }
    }

    // To quote links
    if (enableQuotelinkYou) {
        const quotelinks = element.querySelectorAll('a.quoteLink');
        quotelinks.forEach(link => {
            const match = link.getAttribute('href')?.match(/#q?(\d+)/);
            if (match) {
                const linkPostId = match[1];
                if (myPostIds.includes(linkPostId)) {
                    if (!link.classList.contains('you')) {
                        link.classList.add('you');
                    }
                    link.textContent = `>>${linkPostId} (You)`;
                }
            }
        });
    }

    // To back links
    if (enableBacklinkYou) {
        const backlinks = element.querySelectorAll('.panelBacklinks a');
        backlinks.forEach(link => {
            const match = link.getAttribute('href')?.match(/#q?(\d+)/);
            if (match) {
                const linkPostId = match[1];
                if (myPostIds.includes(linkPostId)) {
                    link.textContent = `>>${linkPostId} (You)`;
                }
            }
        });
    }

    //Process YouTube links
    if (enableYouTubeTitle) {
        await processYouTubeLinks(element);
    }
}

function sortPostCells() {
    const parentContainer = document.querySelector('.postCell').parentElement;

    if (!isSorted) {
        originalPostCells = Array.from(document.querySelectorAll('.postCell'));

        const sortedCells = [...originalPostCells].sort((a, b) => {
            const aLabelId = a.querySelector('.labelId').textContent.trim();
            const bLabelId = b.querySelector('.labelId').textContent.trim();
            return aLabelId.localeCompare(bLabelId);
        });

        sortedCells.forEach(cell => parentContainer.appendChild(cell));
        isSorted = true;
        sortButton.value = 'Revert Judgement';
    } else {
        originalPostCells.forEach(cell => parentContainer.appendChild(cell));
        isSorted = false;
        sortButton.value = 'JUDGE';
    }
}

function addSortButton() {
    const refreshButton = document.getElementById('refreshButton');
    const sortButton = document.createElement('input');
    sortButton.type = 'button';
    sortButton.value = 'JUDGE';
    sortButton.id = 'sortButton';

    sortButton.addEventListener('click', sortPostCells);

    refreshButton.insertAdjacentElement('afterend', sortButton);
}

function addBottomButton() {
    if (!enableBottomButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    const topButton = navButtonContainer?.querySelector('a.navButton[href="#"]');
    if (navButtonContainer && topButton) {
        const space = document.createTextNode(' ');
        const bottomButton = document.createElement('a');
        bottomButton.href = '#footer';
        bottomButton.textContent = 'Bottom';
        bottomButton.classList.add('navButton', 'bottomButton');

        topButton.insertAdjacentElement('afterend', bottomButton);
        topButton.insertAdjacentText('afterend', ' ');
    }
}

function addRefreshButton() {
    if (!enableRefreshButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (navButtonContainer) {
        const refreshButton = document.createElement('a');
        refreshButton.href = 'javascript:void(0);';
        refreshButton.textContent = 'Refresh';
        refreshButton.classList.add('navButton', 'refreshButton');

        refreshButton.onclick = function() {
            thread.refreshPosts(true);
            thread.lastRefresh = 0;
        };

        navButtonContainer.appendChild(document.createTextNode(' '));
        navButtonContainer.appendChild(refreshButton);
    }
}

function removeIndexButton() {
    if (!enableRemoveIndexButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (!navButtonContainer) return;

    const indexButton = Array.from(navButtonContainer.querySelectorAll('a'))
        .find(a => a.textContent.trim() === 'Index' && a.getAttribute('href') === '../');

    if (indexButton) indexButton.remove();
}

function removeCatalogButton() {
    if (!enableRemoveCatalogButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (!navButtonContainer) return;

    const catalogButton = Array.from(navButtonContainer.querySelectorAll('a'))
        .find(a => a.textContent.trim() === 'Catalog' && a.getAttribute('href') === '../catalog.html');

    if (catalogButton) catalogButton.remove();
}

function removeArchiveButton() {
    if (!enableRemoveArchiveButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (!navButtonContainer) return;

    const archiveButton = navButtonContainer.querySelector('a.archiveLinkThread');
    if (archiveButton) archiveButton.remove();
}

const style = document.createElement('style');
style.textContent = `
  .youtube-preview {
    position: absolute;
    display: none;
    z-index: 1000;
    border: 1px solid #ccc;
    background: #fff;
    padding: 2px;
  }
`;
document.head.appendChild(style);

async function fetchYouTubeTitle(videoId) {
  if (titleCache.has(videoId)) {
    return titleCache.get(videoId);
  }

  try {
    const response = await fetch(
      `https://www.youtube.com/oembed?url=https%3A//www.youtube.com/watch%3Fv%3D${videoId}&format=json`
    );
    const data = await response.json();
    if (data.title) {
      titleCache.set(videoId, data.title);
      return data.title;
    }
    return null;
  } catch (error) {
    console.error('Error fetching YouTube title:', error);
    return null;
  }
}

async function processYouTubeLinks(element) {
    const messageDiv = element.querySelector('.divMessage');
    if (!messageDiv) return;

    const links = messageDiv.querySelectorAll('a:not(.quoteLink)');
    for (const link of links) {
      const href = link.getAttribute('href');
      if (!href) continue;

      const youtubeRegex = /(?:youtube\.com\/(?:.*[?&]v=|(?:v|embed|shorts|live)\/)|youtu\.be\/)([\w\-]{11})/;
      const match = href.match(youtubeRegex);
      if (match) {
        const videoId = match[1];
        if (!link.classList.contains('youtube-processed')) {
          // Append title
          const title = await fetchYouTubeTitle(videoId);
          if (title) {
            link.textContent = `${href} | ${title}`;
            link.classList.add('youtube-processed');
          }
        }
      }
    }
}  

function applyManualYouNames(manualIds) {
    const manualPostIds = [];

    document.querySelectorAll('.postCell, .innerPost').forEach(container => {
        const idSpan = container.querySelector('.spanId .labelId');
        const idText = idSpan?.textContent?.trim();

        if (idText && manualIds.includes(idText)) {
            const linkName = container.querySelector('.postInfo .linkName');
            if (linkName && !linkName.classList.contains('youName')) {
                linkName.classList.add('youName');
                linkName.textContent = 'Anonymous (You)';
            }

            const postId =
                container.closest('.postCell')?.id ||
                container.querySelector('.linkQuote, .linkSelf')?.getAttribute('href')?.match(/#q?(\d+)/)?.[1];

            if (postId) {
                manualPostIds.push(postId);
            }
        }
    });

    document.querySelectorAll('a.quoteLink').forEach(link => {
        const href = link.getAttribute('href');
        const match = href?.match(/#q?(\d+)/);
        if (match) {
            const quotedId = match[1];
            if (manualPostIds.includes(quotedId)) {
                if (!link.classList.contains('you')) {
                    link.classList.add('you');
                    if (!link.textContent.includes('(You)')) {
                        link.textContent += ' (You)';
                    }
                }
            }
        }
    });

    return manualPostIds;
}

function initQuoteBeepObserver() {
    if (!enableQuoteBeep) return;

    const beep = new Audio('data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA');

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1 && node.querySelector('a.quoteLink.you')) {
                    console.log('[YouQuoteBeep] You were quoted.');
                    playBeep();
                }
            });
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });

    function playBeep() {
        if (beep.paused) {
            beep.play().catch(e => console.warn("[YouQuoteBeep] Beep failed:", e));
        } else {
            beep.addEventListener('ended', () => beep.play(), { once: true });
        }
    }
}
</script>

If you're annoyed that some of your posts with the same ID don't work, there's a bandaid solution included. Modify the ids in const manualIds = ['aaaaaa', 'bbbbbb']; to your choice to whatever IDs you want to identify as (You).

Python script to reverse soundposts back into videos with audio

Save the code below as rev-sp.py or whatever you want to call it, place it in the same folder as your soundposts, and then run rev-sp [soundpost file] (you can put -ks before the filename to preserve the audio for whatever reason). Note that you need ffmpeg/yt-dlp to either be in your PATH environmental variable or to have them in the same folder. If you want to call it remotely you'll need to write python rev-sp.py [soundpost file] instead.

import sys
import os
import re
import subprocess
import urllib.request
import urllib.parse
import platform

if platform.system() == "Windows":
    os.system("")

# ANSI escape codes for colors
RED = "\033[91m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
BLUE = "\033[94m"
RESET = "\033[0m"

def print_usage():
    print(f"{YELLOW}Usage: rev-sp [-ks] <soundpost_file>{RESET}")
    print(f"{YELLOW}  -ks: Keep the downloaded audio file separately{RESET}")
    sys.exit(1)

def extract_audio_url(filename):
    sound_match = re.search(r'\[sound=(.*?)\]', filename)
    if not sound_match:
        print(f"{RED}[ERROR] No [sound=...] tag found.{RESET}")
        sys.exit(1)

    raw_url = urllib.parse.unquote(sound_match.group(1))
    match = re.match(r'(?:https://)?(files\.catbox\.moe/\S+)', raw_url)
    if not match:
        print(f"{RED}[ERROR] No valid catbox.moe URL found in [sound=...]{RESET}")
        sys.exit(1)

    final_url = f"https://{match.group(1)}"
    print(f"{BLUE}[VERBOSE] Extracted URL: {final_url}{RESET}")
    return final_url

def get_audio_filename(url):
    # Extract the filename from the URL (e.g., lc08pf.mp3 from files.catbox.moe%2Flc08pf.mp3)
    parsed_url = urllib.parse.urlparse(url)
    path = urllib.parse.unquote(os.path.basename(parsed_url.path))
    return path

def extract_audio_from_video(video_path, audio_path):
    print(f"{GREEN}[INFO] Extracting audio from video: {video_path}{RESET}")
    cmd = [
        "ffmpeg", "-y", "-i", video_path, "-vn", "-acodec", "mp3", audio_path
    ]
    print(f"{BLUE}[VERBOSE] Running ffmpeg command: {' '.join(cmd)}{RESET}")
    try:
        subprocess.run(cmd, check=True)
    except subprocess.CalledProcessError as e:
        print(f"{RED}[ERROR] FFmpeg failed to extract audio: {e}{RESET}")
        sys.exit(1)

def download_audio(url, dest_path):
    print(f"{GREEN}[INFO] Downloading audio with yt-dlp from: {url}{RESET}")
    try:
        subprocess.run([
            "yt-dlp", "-o", dest_path, url
        ], check=True)
    except subprocess.CalledProcessError:
        print(f"{RED}[ERROR] yt-dlp failed to download the audio.{RESET}")
        sys.exit(1)

def merge_media(soundpost, audio_file, output_file, ext):
    is_gif = ext.lower() == "gif"
    is_image = ext.lower() in ["jpg", "jpeg", "png"]
    is_video = ext.lower() in ["webm", "mp4"]

    if is_gif:
        cmd = [
            "ffmpeg", "-y", "-ignore_loop", "0", "-i", soundpost, "-i", audio_file,
            "-shortest", "-c:v", "vp9", "-pix_fmt", "yuva420p", "-c:a", "libopus",
            "-vf", "format=yuva420p", output_file
        ]
    # ffmpeg -y -ignore_loop 0 -i soundpost -i audio_file -shortest -c:v vp9 -pix_fmt yuva420p -c:a libopus -vf format=yuva420p output_file
    elif is_image:
        if ext.lower() == "png":
            cmd = [
                "ffmpeg", "-y", "-loop", "1", "-i", soundpost, "-i", audio_file,
                "-shortest", "-c:v", "vp9", "-pix_fmt", "yuva420p", "-c:a", "libopus",
                "-vf", "format=yuva420p", output_file
            ]
    # ffmpeg -y -loop 1 -i soundpost -i audio_file -shortest -c:v vp9 -pix_fmt yuva420p -c:a libopus -vf format=yuva420p output_file
        else:
            cmd = [
                "ffmpeg", "-y", "-loop", "1", "-i", soundpost, "-i", audio_file,
                "-shortest", "-c:v", "vp9", "-c:a", "libopus", output_file
            ]
    # ffmpeg -y -loop 1 -i soundpost -i audio_file -shortest -c:v vp9 -c:a libopus output_file
    elif is_video:
        cmd = [
            "ffmpeg", "-y", "-i", soundpost, "-i", audio_file,
            "-c:v", "copy", "-c:a", "libopus", output_file
        ]
    # ffmpeg -y -i soundpost -i audio_file -c:v copy -c:a libopus output_file
    else:
        print(f"{RED}[ERROR] Unsupported file extension: {ext}{RESET}")
        sys.exit(1)

    print(f"{BLUE}[VERBOSE] Running ffmpeg command: {' '.join(cmd)}{RESET}")
    print(f"{GREEN}[INFO] Merging media...{RESET}")
    try:
        subprocess.run(cmd, check=True)
    except subprocess.CalledProcessError as e:
        print(f"{RED}[ERROR] FFmpeg failed: {e}{RESET}")
        sys.exit(1)

def main():
    keep_sound = False
    args = sys.argv[1:]

    if "-ks" in args:
        keep_sound = True
        args.remove("-ks")

    if len(args) != 1:
        print_usage()

    soundpost = args[0]
    if not os.path.isfile(soundpost):
        print(f"{RED}[ERROR] File not found: {soundpost}{RESET}")
        sys.exit(1)

    filename = os.path.basename(soundpost)
    name, ext = os.path.splitext(filename)
    ext = ext[1:]  # Remove the leading dot

    audio_url = extract_audio_url(filename)
    audio_filename = get_audio_filename(audio_url)
    audio_ext = audio_filename.split('.')[-1].lower()
    audio_path = os.path.join(os.path.dirname(os.path.abspath(soundpost)), audio_filename)

    download_audio(audio_url, audio_path)

    # If the downloaded file is a video (mp4 or webm), extract audio to a temporary mp3
    temp_audio_path = audio_path
    if audio_ext in ["mp4", "webm"]:
        temp_audio_path = os.path.splitext(audio_path)[0] + ".mp3"
        extract_audio_from_video(audio_path, temp_audio_path)

    stripped_name = re.sub(r'\[sound=.*?\]', '', name)
    if ext.lower() in ["png", "gif"]:
        output_ext = "webm"
    else:
        output_ext = "mp4" if ext.lower() in ["jpg", "jpeg"] else ext
    output_file = f"{stripped_name}.{output_ext}"

    merge_media(soundpost, temp_audio_path, output_file, ext)

    # Cleanup temporary files unless -ks is specified
    if not keep_sound:
        try:
            if audio_path != temp_audio_path:  # Remove downloaded video if audio was extracted
                os.remove(audio_path)
            os.remove(temp_audio_path)
            print(f"{GREEN}[INFO] Cleaned up temporary audio file: {temp_audio_path}{RESET}")
        except OSError as e:
            print(f"{RED}[ERROR] Failed to clean up temporary file: {e}{RESET}")

    print(f"{GREEN}[INFO] Output saved as: {output_file}{RESET}")

if __name__ == "__main__":
    main()

Python script to merge a media file (img/gif/video) with an audio file

Same as the script above, save it as merge-sp.py or whatever you want, then run merge-sp [media file] [audio file].

import sys
import subprocess
import os
import platform

if platform.system() == "Windows":
    os.system("")

# ANSI escape codes for colors
RED = "\033[91m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
BLUE = "\033[94m"
RESET = "\033[0m"

def get_audio_length(audio_path):
    try:
        result = subprocess.run([
            "ffprobe", "-v", "error", "-show_entries", "format=duration", 
            "-of", "default=noprint_wrappers=1:nokey=1", audio_path
        ], capture_output=True, text=True, check=True)
        return float(result.stdout.strip())
    except subprocess.CalledProcessError as e:
        print(f"{RED}Error getting audio length: {e}{RESET}")
        sys.exit(1)

def merge_media(media_file, audio_file, output_file, ext, audio_length):
    is_gif = ext.lower() == "gif"
    is_image = ext.lower() in ["jpg", "jpeg", "png"]
    is_video = ext.lower() in ["webm", "mp4"]

    if is_gif:
        cmd = [
            "ffmpeg", "-y", "-ignore_loop", "0", "-i", media_file, "-i", audio_file,
            "-shortest", "-c:v", "vp9", "-pix_fmt", "yuva420p", "-c:a", "libopus",
            "-vf", "format=yuva420p", output_file
        ]
    elif is_image:
        if ext.lower() == "png":
            cmd = [
                "ffmpeg", "-y", "-loop", "1", "-i", media_file, "-i", audio_file,
                "-shortest", "-c:v", "vp9", "-pix_fmt", "yuva420p", "-c:a", "libopus",
                "-vf", "format=yuva420p", "-t", str(audio_length), output_file
            ]
        else:
            cmd = [
                "ffmpeg", "-y", "-loop", "1", "-i", media_file, "-i", audio_file,
                "-shortest", "-c:v", "vp9", "-c:a", "libopus", "-t", str(audio_length), output_file
            ]
    elif is_video:
        cmd = [
            "ffmpeg", "-y", "-i", media_file, "-i", audio_file,
            "-c:v", "copy", "-c:a", "libopus", "-shortest", output_file
        ]
    else:
        print(f"{RED}[ERROR] Unsupported file extension: {ext}{RESET}")
        sys.exit(1)

    print(f"{BLUE}[VERBOSE] Running ffmpeg command: {' '.join(cmd)}{RESET}")
    print(f"{GREEN}[INFO] Merging media...{RESET}")
    try:
        subprocess.run(cmd, check=True)
    except subprocess.CalledProcessError as e:
        print(f"{RED}[ERROR] FFmpeg failed: {e}{RESET}")
        sys.exit(1)

def main():
    try:
        if len(sys.argv) != 3:
            print(f"{YELLOW}Usage: python rev-sp.py [media_file] [audio_file]{RESET}")
            sys.exit(1)

        media_file = sys.argv[1]
        audio_file = sys.argv[2]

        print(f"{GREEN}Checking media file: {media_file}{RESET}")
        if not os.path.isfile(media_file):
            print(f"{RED}Media file not found: {media_file}{RESET}")
            sys.exit(1)

        print(f"{GREEN}Checking audio file: {audio_file}{RESET}")
        if not os.path.isfile(audio_file):
            print(f"{RED}Audio file not found: {audio_file}{RESET}")
            sys.exit(1)

        if os.path.getsize(media_file) == 0:
            print(f"{RED}Media file is empty: {media_file}{RESET}")
            sys.exit(1)
        if os.path.getsize(audio_file) == 0:
            print(f"{RED}Audio file is empty: {audio_file}{RESET}")
            sys.exit(1)

        audio_length = get_audio_length(audio_file)
        print(f"{BLUE}Audio length: {audio_length}{RESET}")

        filename = os.path.basename(media_file)
        name, ext = os.path.splitext(filename)
        ext = ext[1:]  # Remove the leading dot

        output_ext = "webm" if ext.lower() in ["png", "gif", "webm"] else "mp4"
        output_file = f"{name}_merged.{output_ext}"

        merge_media(media_file, audio_file, output_file, ext, audio_length)
        print(f"{GREEN}Output saved as: {output_file}{RESET}")
    except subprocess.CalledProcessError as e:
        print(f"{RED}Error running ffmpeg: {e}{RESET}")
        sys.exit(1)
    except Exception as e:
        print(f"{RED}Unexpected error: {e}{RESET}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Appending a thumbnail to mp3s

Normal method (turns it into a proper video) (filesize scales faster with increasing image dimensions and audio length)

ffmpeg -i thumbnail.png -i audio.mp3 output.webm

Alternative method (turns it into an audio file with a thumbnail) (less heavy for big images/long audio but won't show the thumbnail if clicked on in 8moe, though it does work on hover if you use 8chanSS)

ffmpeg -i thumbnail.png -i audio.mp3 -c:a copy -c:v copy -map 1:a -map 0:v -metadata:s:v title="Album cover" -metadata:s:v comment="Cover (front)" output.mp3
Edit
Pub: 16 Apr 2025 11:36 UTC
Edit: 11 May 2025 05:57 UTC
Views: 768