fix(#1195): 克隆音色播放失败状态修复 + 音频切换竞态防护 + 卸载清理
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 32s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m45s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m42s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m38s
AI Code Review / AI Code Review (pull_request) Failing after 3m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m57s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 32s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m45s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m42s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m38s
AI Code Review / AI Code Review (pull_request) Failing after 3m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m57s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
This commit is contained in:
@@ -2,30 +2,30 @@
|
||||
* Step 5 配音选择 Hook
|
||||
* 封装 AI 推荐、预设音色试听、TTS 自定义合成、存为素材等逻辑
|
||||
*/
|
||||
import { useState, useRef, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import type { VoiceClone } from "@/api/voice-clone"
|
||||
import { VOICE_GENDER_ICON, CLONE_STATUS_CONFIG } from "../constants"
|
||||
import { formatDuration } from "../utils/formatDuration"
|
||||
import { useVoiceAudio } from "./step5-voice/useVoiceAudio"
|
||||
import { useVoiceRecommend } from "./step5-voice/useVoiceRecommend"
|
||||
import { useTtsSynthesis } from "./step5-voice/useTtsSynthesis"
|
||||
import { useSaveToLibrary } from "./step5-voice/useSaveToLibrary"
|
||||
import { useVoiceModeSelection } from "./step5-voice/useVoiceModeSelection"
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { message } from "antd";
|
||||
import type { VoiceClone } from "@/api/voice-clone";
|
||||
import { VOICE_GENDER_ICON, CLONE_STATUS_CONFIG } from "../constants";
|
||||
import { formatDuration } from "../utils/formatDuration";
|
||||
import { useVoiceAudio } from "./step5-voice/useVoiceAudio";
|
||||
import { useVoiceRecommend } from "./step5-voice/useVoiceRecommend";
|
||||
import { useTtsSynthesis } from "./step5-voice/useTtsSynthesis";
|
||||
import { useSaveToLibrary } from "./step5-voice/useSaveToLibrary";
|
||||
import { useVoiceModeSelection } from "./step5-voice/useVoiceModeSelection";
|
||||
|
||||
interface UseStep5VoiceProps {
|
||||
selectedVoice: string
|
||||
onSelectedVoiceChange: (voiceId: string) => void
|
||||
voiceMode: "preset" | "custom" | "clone"
|
||||
onVoiceModeChange: (mode: "preset" | "custom" | "clone") => void
|
||||
selectedClonedVoice: string
|
||||
onSelectedClonedVoiceChange: (voiceId: string) => void
|
||||
clonedVoices: VoiceClone[]
|
||||
addClone: (voice: VoiceClone) => void
|
||||
hasProcessing: boolean
|
||||
cloneModalOpen: boolean
|
||||
onCloneModalOpenChange: (open: boolean) => void
|
||||
titleText: string
|
||||
selectedVoice: string;
|
||||
onSelectedVoiceChange: (voiceId: string) => void;
|
||||
voiceMode: "preset" | "custom" | "clone";
|
||||
onVoiceModeChange: (mode: "preset" | "custom" | "clone") => void;
|
||||
selectedClonedVoice: string;
|
||||
onSelectedClonedVoiceChange: (voiceId: string) => void;
|
||||
clonedVoices: VoiceClone[];
|
||||
addClone: (voice: VoiceClone) => void;
|
||||
hasProcessing: boolean;
|
||||
cloneModalOpen: boolean;
|
||||
onCloneModalOpenChange: (open: boolean) => void;
|
||||
titleText: string;
|
||||
}
|
||||
|
||||
export function useStep5Voice({
|
||||
@@ -43,7 +43,7 @@ export function useStep5Voice({
|
||||
titleText,
|
||||
}: UseStep5VoiceProps) {
|
||||
/* ── 子模块 ── */
|
||||
const { playingVoice, toggleVoicePlay } = useVoiceAudio()
|
||||
const { playingVoice, toggleVoicePlay } = useVoiceAudio();
|
||||
|
||||
const {
|
||||
presetVoices,
|
||||
@@ -52,7 +52,7 @@ export function useStep5Voice({
|
||||
voiceRecommendations,
|
||||
hasVoiceRecommend,
|
||||
handleVoiceRecommend,
|
||||
} = useVoiceRecommend(titleText)
|
||||
} = useVoiceRecommend(titleText);
|
||||
|
||||
const {
|
||||
customVoiceText,
|
||||
@@ -64,7 +64,7 @@ export function useStep5Voice({
|
||||
synthesizeMutation,
|
||||
handleSynthesizeVoice,
|
||||
resetTtsState,
|
||||
} = useTtsSynthesis(selectedVoice)
|
||||
} = useTtsSynthesis(selectedVoice);
|
||||
|
||||
const {
|
||||
saveModalOpen,
|
||||
@@ -80,7 +80,7 @@ export function useStep5Voice({
|
||||
handleOpenSaveModal,
|
||||
handleConfirmSave,
|
||||
handleAddTagInModal,
|
||||
} = useSaveToLibrary(completedTtsJobId, resetTtsState)
|
||||
} = useSaveToLibrary(completedTtsJobId, resetTtsState);
|
||||
|
||||
const {
|
||||
handleSelectRecommendedVoice,
|
||||
@@ -99,46 +99,78 @@ export function useStep5Voice({
|
||||
addClone,
|
||||
cloneModalOpen,
|
||||
onCloneModalOpenChange,
|
||||
})
|
||||
});
|
||||
|
||||
/* ── 克隆音色试听播放 ── */
|
||||
const [playingCloneVoice, setPlayingCloneVoice] = useState<string | null>(null)
|
||||
const cloneAudioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const [playingCloneVoice, setPlayingCloneVoice] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const cloneAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const cloneMountedRef = useRef(true);
|
||||
|
||||
// 组件卸载时清理克隆音色试听的音频资源
|
||||
useEffect(() => {
|
||||
cloneMountedRef.current = true;
|
||||
return () => {
|
||||
cloneMountedRef.current = false;
|
||||
if (cloneAudioRef.current) {
|
||||
cloneAudioRef.current.onended = null;
|
||||
cloneAudioRef.current.pause();
|
||||
cloneAudioRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePlayCloneSample = useCallback(
|
||||
(voice: VoiceClone) => {
|
||||
if (playingCloneVoice === voice.id) {
|
||||
cloneAudioRef.current?.pause()
|
||||
cloneAudioRef.current = null
|
||||
setPlayingCloneVoice(null)
|
||||
return
|
||||
if (cloneAudioRef.current) {
|
||||
cloneAudioRef.current.onended = null;
|
||||
cloneAudioRef.current.pause();
|
||||
}
|
||||
cloneAudioRef.current = null;
|
||||
setPlayingCloneVoice(null);
|
||||
return;
|
||||
}
|
||||
// 暂停上一个音频并清除事件监听,避免竞态
|
||||
if (cloneAudioRef.current) {
|
||||
cloneAudioRef.current.onended = null;
|
||||
cloneAudioRef.current.pause();
|
||||
}
|
||||
cloneAudioRef.current?.pause()
|
||||
// TODO: 后端克隆音色试听接口出来后,改为调用接口获取试听音频
|
||||
// 目前使用 sample_url 字段,如果没有则提示
|
||||
const audioUrl = voice.sample_url
|
||||
const audioUrl = voice.sample_url;
|
||||
if (!audioUrl) {
|
||||
message.warning("该克隆音色暂无试听音频")
|
||||
return
|
||||
message.warning("该克隆音色暂无试听音频");
|
||||
return;
|
||||
}
|
||||
const audio = new Audio(audioUrl)
|
||||
cloneAudioRef.current = audio
|
||||
audio.play().catch(() => {
|
||||
message.error("播放失败,请检查网络")
|
||||
})
|
||||
const audio = new Audio(audioUrl);
|
||||
cloneAudioRef.current = audio;
|
||||
audio
|
||||
.play()
|
||||
.then(() => {
|
||||
if (cloneMountedRef.current) setPlayingCloneVoice(voice.id);
|
||||
})
|
||||
.catch(() => {
|
||||
if (cloneMountedRef.current) {
|
||||
message.error("播放失败,请检查网络");
|
||||
cloneAudioRef.current = null;
|
||||
}
|
||||
});
|
||||
audio.onended = () => {
|
||||
setPlayingCloneVoice(null)
|
||||
cloneAudioRef.current = null
|
||||
}
|
||||
setPlayingCloneVoice(voice.id)
|
||||
if (cloneMountedRef.current) {
|
||||
setPlayingCloneVoice(null);
|
||||
cloneAudioRef.current = null;
|
||||
}
|
||||
};
|
||||
},
|
||||
[playingCloneVoice],
|
||||
)
|
||||
);
|
||||
|
||||
const handleCloneSuccess = (voice: VoiceClone) => {
|
||||
rawCloneSuccess(voice)
|
||||
message.success("音色克隆成功!")
|
||||
}
|
||||
rawCloneSuccess(voice);
|
||||
message.success("音色克隆成功!");
|
||||
};
|
||||
|
||||
return {
|
||||
// 数据
|
||||
@@ -197,8 +229,7 @@ export function useStep5Voice({
|
||||
VOICE_GENDER_ICON,
|
||||
CLONE_STATUS_CONFIG,
|
||||
formatDuration,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default useStep5Voice
|
||||
|
||||
export default useStep5Voice;
|
||||
|
||||
Reference in New Issue
Block a user