AI podcast project

This commit is contained in:
ajaysi
2025-12-16 16:25:52 +05:30
parent eba5210577
commit 1d745c9bc8
50 changed files with 7637 additions and 2813 deletions

View File

@@ -1,8 +1,11 @@
import React, { useCallback } from "react";
import { Box, Stack, Typography, Alert, Paper, alpha } from "@mui/material";
import React, { useCallback, useState, useEffect } from "react";
import { Box, Stack, Typography, Alert, Paper, alpha, Button, CircularProgress, LinearProgress } from "@mui/material";
import {
PlayArrow as PlayArrowIcon,
ArrowBack as ArrowBackIcon,
VideoLibrary as VideoLibraryIcon,
Download as DownloadIcon,
CheckCircle as CheckCircleIcon,
} from "@mui/icons-material";
import { Script, Knobs, Job } from "./types";
import { SecondaryButton } from "./ui";
@@ -10,6 +13,7 @@ import { SceneCard } from "./RenderQueue/SceneCard";
import { SummaryStats } from "./RenderQueue/SummaryStats";
import { GuidancePanel } from "./RenderQueue/GuidancePanel";
import { useRenderQueue } from "./RenderQueue/useRenderQueue";
import { fetchMediaBlobUrl } from "../../utils/fetchMediaBlobUrl";
interface RenderQueueProps {
projectId: string;
@@ -36,6 +40,7 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
onBack,
onError,
}) => {
const [localError, setLocalError] = useState<string>("");
const {
rendering,
generatingImage,
@@ -43,6 +48,10 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
runRender,
runImageGeneration,
runVideoRender,
combiningVideos,
combiningProgress,
finalVideoUrl,
combineFinalVideo,
} = useRenderQueue({
script,
jobs,
@@ -52,7 +61,10 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
avatarImageUrl,
onUpdateJob,
onUpdateScript,
onError,
onError: (msg) => {
setLocalError(msg);
onError(msg);
},
});
const handleDownloadAudio = useCallback((audioUrl: string, title: string) => {
@@ -76,11 +88,11 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
title,
text: `Check out this podcast episode: ${title}`,
url: audioUrl,
});
});
} catch (err) {
// User cancelled or error
}
} else {
} else {
// Fallback: copy to clipboard
await navigator.clipboard.writeText(audioUrl);
alert("Audio URL copied to clipboard!");
@@ -91,6 +103,28 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
(jobs.length > 0 && jobs.every((j) => j.status === "completed" && j.imageUrl)) ||
(script.scenes.length > 0 && script.scenes.every((s) => s.audioUrl && s.imageUrl));
const allVideosReady = jobs.length > 0 && jobs.every((j) => j.videoUrl);
// State for final video blob URL
const [finalVideoBlobUrl, setFinalVideoBlobUrl] = useState<string | null>(null);
// Load final video as blob when URL changes
useEffect(() => {
if (finalVideoUrl) {
fetchMediaBlobUrl(finalVideoUrl)
.then((blobUrl) => {
if (blobUrl) {
setFinalVideoBlobUrl(blobUrl);
}
})
.catch((err) => {
console.error("Failed to load final video blob:", err);
});
} else {
setFinalVideoBlobUrl(null);
}
}, [finalVideoUrl]);
return (
<Box sx={{ mt: 3 }}>
{/* Header */}
@@ -115,6 +149,24 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
</Typography>
</Stack>
{/* Error Display */}
{localError && (
<Alert
severity="error"
onClose={() => setLocalError("")}
sx={{
mb: 3,
background: alpha("#ef4444", 0.1),
border: "1px solid",
borderColor: alpha("#ef4444", 0.3),
}}
>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{localError}
</Typography>
</Alert>
)}
{/* Info Alert */}
<Alert severity="info" sx={{ mb: 3, background: alpha("#3b82f6", 0.1), border: "1px solid rgba(59,130,246,0.3)" }}>
<Typography variant="body2">
@@ -127,21 +179,21 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
{/* Empty State */}
{jobs.length === 0 && script.scenes.length === 0 && (
<Paper
sx={{
<Paper
sx={{
p: 4,
textAlign: "center",
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%)",
border: "2px dashed rgba(102, 126, 234, 0.3)",
borderRadius: 2,
}}
>
}}
>
<Typography variant="h6" sx={{ color: "#0f172a", fontWeight: 600, mb: 1 }}>
No scenes to render
</Typography>
</Typography>
<Typography variant="body2" sx={{ color: "#64748b", mb: 3 }}>
Go back to the script editor to generate and approve scenes first.
</Typography>
</Typography>
<SecondaryButton onClick={onBack} startIcon={<ArrowBackIcon />}>
Back to Script Editor
</SecondaryButton>
@@ -166,7 +218,7 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
avatarImageUrl={avatarImageUrl}
onRender={runRender}
onImageGenerate={runImageGeneration}
onVideoRender={runVideoRender}
onVideoGenerate={(sceneId, settings) => runVideoRender(sceneId, settings)}
onDownloadAudio={handleDownloadAudio}
onDownloadVideo={handleDownloadVideo}
onShare={handleShare}
@@ -176,6 +228,224 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
})}
</Stack>
{/* Final Export Section - Show when all scene videos are ready */}
{allVideosReady && (
<Paper
elevation={3}
sx={{
mt: 4,
p: 4,
background: "linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(6, 182, 212, 0.05) 100%)",
border: "2px solid",
borderColor: finalVideoUrl ? "success.main" : "info.light",
borderRadius: 3,
position: "relative",
overflow: "hidden",
"&::before": {
content: '""',
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "4px",
background: finalVideoUrl
? "linear-gradient(90deg, #10b981 0%, #06b6d4 100%)"
: "linear-gradient(90deg, #667eea 0%, #764ba2 100%)",
},
}}
>
<Stack spacing={3}>
{/* Header */}
<Stack direction="row" alignItems="center" spacing={2}>
<Box
sx={{
p: 1.5,
borderRadius: 2,
background: finalVideoUrl
? "linear-gradient(135deg, #10b981 0%, #059669 100%)"
: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 4px 12px rgba(102, 126, 234, 0.3)",
}}
>
{finalVideoUrl ? (
<CheckCircleIcon sx={{ color: "white", fontSize: 32 }} />
) : (
<VideoLibraryIcon sx={{ color: "white", fontSize: 32 }} />
)}
</Box>
<Box>
<Typography
variant="h5"
sx={{
fontWeight: 700,
color: "#0f172a",
mb: 0.5,
}}
>
{finalVideoUrl ? "🎉 Final Podcast Ready!" : "🎬 Final Podcast Export"}
</Typography>
<Typography variant="body2" sx={{ color: "#64748b" }}>
{finalVideoUrl
? "Your complete podcast video is ready to download"
: `Combine ${script.scenes.length} scene videos into one final podcast`}
</Typography>
</Box>
</Stack>
{finalVideoUrl ? (
<Stack spacing={3}>
<Alert
severity="success"
icon={<CheckCircleIcon />}
sx={{
background: alpha("#10b981", 0.1),
border: "1px solid",
borderColor: alpha("#10b981", 0.3),
}}
>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Your final podcast video has been created successfully!
</Typography>
</Alert>
{/* Video Preview */}
<Box
sx={{
width: "100%",
maxWidth: 900,
mx: "auto",
borderRadius: 2,
overflow: "hidden",
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.12)",
border: "1px solid",
borderColor: alpha("#10b981", 0.2),
}}
>
<video
controls
src={finalVideoBlobUrl || finalVideoUrl}
style={{
width: "100%",
display: "block",
backgroundColor: "#000",
}}
>
Your browser does not support video playback.
</video>
</Box>
{/* Download Button */}
<Stack direction="row" spacing={2} justifyContent="center" sx={{ pt: 2 }}>
<Button
variant="contained"
size="large"
startIcon={<DownloadIcon />}
onClick={async () => {
if (finalVideoBlobUrl) {
const link = document.createElement("a");
link.href = finalVideoBlobUrl;
link.download = `podcast-final-${Date.now()}.mp4`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}}
sx={{
px: 4,
py: 1.5,
background: "linear-gradient(135deg, #10b981 0%, #059669 100%)",
boxShadow: "0 4px 12px rgba(16, 185, 129, 0.4)",
"&:hover": {
background: "linear-gradient(135deg, #059669 0%, #047857 100%)",
boxShadow: "0 6px 16px rgba(16, 185, 129, 0.5)",
},
}}
>
Download Final Podcast
</Button>
</Stack>
</Stack>
) : (
<Stack spacing={3}>
<Alert
severity="info"
sx={{
background: alpha("#3b82f6", 0.08),
border: "1px solid",
borderColor: alpha("#3b82f6", 0.2),
}}
>
<Typography variant="body2">
<strong>Ready to export!</strong> Click below to combine all {script.scenes.length} scene videos into your final podcast video.
</Typography>
</Alert>
{combiningVideos && (
<Box sx={{ width: "100%" }}>
<Stack direction="row" justifyContent="space-between" sx={{ mb: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: "#0f172a" }}>
{combiningProgress?.message || "Combining videos..."}
</Typography>
{combiningProgress && (
<Typography variant="body2" sx={{ color: "#64748b", fontWeight: 600 }}>
{combiningProgress.progress.toFixed(0)}%
</Typography>
)}
</Stack>
<LinearProgress
variant={combiningProgress ? "determinate" : "indeterminate"}
value={combiningProgress?.progress || 0}
sx={{
height: 8,
borderRadius: 4,
background: alpha("#667eea", 0.1),
"& .MuiLinearProgress-bar": {
background: "linear-gradient(90deg, #667eea 0%, #764ba2 100%)",
borderRadius: 4,
},
}}
/>
{combiningProgress && combiningProgress.progress < 100 && (
<Typography variant="caption" sx={{ color: "#64748b", mt: 0.5, display: "block" }}>
Video encoding in progress. This may take a few minutes...
</Typography>
)}
</Box>
)}
<Button
variant="contained"
size="large"
fullWidth
startIcon={combiningVideos ? <CircularProgress size={20} sx={{ color: "white" }} /> : <VideoLibraryIcon />}
onClick={combineFinalVideo}
disabled={combiningVideos}
sx={{
py: 2,
fontSize: "1.1rem",
fontWeight: 700,
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
boxShadow: "0 4px 12px rgba(102, 126, 234, 0.4)",
"&:hover": {
background: "linear-gradient(135deg, #5568d3 0%, #6a3f8f 100%)",
boxShadow: "0 6px 16px rgba(102, 126, 234, 0.5)",
},
"&:disabled": {
background: alpha("#667eea", 0.5),
},
}}
>
{combiningVideos ? "Combining Videos..." : "Combine Scenes into Final Video"}
</Button>
</Stack>
)}
</Stack>
</Paper>
)}
{/* Footer - Video Generation Focus */}
<Paper
sx={{
@@ -191,13 +461,22 @@ export const RenderQueue: React.FC<RenderQueueProps> = ({
<SecondaryButton onClick={onBack} startIcon={<ArrowBackIcon />}>
Back to Script
</SecondaryButton>
{allScenesCompleted ? (
{allVideosReady ? (
<Stack spacing={1} alignItems="flex-end">
<Typography variant="body1" sx={{ color: "#10b981", fontWeight: 700, fontSize: "1rem" }}>
🎉 All scene videos ready!
</Typography>
<Typography variant="body2" sx={{ color: "#64748b" }}>
Scroll up to combine them into your final podcast video.
</Typography>
</Stack>
) : allScenesCompleted ? (
<Stack spacing={1} alignItems="flex-end">
<Typography variant="body1" sx={{ color: "#10b981", fontWeight: 700, fontSize: "1rem" }}>
🎉 All scenes ready for video generation!
</Typography>
<Typography variant="body2" sx={{ color: "#64748b" }}>
Generate videos for individual scenes or download them.
Generate videos for individual scenes above.
</Typography>
</Stack>
) : (