194 lines
6.1 KiB
TypeScript
194 lines
6.1 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import api from "@/shared/api/axios";
|
|
import { ApiResponse, unwrap } from "@/shared/api/types";
|
|
import { toast } from "sonner";
|
|
|
|
export interface GeneratedAudio {
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
duration_sec: number;
|
|
text: string;
|
|
tts_mode: string;
|
|
language: string;
|
|
created_at: number;
|
|
}
|
|
|
|
interface AudioTask {
|
|
status: string;
|
|
progress?: number;
|
|
message?: string;
|
|
output?: GeneratedAudio & { audio_id: string };
|
|
}
|
|
|
|
interface UseGeneratedAudiosOptions {
|
|
selectedAudioId: string | null;
|
|
setSelectedAudioId: React.Dispatch<React.SetStateAction<string | null>>;
|
|
}
|
|
|
|
export const useGeneratedAudios = ({
|
|
selectedAudioId,
|
|
setSelectedAudioId,
|
|
}: UseGeneratedAudiosOptions) => {
|
|
const [generatedAudios, setGeneratedAudios] = useState<GeneratedAudio[]>([]);
|
|
const [selectedAudio, setSelectedAudio] = useState<GeneratedAudio | null>(null);
|
|
const [isGeneratingAudio, setIsGeneratingAudio] = useState(false);
|
|
const [audioTaskId, setAudioTaskId] = useState<string | null>(null);
|
|
const [audioTask, setAudioTask] = useState<AudioTask | null>(null);
|
|
const pollRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const fetchGeneratedAudios = useCallback(async (selectId?: string) => {
|
|
try {
|
|
const { data: res } = await api.get<ApiResponse<{ items: GeneratedAudio[] }>>(
|
|
"/api/generated-audios"
|
|
);
|
|
const payload = unwrap(res);
|
|
const items: GeneratedAudio[] = payload.items || [];
|
|
setGeneratedAudios(items);
|
|
|
|
if (selectId && items.length > 0) {
|
|
if (selectId === "__latest__") {
|
|
setSelectedAudioId(items[0].id);
|
|
setSelectedAudio(items[0]);
|
|
} else {
|
|
const found = items.find((a) => a.id === selectId);
|
|
if (found) {
|
|
setSelectedAudioId(found.id);
|
|
setSelectedAudio(found);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("获取配音列表失败:", error);
|
|
}
|
|
}, [setSelectedAudioId]);
|
|
|
|
// Sync selectedAudio when selectedAudioId changes externally (e.g. from persistence)
|
|
useEffect(() => {
|
|
if (!selectedAudioId || generatedAudios.length === 0) return;
|
|
const found = generatedAudios.find((a) => a.id === selectedAudioId);
|
|
if (found) {
|
|
setSelectedAudio(found);
|
|
}
|
|
}, [selectedAudioId, generatedAudios]);
|
|
|
|
const stopPolling = useCallback(() => {
|
|
if (pollRef.current) {
|
|
clearInterval(pollRef.current);
|
|
pollRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
const startPolling = useCallback((taskId: string) => {
|
|
stopPolling();
|
|
pollRef.current = setInterval(async () => {
|
|
try {
|
|
const { data: res } = await api.get<ApiResponse<AudioTask>>(
|
|
`/api/generated-audios/tasks/${taskId}`
|
|
);
|
|
const task = unwrap(res);
|
|
setAudioTask(task);
|
|
|
|
if (task.status === "completed") {
|
|
stopPolling();
|
|
setIsGeneratingAudio(false);
|
|
setAudioTaskId(null);
|
|
// Refresh list and select the new audio
|
|
await fetchGeneratedAudios("__latest__");
|
|
toast.success(task.message || "配音生成完成");
|
|
} else if (task.status === "failed") {
|
|
stopPolling();
|
|
setIsGeneratingAudio(false);
|
|
setAudioTaskId(null);
|
|
toast.error(task.message || "配音生成失败");
|
|
} else if (task.status === "not_found") {
|
|
stopPolling();
|
|
setIsGeneratingAudio(false);
|
|
setAudioTaskId(null);
|
|
setAudioTask(null);
|
|
toast.error("任务已丢失(服务可能已重启),请重新生成");
|
|
}
|
|
} catch {
|
|
// Network error, keep polling
|
|
}
|
|
}, 1000);
|
|
}, [stopPolling, fetchGeneratedAudios]);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => stopPolling();
|
|
}, [stopPolling]);
|
|
|
|
const generateAudio = useCallback(async (params: {
|
|
text: string;
|
|
tts_mode: string;
|
|
voice?: string;
|
|
ref_audio_id?: string;
|
|
ref_text?: string;
|
|
language: string;
|
|
speed?: number;
|
|
}) => {
|
|
setIsGeneratingAudio(true);
|
|
setAudioTask({ status: "pending", progress: 0, message: "正在提交..." });
|
|
|
|
try {
|
|
const { data: res } = await api.post<ApiResponse<{ task_id: string }>>(
|
|
"/api/generated-audios/generate",
|
|
params
|
|
);
|
|
const { task_id } = unwrap(res);
|
|
setAudioTaskId(task_id);
|
|
startPolling(task_id);
|
|
} catch (err: unknown) {
|
|
setIsGeneratingAudio(false);
|
|
setAudioTask(null);
|
|
const axiosErr = err as { response?: { data?: { message?: string } }; message?: string };
|
|
const errorMsg = axiosErr.response?.data?.message || axiosErr.message || String(err);
|
|
toast.error(`配音生成失败: ${errorMsg}`);
|
|
}
|
|
}, [startPolling]);
|
|
|
|
const deleteAudio = useCallback(async (audioId: string) => {
|
|
if (!confirm("确定要删除这个配音吗?")) return;
|
|
try {
|
|
await api.delete(`/api/generated-audios/${encodeURIComponent(audioId)}`);
|
|
if (selectedAudioId === audioId) {
|
|
setSelectedAudioId(null);
|
|
setSelectedAudio(null);
|
|
}
|
|
fetchGeneratedAudios();
|
|
} catch (error) {
|
|
toast.error("删除失败: " + error);
|
|
}
|
|
}, [fetchGeneratedAudios, selectedAudioId, setSelectedAudioId]);
|
|
|
|
const renameAudio = useCallback(async (audioId: string, newName: string) => {
|
|
try {
|
|
await api.put(`/api/generated-audios/${encodeURIComponent(audioId)}`, {
|
|
new_name: newName,
|
|
});
|
|
fetchGeneratedAudios();
|
|
} catch (err: unknown) {
|
|
toast.error("重命名失败: " + String(err));
|
|
}
|
|
}, [fetchGeneratedAudios]);
|
|
|
|
const selectAudio = useCallback((audio: GeneratedAudio) => {
|
|
setSelectedAudioId(audio.id);
|
|
setSelectedAudio(audio);
|
|
}, [setSelectedAudioId]);
|
|
|
|
return {
|
|
generatedAudios,
|
|
selectedAudio,
|
|
selectedAudioId,
|
|
isGeneratingAudio,
|
|
audioTask,
|
|
fetchGeneratedAudios,
|
|
generateAudio,
|
|
deleteAudio,
|
|
renameAudio,
|
|
selectAudio,
|
|
};
|
|
};
|