Gertie2013 commited on
Commit
c21311d
·
verified ·
1 Parent(s): 8e07754

Create pages/index.tsx

Browse files
Files changed (1) hide show
  1. pages/index.tsx +317 -0
pages/index.tsx ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+ import type { VoiceType } from "../lib/types";
3
+
4
+ interface LibraryItem {
5
+ id: string;
6
+ title: string;
7
+ duration: number;
8
+ voice: VoiceType;
9
+ blobUrl?: string;
10
+ }
11
+
12
+ export default function Home() {
13
+ const [stylePrompt, setStylePrompt] = useState("");
14
+ const [lyricPrompt, setLyricPrompt] = useState("");
15
+ const [voice, setVoice] = useState<VoiceType>("Random");
16
+ const [durationSeconds, setDurationSeconds] = useState<number | undefined>(undefined);
17
+ const [isGenerating, setIsGenerating] = useState(false);
18
+ const [result, setResult] = useState<any>(null);
19
+ const [library, setLibrary] = useState<LibraryItem[]>([]);
20
+ const [selectedTrackId, setSelectedTrackId] = useState<string | null>(null);
21
+ const [editOperation, setEditOperation] = useState<"cover" | "extend" | "swap_vocal" | "crop">("cover");
22
+ const [editResult, setEditResult] = useState<any>(null);
23
+
24
+ useEffect(() => {
25
+ const loadLibrary = async () => {
26
+ try {
27
+ const res = await fetch("/api/library");
28
+ const json = await res.json();
29
+ if (Array.isArray(json.items)) {
30
+ setLibrary(json.items);
31
+ }
32
+ } catch {
33
+ // silent library load failure
34
+ }
35
+ };
36
+ loadLibrary();
37
+ }, []);
38
+
39
+ const handleGenerate = async () => {
40
+ setIsGenerating(true);
41
+ setResult(null);
42
+
43
+ const duration =
44
+ typeof durationSeconds === "number"
45
+ ? durationSeconds
46
+ : undefined; // randomizer handled server-side
47
+
48
+ try {
49
+ // ensure this never fails by guarding inputs and catching errors
50
+ let t = await fetch("/api/generate", {
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ Accept: "application/json",
55
+ },
56
+ body: JSON.stringify({
57
+ stylePrompt,
58
+ lyricPrompt: lyricPrompt || undefined,
59
+ voice,
60
+ durationSeconds: duration,
61
+ }),
62
+ });
63
+
64
+ if (!t.ok) {
65
+ const err = await t.json().catch(() => ({}));
66
+ setResult({ error: err.error || "Generation failed", details: err.details });
67
+ } else {
68
+ const json = await t.json();
69
+ setResult(json);
70
+
71
+ const id = crypto.randomUUID();
72
+ const item: LibraryItem = {
73
+ id,
74
+ title: stylePrompt || "Untitled",
75
+ duration: json.duration,
76
+ voice,
77
+ };
78
+
79
+ setLibrary((prev) => [...prev, item]);
80
+
81
+ try {
82
+ await fetch("/api/library", {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ Accept: "application/json",
87
+ },
88
+ body: JSON.stringify(item),
89
+ });
90
+ } catch {
91
+ // library persistence failure is non-fatal
92
+ }
93
+ }
94
+ } catch (e: any) {
95
+ setResult({
96
+ error: "Unexpected client error during generation",
97
+ details: e?.message ?? "Unknown error",
98
+ });
99
+ } finally {
100
+ setIsGenerating(false);
101
+ }
102
+ };
103
+
104
+ const handleEdit = async () => {
105
+ if (!selectedTrackId) return;
106
+
107
+ try {
108
+ const res = await fetch("/api/edit", {
109
+ method: "POST",
110
+ headers: {
111
+ "Content-Type": "application/json",
112
+ Accept: "application/json",
113
+ },
114
+ body: JSON.stringify({
115
+ operation: editOperation,
116
+ trackId: selectedTrackId,
117
+ params: {},
118
+ }),
119
+ });
120
+
121
+ const json = await res.json();
122
+ setEditResult(json);
123
+ } catch (e: any) {
124
+ setEditResult({
125
+ error: "Unexpected client error during editing",
126
+ details: e?.message ?? "Unknown error",
127
+ });
128
+ }
129
+ };
130
+
131
+ const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
132
+ const file = event.target.files?.[0];
133
+ if (!file) return;
134
+
135
+ const ext = file.name.split(".").pop()?.toLowerCase();
136
+ if (!["mp3", "wav"].includes(ext || "")) {
137
+ alert("Only MP3/WAV files are supported.");
138
+ return;
139
+ }
140
+
141
+ const blobUrl = URL.createObjectURL(file);
142
+ const id = crypto.randomUUID();
143
+
144
+ const item: LibraryItem = {
145
+ id,
146
+ title: file.name,
147
+ duration: 0,
148
+ voice: "Random",
149
+ blobUrl,
150
+ };
151
+
152
+ setLibrary((prev) => [...prev, item]);
153
+
154
+ try {
155
+ await fetch("/api/library", {
156
+ method: "POST",
157
+ headers: {
158
+ "Content-Type": "application/json",
159
+ Accept: "application/json",
160
+ },
161
+ body: JSON.stringify(item),
162
+ });
163
+ } catch {
164
+ // non-fatal
165
+ }
166
+ };
167
+
168
+ const handleExport = async (item: LibraryItem) => {
169
+ if (!item.blobUrl) {
170
+ alert("No audio blob available for export.");
171
+ return;
172
+ }
173
+ const a = document.createElement("a");
174
+ a.href = item.blobUrl;
175
+ a.download = `${item.title || "track"}.wav`;
176
+ a.click();
177
+ };
178
+
179
+ return (
180
+ <main style={{ padding: 24, fontFamily: "system-ui" }}>
181
+ <h1>ACE-Step Riffusion Studio</h1>
182
+
183
+ <section style={{ marginBottom: 24 }}>
184
+ <h2>Generate song</h2>
185
+ <div>
186
+ <label>
187
+ Style prompt:
188
+ <textarea
189
+ value={stylePrompt}
190
+ onChange={(e) => setStylePrompt(e.target.value)}
191
+ rows={3}
192
+ style={{ width: "100%" }}
193
+ />
194
+ </label>
195
+ </div>
196
+ <div>
197
+ <label>
198
+ Lyrics (optional):
199
+ <textarea
200
+ value={lyricPrompt}
201
+ onChange={(e) => setLyricPrompt(e.target.value)}
202
+ rows={3}
203
+ style={{ width: "100%" }}
204
+ />
205
+ </label>
206
+ </div>
207
+ <div>
208
+ <label>
209
+ Voice:
210
+ <select value={voice} onChange={(e) => setVoice(e.target.value as VoiceType)}>
211
+ <option value="Random">Random</option>
212
+ <option value="Male">Male</option>
213
+ <option value="Female">Female</option>
214
+ </select>
215
+ </label>
216
+ </div>
217
+ <div>
218
+ <label>
219
+ Duration (30–240s, leave empty for random):
220
+ <input
221
+ type="number"
222
+ min={30}
223
+ max={240}
224
+ value={durationSeconds ?? ""}
225
+ onChange={(e) =>
226
+ setDurationSeconds(
227
+ e.target.value ? Math.min(240, Math.max(30, Number(e.target.value))) : undefined
228
+ )
229
+ }
230
+ />
231
+ </label>
232
+ </div>
233
+ <button onClick={handleGenerate} disabled={isGenerating || !stylePrompt}>
234
+ {isGenerating ? "Generating..." : "Generate"}
235
+ </button>
236
+ {result && (
237
+ <pre style={{ marginTop: 12, background: "#111", color: "#eee", padding: 12 }}>
238
+ {JSON.stringify(result, null, 2)}
239
+ </pre>
240
+ )}
241
+ </section>
242
+
243
+ <section style={{ marginBottom: 24 }}>
244
+ <h2>Editing suite (DreamVAE)</h2>
245
+ <div>
246
+ <label>
247
+ Operation:
248
+ <select
249
+ value={editOperation}
250
+ onChange={(e) =>
251
+ setEditOperation(e.target.value as "cover" | "extend" | "swap_vocal" | "crop")
252
+ }
253
+ >
254
+ <option value="cover">Musical cover maker</option>
255
+ <option value="extend">Song extender</option>
256
+ <option value="swap_vocal">Vocal swapper</option>
257
+ <option value="crop">Cropping mechanism</option>
258
+ </select>
259
+ </label>
260
+ </div>
261
+ <div>
262
+ <label>
263
+ Track:
264
+ <select
265
+ value={selectedTrackId ?? ""}
266
+ onChange={(e) => setSelectedTrackId(e.target.value || null)}
267
+ >
268
+ <option value="">Select track</option>
269
+ {library.map((item) => (
270
+ <option key={item.id} value={item.id}>
271
+ {item.title}
272
+ </option>
273
+ ))}
274
+ </select>
275
+ </label>
276
+ </div>
277
+ <button onClick={handleEdit} disabled={!selectedTrackId}>
278
+ Apply edit
279
+ </button>
280
+ {editResult && (
281
+ <pre style={{ marginTop: 12, background: "#111", color: "#eee", padding: 12 }}>
282
+ {JSON.stringify(editResult, null, 2)}
283
+ </pre>
284
+ )}
285
+ </section>
286
+
287
+ <section style={{ marginBottom: 24 }}>
288
+ <h2>Import / Export</h2>
289
+ <div>
290
+ <label>
291
+ Import MP3/WAV:
292
+ <input type="file" accept=".mp3,.wav" onChange={handleImport} />
293
+ </label>
294
+ </div>
295
+ <div style={{ marginTop: 12 }}>
296
+ <h3>Library</h3>
297
+ {library.length === 0 && <p>No tracks yet.</p>}
298
+ <ul>
299
+ {library.map((item) => (
300
+ <li key={item.id} style={{ marginBottom: 8 }}>
301
+ <strong>{item.title}</strong> ({item.voice}, {item.duration || 0}s)
302
+ {item.blobUrl && (
303
+ <>
304
+ {" "}
305
+ <button onClick={() => handleExport(item)}>Export</button>
306
+ {" "}
307
+ <audio controls src={item.blobUrl} />
308
+ </>
309
+ )}
310
+ </li>
311
+ ))}
312
+ </ul>
313
+ </div>
314
+ </section>
315
+ </main>
316
+ );
317
+ }