"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { MusicNotes, Pause, Play, Trash, UploadSimple, Warning } from "@phosphor-icons/react"; import { Panel, Switch } from "@/components/ui"; import { Waveform, useAudioPeaks } from "@/components/waveform"; import { formatClock } from "@/lib/presets"; export interface ReferenceState { file: File | null; filename: string | null; status: "empty" | "uploading" | "ready" | "failed"; error: string | null; /** Seconds into the file to start transcribing. */ start: number; /** Seconds to transcribe, or null for the whole thing. */ duration: number | null; } export const EMPTY_REFERENCE: ReferenceState = { file: null, filename: null, status: "empty", error: null, start: 0, duration: null, }; const ACCEPTED = ".wav,.mp3,.flac,.ogg,.m4a,.aac,.opus,.webm"; export function ReferenceDrop({ state, onChange, }: { state: ReferenceState; onChange: (next: ReferenceState) => void; }) { const inputRef = useRef(null); const [dragging, setDragging] = useState(false); const { peaks, duration, failed } = useAudioPeaks(state.file, 420); const preview = useLocalPlayback(state.file); const accept = useCallback( async (file: File) => { onChange({ file, filename: null, status: "uploading", error: null, start: 0, duration: null }); const form = new FormData(); form.append("file", file); try { const res = await fetch("/api/upload", { method: "POST", body: form }); const body = await res.json(); if (!res.ok) throw new Error(body.error ?? "Upload failed"); onChange({ file, filename: body.filename, status: "ready", error: null, start: 0, duration: null }); } catch (error) { onChange({ file, filename: null, status: "failed", error: error instanceof Error ? error.message : "Upload failed", start: 0, duration: null, }); } }, [onChange], ); if (state.file) { return ( onChange(EMPTY_REFERENCE)} className="flex items-center gap-1.5 text-[12px] text-ink-faint transition-colors hover:text-alarm" > Remove } >

{state.file.name}

{(state.file.size / 1024 / 1024).toFixed(1)} MB {duration > 0 ? ` ${formatClock(duration)}` : ""}

{failed ? (

This browser could not decode the file for preview. ComfyUI will still read it.

) : (
{/* The second of the two circular play buttons, deliberately smaller than the dock's so the transport stays the primary one. */}
{formatClock(preview.position)} / {formatClock(preview.length || duration)}
)}
{preview.error ? (

{preview.error}

) : null}
0} onChange={(on) => onChange({ ...state, start: 0, duration: on ? Math.min(120, Math.round(duration) || 120) : null, }) } label="Transcribe only part of it" />

SheetSage2 reads the whole file by default. A long or busy recording can contain a note it cannot place on its grid, which fails the render.

{state.duration !== null || state.start > 0 ? (

{formatClock(state.start)} to{" "} {formatClock(state.start + (state.duration ?? 0))} {duration > 0 ? ` of ${formatClock(duration)}` : ""}

) : null}
{preview.url ? (
); } return (
{ event.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)} onDrop={(event) => { event.preventDefault(); setDragging(false); const file = event.dataTransfer.files?.[0]; if (file) void accept(file); }} className={`flex flex-col items-center justify-center rounded-console border border-dashed px-6 py-9 text-center transition-colors duration-150 ${ dragging ? "border-citron bg-citron/5" : "border-line-strong bg-void" }`} >

Drop a song here

SheetSage2 reads the melody into a score. YuE2 then performs your lyrics over that melody in the style you wrote.

{ const file = event.target.files?.[0]; if (file) void accept(file); event.target.value = ""; }} />
); } function StatusBadge({ status }: { status: ReferenceState["status"] }) { if (status === "uploading") { return ( sending ); } if (status === "ready") { return ( on the box ); } if (status === "failed") { return ( failed ); } return null; } /** * Plays the picked file straight from memory, before it ever leaves the * browser. Hearing the reference matters: it is what decides the melody the * model will sing, so uploading one blind is a bad way to work. */ function useLocalPlayback(file: File | null) { const ref = useRef(null); const [url, setUrl] = useState(null); const [playing, setPlaying] = useState(false); const [position, setPosition] = useState(0); const [length, setLength] = useState(0); const [error, setError] = useState(null); useEffect(() => { if (!file) { setUrl(null); return; } const objectUrl = URL.createObjectURL(file); setUrl(objectUrl); setPlaying(false); setPosition(0); setLength(0); setError(null); // An object URL pins the file in memory until it is released. return () => URL.revokeObjectURL(objectUrl); }, [file]); const toggle = () => { const element = ref.current; if (!element) return; if (!element.paused) { element.pause(); return; } element.play().catch(() => setError("This browser will not play that file.")); }; const seek = (ratio: number) => { const element = ref.current; if (!element || !length) return; element.currentTime = ratio * length; setPosition(ratio * length); }; return { ref, url, playing, position, length, error, progress: length > 0 ? Math.min(1, position / length) : 0, toggle, seek, setPlaying, setPosition, setLength, setError, }; }