Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee69a51406 | |||
| 355431b867 | |||
| 041cec8670 | |||
| 472f371d2f | |||
| 709d65ce52 | |||
| ae1b9f8ddb | |||
| 1c9903574a | |||
| cfedc06df0 | |||
| 4429784a79 | |||
| aa36e63591 | |||
| c3373de4f3 | |||
| 60d95e64ab | |||
| 7775d5d118 |
@@ -3,13 +3,9 @@ export type {
|
||||
CreatePreviewRequest,
|
||||
CreatePreviewResponse,
|
||||
PreviewTaskResponse,
|
||||
ConfirmGenerationRequest,
|
||||
ConfirmGenerationResponse,
|
||||
ConfirmGenerationTaskItem,
|
||||
} from "./types"
|
||||
|
||||
export { createPreview, getPreviewStatus } from "./preview"
|
||||
export { confirmGeneration } from "./confirm"
|
||||
|
||||
export { generateCover } from "./cover"
|
||||
export type { GenerateCoverRequest, GenerateCoverResponse } from "./cover"
|
||||
|
||||
@@ -57,8 +57,33 @@ export interface TaskListResponse {
|
||||
export interface CreateGenerationTaskRequest {
|
||||
template_id: string
|
||||
asset_ids: string[]
|
||||
title_ids: string[]
|
||||
voice_ids: string[]
|
||||
title_ids?: string[]
|
||||
voice_ids?: string[]
|
||||
/** 输出视频宽度 */
|
||||
output_width?: number
|
||||
/** 输出视频高度 */
|
||||
output_height?: number
|
||||
/** 自定义封面图片 URL */
|
||||
cover_url?: string
|
||||
/** 自定义视频标题 */
|
||||
custom_title?: string
|
||||
/** 视频时长(秒) */
|
||||
duration?: number
|
||||
/** 视频宽高比,如 "9:16" */
|
||||
video_ratio?: string
|
||||
/** 标题烧录配置 */
|
||||
title_config?: {
|
||||
text?: string
|
||||
font?: string
|
||||
font_size?: number
|
||||
font_color?: string
|
||||
position?: string
|
||||
bold?: boolean
|
||||
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,
|
||||
@@ -171,7 +173,6 @@ const GeneratePage: React.FC = () => {
|
||||
autoSubtitles,
|
||||
bgm,
|
||||
generateCount,
|
||||
previewTaskId: "",
|
||||
})
|
||||
|
||||
/* ================================================================
|
||||
|
||||
@@ -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[]
|
||||
@@ -19,8 +21,6 @@ export interface UseGenerateVideoProps {
|
||||
autoSubtitles: boolean
|
||||
bgm: boolean
|
||||
generateCount: number
|
||||
/** 预览任务的 task_id(用于新确认生成 API) */
|
||||
previewTaskId: string
|
||||
}
|
||||
|
||||
/** 生成阶段 */
|
||||
|
||||
@@ -10,14 +10,43 @@ import type { Movie, Sample } from "mp4box"
|
||||
|
||||
// ── MP4 Box 解析辅助函数 ──
|
||||
|
||||
/** 在指定范围内查找 avcC / hvcC box,返回其数据 */
|
||||
function findCodecConfig(buffer: ArrayBuffer, start: number, end: number): ArrayBuffer | undefined {
|
||||
// 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 开始递归
|
||||
* - stsd 有额外 8 字节头(version/flags 4B + entry_count 4B),从 offset+16 开始
|
||||
* - VisualSampleEntry (avc1/avc3/hvc1/hev1) 前 78 字节是固定字段,子 box 从 offset+8+78 开始
|
||||
*/
|
||||
function findCodecConfigRecursive(
|
||||
buffer: ArrayBuffer,
|
||||
start: number,
|
||||
end: number,
|
||||
): ArrayBuffer | undefined {
|
||||
const view = new DataView(buffer)
|
||||
let offset = start
|
||||
|
||||
while (offset < end - 8) {
|
||||
const size = view.getUint32(offset)
|
||||
if (size < 8) break
|
||||
if (size < 8 || offset + size > end) break
|
||||
|
||||
const type = String.fromCharCode(
|
||||
view.getUint8(offset + 4),
|
||||
view.getUint8(offset + 5),
|
||||
@@ -25,37 +54,28 @@ function findCodecConfig(buffer: ArrayBuffer, start: number, end: number): Array
|
||||
view.getUint8(offset + 7),
|
||||
)
|
||||
|
||||
// 容器 box(fullbox 多 4 字节)
|
||||
const containerBoxes = ["trak", "mdia", "minf", "stbl"]
|
||||
if (containerBoxes.includes(type)) {
|
||||
// fullbox: size(4) + type(4) + version(1) + flags(3) = 12 bytes header
|
||||
const contentStart = offset + 12
|
||||
const result = findCodecConfig(buffer, contentStart, offset + size)
|
||||
if (result) return result
|
||||
} else if (type === "stsd") {
|
||||
// SampleDescriptionBox 是 fullbox: 8 header + 4 version/flags + 4 entry_count
|
||||
const entryCount = view.getUint32(offset + 12)
|
||||
let entryOffset = offset + 16
|
||||
for (let i = 0; i < entryCount && entryOffset < offset + size; i++) {
|
||||
const entrySize = view.getUint32(entryOffset)
|
||||
// 视觉样本条目: 8 header + 6 reserved + 2 data_ref_index + remaining
|
||||
// 子 box 从 entryOffset + 16 + 62 开始 (skip reserved + data_ref_index + predefined)
|
||||
// 实际结构: 8(header) + 6(reserved) + 2(data_ref_index) + 16(predefined+reserved) + 2(width) + 2(height) + ...
|
||||
// 子 box 从 entryOffset + 8 + 6 + 2 + 16 + 2 + 2 + 2 + 2 + 4 + 2 + 2 + 2 + 2 = entryOffset + 78
|
||||
// 更简单的做法:扫描 entry 内的子 box
|
||||
const entryEnd = entryOffset + entrySize
|
||||
const subBoxStart = entryOffset + 8 + 70 // VisualSampleEntry 固定字段共 70 字节
|
||||
const result = findCodecConfig(buffer, subBoxStart, entryEnd)
|
||||
if (result) return result
|
||||
entryOffset += entrySize
|
||||
}
|
||||
} else if (type === "avcC" || type === "hvcC") {
|
||||
// 找到目标 box,返回完整 box(含 header)
|
||||
// 返回完整 box(含 size + type header),WebCodecs HEVC decoder 需要
|
||||
return buffer.slice(offset, offset + size)
|
||||
// 找到目标 codec 配置 box,返回内容(不含 8 字节 header)
|
||||
if (type === "avcC" || type === "hvcC") {
|
||||
console.log("[findCodecConfig] Found", type, "at offset", offset, "size", size)
|
||||
return buffer.slice(offset + 8, offset + size)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if (size === 0) break
|
||||
offset += size
|
||||
}
|
||||
return undefined
|
||||
@@ -206,7 +226,6 @@ export function useCanvasPlayer(
|
||||
const videoDimRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 })
|
||||
const isDestroyedRef = useRef(false)
|
||||
const lastProgressUpdateRef = useRef<number>(0)
|
||||
const descriptionCache = useRef<Map<string, ArrayBuffer>>(new Map())
|
||||
|
||||
// 计算总时长
|
||||
const totalDuration = segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0)
|
||||
@@ -246,7 +265,15 @@ export function useCanvasPlayer(
|
||||
view.getUint8(offset + 7),
|
||||
)
|
||||
if (type === "moov") {
|
||||
return findCodecConfig(buffer, offset + 8, offset + size)
|
||||
const result = findCodecConfigRecursive(buffer, offset + 8, offset + size)
|
||||
console.log("[useCanvasPlayer] extractCodecDescription:", {
|
||||
moovOffset: offset,
|
||||
moovSize: size,
|
||||
searchRange: [offset + 8, offset + size],
|
||||
found: !!result,
|
||||
resultByteLength: result?.byteLength,
|
||||
})
|
||||
return result
|
||||
}
|
||||
if (size === 0) break
|
||||
offset += size
|
||||
@@ -312,15 +339,7 @@ export function useCanvasPlayer(
|
||||
}
|
||||
|
||||
// 提取编解码器配置数据(HEVC 必需,H.264 也需要)
|
||||
let description = extractCodecDescription(buffer)
|
||||
|
||||
// 如果当前分片没有 description,尝试从缓存获取
|
||||
if (!description) {
|
||||
for (const cached of descriptionCache.current.values()) {
|
||||
description = cached
|
||||
break
|
||||
}
|
||||
}
|
||||
const description = extractCodecDescription(buffer)
|
||||
|
||||
// ✅ 如果 description 缺失,无法解码 HEVC
|
||||
if (!description) {
|
||||
@@ -333,9 +352,6 @@ export function useCanvasPlayer(
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存 description 供后续分片使用
|
||||
descriptionCache.current.set(segment.assetId, description)
|
||||
|
||||
meta = {
|
||||
assetId: segment.assetId,
|
||||
videoUrl: segment.videoUrl,
|
||||
@@ -352,7 +368,7 @@ export function useCanvasPlayer(
|
||||
|
||||
// 提取所有 samples
|
||||
mp4File.setExtractionOptions(videoTrack.id ?? 1, null, {
|
||||
nbSamples: videoTrack.nb_samples || 10000,
|
||||
nbSamples: Infinity, // 提取所有 sample
|
||||
})
|
||||
mp4File.start()
|
||||
}
|
||||
@@ -467,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) {
|
||||
@@ -481,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,
|
||||
@@ -501,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)
|
||||
}
|
||||
@@ -689,7 +724,6 @@ export function useCanvasPlayer(
|
||||
frameQueueRef.current.clear()
|
||||
segmentDataRef.current.clear()
|
||||
segmentMetaRef.current = []
|
||||
descriptionCache.current.clear()
|
||||
}, [])
|
||||
|
||||
// ── 预加载下一个片段的数据 ──
|
||||
@@ -706,7 +740,16 @@ export function useCanvasPlayer(
|
||||
|
||||
// ── 初始化:加载并解码所有片段 ──
|
||||
useEffect(() => {
|
||||
if (!state.hasSupport || segments.length === 0) return
|
||||
if (!state.hasSupport || segments.length === 0) {
|
||||
console.log("[useCanvasPlayer] Skip init:", {
|
||||
hasSupport: state.hasSupport,
|
||||
segmentCount: segments.length,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
console.log("[useCanvasPlayer] Init start, segments:", segments.length)
|
||||
|
||||
const init = async () => {
|
||||
setState((s) => ({ ...s, isBuffering: true }))
|
||||
@@ -714,20 +757,37 @@ export function useCanvasPlayer(
|
||||
// 1. 加载所有片段数据
|
||||
for (const seg of segments) {
|
||||
await loadSegment(seg)
|
||||
if (cancelled) {
|
||||
console.log("[useCanvasPlayer] Cancelled during loadSegment")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isDestroyedRef.current) return
|
||||
// 验证 buffer 是否都已存入
|
||||
const bufferCheck = segments.map((s) => ({
|
||||
assetId: s.assetId,
|
||||
hasBuffer: segmentDataRef.current.has(s.assetId),
|
||||
}))
|
||||
console.log("[useCanvasPlayer] Buffers loaded:", bufferCheck)
|
||||
|
||||
// 2. 解析每个片段的轨道元数据(await 等待 onSamples 回调完成)
|
||||
const metas: SegmentMeta[] = []
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const buffer = segmentDataRef.current.get(segments[i].assetId)
|
||||
if (!buffer) continue
|
||||
if (!buffer) {
|
||||
console.warn("[useCanvasPlayer] No buffer for segment", i, segments[i].assetId)
|
||||
continue
|
||||
}
|
||||
const meta = await demuxSegment(buffer, i)
|
||||
if (cancelled) {
|
||||
console.log("[useCanvasPlayer] Cancelled during demuxSegment")
|
||||
return
|
||||
}
|
||||
if (meta) metas.push(meta)
|
||||
}
|
||||
|
||||
if (isDestroyedRef.current || metas.length === 0) {
|
||||
if (cancelled || metas.length === 0) {
|
||||
console.warn("[useCanvasPlayer] Init failed:", { cancelled, metasCount: metas.length })
|
||||
setState((s) => ({ ...s, isBuffering: false }))
|
||||
return
|
||||
}
|
||||
@@ -744,15 +804,19 @@ export function useCanvasPlayer(
|
||||
const buffer = segmentDataRef.current.get(meta.assetId)
|
||||
if (!buffer) continue
|
||||
await decodeSegment(buffer, meta)
|
||||
if (isDestroyedRef.current) break
|
||||
if (cancelled) break
|
||||
}
|
||||
|
||||
setState((s) => ({ ...s, duration: totalDuration, isReady: true, isBuffering: false }))
|
||||
if (!cancelled) {
|
||||
console.log("[useCanvasPlayer] Init complete, isReady = true")
|
||||
setState((s) => ({ ...s, duration: totalDuration, isReady: true, isBuffering: false }))
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
destroy()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { useState, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import type { GeneratedVideo } from "@/api/template-editor"
|
||||
import { confirmGeneration, createPreview } from "@/api/generation"
|
||||
import { createGenerationTask } from "@/api/tasks/tasks"
|
||||
import type { UseGenerateVideoProps } from "./generate-video/types"
|
||||
import { getGenerationPhase } from "./generate-video/phase"
|
||||
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
|
||||
@@ -55,7 +55,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
|
||||
clearTimer()
|
||||
|
||||
try {
|
||||
// 解析分辨率:videoRatio 可能是 "9:16"(宽高比)或 "1080x1920"(分辨率)
|
||||
// 解析分辨率
|
||||
const ratio = props.videoRatio || "9:16"
|
||||
let outputWidth: number
|
||||
let outputHeight: number
|
||||
@@ -87,20 +87,23 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
|
||||
outputHeight = 1920
|
||||
}
|
||||
|
||||
// 获取或创建后端任务 ID
|
||||
// 预览改为前端播放后,不再有预览任务,需要在此处创建
|
||||
let taskId = props.previewTaskId
|
||||
if (!taskId) {
|
||||
const assetIds =
|
||||
props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials
|
||||
const previewResp = await createPreview({
|
||||
template_id: selectedTemplate,
|
||||
asset_ids: assetIds,
|
||||
duration: props.duration || undefined,
|
||||
video_ratio: props.videoRatio,
|
||||
voice_ids: undefined,
|
||||
title_config: props.titleSettings?.title
|
||||
? {
|
||||
const assetIds =
|
||||
props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials
|
||||
|
||||
// 直接创建正式生成任务
|
||||
await createGenerationTask({
|
||||
template_id: selectedTemplate,
|
||||
asset_ids: assetIds,
|
||||
output_width: outputWidth,
|
||||
output_height: outputHeight,
|
||||
cover_url: props.coverSettings?.upload_url || "",
|
||||
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: {
|
||||
text: props.titleSettings.title,
|
||||
font: props.titleSettings.font,
|
||||
font_size: props.titleSettings.size,
|
||||
@@ -109,17 +112,9 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
|
||||
bold: props.titleSettings.bold,
|
||||
stroke: props.titleSettings.stroke,
|
||||
shadow: props.titleSettings.shadow,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
taskId = previewResp.task_id
|
||||
}
|
||||
|
||||
await confirmGeneration(taskId, {
|
||||
output_width: outputWidth,
|
||||
output_height: outputHeight,
|
||||
cover_url: props.coverSettings.upload_url || "",
|
||||
custom_title: props.titleSettings.title || "",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
startPolling()
|
||||
|
||||
Reference in New Issue
Block a user