Merge PR #473: Move podcast estimate calculation to backend pricing catalog
This commit is contained in:
@@ -59,39 +59,41 @@ 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, " "));
|
||||
const toPodcastEstimate = (raw: any, voiceId?: string): PodcastEstimate | null => {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const numeric = ["ttsCost", "avatarCost", "videoCost", "researchCost", "total"] as const;
|
||||
if (numeric.some((key) => typeof raw[key] !== "number" || Number.isNaN(raw[key]))) {
|
||||
return null;
|
||||
}
|
||||
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)
|
||||
);
|
||||
return {
|
||||
ttsCost: +ttsCost.toFixed(2),
|
||||
avatarCost: +avatarCost.toFixed(2),
|
||||
videoCost: +videoCost.toFixed(2),
|
||||
researchCost,
|
||||
total,
|
||||
voiceName,
|
||||
ttsCost: raw.ttsCost,
|
||||
avatarCost: raw.avatarCost,
|
||||
videoCost: raw.videoCost,
|
||||
researchCost: raw.researchCost,
|
||||
total: raw.total,
|
||||
voiceName: isCustomVoice ? "My Voice Clone" : (!voiceId ? "Wise Woman" : voiceId.replace(/_/g, " ")),
|
||||
isCustomVoice,
|
||||
};
|
||||
};
|
||||
@@ -173,12 +175,14 @@ const mapSourcesToFacts = (sources: ExaSource[]): Fact[] => {
|
||||
type ExaResearchResult = {
|
||||
sources: ExaSource[];
|
||||
search_queries?: string[];
|
||||
cost_est?: {
|
||||
cost_est?: {
|
||||
total?: number;
|
||||
breakdown?: { phase: "Analyze" | "Gather" | "Write" | "Produce"; cost: number }[];
|
||||
currency?: "USD";
|
||||
last_updated?: string;
|
||||
};
|
||||
cost?: { total?: number };
|
||||
estimate?: PodcastEstimate | null;
|
||||
search_type?: string;
|
||||
provider?: string;
|
||||
content?: string;
|
||||
@@ -302,15 +306,7 @@ 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 estimate = toPodcastEstimate(analysisResp.data?.estimate, payload.knobs.voice_id);
|
||||
|
||||
return {
|
||||
projectId,
|
||||
@@ -337,7 +333,7 @@ export const podcastApi = {
|
||||
bible?: any;
|
||||
analysis?: PodcastAnalysis | null;
|
||||
onProgress?: (message: string) => void;
|
||||
}): Promise<{ research: Research; raw: any }> {
|
||||
}): Promise<{ research: Research; raw: any; estimate?: PodcastEstimate | null }> {
|
||||
const keywords = params.approvedQueries.map((q) => q.query).filter(Boolean);
|
||||
if (!keywords.length) {
|
||||
throw new Error("At least one query must be approved for research.");
|
||||
@@ -384,7 +380,11 @@ export const podcastApi = {
|
||||
params.onProgress("Deep research completed with Exa.");
|
||||
}
|
||||
const mapped = mapExaResearchResponse(exaResult);
|
||||
return { research: mapped, raw: exaResult };
|
||||
return {
|
||||
research: mapped,
|
||||
raw: exaResult,
|
||||
estimate: toPodcastEstimate(exaResult.estimate, params.analysis?.suggestedKnobs?.voice_id),
|
||||
};
|
||||
},
|
||||
|
||||
async generateScript(params: {
|
||||
|
||||
Reference in New Issue
Block a user