Skip to content

Commit

Permalink
feat: improve download modal and handle errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ccoreilly committed Nov 17, 2024
1 parent 3cdfc36 commit 8709a8e
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 149 deletions.
108 changes: 53 additions & 55 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { DubbingAPIService } from './services/DubbingAPIService';
import { audioService } from './services/AudioService';
import VideoOptions from './components/VideoOptions';
import DownloadModal from './components/DownloadModal';
import LoadingOverlay from './components/LoadingOverlay';
import { speakerService } from './services/SpeakerService';
import { Voice } from './types/Voice';
import { AudioTrack } from './types/AudioTrack';
Expand Down Expand Up @@ -149,7 +148,6 @@ function App() {
const [selectedSubtitles, setSelectedSubtitles] = useState<string>('none');
const [showSpeakerColors, setShowSpeakerColors] = useState(true);
const [showDownloadModal, setShowDownloadModal] = useState(false);
const [isRebuilding, setIsRebuilding] = useState(false);
const [appMode, setAppMode] = useState<'dubbing' | 'transcription' | 'file' | null>(
process.env.APP_MODE as 'dubbing' | 'transcription' | 'file' | null
);
Expand All @@ -160,6 +158,7 @@ function App() {
const [isEditMode, setIsEditMode] = useState(false);
const [timelineVisible, setTimelineVisible] = useState(false);
const [showRegenerateModal, setShowRegenerateModal] = useState(false);
const [downloadProgress, setDownloadProgress] = useState<string>('');

useEffect(() => {
const params = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -300,11 +299,10 @@ function App() {
}, [serviceParam]);

const loadChunksInBackground = useCallback(async (uuid: string, tracks: Track[]) => {
const dubbedTracks = tracks.filter(track => track.dubbed_path && track.for_dubbing);
const totalChunks = dubbedTracks.length;
let loadedChunks = 0;

try {
const dubbedTracks = tracks.filter(track => track.dubbed_path && track.for_dubbing);
const totalChunks = dubbedTracks.length;
let loadedChunks = 0;

const newChunkBuffers: { [key: string]: ArrayBuffer } = {};

Expand Down Expand Up @@ -419,57 +417,57 @@ function App() {
setShowDownloadModal(false);
};

const handleDownloadWithSelectedTracks = async (selectedAudioTracks: string[], selectedSubtitles: string[]) => {
const handleDownloadWithSelectedTracks = async (
selectedAudioTracks: string[],
selectedSubtitles: string[],
) => {
if (mediaUrl && tracks.length > 0) {
try {
console.log("handleDownloadWithSelectedTracks", selectedAudioTracks, selectedSubtitles);
setIsRebuilding(true);
let fileToProcess: File | string = mediaFile || mediaUrl;

// Download and decode background audio
const backgroundArrayBuffer = await audioService.downloadAudioURL(audioTracks.background.url);
const backgroundBuffer = await audioService.decodeAudioData(backgroundArrayBuffer);

const selectedAudioBuffers: { buffer: AudioBuffer, label: string }[] = [];

for (const selectedAudioTrack of selectedAudioTracks) {
let audioBuffer: AudioBuffer | null = null;
if (selectedAudioTrack === 'dubbed') {
audioBuffer = dubbedAudioBuffer;
} else {
const audioTrack = audioTracks[selectedAudioTrack];
if (audioTrack) {
const audioArrayBuffer = await audioService.downloadAudioURL(audioTrack.url);
audioBuffer = await audioService.decodeAudioData(audioArrayBuffer);
}
}
if (audioBuffer && backgroundBuffer) {
const finalAudioBuffer = await audioService.mixAudioBuffers(
backgroundBuffer,
audioBuffer
);
selectedAudioBuffers.push({ buffer: finalAudioBuffer, label: selectedAudioTrack });
} else {
throw new Error(`Audio track ${selectedAudioTrack} not found`);
let fileToProcess: File | string = mediaFile || mediaUrl;

setDownloadProgress(t('downloadingBackgroundAudio'));
// Download and decode background audio
const backgroundArrayBuffer = await audioService.downloadAudioURL(audioTracks.background.url);
const backgroundBuffer = await audioService.decodeAudioData(backgroundArrayBuffer);

const selectedAudioBuffers: { buffer: AudioBuffer, label: string }[] = [];

for (const selectedAudioTrack of selectedAudioTracks) {
setDownloadProgress(t('downloadingTrack', { track: audioTracks[selectedAudioTrack]?.label.toLowerCase() }));
let audioBuffer: AudioBuffer | null = null;
if (selectedAudioTrack === 'dubbed') {
audioBuffer = dubbedAudioBuffer;
} else {
const audioTrack = audioTracks[selectedAudioTrack];
if (audioTrack) {
const audioArrayBuffer = await audioService.downloadAudioURL(audioTrack.url);
audioBuffer = await audioService.decodeAudioData(audioArrayBuffer);
}
}
console.log("selectedAudioBuffers", selectedAudioBuffers);

const newMediaBlob = await rebuildMedia(fileToProcess, tracks, selectedAudioBuffers, selectedSubtitles);
const url = URL.createObjectURL(newMediaBlob);
const a = document.createElement('a');
a.href = url;
a.download = `output_${mediaFileName}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error("Error downloading result:", error);
} finally {
setIsRebuilding(false);
setShowDownloadModal(false);
if (audioBuffer && backgroundBuffer) {
setDownloadProgress(t('mixingAudio', { track: audioTracks[selectedAudioTrack]?.label.toLowerCase() }));
const finalAudioBuffer = await audioService.mixAudioBuffers(
backgroundBuffer,
audioBuffer
);
selectedAudioBuffers.push({ buffer: finalAudioBuffer, label: selectedAudioTrack });
} else {
throw new Error(`Audio track ${selectedAudioTrack} not found`);
}
}

setDownloadProgress(t('rebuildingMediaOnDownload'));
const newMediaBlob = await rebuildMedia(fileToProcess, tracks, selectedAudioBuffers, selectedSubtitles, setDownloadProgress);

setDownloadProgress(t('preparingDownload'));
throw new Error("test");
const url = URL.createObjectURL(newMediaBlob);
const a = document.createElement('a');
a.href = url;
a.download = `output_${mediaFileName}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
};

Expand Down Expand Up @@ -755,10 +753,10 @@ function App() {
subtitles={['original', 'dubbed']}
onClose={handleDownloadModalClose}
onDownload={handleDownloadWithSelectedTracks}
isRebuilding={isRebuilding}
onRegenerate={() => setShowRegenerateModal(true)}
progressMessage={downloadProgress}
/>
)}
{isRebuilding && <LoadingOverlay message={t('rebuildingMedia')} />}
{showRegenerateModal && (
<RegenerateModal
onClose={() => setShowRegenerateModal(false)}
Expand Down
139 changes: 113 additions & 26 deletions src/components/DownloadModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import styled, { keyframes } from 'styled-components';
import { useTranslation } from 'react-i18next';
import { Button, Label, ModalOverlay, colors } from '../styles/designSystem';
import { Button, Label, ModalOverlay, colors, Title, ErrorMessage, Message, ErrorBox } from '../styles/designSystem';
import { AudioTrack } from '../types/AudioTrack';

const ModalContent = styled.div`
Expand All @@ -12,11 +12,6 @@ const ModalContent = styled.div`
width: 100%;
`;

const Title = styled.h2`
color: ${colors.primary};
margin-bottom: 20px;
`;

const TrackList = styled.div`
margin-bottom: 20px;
`;
Expand All @@ -37,18 +32,54 @@ const ButtonContainer = styled.div`
gap: 10px;
`;

const spin = keyframes`
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
`;

const Spinner = styled.div`
border: 4px solid ${colors.background};
border-top: 4px solid ${colors.primary};
border-radius: 50%;
width: 40px;
height: 40px;
animation: ${spin} 1s linear infinite;
`;

const LoadingContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 20px;
`;

const Center = styled.div`
text-align: center;
`;

interface DownloadModalProps {
audioTracks: { [key: string]: AudioTrack };
subtitles: string[];
onClose: () => void;
onDownload: (selectedAudioTracks: string[], selectedSubtitles: string[]) => void;
isRebuilding: boolean;
onDownload: (selectedAudioTracks: string[], selectedSubtitles: string[]) => Promise<void>;
onRegenerate?: () => void;
progressMessage?: string;
}

const DownloadModal: React.FC<DownloadModalProps> = ({ audioTracks, subtitles, onClose, onDownload, isRebuilding }) => {
const DownloadModal: React.FC<DownloadModalProps> = ({
audioTracks,
subtitles,
onClose,
onDownload,
onRegenerate,
progressMessage,
}) => {
const { t } = useTranslation();
const [selectedAudioTracks, setSelectedAudioTracks] = useState<string[]>([]);
const [selectedAudioTracks, setSelectedAudioTracks] = useState<string[]>(['dubbed']);
const [selectedSubtitles, setSelectedSubtitles] = useState<string[]>([]);
const [isDownloading, setIsDownloading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleAudioTrackToggle = (id: string) => {
setSelectedAudioTracks(prev =>
Expand All @@ -62,24 +93,63 @@ const DownloadModal: React.FC<DownloadModalProps> = ({ audioTracks, subtitles, o
);
};

const handleDownload = () => {
onDownload(selectedAudioTracks, selectedSubtitles);
onClose();
const handleDownload = async () => {
setIsDownloading(true);
setError(null);
try {
await onDownload(
selectedAudioTracks,
selectedSubtitles
);
onClose();
} catch (error) {
console.error('Error downloading video:', error);
setError(`${error}`);
} finally {
setIsDownloading(false);
}
};

const handleRegenerateClick = () => {
if (onRegenerate) {
onRegenerate();
onClose();
}
};

if (error) {
return (
<ModalOverlay>
<ModalContent>
<Title>{t('downloadError')}</Title>
<ErrorMessage>{t('downloadErrorMessage')}</ErrorMessage>
<ErrorBox>{error}</ErrorBox>
<ButtonContainer>
<Button onClick={onClose}>{t('cancel')}</Button>
{onRegenerate && (
<Button onClick={handleRegenerateClick}>{t('regenerate')}</Button>
)}
</ButtonContainer>
</ModalContent>
</ModalOverlay>
);
}

return (
<ModalOverlay>
<ModalContent>
<Title>{t('selectTracksForDownload')}</Title>
<TrackList>
<h3>{t('audioTracks')}</h3>
{Object.entries(audioTracks).filter(([id, track]) => id !== 'background').map(([id, track], index) => (
<Title>{!isDownloading ? t('selectTracksForDownload') : t('preparingDownload')}</Title>
{!isDownloading ? (
<>
<TrackList>
<h3>{t('audioTracks')}</h3>
{Object.entries(audioTracks).filter(([id]) => id !== 'background').map(([id, track], index) => (
<TrackItem key={index}>
<Checkbox
type="checkbox"
checked={selectedAudioTracks.includes(id)}
onChange={() => handleAudioTrackToggle(id)}
disabled={isRebuilding}
disabled={isDownloading}
/>
<Label htmlFor={id}>{track.label}</Label>
</TrackItem>
Expand All @@ -93,16 +163,33 @@ const DownloadModal: React.FC<DownloadModalProps> = ({ audioTracks, subtitles, o
type="checkbox"
checked={selectedSubtitles.includes(subtitle)}
onChange={() => handleSubtitleToggle(subtitle)}
disabled={isRebuilding}
disabled={isDownloading}
/>
<Label htmlFor={subtitle}>{t(`${subtitle}Subtitles`)}</Label>
</TrackItem>
))}
</TrackList>
<ButtonContainer>
<Button onClick={onClose} disabled={isRebuilding}>{t('cancel')}</Button>
<Button onClick={handleDownload} disabled={isRebuilding}>{t('downloadResult')}</Button>
</ButtonContainer>
))}
</TrackList>
<ButtonContainer>
<Button
onClick={onClose}
>
{t('cancel')}
</Button>
<Button
onClick={handleDownload}
>
{t('downloadResult')}
</Button>
</ButtonContainer>
</>
) : (
<LoadingContainer>
<Spinner />
<Center>
<Message>{progressMessage || t('downloading')}</Message>
</Center>
</LoadingContainer>
)}
</ModalContent>
</ModalOverlay>
);
Expand Down
Loading

0 comments on commit 8709a8e

Please sign in to comment.