feat: Enhance API interactions and improve job handling with new request validation and error management
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
VITE_API_URL=http://localhost:8000
|
||||
VITE_API_URL=/api
|
||||
VITE_APP_NAME=Qwen3-TTS
|
||||
|
||||
@@ -47,15 +47,15 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
const PRESET_INSTRUCTS = useMemo(() => tConstants('presetInstructs', { returnObjects: true }) as Array<{ label: string; instruct: string; text: string }>, [tConstants])
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(5000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 5000 })),
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(1000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 1000 })),
|
||||
language: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.language') })),
|
||||
speaker: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.speaker') })),
|
||||
instruct: z.string().optional(),
|
||||
max_new_tokens: z.number().min(1).max(10000).optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
max_new_tokens: z.number().min(128).max(4096).optional(),
|
||||
temperature: z.number().min(0.1).max(2).optional(),
|
||||
top_k: z.number().min(1).max(100).optional(),
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
repetition_penalty: z.number().min(0).max(2).optional(),
|
||||
repetition_penalty: z.number().min(1).max(2).optional(),
|
||||
})
|
||||
const [languages, setLanguages] = useState<Language[]>([])
|
||||
const [unifiedSpeakers, setUnifiedSpeakers] = useState<UnifiedSpeakerItem[]>([])
|
||||
@@ -395,8 +395,8 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
<Input
|
||||
id="dialog-max_new_tokens"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10000}
|
||||
min={128}
|
||||
max={4096}
|
||||
value={tempAdvancedParams.max_new_tokens}
|
||||
onChange={(e) => setTempAdvancedParams({
|
||||
...tempAdvancedParams,
|
||||
@@ -414,7 +414,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
<Input
|
||||
id="dialog-temperature"
|
||||
type="number"
|
||||
min={0}
|
||||
min={0.1}
|
||||
max={2}
|
||||
step={0.1}
|
||||
value={tempAdvancedParams.temperature}
|
||||
|
||||
@@ -49,17 +49,17 @@ function VoiceCloneForm() {
|
||||
const PRESET_REF_TEXTS = useMemo(() => tConstants('presetRefTexts', { returnObjects: true }) as Array<{ label: string; text: string }>, [tConstants])
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(5000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 5000 })),
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(1000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 1000 })),
|
||||
language: z.string().optional(),
|
||||
ref_audio: z.instanceof(File, { message: tErrors('validation.required', { field: tErrors('fieldNames.reference_audio') }) }),
|
||||
ref_text: z.string().optional(),
|
||||
use_cache: z.boolean().optional(),
|
||||
x_vector_only_mode: z.boolean().optional(),
|
||||
max_new_tokens: z.number().min(1).max(10000).optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
max_new_tokens: z.number().min(128).max(4096).optional(),
|
||||
temperature: z.number().min(0.1).max(2).optional(),
|
||||
top_k: z.number().min(1).max(100).optional(),
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
repetition_penalty: z.number().min(0).max(2).optional(),
|
||||
repetition_penalty: z.number().min(1).max(2).optional(),
|
||||
})
|
||||
const [languages, setLanguages] = useState<Language[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -358,8 +358,8 @@ function VoiceCloneForm() {
|
||||
<Input
|
||||
id="dialog-max_new_tokens"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10000}
|
||||
min={128}
|
||||
max={4096}
|
||||
value={tempAdvancedParams.max_new_tokens}
|
||||
onChange={(e) => setTempAdvancedParams({
|
||||
...tempAdvancedParams,
|
||||
|
||||
@@ -46,14 +46,14 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
const PRESET_VOICE_DESIGNS = useMemo(() => tConstants('presetVoiceDesigns', { returnObjects: true }) as Array<{ label: string; instruct: string; text: string }>, [tConstants])
|
||||
|
||||
const formSchema = z.object({
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(5000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 5000 })),
|
||||
text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(1000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 1000 })),
|
||||
language: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.language') })),
|
||||
instruct: z.string().min(10, tErrors('validation.minLength', { field: tErrors('fieldNames.instruct'), min: 10 })).max(500, tErrors('validation.maxLength', { field: tErrors('fieldNames.instruct'), max: 500 })),
|
||||
max_new_tokens: z.number().min(1).max(10000).optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
max_new_tokens: z.number().min(128).max(4096).optional(),
|
||||
temperature: z.number().min(0.1).max(2).optional(),
|
||||
top_k: z.number().min(1).max(100).optional(),
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
repetition_penalty: z.number().min(0).max(2).optional(),
|
||||
repetition_penalty: z.number().min(1).max(2).optional(),
|
||||
})
|
||||
const [languages, setLanguages] = useState<Language[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -310,8 +310,8 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
<Input
|
||||
id="dialog-max_new_tokens"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10000}
|
||||
min={128}
|
||||
max={4096}
|
||||
value={tempAdvancedParams.max_new_tokens}
|
||||
onChange={(e) => setTempAdvancedParams({
|
||||
...tempAdvancedParams,
|
||||
@@ -329,7 +329,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
<Input
|
||||
id="dialog-temperature"
|
||||
type="number"
|
||||
min={0}
|
||||
min={0.1}
|
||||
max={2}
|
||||
step={0.1}
|
||||
value={tempAdvancedParams.temperature}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react'
|
||||
import { createContext, useContext, useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { jobApi } from '@/lib/api'
|
||||
import type { Job, JobStatus } from '@/types/job'
|
||||
@@ -25,13 +25,27 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
||||
const [elapsedTime, setElapsedTime] = useState(0)
|
||||
|
||||
const { refresh: historyRefresh } = useHistoryContext()
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const timeIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const clearIntervals = useCallback(() => {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current)
|
||||
pollIntervalRef.current = null
|
||||
}
|
||||
if (timeIntervalRef.current) {
|
||||
clearInterval(timeIntervalRef.current)
|
||||
timeIntervalRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const stopJob = useCallback(() => {
|
||||
clearIntervals()
|
||||
setCurrentJob(null)
|
||||
setStatus(null)
|
||||
setError(null)
|
||||
setElapsedTime(0)
|
||||
}, [])
|
||||
}, [clearIntervals])
|
||||
|
||||
const resetJob = useCallback(() => {
|
||||
setError(null)
|
||||
@@ -45,15 +59,13 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
||||
}, [])
|
||||
|
||||
const startJob = useCallback((jobId: number) => {
|
||||
clearIntervals()
|
||||
// Reset state for new job
|
||||
setCurrentJob(null)
|
||||
setStatus('pending')
|
||||
setError(null)
|
||||
setElapsedTime(0)
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
let timeInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const job = await jobApi.getJob(jobId)
|
||||
@@ -61,15 +73,13 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
||||
setStatus(job.status)
|
||||
|
||||
if (job.status === 'completed') {
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
clearIntervals()
|
||||
toast.success('任务完成!')
|
||||
try {
|
||||
historyRefresh()
|
||||
} catch {}
|
||||
} else if (job.status === 'failed') {
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
clearIntervals()
|
||||
setError(job.error_message || '任务失败')
|
||||
toast.error(job.error_message || '任务失败')
|
||||
try {
|
||||
@@ -77,8 +87,7 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
||||
} catch {}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
clearIntervals()
|
||||
const message = error.response?.data?.detail || '获取任务状态失败'
|
||||
setError(message)
|
||||
toast.error(message)
|
||||
@@ -86,16 +95,17 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
poll()
|
||||
pollInterval = setInterval(poll, POLL_INTERVAL)
|
||||
timeInterval = setInterval(() => {
|
||||
pollIntervalRef.current = setInterval(poll, POLL_INTERVAL)
|
||||
timeIntervalRef.current = setInterval(() => {
|
||||
setElapsedTime((prev) => prev + 1)
|
||||
}, 1000)
|
||||
}, [historyRefresh, clearIntervals])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
clearIntervals()
|
||||
}
|
||||
}, [historyRefresh])
|
||||
}, [clearIntervals])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -13,9 +13,22 @@ const apiClient = axios.create({
|
||||
},
|
||||
})
|
||||
|
||||
const isTrustedApiRequest = (url?: string, baseURL?: string): boolean => {
|
||||
if (!url) return false
|
||||
if (url.startsWith('/')) return true
|
||||
|
||||
try {
|
||||
const resolvedUrl = new URL(url, baseURL || window.location.origin)
|
||||
const apiOrigin = baseURL ? new URL(baseURL, window.location.origin).origin : window.location.origin
|
||||
return resolvedUrl.origin === apiOrigin
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
if (token && isTrustedApiRequest(config.url, config.baseURL || import.meta.env.VITE_API_URL)) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
@@ -346,11 +359,23 @@ export const jobApi = {
|
||||
getAudioUrl: (id: number, audioPath?: string): string => {
|
||||
if (audioPath) {
|
||||
if (audioPath.startsWith('http')) {
|
||||
const apiBase = import.meta.env.VITE_API_URL
|
||||
if (apiBase) {
|
||||
try {
|
||||
const audioOrigin = new URL(audioPath).origin
|
||||
const apiOrigin = new URL(apiBase, window.location.origin).origin
|
||||
if (audioOrigin !== apiOrigin) {
|
||||
return API_ENDPOINTS.JOBS.AUDIO(id)
|
||||
}
|
||||
} catch {
|
||||
return API_ENDPOINTS.JOBS.AUDIO(id)
|
||||
}
|
||||
}
|
||||
if (audioPath.includes('localhost') || audioPath.includes('127.0.0.1')) {
|
||||
const url = new URL(audioPath)
|
||||
return url.pathname
|
||||
}
|
||||
return audioPath
|
||||
return API_ENDPOINTS.JOBS.AUDIO(id)
|
||||
} else {
|
||||
return audioPath.startsWith('/') ? audioPath : `/${audioPath}`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,15 @@ import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
Reference in New Issue
Block a user