fix(voice-clone): persist clone info in localStorage, auto-merge into project knobs, fix clone ID detection in CreateModal

- Move voice clone cache from module-level memory to localStorage
  so it survives page refresh and works across browser tabs
- VoiceAvatarPlaceholder now syncs clone result to localStorage
  immediately after creation (both design and clone paths)
- usePodcastProjectState auto-merges voice clone cache into project
  knobs when loading a project (fills gap for projects created
  before voice clone or when voice clone was created after)
- CreateModal now detects voice clone IDs by prefix (vc_*) not
  just by VOICE_CLONE_ID constant, fixing the mismatch where
  VoiceSelector passes the actual clone ID but CreateModal
  expected the placeholder ID
- AudioRegenerateModal is intentionally per-scene override and
  does not write back to knobs (by design)
- trends.py handler added for podcast topic trend analysis
This commit is contained in:
ajaysi
2026-04-24 20:36:35 +05:30
parent d518365c87
commit fc47445181
6 changed files with 191 additions and 16 deletions

View File

@@ -11,7 +11,7 @@ import {
PodcastBible,
} from '../components/PodcastMaker/types';
import { BlogResearchResponse, ResearchProvider } from '../services/blogWriterApi';
import { podcastApi } from '../services/podcastApi';
import { podcastApi, getCachedVoiceCloneInfo } from '../services/podcastApi';
export interface PodcastProjectState {
// Project metadata
@@ -79,6 +79,30 @@ const DEFAULT_KNOBS: Knobs = {
bitrate: "standard",
};
/**
* Merge voice clone cache into knobs if the project knobs don't already have it.
* This ensures projects created before voice clone, or after a new clone is made,
* automatically pick up the latest voice clone info.
*/
function mergeVoiceCloneCacheIntoKnobs(knobs: Knobs): Knobs {
// If knobs already has a custom voice ID, trust it (user explicitly set it)
if (knobs.custom_voice_id) {
return knobs;
}
const cached = getCachedVoiceCloneInfo();
if (!cached || !cached.isVoiceClone) {
return knobs;
}
return {
...knobs,
voice_id: knobs.voice_id || "Wise_Woman",
custom_voice_id: cached.customVoiceId,
is_voice_clone: true,
voice_sample_url: cached.voiceSampleUrl,
voice_clone_engine: cached.engine || "qwen3",
};
}
const DEFAULT_STATE: PodcastProjectState = {
project: null,
analysis: null,
@@ -446,7 +470,7 @@ export const usePodcastProjectState = () => {
scriptData: dbProject.script_data,
bible: dbProject.bible,
renderJobs: dbProject.render_jobs || [],
knobs: { ...DEFAULT_KNOBS, ...(dbProject.knobs || {}) },
knobs: mergeVoiceCloneCacheIntoKnobs({ ...DEFAULT_KNOBS, ...(dbProject.knobs || {}) }),
researchProvider: dbProject.research_provider || 'exa',
budgetCap: dbProject.budget_cap || 50,
showScriptEditor: dbProject.show_script_editor || false,