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
This commit is contained in:
ajaysi
2026-04-21 21:19:40 +05:30
parent 91b2f996fd
commit b1ca29f7f7
10 changed files with 66 additions and 30 deletions

View File

@@ -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}")

View File

@@ -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)

View File

@@ -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(

View File

@@ -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:

View File

@@ -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

View File

@@ -140,7 +140,7 @@ export const SceneCard: React.FC<SceneCardProps> = ({
// 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<SceneCardProps> = ({
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<SceneCardProps> = ({
}
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<SceneCardProps> = ({
// 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<SceneCardProps> = ({
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<SceneCardProps> = ({
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<SceneCardProps> = ({
// 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);

View File

@@ -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({

View File

@@ -141,11 +141,11 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ 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;

View File

@@ -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];
}
};

View File

@@ -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})` : '');
}
}
/**