fix: voice clone preview audio authentication + MIME type fixes

- Restore auth on assets_serving.py using get_current_user_with_query_token
  (supports ?token= query param for <audio> elements)
- Add proper MIME type detection on asset serving (fixes NotSupportedError)
- Use storage_paths for path resolution in assets_serving.py
- VoiceSelector: append auth token to preview URLs for /api/ endpoints
- VoiceAvatarPlaceholder: add authenticatedAudioUrl state with async token
  resolution so <audio> elements get ?token= query param
- TestPersonaModal: same auth token pattern for voice preview audio
This commit is contained in:
ajaysi
2026-04-22 08:04:55 +05:30
parent 02d13716f3
commit 913e59a0a8
4 changed files with 92 additions and 31 deletions

View File

@@ -10,6 +10,7 @@ import {
import { createAvatarVideoAsync } from '../../../../api/videoStudioApi';
import { useVideoGenerationPolling } from '../../../../hooks/usePolling';
import { fetchMediaBlobUrl } from '../../../../utils/fetchMediaBlobUrl';
import { getAuthTokenGetter } from '../../../../api/client';
import { VideoCameraFront, SkipNext, PlayArrow, InfoOutlined, Close as CloseIcon, HelpOutline, Refresh, RestartAlt, Undo } from '@mui/icons-material';
import { VideoGenerationLoader } from '../../../shared/VideoGenerationLoader';
import { OperationButton } from '../../../shared/OperationButton';
@@ -31,8 +32,33 @@ export const TestPersonaModal: React.FC<TestPersonaModalProps> = ({
const [model, setModel] = useState<'infinitetalk' | 'hunyuan-avatar'>('infinitetalk');
const [showCapabilities, setShowCapabilities] = useState(false);
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | null>(null);
const [authenticatedVoiceUrl, setAuthenticatedVoiceUrl] = useState<string | null>(null);
const STORAGE_KEY = 'test_persona_video_url';
const STORAGE_BACKUP_KEY = 'test_persona_video_url_backup';
// Append auth token to /api/ asset URLs so <audio> can access them
useEffect(() => {
if (!voiceUrl || !voiceUrl.includes('/api/')) {
setAuthenticatedVoiceUrl(voiceUrl || null);
return;
}
let cancelled = false;
(async () => {
try {
const tokenGetter = getAuthTokenGetter();
if (tokenGetter) {
const token = await tokenGetter();
if (token && !cancelled) {
const sep = voiceUrl.includes('?') ? '&' : '?';
setAuthenticatedVoiceUrl(`${voiceUrl}${sep}token=${encodeURIComponent(token)}`);
return;
}
}
} catch { /* fallback */ }
if (!cancelled) setAuthenticatedVoiceUrl(voiceUrl);
})();
return () => { cancelled = true; };
}, [voiceUrl]);
const [generatedVideoUrl, setGeneratedVideoUrl] = useState<string | null>(() => {
try {
@@ -527,7 +553,7 @@ export const TestPersonaModal: React.FC<TestPersonaModalProps> = ({
<Typography variant="caption" fontWeight="bold" sx={{ mb: 1, display: 'block', color: '#64748b' }}>
Voice Preview
</Typography>
<audio controls src={voiceUrl} style={{ width: '100%', height: 36 }} />
<audio controls src={authenticatedVoiceUrl || undefined} style={{ width: '100%', height: 36 }} />
</Box>
</Stack>

View File

@@ -1,8 +1,9 @@
import React, { useMemo, useRef, useState, useEffect } from 'react';
import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
import { Box, Typography, Paper, Stack, Button, Alert, TextField, CircularProgress, Slider, FormControlLabel, Checkbox, MenuItem, Tooltip, Chip, Divider, Grid, IconButton, Modal, Fade, Backdrop, LinearProgress } from '@mui/material';
import { keyframes } from '@mui/system';
import { Mic, GraphicEq, Timer, CloudUpload, Stop, PlayArrow, InfoOutlined, TextFields, HelpOutline, AutoAwesome, Campaign, MicNone, Podcasts, RestartAlt, Undo, Headphones, Article, VideoLibrary, TrendingUp, CheckCircle, RecordVoiceOver, Settings } from '@mui/icons-material';
import { createVoiceClone, createVoiceDesign, getLatestVoiceClone, setBrandVoice } from '../../../../api/brandAssets';
import { getAuthTokenGetter } from '../../../../api/client';
import { OperationButton } from '../../../shared/OperationButton';
const pulse = keyframes`
@@ -95,6 +96,32 @@ export const VoiceAvatarPlaceholder: React.FC<{ domainName?: string; onVoiceSet?
} catch { return null; }
});
// Append auth token to /api/ asset URLs so <audio> elements can access them
const [authenticatedAudioUrl, setAuthenticatedAudioUrl] = useState<string | null>(null);
useEffect(() => {
if (!resultAudioUrl || !resultAudioUrl.includes('/api/')) {
setAuthenticatedAudioUrl(resultAudioUrl);
return;
}
let cancelled = false;
(async () => {
try {
const tokenGetter = getAuthTokenGetter();
if (tokenGetter) {
const token = await tokenGetter();
if (token && !cancelled) {
const sep = resultAudioUrl.includes('?') ? '&' : '?';
setAuthenticatedAudioUrl(`${resultAudioUrl}${sep}token=${encodeURIComponent(token)}`);
return;
}
}
} catch { /* fallback to unauthenticated */ }
if (!cancelled) setAuthenticatedAudioUrl(resultAudioUrl);
})();
return () => { cancelled = true; };
}, [resultAudioUrl]);
const [archivedResultAudioUrl, setArchivedResultAudioUrl] = useState<string | null>(() => {
try {
const saved = localStorage.getItem(STORAGE_BACKUP_KEY);
@@ -1090,7 +1117,7 @@ export const VoiceAvatarPlaceholder: React.FC<{ domainName?: string; onVoiceSet?
<Typography variant="caption" fontWeight="800" sx={{ color: '#EC4899', textTransform: 'uppercase', mb: 0.25, display: 'block', fontSize: '0.65rem' }}>
Generated AI Voice Preview
</Typography>
<audio controls src={resultAudioUrl} style={{ width: '100%', height: '28px' }} />
<audio controls src={authenticatedAudioUrl || undefined} style={{ width: '100%', height: '28px' }} />
<Stack direction="row" spacing={1} sx={{ mt: 1 }}>
<Button
variant="outlined"