diff --git a/tauri-app/src/pages/VideoGeneration/hooks/useVideoGeneration.ts b/tauri-app/src/pages/VideoGeneration/hooks/useVideoGeneration.ts index 301d988..5cfec90 100644 --- a/tauri-app/src/pages/VideoGeneration/hooks/useVideoGeneration.ts +++ b/tauri-app/src/pages/VideoGeneration/hooks/useVideoGeneration.ts @@ -378,7 +378,14 @@ export function useVideoGeneration({ }; }); } - const actualVideoPoints = Math.ceil(totalAudioDuration) * videoMultiplier; + const baseVideoPoints = Math.ceil(totalAudioDuration) * videoMultiplier; + // 系统素材额外收费:每个空镜使用的系统素材额外 2 积分 + const systemMaterialCount = shots.filter((s: ScriptShot) => { + if (s.type !== 'empty_shot') return false; + const matched = materialMatchMap[String(s.id)]; + return matched && (matched.url.startsWith('http://') || matched.url.startsWith('https://')); + }).length; + const actualVideoPoints = baseVideoPoints + systemMaterialCount * 2; await pointsApi.consumePoints({ points: actualVideoPoints, sourceType: 'video', diff --git a/tauri-app/src/pages/VideoGeneration/index.tsx b/tauri-app/src/pages/VideoGeneration/index.tsx index 6af91fc..27ff879 100644 --- a/tauri-app/src/pages/VideoGeneration/index.tsx +++ b/tauri-app/src/pages/VideoGeneration/index.tsx @@ -60,23 +60,6 @@ export default function VideoGeneration() { const dubbingAudioDuration = useProjectStore(state => state.dubbingAudioDuration); - // 视频生成积分:统一用完整配音音频时长作为唯一口径(与后端预检、实际扣费保持一致) - const estimatedVideoPoints = useMemo(() => { - // 优先使用已保存的配音音频实际时长,确保预估 = 实际扣费 - if (dubbingAudioDuration && dubbingAudioDuration > 0) { - return Math.ceil(dubbingAudioDuration) * videoMultiplier; - } - // 降级:按分镜 actualDuration 累加(配音后已更新) - const totalDur = shots.reduce((sum, shot) => { - if (shot.actualDuration && shot.actualDuration > 0) { - return sum + shot.actualDuration; - } - const durStr = typeof shot.duration === 'string' ? shot.duration : '5s'; - return sum + parseFloat(durStr.replace(/[^0-9.]/g, '') || '0'); - }, 0); - return Math.ceil(totalDur) * videoMultiplier; - }, [shots, videoMultiplier, dubbingAudioDuration]); - const [activeScene, setActiveScene] = useState(1); // 控制右侧预览区显示完整视频还是镜头素材 @@ -149,6 +132,32 @@ export default function VideoGeneration() { setMaterialMatchMap, } = useEmptyShotMaterials(shots, projectId); + // 视频生成积分:统一用完整配音音频时长作为唯一口径(与后端预检、实际扣费保持一致) + const estimatedVideoPoints = useMemo(() => { + // 优先使用已保存的配音音频实际时长,确保预估 = 实际扣费 + let basePoints = 0; + if (dubbingAudioDuration && dubbingAudioDuration > 0) { + basePoints = Math.ceil(dubbingAudioDuration) * videoMultiplier; + } else { + // 降级:按分镜 actualDuration 累加(配音后已更新) + const totalDur = shots.reduce((sum, shot) => { + if (shot.actualDuration && shot.actualDuration > 0) { + return sum + shot.actualDuration; + } + const durStr = typeof shot.duration === 'string' ? shot.duration : '5s'; + return sum + parseFloat(durStr.replace(/[^0-9.]/g, '') || '0'); + }, 0); + basePoints = Math.ceil(totalDur) * videoMultiplier; + } + // 系统素材额外收费:每个空镜使用的系统素材额外 2 积分 + const systemMaterialCount = shots.filter((s) => { + if (s.type !== 'empty_shot') return false; + const matched = materialMatchMap[String(s.id)]; + return matched && (matched.url.startsWith('http://') || matched.url.startsWith('https://')); + }).length; + return basePoints + systemMaterialCount * 2; + }, [shots, videoMultiplier, dubbingAudioDuration, materialMatchMap]); + // 同步 activeScene 与 shots 数据 useEffect(() => { if (shots.length > 0 && !shots.find((s) => Number(s.id) === activeScene)) {