diff --git a/backend/api/story_writer/routes/story_content.py b/backend/api/story_writer/routes/story_content.py index ecc66b41..234aa7f3 100644 --- a/backend/api/story_writer/routes/story_content.py +++ b/backend/api/story_writer/routes/story_content.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException from loguru import logger -from pydantic import BaseModel +from pydantic import BaseModel, Field from middleware.auth_middleware import get_current_user from models.story_models import ( @@ -20,7 +20,56 @@ from ..utils.auth import require_authenticated_user router = APIRouter() story_service = StoryWriterService() -scene_approval_store: Dict[str, Dict[str, Any]] = {} +scene_approval_store: Dict[str, Dict[str, Dict[str, Dict[str, Any]]]] = {} +APPROVAL_TTL_SECONDS = 60 * 60 * 24 +MAX_APPROVALS_PER_USER = 200 + + +def _cleanup_user_approvals(user_id: str) -> None: + user_store = scene_approval_store.get(user_id) + if not user_store: + return + now = datetime.utcnow() + for project_id in list(user_store.keys()): + scenes = user_store.get(project_id, {}) + for scene_id in list(scenes.keys()): + timestamp = scenes[scene_id].get("timestamp") + if isinstance(timestamp, datetime): + if (now - timestamp).total_seconds() > APPROVAL_TTL_SECONDS: + scenes.pop(scene_id, None) + if not scenes: + user_store.pop(project_id, None) + if not user_store: + scene_approval_store.pop(user_id, None) + + +def _enforce_capacity(user_id: str) -> None: + user_store = scene_approval_store.get(user_id) + if not user_store: + return + entries: List[tuple[datetime, str, str]] = [] + for project_id, scenes in user_store.items(): + for scene_id, meta in scenes.items(): + timestamp = meta.get("timestamp") + if isinstance(timestamp, datetime): + entries.append((timestamp, project_id, scene_id)) + if len(entries) <= MAX_APPROVALS_PER_USER: + return + entries.sort(key=lambda item: item[0]) + to_remove = len(entries) - MAX_APPROVALS_PER_USER + for i in range(to_remove): + _, project_id, scene_id = entries[i] + scenes = user_store.get(project_id) + if not scenes: + continue + scenes.pop(scene_id, None) + if not scenes: + user_store.pop(project_id, None) + + +def _get_user_store(user_id: str) -> Dict[str, Dict[str, Dict[str, Any]]]: + _cleanup_user_approvals(user_id) + return scene_approval_store.setdefault(user_id, {}) @router.post("/generate-start", response_model=StoryContentResponse) @@ -197,8 +246,8 @@ async def continue_story( class SceneApprovalRequest(BaseModel): - project_id: str - scene_id: str + project_id: str = Field(..., min_length=1) + scene_id: str = Field(..., min_length=1) approved: bool = True notes: Optional[str] = None @@ -211,13 +260,20 @@ async def approve_script_scene( """Persist scene approval metadata for auditing.""" try: user_id = require_authenticated_user(current_user) - approvals = scene_approval_store.setdefault(request.project_id, {}) - approvals[request.scene_id] = { + if not request.project_id.strip() or not request.scene_id.strip(): + raise HTTPException(status_code=400, detail="project_id and scene_id are required") + + notes = request.notes.strip() if request.notes else None + user_store = _get_user_store(user_id) + project_store = user_store.setdefault(request.project_id, {}) + timestamp = datetime.utcnow() + project_store[request.scene_id] = { "approved": request.approved, - "notes": request.notes, + "notes": notes, "user_id": user_id, - "timestamp": datetime.utcnow().isoformat(), + "timestamp": timestamp, } + _enforce_capacity(user_id) logger.info( "[StoryWriter] Scene approval recorded user=%s project=%s scene=%s approved=%s", user_id, @@ -230,6 +286,7 @@ async def approve_script_scene( "project_id": request.project_id, "scene_id": request.scene_id, "approved": request.approved, + "timestamp": timestamp.isoformat(), } except HTTPException: raise diff --git a/backend/story_audio/scene_1_Welcome_to_the_Cloud_Kitchen___9ddc13cd.mp3 b/backend/story_audio/scene_1_Welcome_to_the_Cloud_Kitchen___9ddc13cd.mp3 deleted file mode 100644 index edf06480..00000000 Binary files a/backend/story_audio/scene_1_Welcome_to_the_Cloud_Kitchen___9ddc13cd.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_13319994.mp3 b/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_13319994.mp3 deleted file mode 100644 index ebf162d3..00000000 Binary files a/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_13319994.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_55129ff0.mp3 b/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_55129ff0.mp3 deleted file mode 100644 index e29fd563..00000000 Binary files a/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_55129ff0.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_d3b0b210.mp3 b/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_d3b0b210.mp3 deleted file mode 100644 index ef2a0e44..00000000 Binary files a/backend/story_audio/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_d3b0b210.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_0c28f687.mp3 b/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_0c28f687.mp3 deleted file mode 100644 index 1008f5b9..00000000 Binary files a/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_0c28f687.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_1799fd46.mp3 b/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_1799fd46.mp3 deleted file mode 100644 index 94e16ea0..00000000 Binary files a/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_1799fd46.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_fabc3240.mp3 b/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_fabc3240.mp3 deleted file mode 100644 index 23cf51f9..00000000 Binary files a/backend/story_audio/scene_2_Meeting_Spark_the_Silver_Spoon_fabc3240.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_2_The_Star_Recipe_Begins_68356250.mp3 b/backend/story_audio/scene_2_The_Star_Recipe_Begins_68356250.mp3 deleted file mode 100644 index d4673a96..00000000 Binary files a/backend/story_audio/scene_2_The_Star_Recipe_Begins_68356250.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_2_The_Star_Recipe_Begins_ed9941a3.mp3 b/backend/story_audio/scene_2_The_Star_Recipe_Begins_ed9941a3.mp3 deleted file mode 100644 index 8349cbc8..00000000 Binary files a/backend/story_audio/scene_2_The_Star_Recipe_Begins_ed9941a3.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_09969868.mp3 b/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_09969868.mp3 deleted file mode 100644 index bba1f913..00000000 Binary files a/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_09969868.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_5cd380d8.mp3 b/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_5cd380d8.mp3 deleted file mode 100644 index 70250e3d..00000000 Binary files a/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_5cd380d8.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_b8b724b5.mp3 b/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_b8b724b5.mp3 deleted file mode 100644 index b16fdb30..00000000 Binary files a/backend/story_audio/scene_3_Gathering_Space_Dust_and_Wishe_b8b724b5.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_3_Gathering_Sparkling_Space_Dust_d8174f84.mp3 b/backend/story_audio/scene_3_Gathering_Sparkling_Space_Dust_d8174f84.mp3 deleted file mode 100644 index b275a80d..00000000 Binary files a/backend/story_audio/scene_3_Gathering_Sparkling_Space_Dust_d8174f84.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_4_Collecting_Wishes_c38d9001.mp3 b/backend/story_audio/scene_4_Collecting_Wishes_c38d9001.mp3 deleted file mode 100644 index d67868cf..00000000 Binary files a/backend/story_audio/scene_4_Collecting_Wishes_c38d9001.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_496a7494.mp3 b/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_496a7494.mp3 deleted file mode 100644 index 160452ab..00000000 Binary files a/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_496a7494.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_a1f7e80d.mp3 b/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_a1f7e80d.mp3 deleted file mode 100644 index d2717bca..00000000 Binary files a/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_a1f7e80d.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_fdd8a3cb.mp3 b/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_fdd8a3cb.mp3 deleted file mode 100644 index 7a5b3cfb..00000000 Binary files a/backend/story_audio/scene_4_Gravity_s_Gentle_Pull_fdd8a3cb.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_5_The_Gravity_Mixer_e6255f00.mp3 b/backend/story_audio/scene_5_The_Gravity_Mixer_e6255f00.mp3 deleted file mode 100644 index e3cc7e2a..00000000 Binary files a/backend/story_audio/scene_5_The_Gravity_Mixer_e6255f00.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_2b8af534.mp3 b/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_2b8af534.mp3 deleted file mode 100644 index c8ec9973..00000000 Binary files a/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_2b8af534.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_9f01caba.mp3 b/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_9f01caba.mp3 deleted file mode 100644 index a2e85c11..00000000 Binary files a/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_9f01caba.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_d2df24ea.mp3 b/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_d2df24ea.mp3 deleted file mode 100644 index f5fa5bfc..00000000 Binary files a/backend/story_audio/scene_5_The_Mixture_Starts_to_Glow_d2df24ea.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_6_The_Birth_of_a_New_Star_0ac0e570.mp3 b/backend/story_audio/scene_6_The_Birth_of_a_New_Star_0ac0e570.mp3 deleted file mode 100644 index e49ec3f6..00000000 Binary files a/backend/story_audio/scene_6_The_Birth_of_a_New_Star_0ac0e570.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_6_The_Birth_of_a_New_Star_245475df.mp3 b/backend/story_audio/scene_6_The_Birth_of_a_New_Star_245475df.mp3 deleted file mode 100644 index cd4f72db..00000000 Binary files a/backend/story_audio/scene_6_The_Birth_of_a_New_Star_245475df.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_6_The_Birth_of_a_New_Star_80b3f63a.mp3 b/backend/story_audio/scene_6_The_Birth_of_a_New_Star_80b3f63a.mp3 deleted file mode 100644 index 66429c91..00000000 Binary files a/backend/story_audio/scene_6_The_Birth_of_a_New_Star_80b3f63a.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_6_The_Glowing_Mixture_c0163e9c.mp3 b/backend/story_audio/scene_6_The_Glowing_Mixture_c0163e9c.mp3 deleted file mode 100644 index 66524a03..00000000 Binary files a/backend/story_audio/scene_6_The_Glowing_Mixture_c0163e9c.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_7_A_New_Star_Is_Born_c3f3f2c4.mp3 b/backend/story_audio/scene_7_A_New_Star_Is_Born_c3f3f2c4.mp3 deleted file mode 100644 index 1433c9e3..00000000 Binary files a/backend/story_audio/scene_7_A_New_Star_Is_Born_c3f3f2c4.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_570f4137.mp3 b/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_570f4137.mp3 deleted file mode 100644 index 21b60605..00000000 Binary files a/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_570f4137.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_7638f4bd.mp3 b/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_7638f4bd.mp3 deleted file mode 100644 index 4b6eaee8..00000000 Binary files a/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_7638f4bd.mp3 and /dev/null differ diff --git a/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_7740f62e.mp3 b/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_7740f62e.mp3 deleted file mode 100644 index 33e4f0d4..00000000 Binary files a/backend/story_audio/scene_7_Celebration_and_Sweet_Goodbyes_7740f62e.mp3 and /dev/null differ diff --git a/backend/story_images/scene_1_Welcome_to_the_Cloud_Kitchen___ae6436d9.png b/backend/story_images/scene_1_Welcome_to_the_Cloud_Kitchen___ae6436d9.png deleted file mode 100644 index 8bdaf148..00000000 Binary files a/backend/story_images/scene_1_Welcome_to_the_Cloud_Kitchen___ae6436d9.png and /dev/null differ diff --git a/backend/story_images/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_6818cab1.png b/backend/story_images/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_6818cab1.png deleted file mode 100644 index 491b7321..00000000 Binary files a/backend/story_images/scene_1_Welcome_to_the_Fluffy_Cloud_Ki_6818cab1.png and /dev/null differ diff --git a/backend/story_images/scene_2_Meeting_Spark_the_Silver_Spoon_c3c1f32a.png b/backend/story_images/scene_2_Meeting_Spark_the_Silver_Spoon_c3c1f32a.png deleted file mode 100644 index 6adc132d..00000000 Binary files a/backend/story_images/scene_2_Meeting_Spark_the_Silver_Spoon_c3c1f32a.png and /dev/null differ diff --git a/backend/story_images/scene_3_Gathering_Space_Dust_and_Wishe_85bbcf02.png b/backend/story_images/scene_3_Gathering_Space_Dust_and_Wishe_85bbcf02.png deleted file mode 100644 index 55b2a6d2..00000000 Binary files a/backend/story_images/scene_3_Gathering_Space_Dust_and_Wishe_85bbcf02.png and /dev/null differ diff --git a/backend/story_images/scene_4_Gravity_s_Gentle_Pull_382cd57c.png b/backend/story_images/scene_4_Gravity_s_Gentle_Pull_382cd57c.png deleted file mode 100644 index be4fc682..00000000 Binary files a/backend/story_images/scene_4_Gravity_s_Gentle_Pull_382cd57c.png and /dev/null differ diff --git a/backend/story_images/scene_5_The_Mixture_Starts_to_Glow_4cdecd01.png b/backend/story_images/scene_5_The_Mixture_Starts_to_Glow_4cdecd01.png deleted file mode 100644 index 02d92ca8..00000000 Binary files a/backend/story_images/scene_5_The_Mixture_Starts_to_Glow_4cdecd01.png and /dev/null differ diff --git a/backend/story_images/scene_6_The_Birth_of_a_New_Star_d68c6f67.png b/backend/story_images/scene_6_The_Birth_of_a_New_Star_d68c6f67.png deleted file mode 100644 index f40f15f8..00000000 Binary files a/backend/story_images/scene_6_The_Birth_of_a_New_Star_d68c6f67.png and /dev/null differ diff --git a/backend/story_images/scene_7_Celebration_and_Sweet_Goodbyes_3a3373a2.png b/backend/story_images/scene_7_Celebration_and_Sweet_Goodbyes_3a3373a2.png deleted file mode 100644 index a1ad225b..00000000 Binary files a/backend/story_images/scene_7_Celebration_and_Sweet_Goodbyes_3a3373a2.png and /dev/null differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6ace50ee..02a0bcf5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,7 +14,6 @@ import BlogWriter from './components/BlogWriter/BlogWriter'; import StoryWriter from './components/StoryWriter/StoryWriter'; import { CreateStudio, EditStudio, UpscaleStudio, ControlStudio, SocialOptimizer, AssetLibrary, ImageStudioDashboard } from './components/ImageStudio'; import { ProductMarketingDashboard } from './components/ProductMarketing'; -import { ProductPhotoshootStudio } from './components/ProductMarketing/ProductPhotoshootStudio'; import PodcastDashboard from './components/PodcastMaker/PodcastDashboard'; import PricingPage from './components/Pricing/PricingPage'; import WixTestPage from './components/WixTestPage/WixTestPage'; @@ -463,7 +462,6 @@ const App: React.FC = () => { } /> } /> } /> - } /> } /> } /> } /> diff --git a/frontend/src/components/PodcastMaker/PodcastDashboard.tsx b/frontend/src/components/PodcastMaker/PodcastDashboard.tsx index 758c7c6b..057ff0b7 100644 --- a/frontend/src/components/PodcastMaker/PodcastDashboard.tsx +++ b/frontend/src/components/PodcastMaker/PodcastDashboard.tsx @@ -109,7 +109,8 @@ const CreateModal: React.FC<{ onCreate: (payload: CreateProjectPayload) => void; open: boolean; defaultKnobs: Knobs; -}> = ({ onCreate, open, defaultKnobs }) => { + isSubmitting?: boolean; +}> = ({ onCreate, open, defaultKnobs, isSubmitting = false }) => { const [idea, setIdea] = useState(""); const [url, setUrl] = useState(""); const [speakers, setSpeakers] = useState(1); @@ -119,8 +120,10 @@ const CreateModal: React.FC<{ const [avatarFile, setAvatarFile] = useState(null); const [knobs, setKnobs] = useState({ ...defaultKnobs }); + const canSubmit = Boolean(idea || url); + const submit = () => { - if (!idea && !url) return; + if (!canSubmit || isSubmitting) return; onCreate({ ideaOrUrl: idea || url, speakers, @@ -150,7 +153,7 @@ const CreateModal: React.FC<{

Enter a short idea or paste a blog URL. We'll analyze and suggest defaults.

- + Analyze & Continue @@ -274,7 +277,9 @@ const CreateModal: React.FC<{ > Reset - Analyze & Continue + + {isSubmitting ? "Analyzing..." : "Analyze & Continue"} + @@ -422,11 +427,13 @@ const SceneEditor: React.FC<{ onUpdateScene: (s: Scene) => void; onApprove: (id: string) => Promise; onPreviewLine: (text: string) => Promise<{ ok: boolean; message: string; audioUrl?: string }>; -}> = ({ scene, onUpdateScene, onApprove, onPreviewLine }) => { + approvingSceneId?: string | null; +}> = ({ scene, onUpdateScene, onApprove, onPreviewLine, approvingSceneId }) => { const updateLine = (updatedLine: Line) => { const updated = { ...scene, lines: scene.lines.map((l) => (l.id === updatedLine.id ? updatedLine : l)) }; onUpdateScene(updated); }; + const approving = approvingSceneId === scene.id; return ( @@ -444,8 +451,9 @@ const SceneEditor: React.FC<{ await onApprove(scene.id); onUpdateScene({ ...scene, approved: true }); }} + disabled={scene.approved || approving} > - Approve Scene + {scene.approved ? "Approved" : approving ? "Approving..." : "Approve Scene"} @@ -469,14 +477,20 @@ const ScriptEditor: React.FC<{ durationMinutes: number; onBackToResearch: () => void; onProceedToRendering: (script: Script) => void; -}> = ({ projectId, idea, research, rawResearch, knobs, speakers, durationMinutes, onBackToResearch, onProceedToRendering }) => { + onError: (message: string) => void; +}> = ({ projectId, idea, research, rawResearch, knobs, speakers, durationMinutes, onBackToResearch, onProceedToRendering, onError }) => { const [script, setScript] = useState