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:
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
frontend/src/utils/devLogger.ts
Normal file
20
frontend/src/utils/devLogger.ts
Normal 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];
|
||||
}
|
||||
};
|
||||
@@ -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})` : '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user