Spaces:
Runtime error
Runtime error
Download src/components/reference-drop.tsx from RalphThings/yue2-studio: direct link, hf CLI and curl.
- Browser
- Download file 12.8 kB
-
https://huggingface.co/spaces/RalphThings/yue2-studio/resolve/main/src/components/reference-drop.tsx
- Command line
-
hf download hf://spaces/RalphThings/yue2-studio/src/components/reference-drop.tsx
-
curl -L -o reference-drop.tsx https://huggingface.co/spaces/RalphThings/yue2-studio/resolve/main/src/components/reference-drop.tsx
12.8 kB
| "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<HTMLInputElement>(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 ( | |
| <Panel | |
| title="Reference recording" | |
| hint="Transcribed to a melody score, then re-sung." | |
| aside={ | |
| <button | |
| type="button" | |
| onClick={() => onChange(EMPTY_REFERENCE)} | |
| className="flex items-center gap-1.5 text-[12px] text-ink-faint transition-colors hover:text-alarm" | |
| > | |
| <Trash size={13} weight="bold" /> | |
| Remove | |
| </button> | |
| } | |
| > | |
| <div className="flex items-start gap-3"> | |
| <MusicNotes size={18} weight="fill" className="mt-0.5 shrink-0 text-citron" /> | |
| <div className="min-w-0 flex-1"> | |
| <p className="truncate text-[13px] text-ink">{state.file.name}</p> | |
| <p className="mt-0.5 font-mono text-[11.5px] text-ink-faint"> | |
| {(state.file.size / 1024 / 1024).toFixed(1)} MB | |
| {duration > 0 ? ` ${formatClock(duration)}` : ""} | |
| </p> | |
| </div> | |
| <StatusBadge status={state.status} /> | |
| </div> | |
| <div className="mt-3 rounded-console border border-line bg-void px-3 py-2.5"> | |
| {failed ? ( | |
| <p className="py-3 text-[12px] text-ink-faint"> | |
| This browser could not decode the file for preview. ComfyUI will still read it. | |
| </p> | |
| ) : ( | |
| <div className="flex items-center gap-3"> | |
| {/* | |
| The second of the two circular play buttons, deliberately | |
| smaller than the dock's so the transport stays the primary one. | |
| */} | |
| <button | |
| type="button" | |
| onClick={preview.toggle} | |
| aria-label={preview.playing ? "Pause the reference" : "Play the reference"} | |
| className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-citron text-void transition-transform duration-150 active:scale-95" | |
| > | |
| {preview.playing ? ( | |
| <Pause size={12} weight="fill" /> | |
| ) : ( | |
| <Play size={12} weight="fill" /> | |
| )} | |
| </button> | |
| <div className="min-w-0 flex-1"> | |
| <Waveform | |
| peaks={peaks} | |
| progress={preview.progress} | |
| onSeek={preview.seek} | |
| height={40} | |
| /> | |
| </div> | |
| <span className="shrink-0 font-mono text-[11px] text-ink-faint"> | |
| {formatClock(preview.position)} / {formatClock(preview.length || duration)} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| {preview.error ? ( | |
| <p className="mt-2 text-[12px] text-alarm">{preview.error}</p> | |
| ) : null} | |
| <div className="mt-3 border-t border-line pt-3"> | |
| <Switch | |
| checked={state.duration !== null || state.start > 0} | |
| onChange={(on) => | |
| onChange({ | |
| ...state, | |
| start: 0, | |
| duration: on ? Math.min(120, Math.round(duration) || 120) : null, | |
| }) | |
| } | |
| label="Transcribe only part of it" | |
| /> | |
| <p className="mt-1 text-[11.5px] leading-snug text-ink-faint"> | |
| 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. | |
| </p> | |
| {state.duration !== null || state.start > 0 ? ( | |
| <div className="mt-3 flex flex-wrap items-end gap-3"> | |
| <label className="flex flex-col gap-1"> | |
| <span className="text-[11px] tracking-[0.06em] text-ink-dim uppercase">From</span> | |
| <input | |
| type="number" | |
| min={0} | |
| max={Math.max(0, Math.floor(duration))} | |
| value={state.start} | |
| onChange={(event) => { | |
| const limit = duration > 0 ? Math.floor(duration) - 1 : Infinity; | |
| const start = Math.min(limit, Math.max(0, Number(event.target.value))); | |
| onChange({ ...state, start }); | |
| }} | |
| className="h-8 w-24 rounded-console border border-line-strong bg-void px-2 font-mono text-[12px] text-ink" | |
| /> | |
| </label> | |
| <label className="flex flex-col gap-1"> | |
| <span className="text-[11px] tracking-[0.06em] text-ink-dim uppercase"> | |
| Seconds | |
| </span> | |
| <input | |
| type="number" | |
| min={1} | |
| max={duration > 0 ? Math.ceil(duration) : undefined} | |
| value={state.duration ?? 60} | |
| onChange={(event) => { | |
| const remaining = duration > 0 ? Math.ceil(duration) - state.start : Infinity; | |
| const seconds = Math.min(remaining, Math.max(1, Number(event.target.value))); | |
| onChange({ ...state, duration: seconds }); | |
| }} | |
| className="h-8 w-24 rounded-console border border-line-strong bg-void px-2 font-mono text-[12px] text-ink" | |
| /> | |
| </label> | |
| <p className="pb-1.5 font-mono text-[11.5px] text-ink-faint"> | |
| {formatClock(state.start)} to{" "} | |
| {formatClock(state.start + (state.duration ?? 0))} | |
| {duration > 0 ? ` of ${formatClock(duration)}` : ""} | |
| </p> | |
| </div> | |
| ) : null} | |
| </div> | |
| {preview.url ? ( | |
| <audio | |
| ref={preview.ref} | |
| src={preview.url} | |
| preload="metadata" | |
| onPlay={() => preview.setPlaying(true)} | |
| onPause={() => preview.setPlaying(false)} | |
| onTimeUpdate={(event) => preview.setPosition(event.currentTarget.currentTime)} | |
| onLoadedMetadata={(event) => { | |
| const value = event.currentTarget.duration; | |
| if (Number.isFinite(value) && value > 0) preview.setLength(value); | |
| }} | |
| onEnded={() => { | |
| preview.setPlaying(false); | |
| preview.setPosition(0); | |
| }} | |
| onError={() => preview.setError("This browser will not play that file.")} | |
| /> | |
| ) : null} | |
| {state.error ? ( | |
| <p className="mt-2 flex items-start gap-1.5 text-[12px] text-alarm"> | |
| <Warning size={13} weight="fill" className="mt-0.5 shrink-0" /> | |
| {state.error} | |
| </p> | |
| ) : null} | |
| </Panel> | |
| ); | |
| } | |
| return ( | |
| <Panel title="Reference recording" hint="Optional. Keeps the tune, changes everything else."> | |
| <div | |
| onDragOver={(event) => { | |
| 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" | |
| }`} | |
| > | |
| <UploadSimple size={20} weight="bold" className={dragging ? "text-citron" : "text-ink-faint"} /> | |
| <p className="mt-3 text-[13px] text-ink">Drop a song here</p> | |
| <p className="mt-1 max-w-[42ch] text-[12px] leading-snug text-ink-faint"> | |
| SheetSage2 reads the melody into a score. YuE2 then performs your lyrics over that melody | |
| in the style you wrote. | |
| </p> | |
| <button | |
| type="button" | |
| onClick={() => inputRef.current?.click()} | |
| className="mt-4 h-8 rounded-console border border-line-strong px-3 text-[12.5px] text-ink transition-colors hover:border-ink-faint hover:bg-raised" | |
| > | |
| Choose a file | |
| </button> | |
| <input | |
| ref={inputRef} | |
| type="file" | |
| accept={ACCEPTED} | |
| className="hidden" | |
| onChange={(event) => { | |
| const file = event.target.files?.[0]; | |
| if (file) void accept(file); | |
| event.target.value = ""; | |
| }} | |
| /> | |
| </div> | |
| </Panel> | |
| ); | |
| } | |
| function StatusBadge({ status }: { status: ReferenceState["status"] }) { | |
| if (status === "uploading") { | |
| return ( | |
| <span className="shrink-0 rounded-console border border-line-strong px-2 py-0.5 font-mono text-[11px] text-ink-dim"> | |
| sending | |
| </span> | |
| ); | |
| } | |
| if (status === "ready") { | |
| return ( | |
| <span className="shrink-0 rounded-console border border-citron/40 bg-citron/10 px-2 py-0.5 font-mono text-[11px] text-citron"> | |
| on the box | |
| </span> | |
| ); | |
| } | |
| if (status === "failed") { | |
| return ( | |
| <span className="shrink-0 rounded-console border border-alarm/40 bg-alarm/10 px-2 py-0.5 font-mono text-[11px] text-alarm"> | |
| failed | |
| </span> | |
| ); | |
| } | |
| 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<HTMLAudioElement>(null); | |
| const [url, setUrl] = useState<string | null>(null); | |
| const [playing, setPlaying] = useState(false); | |
| const [position, setPosition] = useState(0); | |
| const [length, setLength] = useState(0); | |
| const [error, setError] = useState<string | null>(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, | |
| }; | |
| } | |