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

@@ -19,8 +19,15 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
const navigate = useNavigate();
return (
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" flexWrap="wrap" gap={2}>
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
flexWrap="wrap"
gap={2}
sx={{ width: "100%", minWidth: 0 }} // Ensure full width and allow wrapping
>
<Box sx={{ minWidth: 0, flex: { xs: "1 1 100%", md: "0 1 auto" } }}>
<Typography
variant="h3"
sx={{
@@ -30,24 +37,61 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
display: "flex",
alignItems: "center",
gap: 1.5,
fontSize: { xs: "1.5rem", md: "2rem" },
}}
>
<MicIcon fontSize="large" sx={{ color: "#667eea" }} />
AI Podcast Maker
</Typography>
<Typography variant="body2" color="text.secondary">
<Typography variant="body2" color="text.secondary" sx={{ display: { xs: "none", sm: "block" } }}>
Create professional podcast episodes with AI-powered research, smart scriptwriting, and natural voice narration
</Typography>
</Box>
<Stack direction="row" spacing={1} alignItems="center">
<Stack
direction="row"
spacing={1}
alignItems="center"
flexWrap="wrap"
useFlexGap
sx={{
justifyContent: { xs: "flex-start", md: "flex-end" },
gap: { xs: 0.5, md: 1 },
minWidth: 0,
width: { xs: "100%", md: "auto" }, // Full width on mobile to allow wrapping
flex: { xs: "1 1 100%", md: "0 1 auto" }, // Take full width on mobile
}}
>
<HeaderControls colorMode="light" showAlerts={true} showUser={true} />
<SecondaryButton onClick={() => window.open("/docs", "_blank")} startIcon={<InfoIcon />}>
<SecondaryButton
onClick={() => window.open("/docs", "_blank")}
startIcon={<InfoIcon />}
sx={{
display: { xs: "none", lg: "flex" },
// Override for light theme
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
Help
</SecondaryButton>
<SecondaryButton
onClick={() => navigate("/asset-library?source_module=podcast_maker&asset_type=audio")}
startIcon={<LibraryMusicIcon />}
tooltip="View all podcast episodes in Asset Library"
sx={{
display: { xs: "none", xl: "flex" },
// Override for light theme
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
My Episodes
</SecondaryButton>
@@ -55,10 +99,30 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
onClick={onShowProjects}
startIcon={<MicIcon />}
tooltip="View and resume saved projects"
sx={{
flexShrink: 0,
display: "flex !important", // Always show "My Projects" - force display
order: { xs: 1, md: 0 }, // Show first on mobile
// Override button colors for light theme
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
My Projects
</SecondaryButton>
<PrimaryButton onClick={onNewEpisode} startIcon={<AutoAwesomeIcon />}>
<PrimaryButton
onClick={onNewEpisode}
startIcon={<AutoAwesomeIcon />}
sx={{
flexShrink: 0,
display: "flex", // Always show "New Episode"
order: { xs: 0, md: 1 }, // Show first on mobile
}}
>
New Episode
</PrimaryButton>
</Stack>

View File

@@ -1,10 +1,11 @@
import React from "react";
import { Stack, Typography, Chip, Divider, Box, alpha } from "@mui/material";
import React, { useMemo } from "react";
import { Stack, Typography, Chip, Divider, Box, alpha, Paper } from "@mui/material";
import {
Insights as InsightsIcon,
Search as SearchIcon,
AttachMoney as AttachMoneyIcon,
EditNote as EditNoteIcon,
Article as ArticleIcon,
} from "@mui/icons-material";
import { Research } from "../types";
import { GlassyCard, glassyCardSx, PrimaryButton } from "../ui";
@@ -21,17 +22,68 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
canGenerateScript,
onGenerateScript,
}) => {
// Extract key insights from summary if it's long
const summaryParts = useMemo(() => {
const fullSummary = research.summary || "";
if (fullSummary.length > 500) {
// Try to split into paragraphs or sentences
const sentences = fullSummary.split(/[.!?]\s+/).filter(s => s.trim().length > 20);
const keyPoints = sentences.slice(0, 3);
const remainingText = sentences.slice(3).join(". ") + (sentences.length > 3 ? "." : "");
return { keyPoints, remainingText };
}
return { keyPoints: [], remainingText: fullSummary };
}, [research.summary]);
return (
<GlassyCard sx={glassyCardSx}>
<Stack spacing={3}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" flexWrap="wrap" gap={2}>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
<Box sx={{ flex: 1, minWidth: { xs: "100%", md: "60%" } }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1.5 }}>
<InsightsIcon />
Research Summary
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2, lineHeight: 1.7 }}>
{research.summary}
{/* Key Insights */}
{summaryParts.keyPoints.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ mb: 1, color: "#0f172a", fontWeight: 600, display: "flex", alignItems: "center", gap: 0.5 }}>
<ArticleIcon fontSize="small" />
Key Insights
</Typography>
<Stack spacing={1}>
{summaryParts.keyPoints.map((point, idx) => (
<Paper
key={idx}
sx={{
p: 1.25,
background: alpha("#667eea", 0.05),
border: "1px solid rgba(102, 126, 234, 0.15)",
borderRadius: 1.5,
}}
>
<Typography variant="body2" sx={{ color: "#0f172a", lineHeight: 1.6, fontSize: "0.875rem" }}>
{point}
</Typography>
</Paper>
))}
</Stack>
</Box>
)}
{/* Full Summary Text */}
<Typography
variant="body2"
color="text.secondary"
sx={{
mb: 2,
lineHeight: 1.7,
fontSize: "0.875rem",
color: "#475569",
}}
>
{summaryParts.remainingText || research.summary}
</Typography>
{/* Research Metadata */}
@@ -126,15 +178,23 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
{research.factCards.length > 0 && (
<>
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.5 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.5, flexWrap: "wrap", gap: 1 }}>
<Typography variant="subtitle2" sx={{ color: "#0f172a", fontWeight: 600 }}>
Research Sources & Facts ({research.factCards.length})
</Typography>
<Typography variant="caption" sx={{ color: "#64748b" }}>
Click any card to view source details
<Typography variant="caption" sx={{ color: "#64748b", fontSize: "0.75rem" }}>
Click to expand Hover to see source
</Typography>
</Stack>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr", lg: "1fr 1fr 1fr" }, gap: 2 }}>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "repeat(2, 1fr)", md: "repeat(3, 1fr)", lg: "repeat(4, 1fr)" },
gap: 1.5,
width: "100%",
overflow: "hidden",
}}
>
{research.factCards.map((fact) => (
<FactCard key={fact.id} fact={fact} />
))}

View File

@@ -94,17 +94,66 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
setShowRenderQueue(false);
try {
setIsAnalyzing(true);
// Upload avatar if provided, or generate presenters
let avatarUrl: string | null = null;
if (payload.files.avatarFile) {
try {
setAnnouncement("Uploading presenter avatar...");
const uploadResponse = await podcastApi.uploadAvatar(payload.files.avatarFile);
avatarUrl = uploadResponse.avatar_url;
} catch (error) {
console.error('Avatar upload failed:', error);
// Continue without avatar - will generate one later
}
}
setAnnouncement("Analyzing your idea — AI suggestions incoming");
const result = await podcastApi.createProject(payload);
const result = await podcastApi.createProject({ ...payload, avatarUrl });
await initializeProject(payload, result.projectId);
setProject({ id: result.projectId, idea: payload.ideaOrUrl, duration: payload.duration, speakers: payload.speakers });
setProject({ id: result.projectId, idea: payload.ideaOrUrl, duration: payload.duration, speakers: payload.speakers, avatarUrl });
setAnalysis(result.analysis);
setEstimate(result.estimate);
setQueries(result.queries);
setSelectedQueries(new Set(result.queries.map((q) => q.id)));
setKnobs(payload.knobs);
setBudgetCap(payload.budgetCap);
setAnnouncement("Analysis complete");
// Generate presenters AFTER analysis completes (to use analysis insights)
// This happens only if no avatar was uploaded
if (!avatarUrl && payload.speakers > 0 && result.analysis) {
try {
setAnnouncement("Generating presenter avatars using AI insights...");
const presentersResponse = await podcastApi.generatePresenters(
payload.speakers,
result.projectId,
result.analysis.audience,
result.analysis.contentType,
result.analysis.topKeywords
);
if (presentersResponse.avatars && presentersResponse.avatars.length > 0) {
// Store the first presenter avatar URL and prompt
const firstAvatar = presentersResponse.avatars[0];
const prompt = firstAvatar.prompt || null;
setProject({
id: result.projectId,
idea: payload.ideaOrUrl,
duration: payload.duration,
speakers: payload.speakers,
avatarUrl: firstAvatar.avatar_url,
avatarPrompt: prompt,
avatarPersonaId: firstAvatar.persona_id || presentersResponse.persona_id || null,
});
setAnnouncement("Analysis complete - Presenter avatars generated");
}
} catch (error) {
console.error('Presenter generation failed:', error);
setAnnouncement("Analysis complete - Avatar generation will happen later");
// Continue without presenters - can generate later
}
} else {
setAnnouncement("Analysis complete");
}
} catch (error: any) {
if (error?.response?.status === 429 || error?.response?.data?.detail) {
const errorDetail = error.response.data.detail;