Gradio custom HTML component: Audio Gallery
Author: gradio
Category: display
Tags: display, audio, gallery, media
Description: Grid of audio players with waveform visualization and selection

Head:

HTML template:
<div class="audio-gallery-container">
    ${label ? `<label class="container-label">${label}</label>` : ''}
    <div class="audio-gallery-grid" style="grid-template-columns: repeat(${columns}, 1fr);">
        ${audio_urls.map((url, i) => `
            <div class="audio-item" data-index="${i}">
                <div class="audio-label">${labels && labels[i] ? labels[i] : 'Audio ' + (i + 1)}</div>
                <canvas class="waveform-canvas" data-url="${url}" width="300" height="80"></canvas>
                <audio src="${url}" preload="metadata" ${value === url ? 'data-selected="true"' : ''}></audio>
                <div class="audio-controls">
                    <button class="play-btn">▶</button>
                    <div class="time-display">0:00</div>
                </div>
            </div>
        `).join('')}
    </div>
</div>

CSS:
.audio-gallery-container { padding: 8px; }
.container-label { display: block; margin-bottom: 12px; font-weight: 600; }
.audio-gallery-grid { display: grid; gap: 12px; }
.audio-item { border: 2px solid #e5e7eb; border-radius: 10px; padding: 12px; cursor: pointer; transition: all 0.2s; }
.audio-item:hover { border-color: #f97316; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.audio-item[data-selected="true"] { border-color: #f97316; background-color: #fff7ed; }
.audio-label { margin-bottom: 8px; text-align: center; font-size: 13px; font-weight: 500; }
.waveform-canvas { width: 100%; height: 80px; background: #f9fafb; border-radius: 6px; margin-bottom: 8px; }
.audio-controls { display: flex; align-items: center; gap: 8px; }
.play-btn { width: 32px; height: 32px; border-radius: 50%; border: none; background: #f97316; color: white; cursor: pointer; font-size: 14px; }
.play-btn:hover { opacity: 0.8; }
.time-display { font-size: 12px; color: #6b7280; }

JavaScript (on load):
const audioItems = element.querySelectorAll('.audio-item');
audioItems.forEach((item, index) => {
    const canvas = item.querySelector('.waveform-canvas');
    const audio = item.querySelector('audio');
    const playBtn = item.querySelector('.play-btn');
    const timeDisplay = item.querySelector('.time-display');
    const ctx = canvas.getContext('2d');
    drawWaveform(canvas, ctx);
    item.addEventListener('click', (e) => {
        if (e.target === playBtn) return;
        audioItems.forEach(i => i.removeAttribute('data-selected'));
        item.setAttribute('data-selected', 'true');
        props.value = audio.src;
    });
    playBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        if (audio.paused) {
            element.querySelectorAll('audio').forEach(a => a.pause());
            element.querySelectorAll('.play-btn').forEach(b => b.textContent = '▶');
            audio.play();
            playBtn.textContent = '⏸';
        } else {
            audio.pause();
            playBtn.textContent = '▶';
        }
    });
    audio.addEventListener('timeupdate', () => {
        const currentTime = Math.floor(audio.currentTime);
        const minutes = Math.floor(currentTime / 60);
        const seconds = currentTime % 60;
        timeDisplay.textContent = minutes + ':' + seconds.toString().padStart(2, '0');
        const progress = audio.currentTime / audio.duration;
        drawWaveform(canvas, ctx, progress);
    });
    audio.addEventListener('ended', () => {
        playBtn.textContent = '▶';
        drawWaveform(canvas, ctx, 0);
    });
});
function drawWaveform(canvas, ctx, progress) {
    progress = progress || 0;
    const width = canvas.width;
    const height = canvas.height;
    const bars = 50;
    const barWidth = width / bars;
    ctx.clearRect(0, 0, width, height);
    for (let i = 0; i < bars; i++) {
        const barHeight = (Math.sin(i * 0.5) * 0.3 + 0.5) * height * 0.8;
        const x = i * barWidth;
        const y = (height - barHeight) / 2;
        ctx.fillStyle = i / bars < progress ? '#f97316' : '#d1d5db';
        ctx.fillRect(x, y, barWidth - 2, barHeight);
    }
}

Python code:
class AudioGallery(gr.HTML):
    def __init__(self, audio_urls, *, value=None, labels=None,
                 columns=3, label=None, **kwargs):
        html_template = """
        <div class="audio-gallery-container">
            ${label ? `<label>${label}</label>` : ''}
            <div class="audio-gallery-grid"
                 style="grid-template-columns: repeat(${columns}, 1fr);">
                ${audio_urls.map((url, i) => `
                    <div class="audio-item" data-index="${i}">
                        <div class="audio-label">
                            ${labels && labels[i] ? labels[i] : 'Audio ' + (i+1)}
                        </div>
                        <canvas class="waveform-canvas" width="300" height="80"></canvas>
                        <audio src="${url}" preload="metadata"></audio>
                        <div class="audio-controls">
                            <button class="play-btn">▶</button>
                            <div class="time-display">0:00</div>
                        </div>
                    </div>
                `).join('')}
            </div>
        </div>
        """
        super().__init__(
            value=value, audio_urls=audio_urls,
            labels=labels, columns=columns, label=label,
            html_template=html_template,
            css_template=CSS_TEMPLATE,
            js_on_load=JS_ON_LOAD, **kwargs
        )