import { useEffect, useState } from 'react'; import { AudioPlayer } from './AudioPlayer'; import { Podcast, PodcastTurn } from '../utils/types'; import { parse } from 'yaml'; import { addNoise, addSilence, audioBufferToMp3, generateAudio, isBlogMode, joinAudio, loadWavAndDecode, pickRand, uploadFileToHub, } from '../utils/utils'; // taken from https://freesound.org/people/artxmp1/sounds/660540 import openingSoundSrc from '../opening-sound.wav'; import { getBlogComment } from '../utils/prompts'; interface GenerationStep { turn: PodcastTurn; audioBuffer?: AudioBuffer; } const SPEEDS = [ { name: 'slow AF', value: 0.8 }, { name: 'slow', value: 0.9 }, { name: 'a bit slow', value: 1.0 }, { name: 'natural', value: 1.1 }, { name: 'most natural', value: 1.2 }, { name: 'a bit fast', value: 1.3 }, { name: 'fast!', value: 1.4 }, { name: 'fast AF', value: 1.5 }, ]; const SPEAKERS = [ { name: 'πŸ‡ΊπŸ‡Έ 🚺 Heart ❀️', value: 'af_heart' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Bella πŸ”₯', value: 'af_bella' }, // { name: 'πŸ‡ΊπŸ‡Έ 🚺 Nicole 🎧', value: 'af_nicole' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Aoede', value: 'af_aoede' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Kore', value: 'af_kore' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Sarah', value: 'af_sarah' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Nova', value: 'af_nova' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Sky', value: 'af_sky' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Alloy', value: 'af_alloy' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 Jessica', value: 'af_jessica' }, { name: 'πŸ‡ΊπŸ‡Έ 🚺 River', value: 'af_river' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Michael', value: 'am_michael' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Fenrir', value: 'am_fenrir' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Puck', value: 'am_puck' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Echo', value: 'am_echo' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Eric', value: 'am_eric' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Liam', value: 'am_liam' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Onyx', value: 'am_onyx' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Santa', value: 'am_santa' }, { name: 'πŸ‡ΊπŸ‡Έ 🚹 Adam', value: 'am_adam' }, { name: 'πŸ‡¬πŸ‡§ 🚺 Emma', value: 'bf_emma' }, { name: 'πŸ‡¬πŸ‡§ 🚺 Isabella', value: 'bf_isabella' }, { name: 'πŸ‡¬πŸ‡§ 🚺 Alice', value: 'bf_alice' }, { name: 'πŸ‡¬πŸ‡§ 🚺 Lily', value: 'bf_lily' }, { name: 'πŸ‡¬πŸ‡§ 🚹 George', value: 'bm_george' }, { name: 'πŸ‡¬πŸ‡§ 🚹 Fable', value: 'bm_fable' }, { name: 'πŸ‡¬πŸ‡§ 🚹 Lewis', value: 'bm_lewis' }, { name: 'πŸ‡¬πŸ‡§ 🚹 Daniel', value: 'bm_daniel' }, ]; const getRandomSpeakerPair = (): { s1: string; s2: string } => { const s1Gender = Math.random() > 0.5 ? '🚺' : '🚹'; const s2Gender = s1Gender === '🚺' ? '🚹' : '🚺'; const s1 = pickRand( SPEAKERS.filter((s) => s.name.includes(s1Gender) && s.name.includes('πŸ‡ΊπŸ‡Έ')) ).value; const s2 = pickRand( SPEAKERS.filter((s) => s.name.includes(s2Gender) && s.name.includes('πŸ‡ΊπŸ‡Έ')) ).value; return { s1, s2 }; }; const parseYAML = (yaml: string): Podcast => { try { return parse(yaml); } catch (e) { console.error(e); throw new Error( 'invalid YAML, please re-generate the script: ' + (e as any).message ); } }; export const PodcastGenerator = ({ genratedScript, setBusy, blogURL, busy, }: { genratedScript: string; blogURL: string; setBusy: (busy: boolean) => void; busy: boolean; }) => { const [wav, setWav] = useState(null); const [numSteps, setNumSteps] = useState(0); const [numStepsDone, setNumStepsDone] = useState(0); const [script, setScript] = useState(''); const [speaker1, setSpeaker1] = useState(''); const [speaker2, setSpeaker2] = useState(''); const [speed, setSpeed] = useState('1.2'); const [addIntroMusic, setAddIntroMusic] = useState(false); const [blogFilePushToken, setBlogFilePushToken] = useState( localStorage.getItem('blogFilePushToken') || '' ); const [blogCmtOutput, setBlogCmtOutput] = useState(''); useEffect(() => { localStorage.setItem('blogFilePushToken', blogFilePushToken); }, [blogFilePushToken]); const setRandSpeaker = () => { const { s1, s2 } = getRandomSpeakerPair(); setSpeaker1(s1); setSpeaker2(s2); }; useEffect(setRandSpeaker, []); useEffect(() => { setScript(genratedScript); }, [genratedScript]); const generatePodcast = async () => { setWav(null); setBusy(true); setBlogCmtOutput(''); if (isBlogMode && !blogURL) { alert('Please enter a blog slug'); setBusy(false); return; } let outputWav: AudioBuffer; try { const podcast = parseYAML(script); const { speakerNames, turns } = podcast; for (const turn of turns) { // normalize it turn.nextGapMilisecs = Math.max(-600, Math.min(300, turn.nextGapMilisecs)) - 100; turn.text = turn.text .trim() .replace(/’/g, "'") .replace(/β€œ/g, '"') .replace(/”/g, '"'); } const steps: GenerationStep[] = turns.map((turn) => ({ turn })); setNumSteps(steps.length); setNumStepsDone(0); for (let i = 0; i < steps.length; i++) { const step = steps[i]; const speakerIdx = speakerNames.indexOf( step.turn.speakerName as string ) as 1 | 0; const speakerVoice = speakerIdx === 0 ? speaker1 : speaker2; const url = await generateAudio( step.turn.text, speakerVoice, parseFloat(speed) ); step.audioBuffer = await loadWavAndDecode(url); if (i === 0) { outputWav = step.audioBuffer; if (addIntroMusic) { const openingSound = await loadWavAndDecode(openingSoundSrc); outputWav = joinAudio(openingSound, outputWav!, -2000); } else { outputWav = addSilence(outputWav!, true, 200); } } else { const lastStep = steps[i - 1]; outputWav = joinAudio( outputWav!, step.audioBuffer, lastStep.turn.nextGapMilisecs ); } setNumStepsDone(i + 1); } outputWav = addNoise(outputWav!, 0.002); setWav(outputWav! ?? null); } catch (e) { console.error(e); alert(`Error: ${(e as any).message}`); setWav(null); } setBusy(false); setNumStepsDone(0); setNumSteps(0); // maybe upload if (isBlogMode && outputWav!) { const repoId = 'ngxson/hf-blog-podcast'; const blogSlug = blogURL.split('/blog/').pop() ?? '_noname'; const filename = `${blogSlug}.mp3`; setBlogCmtOutput(`Uploading '${filename}' ...`); await uploadFileToHub( audioBufferToMp3(outputWav), filename, repoId, blogFilePushToken ); setBlogCmtOutput(getBlogComment(filename)); } }; const isGenerating = numSteps > 0; return ( <>

Step 2: Script (YAML format)

{isBlogMode && ( <> setBlogFilePushToken(e.target.value)} /> )}
setAddIntroMusic(e.target.checked)} disabled={isGenerating || busy} /> Add intro music
{isGenerating && ( )}
{wav && (

Step 3: Listen to your podcast

{isBlogMode && (
-------------------

Comment to be posted:

                  {blogCmtOutput}
                
)}
)} ); }; // copy text to clipboard export const copyStr = (textToCopy: string) => { // Navigator clipboard api needs a secure context (https) if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy); } else { // Use the 'out of viewport hidden text area' trick const textArea = document.createElement('textarea'); textArea.value = textToCopy; // Move textarea out of the viewport so it's not visible textArea.style.position = 'absolute'; textArea.style.left = '-999999px'; document.body.prepend(textArea); textArea.select(); document.execCommand('copy'); } };