Use backend-provided podcast estimates and remove UI heuristics

This commit is contained in:
ي
2026-04-19 16:28:39 +05:30
parent 196ea65af9
commit f210310177
6 changed files with 122 additions and 71 deletions

View File

@@ -253,26 +253,6 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
setShowAIDetailsButton(topicInput.trim().length > 0 && !isUrl);
}, [topicInput, isUrl]);
// Calculate estimated cost
const estimatedCost = useMemo(() => {
const chars = Math.max(1000, duration * 900); // ~900 chars per minute
const secs = duration * 60;
const ttsCost = (chars / 1000) * 0.05;
const avatarCost = speakers * 0.15;
const videoRate = knobs.bitrate === 'hd' ? 0.06 : 0.03;
const videoCost = secs * videoRate;
const researchCost = 0.3; // Fixed research cost
return {
ttsCost: +ttsCost.toFixed(2),
avatarCost: +avatarCost.toFixed(2),
videoCost: +videoCost.toFixed(2),
researchCost: +researchCost.toFixed(2),
total: +(ttsCost + avatarCost + videoCost + researchCost).toFixed(2),
};
}, [duration, speakers, knobs.bitrate, knobs.scene_length_target]);
// Check if avatar is present (from any source: upload, brand avatar, or generated)
const hasAvatar = Boolean(
avatarFile || // User uploaded an image
@@ -560,7 +540,7 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
placeholderIndex={placeholderIndex}
loading={enhancingTopic}
loadingMessage={enhanceTopicMessage}
estimatedCost={estimatedCost}
estimatedCost={null}
duration={duration}
speakers={speakers}
knobs={knobs}

View File

@@ -27,7 +27,7 @@ interface TopicUrlInputProps {
videoCost: number;
researchCost: number;
total: number;
};
} | null;
duration?: number;
speakers?: number;
knobs?: Knobs;
@@ -115,7 +115,7 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
</Typography>
</Stack>
{estimatedCost && (
{estimatedCost ? (
<Tooltip
title={
<Box>
@@ -171,6 +171,20 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
}}
/>
</Tooltip>
) : (
<Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
label="Est. Unavailable"
size="small"
sx={{
background: "rgba(148, 163, 184, 0.12)",
color: "#64748b",
fontWeight: 600,
border: "1px solid rgba(148, 163, 184, 0.2)",
fontSize: "0.75rem",
height: 26,
}}
/>
)}
</Stack>
<Tooltip

View File

@@ -145,6 +145,9 @@ export type PodcastEstimate = {
videoCost: number;
researchCost: number;
total: number;
breakdown?: Array<{ phase: "Analyze" | "Gather" | "Write" | "Produce"; cost: number }>;
currency?: "USD";
lastUpdated?: string;
voiceName?: string;
isCustomVoice?: boolean;
};
@@ -196,7 +199,7 @@ export type CreateProjectPayload = {
export type CreateProjectResult = {
projectId: string;
analysis: PodcastAnalysis;
estimate: PodcastEstimate;
estimate: PodcastEstimate | null;
queries: Query[];
bible?: PodcastBible;
avatar_url?: string | null;

View File

@@ -59,43 +59,6 @@ const deriveSegments = (option?: OptionLike): string[] => {
return segments.slice(0, 5);
};
const estimateCosts = ({
minutes,
scenes,
chars,
quality,
avatars,
queryCount = 3,
voiceId,
}: {
minutes: number;
scenes: number;
chars: number;
quality: string;
avatars: number;
queryCount?: number;
voiceId?: string;
}): PodcastEstimate => {
const secs = Math.max(60, minutes * 60);
const ttsCost = (chars / 1000) * 0.05;
const avatarCost = avatars * 0.15;
const videoRate = quality === "hd" ? 0.06 : 0.03;
const videoCost = secs * videoRate;
const researchCost = +(Math.max(1, queryCount) * 0.1).toFixed(2);
const total = +(ttsCost + avatarCost + videoCost + researchCost).toFixed(2);
const isCustomVoice = Boolean(voiceId && !["Wise_Woman", "Friendly_Person", "Inspirational_girl", "Deep_Voice_Man", "Calm_Woman", "Casual_Guy", "Lively_Girl", "Patient_Man", "Young_Knight", "Determined_Man", "Lovely_Girl", "Decent_Boy", "Imposing_Manner", "Elegant_Man", "Abbess", "Sweet_Girl_2", "Exuberant_Girl"].includes(voiceId));
const voiceName = isCustomVoice ? "My Voice Clone" : (!voiceId ? "Wise Woman" : voiceId.replace(/_/g, " "));
return {
ttsCost: +ttsCost.toFixed(2),
avatarCost: +avatarCost.toFixed(2),
videoCost: +videoCost.toFixed(2),
researchCost,
total,
voiceName,
isCustomVoice,
};
};
const mapPersonaQueries = (persona: ResearchPersona | undefined, seed: string): Query[] => {
const baseIdea = seed || "AI marketing for small businesses";
const personaKeywords = persona?.suggested_keywords?.filter(Boolean) || [];
@@ -302,15 +265,21 @@ export const podcastApi = {
// so users can manually choose which queries to run
const projectId = createId("podcast");
const estimate = estimateCosts({
minutes: payload.duration,
scenes: Math.ceil((payload.duration * 60) / (payload.knobs.scene_length_target || DEFAULT_KNOBS.scene_length_target)),
chars: Math.max(1000, payload.duration * 900),
quality: payload.knobs.bitrate || "standard",
avatars: payload.speakers,
queryCount: queries.length || 3,
voiceId: payload.knobs.voice_id,
});
const estimateData = analysisResp.data?.estimate;
const estimate: PodcastEstimate | null = estimateData
? {
ttsCost: Number(estimateData.ttsCost ?? 0),
avatarCost: Number(estimateData.avatarCost ?? 0),
videoCost: Number(estimateData.videoCost ?? 0),
researchCost: Number(estimateData.researchCost ?? 0),
total: Number(estimateData.total ?? 0),
breakdown: Array.isArray(estimateData.breakdown) ? estimateData.breakdown : [],
currency: estimateData.currency || "USD",
lastUpdated: estimateData.last_updated || estimateData.lastUpdated,
voiceName: estimateData.voiceName,
isCustomVoice: estimateData.isCustomVoice,
}
: null;
return {
projectId,