Compare commits

...

3 Commits

Author SHA1 Message Date
xiaoxia ee69a51406 fix: 正式生成任务传 source_edit_plan_id,仅取URL参数不回退模板ID
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 39s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m28s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m9s
AI Code Review / AI Code Review (pull_request) Successful in 1m51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m8s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m12s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m2s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m39s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m53s
CI/CD Pipeline / CI Gate (pull_request) Successful in 12s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 15s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 24s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 556h35m57s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 556h35m58s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 556h35m59s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 556h35m59s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 556h35m59s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 556h39m20s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 556h39m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 556h40m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 556h40m15s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 556h39m24s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 556h40m53s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 556h39m26s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 556h40m49s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 557h9m49s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 557h9m50s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 557h13m50s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 557h14m42s
新增 sourceEditPlanId 完整传递链路(前端→后端):
- tasks/types.ts: CreateGenerationTaskRequest 新增 source_edit_plan_id 可选字段
- generate-video/types.ts: UseGenerateVideoProps 新增 sourceEditPlanId
- useGenerateFormState: sourceEditPlanId = editPlanId || null
  selectedTemplate 是模板 ID 不是 edit_plan_id,不能混淆
- useGenerateVideo: 条件传递 source_edit_plan_id 给 createGenerationTask
- GeneratePage: 从 formState 解构并传递给 useGenerateVideo
- useStep6Cover: createPreview 不传 source_edit_plan_id,后端通过 template_id+user_id 兜底
2026-08-24 10:45:07 +08:00
张宏杰 355431b867 feat: decodeSegment 增加解码进度日志和 flush 超时保护
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 54s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m6s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m30s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 44s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m20s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Unit Tests (push) Successful in 11m38s
CI/CD Pipeline / Production Browser E2E (push) Failing after 663h16m13s
CI/CD Pipeline / Deploy Production (push) Failing after 663h16m15s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 663h16m16s
CI/CD Pipeline / Build Production Web Image (push) Failing after 663h16m16s
CI/CD Pipeline / Build Production API Image (push) Failing after 663h16m16s
CI/CD Pipeline / PR Build Web Image (push) Failing after 663h27m54s
CI/CD Pipeline / PR Build API Image (push) Failing after 663h27m54s
CI/CD Pipeline / Frontend Lint (push) Failing after 663h27m55s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 663h27m58s
CI/CD Pipeline / Canary Release to Production (push) Failing after 663h49m59s
CI/CD Pipeline / CI Gate (push) Failing after 663h50m1s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 664h1m37s
- 记录实际 decode 的 sample 数量、跳过数量、decoder 状态
- flush 增加 10 秒超时保护,防止无限卡住
- configure 成功后立即清除 isBuffering 状态,让 UI 不再显示'准备中'
2026-08-19 23:58:23 +08:00
张宏杰 041cec8670 fix: findCodecConfig 重写,正确处理 stsd 和容器 box 递归
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 36s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m25s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m29s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Unit Tests (push) Successful in 11m10s
CI/CD Pipeline / Production Browser E2E (push) Failing after 663h51m3s
CI/CD Pipeline / Deploy Production (push) Failing after 663h51m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 663h51m5s
CI/CD Pipeline / Build Production Web Image (push) Failing after 663h51m5s
CI/CD Pipeline / Build Production API Image (push) Failing after 663h51m6s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 664h1m17s
CI/CD Pipeline / PR Build API Image (push) Failing after 664h1m21s
CI/CD Pipeline / Frontend Lint (push) Failing after 664h2m17s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 664h2m19s
CI/CD Pipeline / Canary Release to Production (push) Failing after 664h24m49s
CI/CD Pipeline / CI Gate (push) Failing after 664h24m50s
CI/CD Pipeline / PR Build Web Image (push) Failing after 664h35m4s
- 新增 MP4_CONTAINER_TYPES 常量,包含 trak/mdia/minf/stbl 等标准容器
- stsd 特殊处理:额外 8 字节头(version/flags + entry_count),从 offset+16 开始
- VisualSampleEntry 保持 offset+8+78(78 字节固定字段)
- 删除 fallback 双重搜索逻辑,改为明确的容器类型判断
2026-08-19 23:24:00 +08:00
6 changed files with 73 additions and 21 deletions
+2
View File
@@ -82,6 +82,8 @@ export interface CreateGenerationTaskRequest {
stroke?: boolean
shadow?: boolean
}
/** 来源剪辑计划 ID,后端据此从 edit_plan_clips 读取素材/音频/标题配置 */
source_edit_plan_id?: string
}
/** 创建生成任务响应(对齐后端 GenerationTaskResponse */
@@ -72,6 +72,7 @@ const GeneratePage: React.FC = () => {
autoSubtitles,
bgm,
editPlanId,
sourceEditPlanId,
previewVideo,
setPreviewVideo,
previewModalOpen,
@@ -158,6 +159,7 @@ const GeneratePage: React.FC = () => {
} = useGenerateVideo({
titleSettings,
selectedTemplate,
sourceEditPlanId,
selectedMaterials,
materialMode,
smartSelectedIds,
@@ -6,6 +6,8 @@ import type { TitleSettings } from "../../types"
export interface UseGenerateVideoProps {
titleSettings: TitleSettings
selectedTemplate: string
/** 来源剪辑计划 ID,仅取 URL edit_plan_id,无则 null 让后端兜底 */
sourceEditPlanId?: string | null
selectedMaterials: string[]
materialMode: "manual" | "auto"
smartSelectedIds: string[]
@@ -10,12 +10,30 @@ import type { Movie, Sample } from "mp4box"
// ── MP4 Box 解析辅助函数 ──
// MP4 标准容器 box 列表(递归时会进入这些 box 内部搜索子 box)
const MP4_CONTAINER_TYPES = [
"moov",
"trak",
"mdia",
"minf",
"stbl",
"stsd",
"dinf",
"edts",
"udta",
"meta",
"tref",
]
const VISUAL_SAMPLE_ENTRY_TYPES = ["avc1", "avc3", "hvc1", "hev1"]
/**
* 递归搜索 box 树,找到 hvcC 或 avcC box 并返回其配置数据(不含 8 字节 box header
*
* MP4 box 嵌套结构:moov → trak → mdia → minf → stbl → stsd → hev1 → hvcC
* 普通容器 box 从 offset+8 开始递归VisualSampleEntry (avc1/avc3/hvc1/hev1) 前 78 字节
* 是固定字段(ISO 14496-12),子 box 从 offset+8+78 开始
* - 普通容器 box 从 offset+8 开始递归
* - stsd 有额外 8 字节头(version/flags 4B + entry_count 4B),从 offset+16 开始
* - VisualSampleEntry (avc1/avc3/hvc1/hev1) 前 78 字节是固定字段,子 box 从 offset+8+78 开始
*/
function findCodecConfigRecursive(
buffer: ArrayBuffer,
@@ -42,21 +60,18 @@ function findCodecConfigRecursive(
return buffer.slice(offset + 8, offset + size)
}
const visualSampleEntryTypes = ["avc1", "avc3", "hvc1", "hev1"]
const isVisualSampleEntry = visualSampleEntryTypes.includes(type)
if (isVisualSampleEntry) {
// VisualSampleEntry: 前 78 字节是固定字段,子 box 在 78 字节之后
// 先尝试 offset+8+78,如果没找到再尝试 offset+8(兼容不同 MP4 结构)
console.log("[findCodecConfig] VisualSampleEntry:", type, "at", offset, "size", size, "trying offset+8+78")
const childResult1 = findCodecConfigRecursive(buffer, offset + 8 + 78, offset + size)
if (childResult1) return childResult1
console.log("[findCodecConfig] VisualSampleEntry:", type, "at", offset, "trying offset+8 (fallback)")
const childResult2 = findCodecConfigRecursive(buffer, offset + 8, offset + size)
if (childResult2) return childResult2
} else {
// 普通容器 box,从 offset+8 开始递归
// VisualSampleEntry:前 78 字节是固定字段,子 box 在 78 字节之后
if (VISUAL_SAMPLE_ENTRY_TYPES.includes(type)) {
const childResult = findCodecConfigRecursive(buffer, offset + 8 + 78, offset + size)
if (childResult) return childResult
}
// stsd:额外 8 字节头(version/flags 4B + entry_count 4B),子 box 在 offset+16
else if (type === "stsd") {
const childResult = findCodecConfigRecursive(buffer, offset + 8 + 8, offset + size)
if (childResult) return childResult
}
// 标准容器 box:从 offset+8 开始递归
else if (MP4_CONTAINER_TYPES.includes(type)) {
const childResult = findCodecConfigRecursive(buffer, offset + 8, offset + size)
if (childResult) return childResult
}
@@ -468,6 +483,8 @@ export function useCanvasPlayer(
})
decoderRef.current = decoder
decoderReady = true
// 标记缓冲结束,让 UI 开始渲染
setState((s) => ({ ...s, isBuffering: false }))
// 更新视频尺寸(用于 aspect ratio
if (meta.videoWidth > 0 && meta.videoHeight > 0) {
@@ -482,17 +499,24 @@ export function useCanvasPlayer(
// 使用 demuxSegment 中已提取并过滤的 samples(前端切片)
const samplesCollected = meta.samples
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: ${samplesCollected.length} samples to decode`,
)
if (samplesCollected.length === 0) {
console.warn("[useCanvasPlayer] No samples to decode for segment", meta.assetId)
return
}
// 送入解码器
let decodedCount = 0
let skippedCount = 0
for (const sample of samplesCollected) {
if (!sample.data || isDestroyedRef.current) continue
if (!sample.data || isDestroyedRef.current) {
skippedCount++
continue
}
if (decoder.state === "closed") break
if (!sample.data) continue
const chunk = new EncodedVideoChunk({
type: sample.is_sync ? "key" : "delta",
timestamp: ((sample.cts ?? 0) / (meta.timescale || 90000)) * 1_000_000,
@@ -502,14 +526,24 @@ export function useCanvasPlayer(
try {
decoder.decode(chunk)
decodedCount++
} catch (e) {
console.warn("[useCanvasPlayer] Decode chunk error:", e)
}
}
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: decoded ${decodedCount}, skipped ${skippedCount}, decoder.state=${decoder.state}`,
)
// flush 确保所有帧输出
// flush 超时保护:10秒
try {
await decoder.flush()
await Promise.race([
decoder.flush(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("flush timeout 10s")), 10_000),
),
])
console.log(`[useCanvasPlayer] Segment ${meta.assetId}: flush complete`)
} catch (e) {
console.warn("[useCanvasPlayer] Decoder flush error:", e)
}
@@ -82,6 +82,14 @@ export interface GenerateFormState {
editPlanId: string | null
planConfigStr: string | null
/**
* 传给后端的 source_edit_plan_id。
* 仅使用 URL 中的 edit_plan_id(从剪辑模板编辑器跳转时携带)。
* URL 没有时传 null,后端正式生成 API 会通过 template_id+user_id 兜底查找正确的 plan。
* 注意:selectedTemplate 是模板 ID,不是 edit_plan_id,不能作为此值传递。
*/
sourceEditPlanId: string | null
/* 预览弹窗 */
previewVideo: GeneratedVideo | null
setPreviewVideo: (v: GeneratedVideo | null) => void
@@ -93,6 +101,8 @@ export const useGenerateFormState = (): GenerateFormState => {
const [searchParams] = useSearchParams()
const editPlanId = searchParams.get("edit_plan_id")
const planConfigStr = searchParams.get("plan_config")
/* ── source_edit_plan_id:仅取 URL 参数,无则 null 让后端兜底 ── */
const sourceEditPlanId = editPlanId || null
/* ── 步骤状态 ── */
const [currentStep, setCurrentStep] = useState(1)
@@ -190,6 +200,7 @@ export const useGenerateFormState = (): GenerateFormState => {
bgm,
editPlanId,
planConfigStr,
sourceEditPlanId,
previewVideo,
setPreviewVideo,
previewModalOpen,
@@ -100,6 +100,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
custom_title: props.titleSettings?.title || "",
duration: props.duration || undefined,
video_ratio: props.videoRatio,
...(props.sourceEditPlanId ? { source_edit_plan_id: props.sourceEditPlanId } : {}),
...(props.titleSettings?.title
? {
title_config: {