emresar's picture
Upload folder using huggingface_hub
6678fa1 verified
import { useState } from "react";
import { useLocation } from "wouter";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Upload, Loader2, ArrowLeft, FileAudio } from "lucide-react";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
export default function UploadPage() {
const [, setLocation] = useLocation();
const [title, setTitle] = useState("");
const [artist, setArtist] = useState("");
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [enableStemming, setEnableStemming] = useState(false);
const uploadMutation = trpc.tracks.upload.useMutation({
onSuccess: (data) => {
toast.success("Track uploaded successfully!");
setLocation(`/track/${data.trackId}`);
},
onError: (error) => {
toast.error(`Upload failed: ${error.message}`);
setUploading(false);
},
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
// Validate file type
const validTypes = ["audio/mpeg", "audio/mp3", "audio/wav", "audio/ogg", "audio/m4a"];
if (!validTypes.includes(selectedFile.type) && !selectedFile.name.match(/\.(mp3|wav|ogg|m4a)$/i)) {
toast.error("Please select a valid audio file (MP3, WAV, OGG, or M4A)");
return;
}
// Validate file size (max 50MB for demo)
if (selectedFile.size > 50 * 1024 * 1024) {
toast.error("File size must be less than 50MB");
return;
}
setFile(selectedFile);
// Auto-fill title from filename if empty
if (!title) {
const nameWithoutExt = selectedFile.name.replace(/\.[^/.]+$/, "");
setTitle(nameWithoutExt);
}
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
toast.error("Please select a file");
return;
}
if (!title.trim()) {
toast.error("Please enter a track title");
return;
}
setUploading(true);
try {
// Convert file to base64
const reader = new FileReader();
reader.onload = async () => {
const base64Data = reader.result as string;
const base64Content = base64Data.split(',')[1] || base64Data;
await uploadMutation.mutateAsync({
title: title.trim(),
artist: artist.trim() || undefined,
fileData: base64Content,
mimeType: file.type || "audio/mpeg",
fileName: file.name,
enableStemming,
});
};
reader.onerror = () => {
toast.error("Failed to read file");
setUploading(false);
};
reader.readAsDataURL(file);
} catch (error) {
console.error("Upload error:", error);
setUploading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-blue-50">
<div className="container py-8">
<div className="max-w-2xl mx-auto">
<Button
variant="ghost"
onClick={() => setLocation("/")}
className="mb-6"
>
<ArrowLeft className="mr-2 w-4 h-4" />
Back to Tracks
</Button>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Upload AI-Generated Music</CardTitle>
<CardDescription>
Upload your AI-generated track to analyze its training data sources
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="file">Audio File *</Label>
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center hover:border-primary/50 transition-colors">
<input
id="file"
type="file"
accept="audio/*,.mp3,.wav,.ogg,.m4a"
onChange={handleFileChange}
className="hidden"
disabled={uploading}
/>
<label
htmlFor="file"
className="cursor-pointer flex flex-col items-center"
>
{file ? (
<>
<FileAudio className="w-12 h-12 text-primary mb-3" />
<p className="font-medium">{file.name}</p>
<p className="text-sm text-muted-foreground mt-1">
{(file.size / (1024 * 1024)).toFixed(2)} MB
</p>
<Button
type="button"
variant="outline"
size="sm"
className="mt-4"
disabled={uploading}
>
Change File
</Button>
</>
) : (
<>
<Upload className="w-12 h-12 text-muted-foreground mb-3" />
<p className="font-medium">Click to upload audio file</p>
<p className="text-sm text-muted-foreground mt-1">
MP3, WAV, OGG, or M4A (max 50MB)
</p>
</>
)}
</label>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title">Track Title *</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter track title"
required
disabled={uploading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="artist">Artist (Optional)</Label>
<Input
id="artist"
value={artist}
onChange={(e) => setArtist(e.target.value)}
placeholder="Enter artist name"
disabled={uploading}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<Label htmlFor="stemming" className="text-base">Enable Stem Separation</Label>
<p className="text-sm text-muted-foreground">
Separate track into vocals, drums, bass, and other before analysis
</p>
</div>
<Switch
id="stemming"
checked={enableStemming}
onCheckedChange={setEnableStemming}
disabled={uploading}
/>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-sm mb-2 text-blue-900">What happens next?</h4>
<ul className="text-sm text-blue-800 space-y-1">
{enableStemming ? (
<>
<li>• Your track will be separated into stems (vocals, drums, bass, other)</li>
<li>• Each stem will be split into 10-second chunks</li>
<li>• We'll match each chunk against training data</li>
</>
) : (
<>
<li>• Your track will be split into 10-second chunks</li>
<li>• Each chunk will be embedded using CLAP</li>
<li>• We'll match chunks against training reference tracks</li>
</>
)}
<li>• Hover over the waveform to see matches for each section</li>
</ul>
</div>
<div className="flex gap-3">
<Button
type="submit"
disabled={!file || !title.trim() || uploading}
className="flex-1"
>
{uploading ? (
<>
<Loader2 className="mr-2 w-4 h-4 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="mr-2 w-4 h-4" />
Upload & Analyze
</>
)}
</Button>
<Button
type="button"
variant="outline"
onClick={() => setLocation("/")}
disabled={uploading}
>
Cancel
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</div>
</div>
);
}