ngxson HF Staff commited on
Commit
5d4d4d4
·
1 Parent(s): 64db5cc

fix filename, add convert to mp3

Browse files
front/src/components/AudioPlayer.tsx CHANGED
@@ -1,30 +1,59 @@
1
- import React, { useMemo, useEffect } from 'react';
2
- import { blobFromAudioBuffer } from '../utils/utils';
 
 
 
 
 
3
 
4
  interface AudioPlayerProps {
5
  audioBuffer: AudioBuffer;
 
6
  }
7
 
8
- export const AudioPlayer: React.FC<AudioPlayerProps> = ({ audioBuffer }) => {
9
- // Create a Blob URL from the audioBuffer.
 
 
 
 
 
10
  const blobUrl = useMemo(() => {
11
  const wavBlob = blobFromAudioBuffer(audioBuffer);
12
  return URL.createObjectURL(wavBlob);
13
  }, [audioBuffer]);
14
 
15
- const downloadUrl = useMemo(() => {
16
  const wavBlob = blobFromAudioBuffer(audioBuffer);
17
  return URL.createObjectURL(wavBlob);
18
  }, [audioBuffer]);
19
 
 
 
 
 
 
 
20
  // Clean up the object URL when the component unmounts or audioBuffer changes.
21
  useEffect(() => {
22
- return () => {
23
- URL.revokeObjectURL(blobUrl);
24
- URL.revokeObjectURL(downloadUrl);
25
- };
26
  }, [blobUrl]);
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  return (
29
  <div className="mt-4 flex items-center">
30
  <audio controls src={blobUrl}>
@@ -33,11 +62,30 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({ audioBuffer }) => {
33
 
34
  <a
35
  className="btn btn-sm btn-primary ml-2"
36
- href={downloadUrl}
37
- download={'podcast.wav'}
38
  >
39
  Download WAV
40
  </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </div>
42
  );
43
  };
 
1
+ import React, { useMemo, useEffect, useState } from 'react';
2
+ import {
3
+ audioBufferToMp3,
4
+ blobFromAudioBuffer,
5
+ cleanupFilename,
6
+ delay,
7
+ } from '../utils/utils';
8
 
9
  interface AudioPlayerProps {
10
  audioBuffer: AudioBuffer;
11
+ title: string;
12
  }
13
 
14
+ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
15
+ audioBuffer,
16
+ title,
17
+ }) => {
18
+ const [isConverting, setIsConverting] = useState(false);
19
+ const [mp3Buffer, setMp3Buffer] = useState<ArrayBuffer | null>(null);
20
+
21
  const blobUrl = useMemo(() => {
22
  const wavBlob = blobFromAudioBuffer(audioBuffer);
23
  return URL.createObjectURL(wavBlob);
24
  }, [audioBuffer]);
25
 
26
+ const downloadWavUrl = useMemo(() => {
27
  const wavBlob = blobFromAudioBuffer(audioBuffer);
28
  return URL.createObjectURL(wavBlob);
29
  }, [audioBuffer]);
30
 
31
+ const downloadMp3Url = useMemo(() => {
32
+ if (!mp3Buffer) return '';
33
+ const mp3Blob = new Blob([mp3Buffer], { type: 'audio/mp3' });
34
+ return URL.createObjectURL(mp3Blob);
35
+ }, [audioBuffer]);
36
+
37
  // Clean up the object URL when the component unmounts or audioBuffer changes.
38
  useEffect(() => {
39
+ return () => URL.revokeObjectURL(blobUrl);
 
 
 
40
  }, [blobUrl]);
41
 
42
+ useEffect(() => {
43
+ return () => URL.revokeObjectURL(downloadWavUrl);
44
+ }, [downloadWavUrl]);
45
+
46
+ useEffect(() => {
47
+ return () => URL.revokeObjectURL(downloadMp3Url);
48
+ }, [downloadMp3Url]);
49
+
50
+ const convertToMp3 = async () => {
51
+ setIsConverting(true);
52
+ await delay(10); // wait a bit for the button to be rendered
53
+ setMp3Buffer(audioBufferToMp3(audioBuffer));
54
+ setIsConverting(false);
55
+ };
56
+
57
  return (
58
  <div className="mt-4 flex items-center">
59
  <audio controls src={blobUrl}>
 
62
 
63
  <a
64
  className="btn btn-sm btn-primary ml-2"
65
+ href={downloadWavUrl}
66
+ download={`Podcast_${cleanupFilename(title)}.wav`}
67
  >
68
  Download WAV
69
  </a>
70
+ {mp3Buffer ? (
71
+ <a
72
+ className="btn btn-sm btn-primary ml-2"
73
+ href={downloadMp3Url}
74
+ download={`Podcast_${cleanupFilename(title)}.mp3`}
75
+ >
76
+ Download MP3
77
+ </a>
78
+ ) : (
79
+ <button
80
+ className="btn btn-sm ml-2"
81
+ disabled={isConverting}
82
+ onClick={convertToMp3}
83
+ >
84
+ {isConverting
85
+ ? 'Converting...'
86
+ : 'Convert to MP3 (may take ~10 seconds)'}
87
+ </button>
88
+ )}
89
  </div>
90
  );
91
  };
front/src/components/PodcastGenerator.tsx CHANGED
@@ -89,6 +89,7 @@ export const PodcastGenerator = ({
89
  busy: boolean;
90
  }) => {
91
  const [wav, setWav] = useState<AudioBuffer | null>(null);
 
92
  const [numSteps, setNumSteps] = useState<number>(0);
93
  const [numStepsDone, setNumStepsDone] = useState<number>(0);
94
 
@@ -138,6 +139,7 @@ export const PodcastGenerator = ({
138
  let outputWav: AudioBuffer;
139
  try {
140
  const podcast = parseYAML(script);
 
141
  outputWav = await pipelineGeneratePodcast(
142
  {
143
  podcast,
@@ -312,7 +314,7 @@ export const PodcastGenerator = ({
312
  <div className="card bg-base-100 w-full shadow-xl">
313
  <div className="card-body">
314
  <h2 className="card-title">Step 3: Listen to your podcast</h2>
315
- <AudioPlayer audioBuffer={wav} />
316
 
317
  {isBlogMode && (
318
  <div>
 
89
  busy: boolean;
90
  }) => {
91
  const [wav, setWav] = useState<AudioBuffer | null>(null);
92
+ const [outTitle, setOutTitle] = useState<string>('');
93
  const [numSteps, setNumSteps] = useState<number>(0);
94
  const [numStepsDone, setNumStepsDone] = useState<number>(0);
95
 
 
139
  let outputWav: AudioBuffer;
140
  try {
141
  const podcast = parseYAML(script);
142
+ setOutTitle(podcast.title ?? 'Untitled podcast');
143
  outputWav = await pipelineGeneratePodcast(
144
  {
145
  podcast,
 
314
  <div className="card bg-base-100 w-full shadow-xl">
315
  <div className="card-body">
316
  <h2 className="card-title">Step 3: Listen to your podcast</h2>
317
+ <AudioPlayer audioBuffer={wav} title={outTitle} />
318
 
319
  {isBlogMode && (
320
  <div>
front/src/utils/utils.ts CHANGED
@@ -12,6 +12,8 @@ export const isDev: boolean = import.meta.env.MODE === 'development';
12
  export const testToken: string = import.meta.env.VITE_TEST_TOKEN;
13
  export const isBlogMode: boolean = !!window.location.href.match(/blogmode/);
14
 
 
 
15
  // return URL to the WAV file
16
  export const generateAudio = async (
17
  content: string,
@@ -507,3 +509,8 @@ function floatTo16BitPCM(input: Float32Array): Int16Array {
507
  }
508
  return output;
509
  }
 
 
 
 
 
 
12
  export const testToken: string = import.meta.env.VITE_TEST_TOKEN;
13
  export const isBlogMode: boolean = !!window.location.href.match(/blogmode/);
14
 
15
+ export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
16
+
17
  // return URL to the WAV file
18
  export const generateAudio = async (
19
  content: string,
 
509
  }
510
  return output;
511
  }
512
+
513
+ // clean up filename for saving
514
+ export const cleanupFilename = (name: string): string => {
515
+ return name.replace(/[^a-zA-Z0-9-_]/g, '_');
516
+ };
index.html CHANGED
@@ -25406,6 +25406,7 @@ async function whoAmI(params) {
25406
  return response;
25407
  }
25408
  const isBlogMode = !!window.location.href.match(/blogmode/);
 
25409
  const generateAudio = async (content, voice, speed = 1.1) => {
25410
  const maxRetries = 3;
25411
  for (let i = 0; i < maxRetries; i++) {
@@ -25704,31 +25705,70 @@ function floatTo16BitPCM(input) {
25704
  }
25705
  return output;
25706
  }
25707
- const AudioPlayer = ({ audioBuffer }) => {
 
 
 
 
 
 
 
 
25708
  const blobUrl = reactExports.useMemo(() => {
25709
  const wavBlob = blobFromAudioBuffer(audioBuffer);
25710
  return URL.createObjectURL(wavBlob);
25711
  }, [audioBuffer]);
25712
- const downloadUrl = reactExports.useMemo(() => {
25713
  const wavBlob = blobFromAudioBuffer(audioBuffer);
25714
  return URL.createObjectURL(wavBlob);
25715
  }, [audioBuffer]);
 
 
 
 
 
25716
  reactExports.useEffect(() => {
25717
- return () => {
25718
- URL.revokeObjectURL(blobUrl);
25719
- URL.revokeObjectURL(downloadUrl);
25720
- };
25721
  }, [blobUrl]);
 
 
 
 
 
 
 
 
 
 
 
 
25722
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 flex items-center", children: [
25723
  /* @__PURE__ */ jsxRuntimeExports.jsx("audio", { controls: true, src: blobUrl, children: "Your browser does not support the audio element." }),
25724
  /* @__PURE__ */ jsxRuntimeExports.jsx(
25725
  "a",
25726
  {
25727
  className: "btn btn-sm btn-primary ml-2",
25728
- href: downloadUrl,
25729
- download: "podcast.wav",
25730
  children: "Download WAV"
25731
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25732
  )
25733
  ] });
25734
  };
@@ -31966,6 +32006,7 @@ const PodcastGenerator = ({
31966
  busy
31967
  }) => {
31968
  const [wav, setWav] = reactExports.useState(null);
 
31969
  const [numSteps, setNumSteps] = reactExports.useState(0);
31970
  const [numStepsDone, setNumStepsDone] = reactExports.useState(0);
31971
  const [script, setScript] = reactExports.useState("");
@@ -32008,6 +32049,7 @@ const PodcastGenerator = ({
32008
  let outputWav;
32009
  try {
32010
  const podcast = parseYAML(script);
 
32011
  outputWav = await pipelineGeneratePodcast(
32012
  {
32013
  podcast,
@@ -32165,7 +32207,7 @@ const PodcastGenerator = ({
32165
  ] }) }),
32166
  wav && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "card bg-base-100 w-full shadow-xl", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "card-body", children: [
32167
  /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "card-title", children: "Step 3: Listen to your podcast" }),
32168
- /* @__PURE__ */ jsxRuntimeExports.jsx(AudioPlayer, { audioBuffer: wav }),
32169
  isBlogMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
32170
  "-------------------",
32171
  /* @__PURE__ */ jsxRuntimeExports.jsx("br", {}),
 
25406
  return response;
25407
  }
25408
  const isBlogMode = !!window.location.href.match(/blogmode/);
25409
+ const delay$1 = (ms) => new Promise((res) => setTimeout(res, ms));
25410
  const generateAudio = async (content, voice, speed = 1.1) => {
25411
  const maxRetries = 3;
25412
  for (let i = 0; i < maxRetries; i++) {
 
25705
  }
25706
  return output;
25707
  }
25708
+ const cleanupFilename = (name2) => {
25709
+ return name2.replace(/[^a-zA-Z0-9-_]/g, "_");
25710
+ };
25711
+ const AudioPlayer = ({
25712
+ audioBuffer,
25713
+ title
25714
+ }) => {
25715
+ const [isConverting, setIsConverting] = reactExports.useState(false);
25716
+ const [mp3Buffer, setMp3Buffer] = reactExports.useState(null);
25717
  const blobUrl = reactExports.useMemo(() => {
25718
  const wavBlob = blobFromAudioBuffer(audioBuffer);
25719
  return URL.createObjectURL(wavBlob);
25720
  }, [audioBuffer]);
25721
+ const downloadWavUrl = reactExports.useMemo(() => {
25722
  const wavBlob = blobFromAudioBuffer(audioBuffer);
25723
  return URL.createObjectURL(wavBlob);
25724
  }, [audioBuffer]);
25725
+ const downloadMp3Url = reactExports.useMemo(() => {
25726
+ if (!mp3Buffer) return "";
25727
+ const mp3Blob = new Blob([mp3Buffer], { type: "audio/mp3" });
25728
+ return URL.createObjectURL(mp3Blob);
25729
+ }, [audioBuffer]);
25730
  reactExports.useEffect(() => {
25731
+ return () => URL.revokeObjectURL(blobUrl);
 
 
 
25732
  }, [blobUrl]);
25733
+ reactExports.useEffect(() => {
25734
+ return () => URL.revokeObjectURL(downloadWavUrl);
25735
+ }, [downloadWavUrl]);
25736
+ reactExports.useEffect(() => {
25737
+ return () => URL.revokeObjectURL(downloadMp3Url);
25738
+ }, [downloadMp3Url]);
25739
+ const convertToMp3 = async () => {
25740
+ setIsConverting(true);
25741
+ await delay$1(10);
25742
+ setMp3Buffer(audioBufferToMp3(audioBuffer));
25743
+ setIsConverting(false);
25744
+ };
25745
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 flex items-center", children: [
25746
  /* @__PURE__ */ jsxRuntimeExports.jsx("audio", { controls: true, src: blobUrl, children: "Your browser does not support the audio element." }),
25747
  /* @__PURE__ */ jsxRuntimeExports.jsx(
25748
  "a",
25749
  {
25750
  className: "btn btn-sm btn-primary ml-2",
25751
+ href: downloadWavUrl,
25752
+ download: `Podcast_${cleanupFilename(title)}.wav`,
25753
  children: "Download WAV"
25754
  }
25755
+ ),
25756
+ mp3Buffer ? /* @__PURE__ */ jsxRuntimeExports.jsx(
25757
+ "a",
25758
+ {
25759
+ className: "btn btn-sm btn-primary ml-2",
25760
+ href: downloadMp3Url,
25761
+ download: `Podcast_${cleanupFilename(title)}.mp3`,
25762
+ children: "Download MP3"
25763
+ }
25764
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
25765
+ "button",
25766
+ {
25767
+ className: "btn btn-sm ml-2",
25768
+ disabled: isConverting,
25769
+ onClick: convertToMp3,
25770
+ children: isConverting ? "Converting..." : "Convert to MP3 (may take ~10 seconds)"
25771
+ }
25772
  )
25773
  ] });
25774
  };
 
32006
  busy
32007
  }) => {
32008
  const [wav, setWav] = reactExports.useState(null);
32009
+ const [outTitle, setOutTitle] = reactExports.useState("");
32010
  const [numSteps, setNumSteps] = reactExports.useState(0);
32011
  const [numStepsDone, setNumStepsDone] = reactExports.useState(0);
32012
  const [script, setScript] = reactExports.useState("");
 
32049
  let outputWav;
32050
  try {
32051
  const podcast = parseYAML(script);
32052
+ setOutTitle(podcast.title ?? "Untitled podcast");
32053
  outputWav = await pipelineGeneratePodcast(
32054
  {
32055
  podcast,
 
32207
  ] }) }),
32208
  wav && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "card bg-base-100 w-full shadow-xl", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "card-body", children: [
32209
  /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "card-title", children: "Step 3: Listen to your podcast" }),
32210
+ /* @__PURE__ */ jsxRuntimeExports.jsx(AudioPlayer, { audioBuffer: wav, title: outTitle }),
32211
  isBlogMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
32212
  "-------------------",
32213
  /* @__PURE__ */ jsxRuntimeExports.jsx("br", {}),