From b1ca29f7f7a245a9ef598b0a7fa91f6a12f02dcf Mon Sep 17 00:00:00 2001 From: ajaysi Date: Tue, 21 Apr 2026 21:19:40 +0530 Subject: [PATCH] fix: workspace-aware media resolution + production-ready logging - load_podcast_image_bytes now accepts user_id for workspace-aware resolution - Video and avatar handlers pass user_id to image loading - Strip JWT tokens from console logs (dev-only verbose logging) - Guard debug logs behind NODE_ENV===development in SceneCard, useRenderQueue, SubscriptionContext, mediaCache - Add devLogger utility --- backend/api/podcast/handlers/avatar.py | 2 +- backend/api/podcast/handlers/images.py | 2 +- backend/api/podcast/handlers/video.py | 4 ++-- backend/api/podcast/utils.py | 23 ++++++++++++++++--- frontend/src/api/client.ts | 6 +++-- .../PodcastMaker/RenderQueue/SceneCard.tsx | 14 +++++------ .../RenderQueue/useRenderQueue.ts | 13 ++++------- frontend/src/contexts/SubscriptionContext.tsx | 4 ++-- frontend/src/utils/devLogger.ts | 20 ++++++++++++++++ frontend/src/utils/mediaCache.ts | 8 ++++--- 10 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 frontend/src/utils/devLogger.ts diff --git a/backend/api/podcast/handlers/avatar.py b/backend/api/podcast/handlers/avatar.py index e197f5c0..33fab274 100644 --- a/backend/api/podcast/handlers/avatar.py +++ b/backend/api/podcast/handlers/avatar.py @@ -128,7 +128,7 @@ async def make_avatar_presentable( # Load the uploaded avatar image from ..utils import load_podcast_image_bytes logger.info(f"[Podcast] Loading avatar image from {avatar_url}") - avatar_bytes = load_podcast_image_bytes(avatar_url) + avatar_bytes = load_podcast_image_bytes(avatar_url, user_id=user_id) logger.info(f"[Podcast] Avatar loaded successfully - size={len(avatar_bytes)} bytes") logger.info(f"[Podcast] Transforming avatar to podcast presenter for project {project_id}") diff --git a/backend/api/podcast/handlers/images.py b/backend/api/podcast/handlers/images.py index 70722a52..b7dabcc2 100644 --- a/backend/api/podcast/handlers/images.py +++ b/backend/api/podcast/handlers/images.py @@ -69,7 +69,7 @@ async def generate_podcast_scene_image( from ..utils import load_podcast_image_bytes try: logger.info(f"[Podcast] Attempting to load base avatar from: {request.base_avatar_url}") - base_avatar_bytes = load_podcast_image_bytes(request.base_avatar_url) + base_avatar_bytes = load_podcast_image_bytes(request.base_avatar_url, user_id=user_id) logger.info(f"[Podcast] ✅ Successfully loaded base avatar ({len(base_avatar_bytes)} bytes) for scene {request.scene_id}") except Exception as e: logger.error(f"[Podcast] ❌ Failed to load base avatar from {request.base_avatar_url}: {e}", exc_info=True) diff --git a/backend/api/podcast/handlers/video.py b/backend/api/podcast/handlers/video.py index 5e2f26bf..cb799bb7 100644 --- a/backend/api/podcast/handlers/video.py +++ b/backend/api/podcast/handlers/video.py @@ -321,7 +321,7 @@ async def generate_podcast_video( # Load image bytes (scene image is required for video generation) if body.avatar_image_url: - image_bytes = load_podcast_image_bytes(body.avatar_image_url) + image_bytes = load_podcast_image_bytes(body.avatar_image_url, user_id=user_id) else: # Scene-specific image should be generated before video generation raise HTTPException( @@ -332,7 +332,7 @@ async def generate_podcast_video( mask_image_bytes = None if body.mask_image_url: try: - mask_image_bytes = load_podcast_image_bytes(body.mask_image_url) + mask_image_bytes = load_podcast_image_bytes(body.mask_image_url, user_id=user_id) except Exception as e: logger.error(f"[Podcast] Failed to load mask image: {e}") raise HTTPException( diff --git a/backend/api/podcast/utils.py b/backend/api/podcast/utils.py index 14248f5f..6f6df85c 100644 --- a/backend/api/podcast/utils.py +++ b/backend/api/podcast/utils.py @@ -67,15 +67,32 @@ def load_podcast_audio_bytes(audio_url: str, user_id: str | None = None) -> byte raise HTTPException(status_code=500, detail=f"Failed to load audio: {str(exc)}") -def load_podcast_image_bytes(image_url: str) -> bytes: - """Load podcast image bytes from URL. Uses centralized media loader.""" +def load_podcast_image_bytes(image_url: str, user_id: str | None = None) -> bytes: + """Load podcast image bytes from URL. Resolves from workspace first.""" if not image_url: raise HTTPException(status_code=400, detail="Image URL is required") logger.info(f"[Podcast] Loading image from URL: {image_url}") try: - # REUSE: Use centralized media loader which handles cross-module lookups + # Extract filename from URL path + prefix = "/api/podcast/images/" + if prefix in image_url: + filename = image_url.split(prefix, 1)[1].split("?", 1)[0].strip() + # Handle subdirectories like avatars/ + subdir = None + if "/" in filename: + subdir_part = filename.rsplit("/", 1)[0] + subdir = Path(subdir_part) + filename = filename.rsplit("/", 1)[1] + + try: + image_path = _resolve_podcast_media_file(filename, "image", user_id, subdir=subdir) + return image_path.read_bytes() + except HTTPException: + pass # Fall through to centralized loader + + # Fall back to centralized media loader image_bytes = load_media_bytes(image_url) if not image_bytes: diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index b5884311..2493cb83 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -224,8 +224,10 @@ apiClient.interceptors.request.use( if (token) { config.headers = config.headers || {}; (config.headers as any)['Authorization'] = `Bearer ${token}`; - const safeUrlWithToken = sanitizeUrlForLogging(config.url); - console.log(`[apiClient] ✅ Auth token attached for request to ${safeUrlWithToken}`); + if (process.env.NODE_ENV === 'development') { + const safeUrlWithToken = sanitizeUrlForLogging(config.url); + console.log(`[apiClient] ✅ Auth token attached for request to ${safeUrlWithToken}`); + } } else { // Token getter returned null - reject request to prevent 401 errors // ProtectedRoute should ensure user is authenticated before components render diff --git a/frontend/src/components/PodcastMaker/RenderQueue/SceneCard.tsx b/frontend/src/components/PodcastMaker/RenderQueue/SceneCard.tsx index 6471d8b9..5fa6eb01 100644 --- a/frontend/src/components/PodcastMaker/RenderQueue/SceneCard.tsx +++ b/frontend/src/components/PodcastMaker/RenderQueue/SceneCard.tsx @@ -140,7 +140,7 @@ export const SceneCard: React.FC = ({ // Check cache first with scene context const cachedUrl = getCachedMedia(imageUrl, scene.id); if (cachedUrl) { - console.log('[SceneCard] Using cached image:', imageUrl, `(scene: ${scene.id})`); + if (process.env.NODE_ENV === 'development') console.log('[SceneCard] Using cached image:', imageUrl, `(scene: ${scene.id})`); setImageBlobUrl(cachedUrl); setImageLoading(false); setImageError(null); @@ -167,7 +167,7 @@ export const SceneCard: React.FC = ({ try { setImageLoading(true); setImageError(null); - console.log('[SceneCard] Loading image blob for:', currentImageUrl); + if (process.env.NODE_ENV === 'development') console.log('[SceneCard] Loading image blob for:', currentImageUrl.split('?')[0]); // Check cache again in case it was loaded while we were waiting const cachedUrl = getCachedMedia(currentImageUrl, scene.id); @@ -219,7 +219,7 @@ export const SceneCard: React.FC = ({ } return newBlobUrl; }); - console.log('[SceneCard] Image blob loaded and cached successfully:', currentImageUrl); + if (process.env.NODE_ENV === 'development') console.log('[SceneCard] Image blob loaded and cached successfully:', currentImageUrl.split('?')[0]); } catch (err) { console.error('[SceneCard] Failed to load image blob:', err); if (isMounted && imageUrl === currentImageUrl) { @@ -287,7 +287,7 @@ export const SceneCard: React.FC = ({ // Check cache first with scene context const cachedUrl = getCachedMedia(job.videoUrl, scene.id); if (cachedUrl) { - console.log('[SceneCard] Using cached video:', job.videoUrl, `(scene: ${scene.id})`); + if (process.env.NODE_ENV === 'development') console.log('[SceneCard] Using cached video:', job.videoUrl?.split('?')[0], `(scene: ${scene.id})`); setVideoBlobUrl(cachedUrl); setVideoLoading(false); setVideoError(null); @@ -312,7 +312,7 @@ export const SceneCard: React.FC = ({ return; } - console.log('[SceneCard] Loading video blob for:', job.videoUrl); + if (process.env.NODE_ENV === 'development') console.log('[SceneCard] Loading video blob for:', (job.videoUrl || '').split('?')[0]); const blobUrl = await fetchMediaBlobUrl(job.videoUrl!); if (blobUrl) { @@ -332,7 +332,7 @@ export const SceneCard: React.FC = ({ testVideo.onloadedmetadata = () => { clearTimeout(timeout); - console.log('[SceneCard] Video blob validation successful:', { + if (process.env.NODE_ENV === 'development') console.log('[SceneCard] Video blob validation successful:', { duration: testVideo.duration, videoWidth: testVideo.videoWidth, videoHeight: testVideo.videoHeight, @@ -353,7 +353,7 @@ export const SceneCard: React.FC = ({ // Cache the validated blob URL with scene context setCachedMedia(job.videoUrl!, blobUrl, 'video', undefined, scene.id); - console.log('[SceneCard] Video blob loaded, validated, and cached successfully:', job.videoUrl); + if (process.env.NODE_ENV === 'development') console.log('[SceneCard] Video blob loaded, validated, and cached successfully:', (job.videoUrl || '').split('?')[0]); } else { // Direct URL fallback setVideoBlobUrl(blobUrl); diff --git a/frontend/src/components/PodcastMaker/RenderQueue/useRenderQueue.ts b/frontend/src/components/PodcastMaker/RenderQueue/useRenderQueue.ts index 37afb773..74285e4d 100644 --- a/frontend/src/components/PodcastMaker/RenderQueue/useRenderQueue.ts +++ b/frontend/src/components/PodcastMaker/RenderQueue/useRenderQueue.ts @@ -132,7 +132,7 @@ export const useRenderQueue = ({ // Skip if job already has imageUrl from script phase - don't override with old video if (job?.imageUrl) { - console.log("[useRenderQueue] Skipping old video - job has imageUrl from script phase:", scene.id, "imageUrl:", job.imageUrl); + if (process.env.NODE_ENV === 'development') console.log("[useRenderQueue] Skipping old video - job has imageUrl from script phase:", scene.id, "imageUrl:", (job.imageUrl || '').split('?')[0]); return; } @@ -143,7 +143,7 @@ export const useRenderQueue = ({ // If job has finalUrl (audio) or imageUrl from script phase, don't attach old video const isJobEmpty = !job || (!job.imageUrl && !job.videoUrl && !job.finalUrl); if (!isJobEmpty) { - console.log("[useRenderQueue] Skipping old video - job has content already:", scene.id, "job:", job); + if (process.env.NODE_ENV === 'development') console.log("[useRenderQueue] Skipping old video - job has content already:", scene.id); return; } @@ -581,15 +581,10 @@ export const useRenderQueue = ({ }); try { - console.log("[useRenderQueue] Starting video generation", { + if (process.env.NODE_ENV === 'development') console.log("[useRenderQueue] Starting video generation", { sceneId, sceneTitle: scene.title, - audioUrl, - avatarImageUrl: sceneImageUrl, resolution: targetResolution, - prompt: settings?.prompt, - seed: settings?.seed, - maskImageUrl: settings?.maskImageUrl, }); const result = await podcastApi.generateVideo({ @@ -708,7 +703,7 @@ export const useRenderQueue = ({ sceneVideoUrls.push(videoUrl); } - console.log("[combineFinalVideo] Starting combination with", sceneVideoUrls.length, "videos"); + if (process.env.NODE_ENV === 'development') console.log("[combineFinalVideo] Starting combination with", sceneVideoUrls.length, "videos"); // Start combination task const result = await podcastApi.combineVideos({ diff --git a/frontend/src/contexts/SubscriptionContext.tsx b/frontend/src/contexts/SubscriptionContext.tsx index 18ec103f..773060d4 100644 --- a/frontend/src/contexts/SubscriptionContext.tsx +++ b/frontend/src/contexts/SubscriptionContext.tsx @@ -141,11 +141,11 @@ export const SubscriptionProvider: React.FC = ({ chil // Continue anyway - apiClient interceptor will handle missing token gracefully } - console.log('SubscriptionContext: Checking subscription for user:', userId); + if (process.env.NODE_ENV === 'development') console.log('SubscriptionContext: Checking subscription for user:', userId); const response = await apiClient.get(`/api/subscription/status/${userId}`); const subscriptionData = response.data.data; - console.log('SubscriptionContext: Received subscription data from backend:', subscriptionData); + if (process.env.NODE_ENV === 'development') console.log('SubscriptionContext: Subscription data received:', { active: subscriptionData?.active, plan: subscriptionData?.plan }); setSubscription(subscriptionData); // Update ref immediately so callbacks can access latest value subscriptionRef.current = subscriptionData; diff --git a/frontend/src/utils/devLogger.ts b/frontend/src/utils/devLogger.ts new file mode 100644 index 00000000..7d0aa42e --- /dev/null +++ b/frontend/src/utils/devLogger.ts @@ -0,0 +1,20 @@ +const isDev = process.env.NODE_ENV === 'development'; + +export const devLog = { + log: (...args: any[]) => { if (isDev) console.log(...args); }, + warn: (...args: any[]) => { if (isDev) console.warn(...args); }, + error: (...args: any[]) => { console.error(...args); }, + info: (...args: any[]) => { if (isDev) console.info(...args); }, +}; + +export const sanitizeUrl = (url: string): string => { + try { + const parsed = new URL(url, window.location.origin); + if (parsed.searchParams.has('token')) { + parsed.searchParams.set('token', '***'); + } + return parsed.pathname + (parsed.search ? parsed.search : ''); + } catch { + return url.split('?')[0]; + } +}; \ No newline at end of file diff --git a/frontend/src/utils/mediaCache.ts b/frontend/src/utils/mediaCache.ts index c223b5d1..7849b1eb 100644 --- a/frontend/src/utils/mediaCache.ts +++ b/frontend/src/utils/mediaCache.ts @@ -102,9 +102,11 @@ class MediaCache { this.evictOldest(); } - console.log(`[MediaCache] Cached ${mediaType}:`, url, - sceneId ? `(scene: ${sceneId})` : '', - projectId ? `(project: ${projectId})` : ''); +if (process.env.NODE_ENV === 'development') { + console.log(`[MediaCache] Cached ${mediaType}:`, url.split('?')[0], + sceneId ? `(scene: ${sceneId})` : '', + projectId ? `(project: ${projectId})` : ''); + } } /**