Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |