Upload 2 files
#1
by
AcTePuKc
- opened
front/src/components/AudioPlayer.tsx
CHANGED
@@ -1,43 +1,39 @@
|
|
1 |
-
import React, { useMemo, useEffect } from 'react';
|
2 |
-
import { blobFromAudioBuffer } from '../utils/utils';
|
3 |
-
|
4 |
-
interface AudioPlayerProps {
|
5 |
-
audioBuffer: AudioBuffer;
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
<
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
</a>
|
41 |
-
</div>
|
42 |
-
);
|
43 |
-
};
|
|
|
1 |
+
import React, { useMemo, useEffect } from 'react';
|
2 |
+
import { blobFromAudioBuffer } from '../utils/utils';
|
3 |
+
|
4 |
+
interface AudioPlayerProps {
|
5 |
+
audioBuffer: AudioBuffer;
|
6 |
+
filename?: string; // Optional filename prop
|
7 |
+
}
|
8 |
+
|
9 |
+
export const AudioPlayer: React.FC<AudioPlayerProps> = ({ audioBuffer, filename = 'podcast.wav' }) => {
|
10 |
+
// Create a Blob URL from the audioBuffer.
|
11 |
+
const blobUrl = useMemo(() => {
|
12 |
+
const wavBlob = blobFromAudioBuffer(audioBuffer);
|
13 |
+
return URL.createObjectURL(wavBlob);
|
14 |
+
}, [audioBuffer]);
|
15 |
+
|
16 |
+
|
17 |
+
// Clean up the object URL when the component unmounts or audioBuffer changes.
|
18 |
+
useEffect(() => {
|
19 |
+
return () => {
|
20 |
+
URL.revokeObjectURL(blobUrl);
|
21 |
+
};
|
22 |
+
}, [blobUrl]);
|
23 |
+
|
24 |
+
return (
|
25 |
+
<div className="mt-4 flex items-center">
|
26 |
+
<audio controls src={blobUrl}>
|
27 |
+
Your browser does not support the audio element.
|
28 |
+
</audio>
|
29 |
+
|
30 |
+
<a
|
31 |
+
className="btn btn-sm btn-primary ml-2"
|
32 |
+
href={blobUrl} // Use the same blobUrl for download
|
33 |
+
download={filename}
|
34 |
+
>
|
35 |
+
Download
|
36 |
+
</a>
|
37 |
+
</div>
|
38 |
+
);
|
39 |
+
};
|
|
|
|
|
|
|
|
front/src/components/PodcastGenerator.tsx
CHANGED
@@ -1,377 +1,454 @@
|
|
1 |
-
import { useEffect, useState } from 'react';
|
2 |
-
import { AudioPlayer } from './AudioPlayer';
|
3 |
-
import { Podcast, PodcastTurn } from '../utils/types';
|
4 |
-
import { parse } from 'yaml';
|
5 |
-
import {
|
6 |
-
addNoise,
|
7 |
-
addSilence,
|
8 |
-
audioBufferToMp3,
|
9 |
-
generateAudio,
|
10 |
-
isBlogMode,
|
11 |
-
joinAudio,
|
12 |
-
loadWavAndDecode,
|
13 |
-
pickRand,
|
14 |
-
uploadFileToHub,
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
import
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
{ name: 'slow', value: 0.
|
29 |
-
{ name: '
|
30 |
-
{ name: '
|
31 |
-
{ name: '
|
32 |
-
{ name: '
|
33 |
-
{ name: 'fast
|
34 |
-
{ name: 'fast
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
{ name: '🇺🇸 🚺
|
40 |
-
|
41 |
-
{ name: '🇺🇸 🚺
|
42 |
-
{ name: '🇺🇸 🚺
|
43 |
-
{ name: '🇺🇸 🚺
|
44 |
-
{ name: '🇺🇸 🚺
|
45 |
-
{ name: '🇺🇸 🚺
|
46 |
-
{ name: '🇺🇸 🚺
|
47 |
-
{ name: '🇺🇸 🚺
|
48 |
-
{ name: '🇺🇸 🚺
|
49 |
-
{ name: '🇺🇸
|
50 |
-
{ name: '🇺🇸 🚹
|
51 |
-
{ name: '🇺🇸 🚹
|
52 |
-
{ name: '🇺🇸 🚹
|
53 |
-
{ name: '🇺🇸 🚹
|
54 |
-
{ name: '🇺🇸 🚹
|
55 |
-
{ name: '🇺🇸 🚹
|
56 |
-
{ name: '🇺🇸 🚹
|
57 |
-
{ name: '🇺🇸 🚹
|
58 |
-
{ name: '
|
59 |
-
{ name: '🇬🇧 🚺
|
60 |
-
{ name: '🇬🇧 🚺
|
61 |
-
{ name: '🇬🇧 🚺
|
62 |
-
{ name: '🇬🇧
|
63 |
-
{ name: '🇬🇧 🚹
|
64 |
-
{ name: '🇬🇧 🚹
|
65 |
-
{ name: '🇬🇧 🚹
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
const
|
71 |
-
const
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
};
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
}
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
setBusy
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
const [
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
const [
|
113 |
-
|
114 |
-
);
|
115 |
-
const [
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
for (
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
className="
|
265 |
-
value={
|
266 |
-
onChange={(e) =>
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
<
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState } from 'react';
|
2 |
+
import { AudioPlayer } from './AudioPlayer';
|
3 |
+
import { Podcast, PodcastTurn } from '../utils/types';
|
4 |
+
import { parse } from 'yaml';
|
5 |
+
import {
|
6 |
+
addNoise,
|
7 |
+
addSilence,
|
8 |
+
audioBufferToMp3,
|
9 |
+
generateAudio,
|
10 |
+
isBlogMode,
|
11 |
+
joinAudio,
|
12 |
+
loadWavAndDecode,
|
13 |
+
pickRand,
|
14 |
+
uploadFileToHub,
|
15 |
+
denoiseAudioBuffer,
|
16 |
+
} from '../utils/utils';
|
17 |
+
|
18 |
+
// taken from https://freesound.org/people/artxmp1/sounds/660540
|
19 |
+
import openingSoundSrc from '../opening-sound.wav';
|
20 |
+
import { getBlogComment } from '../utils/prompts';
|
21 |
+
|
22 |
+
interface GenerationStep {
|
23 |
+
turn: PodcastTurn;
|
24 |
+
audioBuffer?: AudioBuffer;
|
25 |
+
}
|
26 |
+
|
27 |
+
const SPEEDS = [
|
28 |
+
{ name: 'slow AF', value: 0.8 },
|
29 |
+
{ name: 'slow', value: 0.9 },
|
30 |
+
{ name: 'a bit slow', value: 1.0 },
|
31 |
+
{ name: 'natural', value: 1.1 },
|
32 |
+
{ name: 'most natural', value: 1.2 },
|
33 |
+
{ name: 'a bit fast', value: 1.3 },
|
34 |
+
{ name: 'fast!', value: 1.4 },
|
35 |
+
{ name: 'fast AF', value: 1.5 },
|
36 |
+
];
|
37 |
+
|
38 |
+
const SPEAKERS = [
|
39 |
+
{ name: '🇺🇸 🚺 Heart ❤️', value: 'af_heart' },
|
40 |
+
{ name: '🇺🇸 🚺 Bella 🔥', value: 'af_bella' },
|
41 |
+
// { name: '🇺🇸 🚺 Nicole 🎧', value: 'af_nicole' },
|
42 |
+
{ name: '🇺🇸 🚺 Aoede', value: 'af_aoede' },
|
43 |
+
{ name: '🇺🇸 🚺 Kore', value: 'af_kore' },
|
44 |
+
{ name: '🇺🇸 🚺 Sarah', value: 'af_sarah' },
|
45 |
+
{ name: '🇺🇸 🚺 Nova', value: 'af_nova' },
|
46 |
+
{ name: '🇺🇸 🚺 Sky', value: 'af_sky' },
|
47 |
+
{ name: '🇺🇸 🚺 Alloy', value: 'af_alloy' },
|
48 |
+
{ name: '🇺🇸 🚺 Jessica', value: 'af_jessica' },
|
49 |
+
{ name: '🇺🇸 🚺 River', value: 'af_river' },
|
50 |
+
{ name: '🇺🇸 🚹 Michael', value: 'am_michael' },
|
51 |
+
{ name: '🇺🇸 🚹 Fenrir', value: 'am_fenrir' },
|
52 |
+
{ name: '🇺🇸 🚹 Puck', value: 'am_puck' },
|
53 |
+
{ name: '🇺🇸 🚹 Echo', value: 'am_echo' },
|
54 |
+
{ name: '🇺🇸 🚹 Eric', value: 'am_eric' },
|
55 |
+
{ name: '🇺🇸 🚹 Liam', value: 'am_liam' },
|
56 |
+
{ name: '🇺🇸 🚹 Onyx', value: 'am_onyx' },
|
57 |
+
{ name: '🇺🇸 🚹 Santa', value: 'am_santa' },
|
58 |
+
{ name: '🇺🇸 🚹 Adam', value: 'am_adam' },
|
59 |
+
{ name: '🇬🇧 🚺 Emma', value: 'bf_emma' },
|
60 |
+
{ name: '🇬🇧 🚺 Isabella', value: 'bf_isabella' },
|
61 |
+
{ name: '🇬🇧 🚺 Alice', value: 'bf_alice' },
|
62 |
+
{ name: '🇬🇧 🚺 Lily', value: 'bf_lily' },
|
63 |
+
{ name: '🇬🇧 🚹 George', value: 'bm_george' },
|
64 |
+
{ name: '🇬🇧 🚹 Fable', value: 'bm_fable' },
|
65 |
+
{ name: '🇬🇧 🚹 Lewis', value: 'bm_lewis' },
|
66 |
+
{ name: '🇬🇧 🚹 Daniel', value: 'bm_daniel' },
|
67 |
+
];
|
68 |
+
|
69 |
+
const getRandomSpeakerPair = (): { s1: string; s2: string } => {
|
70 |
+
const s1Gender = Math.random() > 0.5 ? '🚺' : '🚹';
|
71 |
+
const s2Gender = s1Gender === '🚺' ? '🚹' : '🚺';
|
72 |
+
const s1 = pickRand(
|
73 |
+
SPEAKERS.filter((s) => s.name.includes(s1Gender) && s.name.includes('🇺🇸'))
|
74 |
+
).value;
|
75 |
+
const s2 = pickRand(
|
76 |
+
SPEAKERS.filter((s) => s.name.includes(s2Gender) && s.name.includes('🇺🇸'))
|
77 |
+
).value;
|
78 |
+
return { s1, s2 };
|
79 |
+
};
|
80 |
+
|
81 |
+
const parseYAML = (yaml: string): Podcast => {
|
82 |
+
try {
|
83 |
+
return parse(yaml);
|
84 |
+
} catch (e) {
|
85 |
+
console.error(e);
|
86 |
+
throw new Error(
|
87 |
+
'invalid YAML, please re-generate the script: ' + (e as any).message
|
88 |
+
);
|
89 |
+
}
|
90 |
+
};
|
91 |
+
|
92 |
+
const getTimestampedFilename = (ext: string) => {
|
93 |
+
return `podcast_${new Date().toISOString().replace(/:/g, "-").split(".")[0]}.${ext}`;
|
94 |
+
};
|
95 |
+
|
96 |
+
|
97 |
+
export const PodcastGenerator = ({
|
98 |
+
genratedScript,
|
99 |
+
setBusy,
|
100 |
+
blogURL,
|
101 |
+
busy,
|
102 |
+
}: {
|
103 |
+
genratedScript: string;
|
104 |
+
blogURL: string;
|
105 |
+
setBusy: (busy: boolean) => void;
|
106 |
+
busy: boolean;
|
107 |
+
}) => {
|
108 |
+
const [wav, setWav] = useState<AudioBuffer | null>(null);
|
109 |
+
const [wavBlob, setWavBlob] = useState<Blob | null>(null);
|
110 |
+
const [mp3Blob, setMp3Blob] = useState<Blob | null>(null);
|
111 |
+
const [numSteps, setNumSteps] = useState<number>(0);
|
112 |
+
const [numStepsDone, setNumStepsDone] = useState<number>(0);
|
113 |
+
|
114 |
+
const [script, setScript] = useState<string>('');
|
115 |
+
const [speaker1, setSpeaker1] = useState<string>('');
|
116 |
+
const [speaker2, setSpeaker2] = useState<string>('');
|
117 |
+
const [speed, setSpeed] = useState<string>('1.2');
|
118 |
+
const [addIntroMusic, setAddIntroMusic] = useState<boolean>(false);
|
119 |
+
const [enableDenoise, setEnableDenoise] = useState<boolean>(false);
|
120 |
+
|
121 |
+
|
122 |
+
const [blogFilePushToken, setBlogFilePushToken] = useState<string>(
|
123 |
+
localStorage.getItem('blogFilePushToken') || ''
|
124 |
+
);
|
125 |
+
const [blogCmtOutput, setBlogCmtOutput] = useState<string('');
|
126 |
+
const [uploadToBlog, setUploadToBlog] = useState<boolean>(false); // Control blog upload
|
127 |
+
|
128 |
+
|
129 |
+
useEffect(() => {
|
130 |
+
localStorage.setItem('blogFilePushToken', blogFilePushToken);
|
131 |
+
}, [blogFilePushToken]);
|
132 |
+
|
133 |
+
const setRandSpeaker = () => {
|
134 |
+
const { s1, s2 } = getRandomSpeakerPair();
|
135 |
+
setSpeaker1(s1);
|
136 |
+
setSpeaker2(s2);
|
137 |
+
};
|
138 |
+
useEffect(setRandSpeaker, []);
|
139 |
+
|
140 |
+
useEffect(() => {
|
141 |
+
setScript(genratedScript);
|
142 |
+
}, [genratedScript]);
|
143 |
+
|
144 |
+
const generatePodcast = async () => {
|
145 |
+
setWav(null);
|
146 |
+
setWavBlob(null);
|
147 |
+
setMp3Blob(null);
|
148 |
+
setBusy(true);
|
149 |
+
setBlogCmtOutput('');
|
150 |
+
|
151 |
+
// Blog mode check moved inside the upload section
|
152 |
+
|
153 |
+
let outputWav: AudioBuffer;
|
154 |
+
try {
|
155 |
+
const podcast = parseYAML(script);
|
156 |
+
const { speakerNames, turns } = podcast;
|
157 |
+
for (const turn of turns) {
|
158 |
+
// normalize it
|
159 |
+
turn.nextGapMilisecs =
|
160 |
+
Math.max(-600, Math.min(300, turn.nextGapMilisecs)) - 100;
|
161 |
+
turn.text = turn.text
|
162 |
+
.trim()
|
163 |
+
.replace(/’/g, "'")
|
164 |
+
.replace(/“/g, '"')
|
165 |
+
.replace(/”/g, '"');
|
166 |
+
}
|
167 |
+
const steps: GenerationStep[] = turns.map((turn) => ({ turn }));
|
168 |
+
setNumSteps(steps.length);
|
169 |
+
setNumStepsDone(0);
|
170 |
+
for (let i = 0; i < steps.length; i++) {
|
171 |
+
const step = steps[i];
|
172 |
+
const speakerIdx = speakerNames.indexOf(
|
173 |
+
step.turn.speakerName as string
|
174 |
+
) as 1 | 0;
|
175 |
+
const speakerVoice = speakerIdx === 0 ? speaker1 : speaker2;
|
176 |
+
const url = await generateAudio(
|
177 |
+
step.turn.text,
|
178 |
+
speakerVoice,
|
179 |
+
parseFloat(speed)
|
180 |
+
);
|
181 |
+
step.audioBuffer = await loadWavAndDecode(url);
|
182 |
+
if (i === 0) {
|
183 |
+
outputWav = step.audioBuffer;
|
184 |
+
if (addIntroMusic) {
|
185 |
+
const openingSound = await loadWavAndDecode(openingSoundSrc);
|
186 |
+
outputWav = joinAudio(openingSound, outputWav!, -2000);
|
187 |
+
} else {
|
188 |
+
outputWav = addSilence(outputWav!, true, 200);
|
189 |
+
}
|
190 |
+
} else {
|
191 |
+
const lastStep = steps[i - 1];
|
192 |
+
outputWav = joinAudio(
|
193 |
+
outputWav!,
|
194 |
+
step.audioBuffer,
|
195 |
+
lastStep.turn.nextGapMilisecs
|
196 |
+
);
|
197 |
+
}
|
198 |
+
setNumStepsDone(i + 1);
|
199 |
+
}
|
200 |
+
|
201 |
+
// Denoise if enabled
|
202 |
+
if (enableDenoise) {
|
203 |
+
outputWav = await denoiseAudioBuffer(outputWav!);
|
204 |
+
}
|
205 |
+
|
206 |
+
setWav(outputWav! ?? null);
|
207 |
+
// Create WAV blob
|
208 |
+
const wavArrayBuffer = outputWav.getChannelData(0).buffer;
|
209 |
+
setWavBlob(new Blob([wavArrayBuffer], { type: 'audio/wav' }));
|
210 |
+
|
211 |
+
// Convert to MP3 and create MP3 blob
|
212 |
+
const mp3Data = await audioBufferToMp3(outputWav);
|
213 |
+
setMp3Blob(new Blob([mp3Data], { type: 'audio/mp3' }));
|
214 |
+
|
215 |
+
} catch (e) {
|
216 |
+
console.error(e);
|
217 |
+
alert(`Error: ${(e as any).message}`);
|
218 |
+
setWav(null);
|
219 |
+
setWavBlob(null);
|
220 |
+
setMp3Blob(null);
|
221 |
+
}
|
222 |
+
setBusy(false);
|
223 |
+
setNumStepsDone(0);
|
224 |
+
setNumSteps(0);
|
225 |
+
|
226 |
+
// Maybe upload to blog
|
227 |
+
if (isBlogMode && uploadToBlog && outputWav && blogFilePushToken) {
|
228 |
+
if (!blogURL) {
|
229 |
+
alert('Please enter a blog slug');
|
230 |
+
return; // Stop if no blog URL
|
231 |
+
}
|
232 |
+
const repoId = 'ngxson/hf-blog-podcast';
|
233 |
+
const blogSlug = blogURL.split('/blog/').pop() ?? '_noname';
|
234 |
+
const filename = `${blogSlug}.mp3`; //Use Consistent name for blog
|
235 |
+
setBlogCmtOutput(`Uploading '${filename}' ...`);
|
236 |
+
try {
|
237 |
+
await uploadFileToHub(
|
238 |
+
mp3Data, // Use mp3 data from conversion
|
239 |
+
filename,
|
240 |
+
repoId,
|
241 |
+
blogFilePushToken
|
242 |
+
);
|
243 |
+
setBlogCmtOutput(getBlogComment(filename));
|
244 |
+
} catch (uploadError) {
|
245 |
+
console.error("Upload failed:", uploadError);
|
246 |
+
setBlogCmtOutput(`Upload failed: ${(uploadError as any).message}`);
|
247 |
+
}
|
248 |
+
}
|
249 |
+
};
|
250 |
+
|
251 |
+
const isGenerating = numSteps > 0;
|
252 |
+
|
253 |
+
return (
|
254 |
+
<>
|
255 |
+
<div className="card bg-base-100 w-full shadow-xl">
|
256 |
+
<div className="card-body">
|
257 |
+
<h2 className="card-title">Step 2: Script (YAML format)</h2>
|
258 |
+
|
259 |
+
{isBlogMode && (
|
260 |
+
<>
|
261 |
+
<input
|
262 |
+
type="password"
|
263 |
+
placeholder="Repo push HF_TOKEN"
|
264 |
+
className="input input-bordered w-full"
|
265 |
+
value={blogFilePushToken}
|
266 |
+
onChange={(e) => setBlogFilePushToken(e.target.value)}
|
267 |
+
/>
|
268 |
+
<div className="flex items-center gap-2">
|
269 |
+
<input
|
270 |
+
type="checkbox"
|
271 |
+
className="checkbox"
|
272 |
+
checked={uploadToBlog}
|
273 |
+
onChange={(e) => setUploadToBlog(e.target.checked)}
|
274 |
+
disabled={isGenerating || busy}
|
275 |
+
/>
|
276 |
+
Upload to Blog
|
277 |
+
</div>
|
278 |
+
</>
|
279 |
+
)}
|
280 |
+
|
281 |
+
<textarea
|
282 |
+
className="textarea textarea-bordered w-full h-72 p-2"
|
283 |
+
placeholder="Type your script here..."
|
284 |
+
value={script}
|
285 |
+
onChange={(e) => setScript(e.target.value)}
|
286 |
+
></textarea>
|
287 |
+
|
288 |
+
<div className="grid grid-cols-2 gap-4">
|
289 |
+
<label className="form-control w-full">
|
290 |
+
<div className="label">
|
291 |
+
<span className="label-text">Speaker 1</span>
|
292 |
+
</div>
|
293 |
+
<select
|
294 |
+
className="select select-bordered"
|
295 |
+
value={speaker1}
|
296 |
+
onChange={(e) => setSpeaker1(e.target.value)}
|
297 |
+
>
|
298 |
+
{SPEAKERS.map((s) => (
|
299 |
+
<option key={s.value} value={s.value}>
|
300 |
+
{s.name}
|
301 |
+
</option>
|
302 |
+
))}
|
303 |
+
</select>
|
304 |
+
</label>
|
305 |
+
|
306 |
+
<label className="form-control w-full">
|
307 |
+
<div className="label">
|
308 |
+
<span className="label-text">Speaker 2</span>
|
309 |
+
</div>
|
310 |
+
<select
|
311 |
+
className="select select-bordered"
|
312 |
+
value={speaker2}
|
313 |
+
onChange={(e) => setSpeaker2(e.target.value)}
|
314 |
+
>
|
315 |
+
{SPEAKERS.map((s) => (
|
316 |
+
<option key={s.value} value={s.value}>
|
317 |
+
{s.name}
|
318 |
+
</option>
|
319 |
+
))}
|
320 |
+
</select>
|
321 |
+
</label>
|
322 |
+
|
323 |
+
<button className="btn" onClick={setRandSpeaker}>
|
324 |
+
Randomize speakers
|
325 |
+
</button>
|
326 |
+
|
327 |
+
<label className="form-control w-full">
|
328 |
+
<select
|
329 |
+
className="select select-bordered"
|
330 |
+
value={speed.toString()}
|
331 |
+
onChange={(e) => setSpeed(e.target.value)}
|
332 |
+
>
|
333 |
+
{SPEEDS.map((s) => (
|
334 |
+
<option key={s.value} value={s.value.toString()}>
|
335 |
+
Speed: {s.name} ({s.value})
|
336 |
+
</option>
|
337 |
+
))}
|
338 |
+
</select>
|
339 |
+
</label>
|
340 |
+
|
341 |
+
<div className="flex items-center gap-2">
|
342 |
+
<input
|
343 |
+
type="checkbox"
|
344 |
+
className="checkbox"
|
345 |
+
checked={addIntroMusic}
|
346 |
+
onChange={(e) => setAddIntroMusic(e.target.checked)}
|
347 |
+
disabled={isGenerating || busy}
|
348 |
+
/>
|
349 |
+
Add intro music
|
350 |
+
</div>
|
351 |
+
|
352 |
+
<div className="flex items-center gap-2">
|
353 |
+
<input
|
354 |
+
type="checkbox"
|
355 |
+
className="checkbox"
|
356 |
+
checked={enableDenoise}
|
357 |
+
onChange={(e) => setEnableDenoise(e.target.checked)}
|
358 |
+
disabled={isGenerating || busy}
|
359 |
+
/>
|
360 |
+
Enable Noise Reduction
|
361 |
+
</div>
|
362 |
+
</div>
|
363 |
+
|
364 |
+
<button
|
365 |
+
id="btn-generate-podcast"
|
366 |
+
className="btn btn-primary mt-2"
|
367 |
+
onClick={generatePodcast}
|
368 |
+
disabled={busy || !script || isGenerating}
|
369 |
+
>
|
370 |
+
{isGenerating ? (
|
371 |
+
<>
|
372 |
+
<span className="loading loading-spinner loading-sm"></span>
|
373 |
+
Generating ({numStepsDone}/{numSteps})...
|
374 |
+
</>
|
375 |
+
) : (
|
376 |
+
'Generate podcast'
|
377 |
+
)}
|
378 |
+
</button>
|
379 |
+
|
380 |
+
{isGenerating && (
|
381 |
+
<progress
|
382 |
+
className="progress progress-primary mt-2"
|
383 |
+
value={numStepsDone}
|
384 |
+
max={numSteps}
|
385 |
+
></progress>
|
386 |
+
)}
|
387 |
+
</div>
|
388 |
+
</div>
|
389 |
+
|
390 |
+
{wav && (
|
391 |
+
<div className="card bg-base-100 w-full shadow-xl">
|
392 |
+
<div className="card-body">
|
393 |
+
<h2 className="card-title">Step 3: Listen to your podcast</h2>
|
394 |
+
<AudioPlayer audioBuffer={wav} />
|
395 |
+
{wavBlob && (
|
396 |
+
<a
|
397 |
+
className="btn btn-sm btn-primary ml-2"
|
398 |
+
href={URL.createObjectURL(wavBlob)}
|
399 |
+
download={getTimestampedFilename('wav')}
|
400 |
+
>
|
401 |
+
Download WAV
|
402 |
+
</a>
|
403 |
+
)}
|
404 |
+
|
405 |
+
{mp3Blob && (
|
406 |
+
<a
|
407 |
+
className="btn btn-sm btn-primary ml-2"
|
408 |
+
href={URL.createObjectURL(mp3Blob)}
|
409 |
+
download={getTimestampedFilename('mp3')}
|
410 |
+
>
|
411 |
+
Download MP3
|
412 |
+
</a>
|
413 |
+
)}
|
414 |
+
|
415 |
+
{isBlogMode && (
|
416 |
+
<div>
|
417 |
+
-------------------
|
418 |
+
<br />
|
419 |
+
<h2>Comment to be posted:</h2>
|
420 |
+
<pre className="p-2 bg-base-200 rounded-md my-2 whitespace-pre-wrap break-words">
|
421 |
+
{blogCmtOutput}
|
422 |
+
</pre>
|
423 |
+
<button
|
424 |
+
className="btn btn-sm btn-secondary"
|
425 |
+
onClick={() => copyStr(blogCmtOutput)}
|
426 |
+
>
|
427 |
+
Copy comment
|
428 |
+
</button>
|
429 |
+
</div>
|
430 |
+
)}
|
431 |
+
</div>
|
432 |
+
</div>
|
433 |
+
)}
|
434 |
+
</>
|
435 |
+
);
|
436 |
+
};
|
437 |
+
|
438 |
+
// copy text to clipboard
|
439 |
+
export const copyStr = (textToCopy: string) => {
|
440 |
+
// Navigator clipboard api needs a secure context (https)
|
441 |
+
if (navigator.clipboard && window.isSecureContext) {
|
442 |
+
navigator.clipboard.writeText(textToCopy);
|
443 |
+
} else {
|
444 |
+
// Use the 'out of viewport hidden text area' trick
|
445 |
+
const textArea = document.createElement('textarea');
|
446 |
+
textArea.value = textToCopy;
|
447 |
+
// Move textarea out of the viewport so it's not visible
|
448 |
+
textArea.style.position = 'absolute';
|
449 |
+
textArea.style.left = '-999999px';
|
450 |
+
document.body.prepend(textArea);
|
451 |
+
textArea.select();
|
452 |
+
document.execCommand('copy');
|
453 |
+
}
|
454 |
+
};
|