feat(generate): 预览播放器对接服务端 clips,使用随机 start_time 和正确片段数
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m1s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m58s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 5m2s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m50s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 5m53s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 7m14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m15s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m54s
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 / PR Build Worker Image (pull_request) Successful in 48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 10m31s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
AI Code Review / AI Code Review (pull_request) Successful in 7m44s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 12m8s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 12m54s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m1s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m58s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 5m2s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m50s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 5m53s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 7m14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m15s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m54s
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 / PR Build Worker Image (pull_request) Successful in 48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 10m31s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
AI Code Review / AI Code Review (pull_request) Successful in 7m44s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 12m8s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 12m54s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
- useStep2Materials 在调用 /clips/from-assets 后获取服务端 clips 并通过回调解构 - GeneratePage 将 serverClips 传递给 FrontendPreviewPlayer - buildPlaybackSegments 优先使用服务端 clips 的 start_time/duration,无 clips 时 fallback 到本地构建 - 确保预览与最终生成结果一致(片段数量、起始时间随机)
This commit is contained in:
@@ -74,6 +74,8 @@ const GeneratePage: React.FC = () => {
|
||||
setPreviewTaskId,
|
||||
storedSourceEditPlanId,
|
||||
setStoredSourceEditPlanId,
|
||||
serverClips,
|
||||
setServerClips,
|
||||
} = formState
|
||||
|
||||
/* ── 标题样式回调 ── */
|
||||
@@ -263,6 +265,7 @@ const GeneratePage: React.FC = () => {
|
||||
selectedVoice={selectedVoice}
|
||||
onSelectedVoiceChange={setSelectedVoice}
|
||||
totalVideoDuration={totalVideoDuration}
|
||||
onServerClipsChange={setServerClips}
|
||||
voiceMode={voiceMode}
|
||||
onVoiceModeChange={setVoiceMode}
|
||||
selectedClonedVoice={selectedClonedVoice}
|
||||
@@ -301,6 +304,7 @@ const GeneratePage: React.FC = () => {
|
||||
template={currentTemplate}
|
||||
videoRatio={videoRatio}
|
||||
ready={previewAssets.length > 0}
|
||||
serverClips={serverClips}
|
||||
voiceAudioUrl={previewVoiceAudioUrl || undefined}
|
||||
titleSettings={{
|
||||
title: titleSettings.title,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@ant-design/icons"
|
||||
import type { AssetItem } from "@/api/assets"
|
||||
import type { EditingTemplate } from "@/api/editing-planner"
|
||||
import type { EditPlanClip } from "@/api/template-editor"
|
||||
import { useSegmentScheduler, type PlaybackSegment } from "../hooks/useSegmentScheduler"
|
||||
import { useCanvasPlayer } from "../hooks/useCanvasPlayer"
|
||||
|
||||
@@ -24,6 +25,7 @@ interface FrontendPreviewPlayerProps {
|
||||
template: EditingTemplate | null
|
||||
videoRatio: string
|
||||
ready: boolean
|
||||
serverClips?: EditPlanClip[]
|
||||
voiceAudioUrl?: string
|
||||
titleSettings?: {
|
||||
title: string
|
||||
@@ -50,9 +52,31 @@ function formatTime(seconds: number): string {
|
||||
function buildPlaybackSegments(
|
||||
assets: AssetItem[],
|
||||
template: EditingTemplate | null,
|
||||
serverClips?: EditPlanClip[],
|
||||
): PlaybackSegment[] {
|
||||
if (!assets.length) return []
|
||||
|
||||
// Build asset lookup map
|
||||
const assetMap = new Map(assets.map((a) => [a.id, a]))
|
||||
|
||||
// 优先使用服务端 clips(含随机 start_time 和正确数量),与最终生成结果一致
|
||||
if (serverClips && serverClips.length > 0) {
|
||||
const segments: PlaybackSegment[] = []
|
||||
for (const clip of serverClips) {
|
||||
const asset = assetMap.get(clip.asset_id)
|
||||
if (!asset) continue
|
||||
const assetDuration = asset.duration || asset.metadata?.duration || 30
|
||||
const startTime = clip.start_time || 0
|
||||
const endTime = Math.min(startTime + (clip.duration || assetDuration), assetDuration)
|
||||
const videoUrl = asset.file_url || asset.storage_key
|
||||
segments.push({ assetId: asset.id, videoUrl, startTime, endTime, order: clip.order })
|
||||
}
|
||||
if (segments.length > 0) {
|
||||
return segments.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: 本地构建片段(与旧行为一致)
|
||||
const templateSegments = template?.segments || []
|
||||
const segments: PlaybackSegment[] = []
|
||||
|
||||
@@ -78,10 +102,14 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
template,
|
||||
videoRatio,
|
||||
ready,
|
||||
serverClips,
|
||||
voiceAudioUrl,
|
||||
titleSettings,
|
||||
}) => {
|
||||
const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template])
|
||||
const segments = useMemo(
|
||||
() => buildPlaybackSegments(assets, template, serverClips),
|
||||
[assets, template, serverClips],
|
||||
)
|
||||
|
||||
// ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ──
|
||||
const TITLE_MARGIN_TOP = 120
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import React from "react"
|
||||
import type { EditingTemplate } from "@/api/editing-planner"
|
||||
import type { EditPlanClip } from "@/api/template-editor"
|
||||
import type { PresetVoiceItem } from "@/api/voices"
|
||||
import type { VoiceClone } from "@/api/voice-clone"
|
||||
import type { CoverConfig } from "../types/cover"
|
||||
@@ -55,6 +56,7 @@ export interface GenerateStepContentProps {
|
||||
selectedVoice: string
|
||||
onSelectedVoiceChange: (id: string) => void
|
||||
totalVideoDuration?: number
|
||||
onServerClipsChange: (clips: EditPlanClip[]) => void
|
||||
voiceMode: "preset" | "custom" | "clone"
|
||||
onVoiceModeChange: (mode: "preset" | "custom" | "clone") => void
|
||||
selectedClonedVoice: string
|
||||
@@ -114,6 +116,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
|
||||
selectedVoice,
|
||||
onSelectedVoiceChange,
|
||||
totalVideoDuration,
|
||||
onServerClipsChange,
|
||||
voiceMode,
|
||||
selectedClonedVoice,
|
||||
clonedVoices,
|
||||
@@ -155,6 +158,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
|
||||
onSmartSelectedIdsChange={onSmartSelectedIdsChange}
|
||||
selectedTemplate={selectedTemplate}
|
||||
templateSegments={templateSegments}
|
||||
onServerClipsChange={onServerClipsChange}
|
||||
/>
|
||||
)
|
||||
case 3:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import React from "react"
|
||||
import type { TemplateSegment } from "@/api/templates/types"
|
||||
import type { EditPlanClip } from "@/api/template-editor"
|
||||
import { useStep2Materials } from "../hooks/useStep2Materials"
|
||||
import MaterialModeTabs from "./material/MaterialModeTabs"
|
||||
import ManualMaterialList from "./material/ManualMaterialList"
|
||||
@@ -20,6 +21,8 @@ interface Step2MaterialSelectProps {
|
||||
selectedTemplate?: string
|
||||
/** 当前模板的 segments(用于构建 clips duration) */
|
||||
templateSegments?: TemplateSegment[]
|
||||
/** 服务端 clips 创建成功后的回调 */
|
||||
onServerClipsChange?: (clips: EditPlanClip[]) => void
|
||||
}
|
||||
|
||||
const Step2MaterialSelect: React.FC<Step2MaterialSelectProps> = (props) => {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import type { EditingTemplate } from "@/api/editing-planner"
|
||||
import type { EditPlanClip } from "@/api/template-editor"
|
||||
import type { CoverConfig } from "../../types/cover"
|
||||
import type { PresetVoiceItem } from "@/api/voices"
|
||||
import { DEFAULT_COVER_SETTINGS } from "../../constants"
|
||||
@@ -46,6 +47,10 @@ export interface GenerateFormState {
|
||||
smartSelectedIds: string[]
|
||||
setSmartSelectedIds: (ids: string[]) => void
|
||||
|
||||
/* 服务端片段(/clips/from-assets 创建后获取) */
|
||||
serverClips: EditPlanClip[]
|
||||
setServerClips: (clips: EditPlanClip[]) => void
|
||||
|
||||
/* 标题 */
|
||||
titleSettings: TitleSettings
|
||||
setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void
|
||||
@@ -116,6 +121,9 @@ export const useGenerateFormState = (): GenerateFormState => {
|
||||
const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual")
|
||||
const [smartSelectedIds, setSmartSelectedIds] = useState<string[]>([])
|
||||
|
||||
/* ── 服务端片段(供预览播放器使用)── */
|
||||
const [serverClips, setServerClips] = useState<EditPlanClip[]>([])
|
||||
|
||||
/* ── 标题设置 ── */
|
||||
const [titleSettings, setTitleSettings] = useState<TitleSettings>(DEFAULT_TITLE_SETTINGS)
|
||||
|
||||
@@ -196,6 +204,8 @@ export const useGenerateFormState = (): GenerateFormState => {
|
||||
setMaterialMode,
|
||||
smartSelectedIds,
|
||||
setSmartSelectedIds,
|
||||
serverClips,
|
||||
setServerClips,
|
||||
titleSettings,
|
||||
setTitleSettings,
|
||||
coverSettings,
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
import type { TemplateSegment } from "@/api/templates/types"
|
||||
import { updateEditPlanClips, createClipsFromAssets } from "@/api/template-editor"
|
||||
import type { EditPlanClip } from "@/api/template-editor"
|
||||
import { updateEditPlanClips, createClipsFromAssets, getEditPlanClips } from "@/api/template-editor"
|
||||
import { formatDuration } from "../utils/formatDuration"
|
||||
import { useMaterialLibrary } from "./step2-materials/useMaterialLibrary"
|
||||
import { useSmartMatch } from "./step2-materials/useSmartMatch"
|
||||
@@ -21,6 +22,8 @@ interface UseStep2MaterialsProps {
|
||||
selectedTemplate?: string
|
||||
/** 当前模板的 segments(用于构建 clips duration) */
|
||||
templateSegments?: TemplateSegment[]
|
||||
/** 服务端 clips 创建成功后的回调,用于通知预览播放器 */
|
||||
onServerClipsChange?: (clips: EditPlanClip[]) => void
|
||||
}
|
||||
|
||||
export function useStep2Materials({
|
||||
@@ -32,6 +35,7 @@ export function useStep2Materials({
|
||||
onSmartSelectedIdsChange,
|
||||
selectedTemplate,
|
||||
templateSegments,
|
||||
onServerClipsChange,
|
||||
}: UseStep2MaterialsProps) {
|
||||
const { libraries, selectedLibraryId, setSelectedLibraryId, materials, materialsLoading } =
|
||||
useMaterialLibrary()
|
||||
@@ -83,12 +87,17 @@ export function useStep2Materials({
|
||||
templateSegmentsRef.current = templateSegments
|
||||
const selectedTemplateRef = useRef(selectedTemplate)
|
||||
selectedTemplateRef.current = selectedTemplate
|
||||
const onServerClipsChangeRef = useRef(onServerClipsChange)
|
||||
onServerClipsChangeRef.current = onServerClipsChange
|
||||
|
||||
useEffect(() => {
|
||||
const tid = selectedTemplateRef.current
|
||||
if (!tid) return
|
||||
const ids = materialMode === "auto" ? smartSelectedIds : selectedMaterials
|
||||
if (!ids.length) return
|
||||
if (!ids.length) {
|
||||
onServerClipsChangeRef.current?.([])
|
||||
return
|
||||
}
|
||||
|
||||
if (clipsTimerRef.current) clearTimeout(clipsTimerRef.current)
|
||||
clipsTimerRef.current = setTimeout(async () => {
|
||||
@@ -105,6 +114,12 @@ export function useStep2Materials({
|
||||
await updateEditPlanClips(tid, [], controller.signal)
|
||||
// 2. 调用后端 from-assets 接口创建片段
|
||||
await createClipsFromAssets(tid, ids, "main", requiredClipsCount)
|
||||
// 3. 获取服务端生成的 clips(含 start_time/duration),供预览播放器使用
|
||||
const clipList = await getEditPlanClips(tid, { limit: 500 })
|
||||
const readyClips = clipList.items
|
||||
.filter((c) => c.status === "ready")
|
||||
.sort((a, b) => a.order - b.order)
|
||||
onServerClipsChangeRef.current?.(readyClips)
|
||||
} catch (err) {
|
||||
const name = (err as { name?: string })?.name
|
||||
if (name !== "CanceledError" && name !== "AbortError") {
|
||||
|
||||
Reference in New Issue
Block a user