Spaces:
Sleeping
Sleeping
Download frontend/src/components/generation/GenerationOutput.jsx from MusoraProductDepartment/jam-tracks: direct link, hf CLI and curl.
- Browser
- Download file 7.19 kB
-
https://huggingface.co/spaces/MusoraProductDepartment/jam-tracks/resolve/main/frontend/src/components/generation/GenerationOutput.jsx
- Command line
-
hf download hf://spaces/MusoraProductDepartment/jam-tracks/frontend/src/components/generation/GenerationOutput.jsx
-
curl -L -o GenerationOutput.jsx https://huggingface.co/spaces/MusoraProductDepartment/jam-tracks/resolve/main/frontend/src/components/generation/GenerationOutput.jsx
7.19 kB
| import React, { useRef, useState, useEffect } from 'react' | |
| export default function GenerationOutput({ | |
| sessionId, | |
| generationResult, | |
| onRegenerate, | |
| onUseAsSource, | |
| onPlaySection, | |
| }) { | |
| const audioRef = useRef(null) | |
| const [isPlaying, setIsPlaying] = useState(false) | |
| const [currentTime, setCurrentTime] = useState(0) | |
| const [duration, setDuration] = useState(0) | |
| const [abSource, setAbSource] = useState('generated') | |
| const [downloadFormat, setDownloadFormat] = useState('wav') | |
| const [audioSrc, setAudioSrc] = useState(null) | |
| const [loadError, setLoadError] = useState(false) | |
| // Only set the audio src when we get a NEW generation result | |
| useEffect(() => { | |
| if (generationResult && sessionId) { | |
| setAudioSrc(`/api/stem/${sessionId}/_continuation?format=wav&v=${Date.now()}`) | |
| setIsPlaying(false) | |
| setCurrentTime(0) | |
| setDuration(0) | |
| setAbSource('generated') | |
| setLoadError(false) | |
| } else { | |
| setAudioSrc(null) | |
| } | |
| }, [generationResult, sessionId]) | |
| const handleTimeUpdate = () => { | |
| if (audioRef.current) setCurrentTime(audioRef.current.currentTime) | |
| } | |
| const handleLoadedMetadata = () => { | |
| if (audioRef.current) setDuration(audioRef.current.duration) | |
| } | |
| const handleEnded = () => setIsPlaying(false) | |
| // Stop retrying on error — clear the src so the browser doesn't loop | |
| const handleError = () => { | |
| setLoadError(true) | |
| setIsPlaying(false) | |
| } | |
| const togglePlay = () => { | |
| if (!audioRef.current || loadError) return | |
| if (isPlaying) { | |
| audioRef.current.pause() | |
| setIsPlaying(false) | |
| } else { | |
| audioRef.current.play().catch(() => setIsPlaying(false)) | |
| setIsPlaying(true) | |
| } | |
| } | |
| const handleSeek = (e) => { | |
| if (!audioRef.current || !duration) return | |
| const rect = e.currentTarget.getBoundingClientRect() | |
| const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) | |
| audioRef.current.currentTime = pct * duration | |
| } | |
| const formatTime = (t) => { | |
| const m = Math.floor(t / 60) | |
| const s = Math.floor(t % 60).toString().padStart(2, '0') | |
| return `${m}:${s}` | |
| } | |
| const handleDownload = () => { | |
| if (!sessionId) return | |
| const url = `/api/stem/${sessionId}/_continuation?format=${downloadFormat}` | |
| const a = document.createElement('a') | |
| a.href = url | |
| a.download = `generation.${downloadFormat}` | |
| a.click() | |
| } | |
| // Don't render anything until we have a generation result | |
| if (!generationResult) return null | |
| return ( | |
| <div className="space-y-3 pt-3 border-t border-white/[0.06]"> | |
| {/* Only mount <audio> when we have a valid src */} | |
| {audioSrc && !loadError && ( | |
| <audio | |
| ref={audioRef} | |
| src={audioSrc} | |
| preload="auto" | |
| onTimeUpdate={handleTimeUpdate} | |
| onLoadedMetadata={handleLoadedMetadata} | |
| onEnded={handleEnded} | |
| onError={handleError} | |
| /> | |
| )} | |
| {loadError && ( | |
| <div className="text-xs text-red-400 bg-red-500/10 rounded px-3 py-2"> | |
| Audio playback unavailable — session may have expired. Try generating again. | |
| </div> | |
| )} | |
| {/* A/B Toggle */} | |
| <div className="flex gap-1.5"> | |
| <button | |
| onClick={() => setAbSource('generated')} | |
| className={`px-3 py-1 rounded text-xs font-medium transition-all ${ | |
| abSource === 'generated' | |
| ? 'bg-accent-500/20 text-accent-400 border border-accent-500/30' | |
| : 'bg-white/[0.04] text-gray-500 border border-transparent hover:bg-white/[0.06]' | |
| }`} | |
| > | |
| Seed + Generated | |
| </button> | |
| <button | |
| onClick={() => { | |
| setAbSource('seed') | |
| if (audioRef.current) { audioRef.current.pause(); setIsPlaying(false) } | |
| if (onPlaySection) onPlaySection() | |
| }} | |
| className={`px-3 py-1 rounded text-xs font-medium transition-all ${ | |
| abSource === 'seed' | |
| ? 'bg-accent-500/20 text-accent-400 border border-accent-500/30' | |
| : 'bg-white/[0.04] text-gray-500 border border-transparent hover:bg-white/[0.06]' | |
| }`} | |
| > | |
| Seed Only | |
| </button> | |
| </div> | |
| {/* Playback controls */} | |
| {!loadError && ( | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-3"> | |
| <button | |
| onClick={togglePlay} | |
| disabled={abSource === 'seed'} | |
| className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors text-white text-sm ${ | |
| abSource === 'seed' ? 'bg-gray-700 cursor-not-allowed' : 'bg-accent-500 hover:bg-accent-400' | |
| }`} | |
| > | |
| {isPlaying ? '⏸' : '▶'} | |
| </button> | |
| <div | |
| className="flex-1 h-1.5 rounded-full bg-white/[0.06] cursor-pointer" | |
| onClick={handleSeek} | |
| > | |
| <div | |
| className="h-full bg-accent-500 rounded-full transition-all" | |
| style={{ width: duration ? `${(currentTime / duration) * 100}%` : '0%' }} | |
| /> | |
| </div> | |
| <span className="text-xs font-mono text-gray-400 w-16 text-right"> | |
| {formatTime(currentTime)} / {formatTime(duration)} | |
| </span> | |
| </div> | |
| </div> | |
| )} | |
| {/* Metadata */} | |
| <div className="flex flex-wrap gap-2 text-[10px]"> | |
| <span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500"> | |
| Model: {generationResult.model || 'unknown'} | |
| </span> | |
| <span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500"> | |
| Duration: {generationResult.duration_seconds?.toFixed(1)}s | |
| </span> | |
| <span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500"> | |
| Generated in {generationResult.generation_time_seconds?.toFixed(1)}s | |
| </span> | |
| </div> | |
| {/* Action buttons */} | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={onRegenerate} | |
| className="px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-xs text-gray-300 transition-colors" | |
| > | |
| Regenerate | |
| </button> | |
| <button | |
| onClick={onUseAsSource} | |
| className="px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-xs text-gray-300 transition-colors" | |
| > | |
| Use as Source | |
| </button> | |
| {/* Download */} | |
| <div className="ml-auto flex items-center gap-1"> | |
| <select | |
| value={downloadFormat} | |
| onChange={e => setDownloadFormat(e.target.value)} | |
| className="bg-surface-elevated border border-white/[0.06] rounded px-1.5 py-1 text-[10px] text-gray-400 focus:outline-none" | |
| > | |
| <option value="wav">.wav</option> | |
| <option value="flac">.flac</option> | |
| </select> | |
| <button | |
| onClick={handleDownload} | |
| className="px-2 py-1 rounded bg-white/[0.06] hover:bg-white/[0.1] text-[10px] text-gray-400 transition-colors" | |
| > | |
| Download | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |