feat: voice clone audio generation + podcast workspace architecture

- Voice clone integration: When user selects voice clone in Write phase,
  backend uses their uploaded voice sample + scene script text to generate
  audio via qwen3/minimax/cosyvoice voice clone APIs
- Multi-tenant workspace storage: All podcast assets (audio, video, images,
  charts) now use workspace-specific directories per user
- Chart preview improvements: Card-based B-Roll charts UI with thumbnails,
  takeaway text, and action buttons; public endpoint for image serving
- Voice clone caching: In-memory LRU cache for voice samples (avoids
  re-downloading per scene); frontend caches voice clone metadata
- Thread pool for voice clone: Audio generation uses ThreadPoolExecutor to
  avoid blocking the FastAPI event loop
- Auto-detect voice clone IDs (vc_*, MY_VOICE_CLONE) to route correctly
- DB fallback for voice sample URL: Fetches from ContentAsset if not passed
- Fixed API URL resolution for chart previews
- Fixed GlassyCard DOM warnings for motion props
- Fixed ScriptGenerationProgressView syntax error
- Fixed usePodcastWorkflow scriptData reference
This commit is contained in:
ajaysi
2026-04-21 19:38:50 +05:30
parent 7637babd7d
commit 91b2f996fd
33 changed files with 1642 additions and 457 deletions

View File

@@ -20,6 +20,7 @@ import {
DEFAULT_KNOBS,
getStepLabel,
} from "./PodcastDashboard/index";
import { ScriptGenerationProgressView } from "./PodcastDashboard/ScriptGenerationProgressView";
const PodcastDashboard: React.FC = () => {
useEffect(() => {
@@ -400,6 +401,69 @@ const PodcastDashboard: React.FC = () => {
</Button>
</DialogActions>
</Dialog>
{/* Script Generation Progress Modal */}
<Dialog
open={workflow.showScriptGenModal}
disableEscapeKeyDown={workflow.isGeneratingScript}
onClose={(event, reason) => {
// Only allow closing if NOT generating and generation hasn't started
if (!workflow.isGeneratingScript && !workflow.scriptGenStarted) {
workflow.setShowScriptGenModal(false);
}
}}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
background: "linear-gradient(135deg, #1e293b 0%, #0f172a 100%)",
border: "1px solid rgba(52, 211, 153, 0.3)",
borderRadius: 3,
},
}}
>
<DialogTitle sx={{ color: "#fff", display: "flex", alignItems: "center", gap: 1, fontSize: "1.25rem" }}>
{workflow.isGeneratingScript ? (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<CircularProgress size={20} sx={{ color: "#34d399" }} />
Generating Your Script
</Box>
) : (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
Script Complete
</Box>
)}
</DialogTitle>
<DialogContent sx={{ color: "rgba(255,255,255,0.8)" }}>
<ScriptGenerationProgressView
currentMessage={workflow.announcement}
progressIndex={workflow.scriptGenProgressIndex}
idea={projectState.project?.idea}
analysis={projectState.analysis}
research={projectState.research}
sourceCount={projectState.research?.sourceCount}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>
{workflow.isGeneratingScript ? (
<Button
onClick={() => workflow.setShowScriptGenModal(false)}
disabled={workflow.isGeneratingScript}
sx={{ color: "rgba(255,255,255,0.6)" }}
>
Cancel
</Button>
) : (
<Button
onClick={() => workflow.setShowScriptGenModal(false)}
variant="contained"
sx={{ bgcolor: "#34d399", "&:hover": { bgcolor: "#10b981" } }}
>
Continue to Editor
</Button>
)}
</DialogActions>
</Dialog>
</Box>
);
};