爆款/拆镜生成简化3个步骤 | 项目生成可携带附件控制
This commit is contained in:
@@ -269,8 +269,11 @@ export async function updateRecordPrompt(recordId: string, optimizedPrompt: stri
|
||||
export async function generateVideo(recordId: string, params: GenerateParams): Promise<GenerationRecord> {
|
||||
if (USE_MOCK) return mock.mockGenerateVideo(recordId);
|
||||
return api.post<GenerationRecord>(`/generation-records/${recordId}/generate`, {
|
||||
engine_id: params.engineId,
|
||||
include_media_references: params.includeMediaReferences ?? false,
|
||||
aspect_ratio: params.aspectRatio,
|
||||
resolution: params.resolution,
|
||||
image_size: params.imageSize,
|
||||
});
|
||||
}
|
||||
// ── Credits ───────────────────────────────────────────────
|
||||
@@ -485,7 +488,7 @@ export async function getAuthorizationList(params: OAuthAppParam): Promise<OAuth
|
||||
|
||||
// 爆款开头复刻
|
||||
export async function generateReplication(params: any): Promise<any> {
|
||||
return api.post('/hot-opening-replications/tasks', params);
|
||||
return api.post('/v2/hot-opening-replications/tasks', params);
|
||||
}
|
||||
// 获取爆款开头复刻任务列表
|
||||
export async function getReplicationList(page: number, page_size: number, keyword?: string): Promise<any[]> {
|
||||
@@ -495,9 +498,37 @@ export async function getReplicationList(page: number, page_size: number, keywor
|
||||
}
|
||||
return api.get(url);
|
||||
}
|
||||
// 获取爆款开头复刻任务详情
|
||||
export async function getReplicationDetail(id: string): Promise<any> {
|
||||
return api.get(`/hot-opening-replications/tasks/${id}`);
|
||||
// 获取爆款开头复刻任务详情。版本必须由列表/创建结果/URL 明确传入,禁止错误降级。
|
||||
export async function getReplicationDetail(id: string, flowVersion: 'v1' | 'v2'): Promise<any> {
|
||||
return flowVersion === 'v2'
|
||||
? api.get(`/v2/hot-opening-replications/tasks/${id}`)
|
||||
: api.get(`/hot-opening-replications/tasks/${id}`);
|
||||
}
|
||||
|
||||
export interface RetryVideoPromptV2Params {
|
||||
video_config: {
|
||||
engine_id: string;
|
||||
duration: number;
|
||||
aspect_ratio: string;
|
||||
resolution: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function retryHotOpeningVideoPromptV2(
|
||||
projectId: string,
|
||||
stepId: string,
|
||||
params: RetryVideoPromptV2Params,
|
||||
): Promise<any> {
|
||||
return api.post(`/v2/hot-opening-replications/tasks/${projectId}/steps/${stepId}/retry-video-prompt`, params);
|
||||
}
|
||||
|
||||
|
||||
export async function updateHotOpeningVideoPromptSchemaV2(projectId: string, stepId: string, params: { prompt_schema: Record<string, any> }): Promise<any> {
|
||||
return api.put(`/v2/hot-opening-replications/tasks/${projectId}/steps/${stepId}/video-prompt-schema`, params);
|
||||
}
|
||||
|
||||
export async function generateHotOpeningVideoV2(projectId: string, stepId: string): Promise<any> {
|
||||
return api.post(`/v2/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-video`);
|
||||
}
|
||||
// 第一步,生成提示词
|
||||
export async function getone(projectId: string, stepId: string): Promise<any> {
|
||||
@@ -645,11 +676,30 @@ export async function Removelist(taskSetId: string): Promise<any> {
|
||||
}
|
||||
// 生成视频
|
||||
export async function removeCreate(recordId: string, params: any): Promise<any> {
|
||||
return api.post(`/shot-replications/segments/${recordId}/replication-projects`, params);
|
||||
return api.post(`/v2/shot-replications/segments/${recordId}/replication-projects`, params);
|
||||
}
|
||||
// 获取爆款开头复刻任务详情
|
||||
export async function removeDetail(id: string): Promise<any> {
|
||||
return api.get(`/shot-replications/projects/${id}`);
|
||||
// 获取拆镜复刻项目详情。版本必须明确传入,禁止任何异常回退 V1。
|
||||
export async function removeDetail(id: string, flowVersion: 'v1' | 'v2'): Promise<any> {
|
||||
return flowVersion === 'v2'
|
||||
? api.get(`/v2/shot-replications/projects/${id}`)
|
||||
: api.get(`/shot-replications/projects/${id}`);
|
||||
}
|
||||
|
||||
export async function retryShotVideoPromptV2(
|
||||
projectId: string,
|
||||
stepId: string,
|
||||
params: RetryVideoPromptV2Params,
|
||||
): Promise<any> {
|
||||
return api.post(`/v2/shot-replications/projects/${projectId}/steps/${stepId}/retry-video-prompt`, params);
|
||||
}
|
||||
|
||||
|
||||
export async function updateShotVideoPromptSchemaV2(projectId: string, stepId: string, params: any): Promise<any> {
|
||||
return api.put(`/v2/shot-replications/projects/${projectId}/steps/${stepId}/video-prompt-schema`, params);
|
||||
}
|
||||
|
||||
export async function generateShotVideoV2(projectId: string, stepId: string): Promise<any> {
|
||||
return api.post(`/v2/shot-replications/projects/${projectId}/steps/${stepId}/generate-video`);
|
||||
}
|
||||
// 第一步,生成提示词
|
||||
export async function removeone(projectId: string, stepId: string): Promise<any> {
|
||||
@@ -697,7 +747,11 @@ export async function deleteShotReplicationProject(taskSetId: string): Promise<v
|
||||
await api.delete(`/shot-replications/task-sets/${taskSetId}`);
|
||||
}
|
||||
// 删除爆款开头复刻任务
|
||||
export async function deleteHotOpeningReplicationTask(taskId: string): Promise<void> {
|
||||
export async function deleteHotOpeningReplicationTask(taskId: string, flowVersion?: string): Promise<void> {
|
||||
if (flowVersion === 'v2') {
|
||||
await api.delete(`/v2/hot-opening-replications/tasks/${taskId}`);
|
||||
return;
|
||||
}
|
||||
await api.delete(`/hot-opening-replications/tasks/${taskId}`);
|
||||
}
|
||||
// 获取地区信息
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
Typography,
|
||||
Upload,
|
||||
Image,
|
||||
Select,
|
||||
Switch,
|
||||
} from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
@@ -635,7 +637,7 @@ const GeneratePage: React.FC = () => {
|
||||
const [expandedGroup, setExpandedGroup] = useState<string | null>(null);
|
||||
// Per-record param selections for history prompt_optimized records
|
||||
const [historyParams, setHistoryParams] = useState<
|
||||
Record<string, { aspectRatio: AspectRatio; resolution: Resolution }>
|
||||
Record<string, { aspectRatio?: AspectRatio; resolution?: Resolution; engineId?: string; includeMediaReferences?: boolean }>
|
||||
>({});
|
||||
|
||||
// Video preview modal
|
||||
@@ -645,6 +647,10 @@ const GeneratePage: React.FC = () => {
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
const [videoAspectRatio, setVideoAspectRatio] = useState<AspectRatio>("16:9");
|
||||
const [videoResolution, setVideoResolution] = useState<Resolution>("720p");
|
||||
const [videoEngines, setVideoEngines] = useState<any[]>([]);
|
||||
const [imageEngines, setImageEngines] = useState<any[]>([]);
|
||||
const [selectedEngineId, setSelectedEngineId] = useState("");
|
||||
const [includeMediaReferences, setIncludeMediaReferences] = useState(false);
|
||||
const [expandedEngine, setExpandedEngine] = useState<string | null>(null);
|
||||
const [engineOptions, setEngineOptions] = useState<{
|
||||
ratios: string[];
|
||||
@@ -864,29 +870,46 @@ const GeneratePage: React.FC = () => {
|
||||
}, [showImageSettingsModal]);
|
||||
|
||||
|
||||
const calcVideoCredits = (duration: number, resolution: Resolution): any => {
|
||||
const getReferenceUsage = (items?: MediaReference[]) => {
|
||||
const refs = Array.isArray(items) ? items : [];
|
||||
return {
|
||||
inputImageCount: refs.filter((item) => item.type === 'image').length,
|
||||
inputVideoDuration: refs
|
||||
.filter((item) => item.type === 'video')
|
||||
.reduce((sum, item: any) => sum + Number(item.duration || 0), 0),
|
||||
};
|
||||
};
|
||||
|
||||
// console.log(duration,resolution);
|
||||
for (let i = 0; i < creditRatios.length; i++) {
|
||||
const ratio = creditRatios[i];
|
||||
if (ratio.resolution === resolution) {
|
||||
const cfg = ratio;
|
||||
return Math.round((cfg.baseCredits + cfg.perSecondCredits * duration) * cfg.ratio);
|
||||
const calcVideoCredits = (
|
||||
duration: number,
|
||||
resolution: Resolution,
|
||||
engineId: string = selectedEngineId,
|
||||
includeReferences: boolean = includeMediaReferences,
|
||||
referenceItems: MediaReference[] | undefined = currentRecord?.references,
|
||||
): number => {
|
||||
const cfg = creditRatios.find((item: any) =>
|
||||
item.resolution === resolution && (!engineId || item.modelConfigId === engineId),
|
||||
) || creditRatios.find((item: any) => item.resolution === resolution);
|
||||
if (!cfg) return 0;
|
||||
let total = (Number(cfg.baseCredits || 0) + Number(cfg.perSecondCredits || 0) * duration) * Number(cfg.ratio || 1);
|
||||
if (includeReferences) {
|
||||
const usage = getReferenceUsage(referenceItems);
|
||||
if (usage.inputVideoDuration > 0) {
|
||||
total += (Number(cfg.inputVideoBaseCredits || 0) + Number(cfg.inputVideoPerSecondCredits || 0) * usage.inputVideoDuration) * Number(cfg.inputVideoRatio || 1);
|
||||
}
|
||||
if (usage.inputImageCount > 0) {
|
||||
total += (Number(cfg.inputImageBaseCredits || 0) + Number(cfg.inputImagePerImageCredits || 0) * usage.inputImageCount) * Number(cfg.inputImageRatio || 1);
|
||||
}
|
||||
}
|
||||
|
||||
// if (condition) {
|
||||
|
||||
// }
|
||||
|
||||
// return Math.round((cfg.base + cfg.perSecond * duration) * cfg.ratio);
|
||||
return Number(total.toFixed(2));
|
||||
};
|
||||
|
||||
// 根据图片分辨率获取积分
|
||||
const getImageCredits = (imageSize: string): any => {
|
||||
|
||||
// 将 imageSize 转换为 creditRatios 中的 resolution 格式
|
||||
const resolution = imageSize === '2k' ? '2048' : imageSize === '4k' ? '4096' : imageSize;
|
||||
const normalizedSize = String(imageSize || '').toLowerCase();
|
||||
const resolution = normalizedSize === '2k' ? '2048' : normalizedSize === '4k' ? '4096' : imageSize;
|
||||
|
||||
for (let i = 0; i < cimage.length; i++) {
|
||||
const ratio = cimage[i];
|
||||
@@ -1005,7 +1028,9 @@ const GeneratePage: React.FC = () => {
|
||||
|
||||
getParameters()
|
||||
.then((data) => {
|
||||
const sizes = (data as any).items?.[0]?.supportedSizes || {};
|
||||
const imageItems = (data as any).items || [];
|
||||
setImageEngines(imageItems);
|
||||
const sizes = imageItems?.[0]?.supportedSizes || {};
|
||||
setSupportedSizes(sizes);
|
||||
|
||||
const resolutionKeys = Object.keys(sizes);
|
||||
@@ -1037,8 +1062,10 @@ const GeneratePage: React.FC = () => {
|
||||
.catch(() => { });
|
||||
getVideoEngines()
|
||||
.then((data) => {
|
||||
setVideoEngines(data.items || []);
|
||||
if (data.items?.length) {
|
||||
const e = data.items[0];
|
||||
setSelectedEngineId((prev) => prev || e.id);
|
||||
setEngineOptions({
|
||||
ratios: e.supportedRatios?.length
|
||||
? e.supportedRatios
|
||||
@@ -1055,6 +1082,87 @@ const GeneratePage: React.FC = () => {
|
||||
.catch(() => { });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentRecord) return;
|
||||
const type = currentRecord.genType || mediaType;
|
||||
const engines = type === 'image' ? imageEngines : videoEngines;
|
||||
const savedEngineId = currentRecord.engineId || '';
|
||||
const engine = engines.find((item: any) => item.id === savedEngineId) || engines[0];
|
||||
if (engine) {
|
||||
setSelectedEngineId(engine.id);
|
||||
if (type === 'image') {
|
||||
const sizes = engine.supportedSizes || {};
|
||||
setSupportedSizes(sizes);
|
||||
const resolutionKeys = Object.keys(sizes);
|
||||
const nextResolution = resolutionKeys.includes(currentRecord.imageSize || '')
|
||||
? currentRecord.imageSize
|
||||
: resolutionKeys[0] || currentRecord.imageSize || '2K';
|
||||
setSelectedResolution(nextResolution);
|
||||
const ratioKeys = Object.keys(sizes[nextResolution] || {});
|
||||
const nextRatio = ratioKeys.includes(currentRecord.imageProportion || '')
|
||||
? currentRecord.imageProportion
|
||||
: ratioKeys[0] || currentRecord.imageProportion || '1:1';
|
||||
setSelectedRatio(nextRatio);
|
||||
const pixelSize = sizes[nextResolution]?.[nextRatio] || currentRecord.imagePx || '';
|
||||
if (pixelSize) {
|
||||
const [nextWidth, nextHeight] = String(pixelSize).split(/x/i).map(Number);
|
||||
if (nextWidth > 0 && nextHeight > 0) {
|
||||
setWidth(nextWidth);
|
||||
setHeight(nextHeight);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setEngineOptions({
|
||||
ratios: engine.supportedRatios?.length ? engine.supportedRatios : ['16:9'],
|
||||
resolutions: engine.supportedResolutions?.length ? engine.supportedResolutions : ['720p'],
|
||||
durations: engine.supportedDurations?.length ? engine.supportedDurations : [5],
|
||||
});
|
||||
if (!engine.supportedRatios?.includes(videoAspectRatio)) setVideoAspectRatio((engine.supportedRatios?.[0] || '16:9') as AspectRatio);
|
||||
if (!engine.supportedResolutions?.includes(videoResolution)) setVideoResolution((engine.supportedResolutions?.[0] || '720p') as Resolution);
|
||||
if (!engine.supportedDurations?.includes(videoDuration)) setVideoDuration(engine.supportedDurations?.[0] || currentRecord.duration || 5);
|
||||
}
|
||||
}
|
||||
setIncludeMediaReferences(Boolean(currentRecord.includeMediaReferences));
|
||||
}, [currentRecord?.id, currentRecord?.engineId, videoEngines, imageEngines]);
|
||||
|
||||
const getVideoEngineOptions = (engineId?: string) => {
|
||||
const engine = videoEngines.find((item: any) => item.id === engineId) || videoEngines[0];
|
||||
return {
|
||||
ratios: engine?.supportedRatios?.length ? engine.supportedRatios : ['16:9'],
|
||||
resolutions: engine?.supportedResolutions?.length ? engine.supportedResolutions : ['720p'],
|
||||
durations: engine?.supportedDurations?.length ? engine.supportedDurations : [5],
|
||||
};
|
||||
};
|
||||
|
||||
const handleGenerationEngineChange = (engineId: string) => {
|
||||
setSelectedEngineId(engineId);
|
||||
const engines = mediaType === 'image' ? imageEngines : videoEngines;
|
||||
const engine = engines.find((item: any) => item.id === engineId);
|
||||
if (!engine) return;
|
||||
if (mediaType === 'image') {
|
||||
const sizes = engine.supportedSizes || {};
|
||||
setSupportedSizes(sizes);
|
||||
const resolution = Object.keys(sizes)[0] || '2K';
|
||||
const ratio = Object.keys(sizes[resolution] || {})[0] || '1:1';
|
||||
setSelectedResolution(resolution);
|
||||
setSelectedRatio(ratio);
|
||||
const pixelSize = sizes[resolution]?.[ratio] || '';
|
||||
if (pixelSize) {
|
||||
const [nextWidth, nextHeight] = String(pixelSize).split(/x/i).map(Number);
|
||||
if (nextWidth > 0 && nextHeight > 0) {
|
||||
setWidth(nextWidth);
|
||||
setHeight(nextHeight);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const options = getVideoEngineOptions(engineId);
|
||||
setEngineOptions(options);
|
||||
setVideoAspectRatio(options.ratios[0] as AspectRatio);
|
||||
setVideoResolution(options.resolutions[0] as Resolution);
|
||||
setVideoDuration(options.durations[0]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
fetchRecords({ projectId, page: 1, pageSize: 100 });
|
||||
@@ -1239,17 +1347,25 @@ const GeneratePage: React.FC = () => {
|
||||
const userCredits = user?.credits ?? 0;
|
||||
|
||||
// 根据图片分辨率从 cimage 获取积分
|
||||
const getImageCreditsFromCimage = (imageSize: string): number => {
|
||||
// 将 imageSize 转换为 cimage 中的 resolution 格式
|
||||
const resolution = imageSize === '2k' ? '2048' : imageSize === '4k' ? '4096' : imageSize;
|
||||
|
||||
for (let i = 0; i < cimage.length; i++) {
|
||||
const item = cimage[i];
|
||||
if (item.resolution === resolution) {
|
||||
return item.baseCredits;
|
||||
const getImageCreditsFromCimage = (
|
||||
imageSize: string,
|
||||
engineId: string = selectedEngineId,
|
||||
includeReferences: boolean = includeMediaReferences,
|
||||
referenceItems: MediaReference[] | undefined = currentRecord?.references,
|
||||
): number => {
|
||||
const normalizedSize = String(imageSize || '').toLowerCase();
|
||||
const resolution = normalizedSize === '2k' ? '2048' : normalizedSize === '4k' ? '4096' : imageSize;
|
||||
const item = cimage.find((rule: any) => rule.resolution === resolution && (!engineId || rule.modelConfigId === engineId))
|
||||
|| cimage.find((rule: any) => rule.resolution === resolution);
|
||||
if (!item) return 0;
|
||||
let total = Number(item.baseCredits || 0) * Number(item.ratio || 1);
|
||||
if (includeReferences) {
|
||||
const inputImageCount = getReferenceUsage(referenceItems).inputImageCount;
|
||||
if (inputImageCount > 0) {
|
||||
total += (Number(item.inputImageBaseCredits || 0) + Number(item.inputImagePerImageCredits || 0) * inputImageCount) * Number(item.inputImageRatio || 1);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return Number(total.toFixed(2));
|
||||
};
|
||||
|
||||
// Media credits estimate for step 2 (video or image)
|
||||
@@ -1522,8 +1638,11 @@ const GeneratePage: React.FC = () => {
|
||||
});
|
||||
try {
|
||||
const result = await generateVideo(recordId, {
|
||||
engineId: selectedEngineId || undefined,
|
||||
includeMediaReferences,
|
||||
aspectRatio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
imageSize: currentRecord?.imageSize || selectedResolution,
|
||||
});
|
||||
if (result.status === "failed") {
|
||||
setRecordStates((p) => ({ ...p, [recordId]: "failed" }));
|
||||
@@ -1585,8 +1704,11 @@ const GeneratePage: React.FC = () => {
|
||||
await updateRecordPrompt(recordId, editedPrompt);
|
||||
}
|
||||
const result = await generateVideo(recordId, {
|
||||
engineId: record.engineId || selectedEngineId || undefined,
|
||||
includeMediaReferences: Boolean(record.includeMediaReferences),
|
||||
aspectRatio: record.aspectRatio || "16:9",
|
||||
resolution: record.resolution || "720p",
|
||||
imageSize: record.imageSize,
|
||||
});
|
||||
|
||||
if (result.status === "failed") {
|
||||
@@ -3238,6 +3360,22 @@ const GeneratePage: React.FC = () => {
|
||||
border: "1px solid #f0f0f5",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 220, marginRight: 16 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>生成引擎</Typography.Text>
|
||||
<Select
|
||||
value={selectedEngineId || undefined}
|
||||
onChange={handleGenerationEngineChange}
|
||||
style={{ width: '100%' }}
|
||||
options={(mediaType === 'image' ? imageEngines : videoEngines).map((item: any) => ({ value: item.id, label: item.name }))}
|
||||
/>
|
||||
{currentRecord?.references?.length ? (
|
||||
<div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch size="small" checked={includeMediaReferences} onChange={setIncludeMediaReferences} />
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b' }}>携带参考附件生成</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Video params selection */}
|
||||
{mediaType !== "image" && (
|
||||
<div
|
||||
@@ -4326,6 +4464,29 @@ const GeneratePage: React.FC = () => {
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
size="small"
|
||||
value={historyParams[record.id]?.engineId || record.engineId || (type === 'image' ? imageEngines[0]?.id : videoEngines[0]?.id)}
|
||||
onChange={(value) => {
|
||||
const options = type === 'video' ? getVideoEngineOptions(value) : null;
|
||||
setHistoryParams((prev) => ({
|
||||
...prev,
|
||||
[record.id]: {
|
||||
...prev[record.id],
|
||||
engineId: value,
|
||||
aspectRatio: type === 'video' ? options!.ratios[0] as AspectRatio : prev[record.id]?.aspectRatio,
|
||||
resolution: type === 'video' ? options!.resolutions[0] as Resolution : prev[record.id]?.resolution,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
options={(type === 'image' ? imageEngines : videoEngines).map((item: any) => ({ value: item.id, label: item.name }))}
|
||||
style={{ minWidth: 160 }}
|
||||
/>
|
||||
{record.references?.length ? (
|
||||
<><Switch size="small" checked={Boolean(historyParams[record.id]?.includeMediaReferences ?? record.includeMediaReferences)} onChange={(checked) => setHistoryParams((prev) => ({ ...prev, [record.id]: { ...prev[record.id], includeMediaReferences: checked, aspectRatio: prev[record.id]?.aspectRatio || '' as AspectRatio, resolution: prev[record.id]?.resolution || '' as Resolution } }))} /><Typography.Text style={{ fontSize: 12 }}>携带附件</Typography.Text></>
|
||||
) : null}
|
||||
</div>
|
||||
{type === "video" && (
|
||||
<div
|
||||
style={{
|
||||
@@ -4367,7 +4528,7 @@ const GeneratePage: React.FC = () => {
|
||||
historyParams[record.id]?.aspectRatio ||
|
||||
"选择比例"
|
||||
}
|
||||
options={engineOptions.ratios}
|
||||
options={getVideoEngineOptions(historyParams[record.id]?.engineId || record.engineId || videoEngines[0]?.id).ratios}
|
||||
expanded={
|
||||
expandedEngine === `ratio-${record.id}`
|
||||
}
|
||||
@@ -4397,7 +4558,7 @@ const GeneratePage: React.FC = () => {
|
||||
historyParams[record.id]?.resolution ||
|
||||
"选择分辨率"
|
||||
}
|
||||
options={engineOptions.resolutions}
|
||||
options={getVideoEngineOptions(historyParams[record.id]?.engineId || record.engineId || videoEngines[0]?.id).resolutions}
|
||||
expanded={
|
||||
expandedEngine === `res-${record.id}`
|
||||
}
|
||||
@@ -4450,6 +4611,9 @@ const GeneratePage: React.FC = () => {
|
||||
calcVideoCredits(
|
||||
record.duration || 5,
|
||||
params.resolution,
|
||||
params.engineId || record.engineId || videoEngines[0]?.id,
|
||||
Boolean(params.includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)
|
||||
) {
|
||||
message.error("积分不足,请先充值");
|
||||
@@ -4473,8 +4637,11 @@ const GeneratePage: React.FC = () => {
|
||||
});
|
||||
try {
|
||||
await generateVideo(record.id, {
|
||||
engineId: params?.engineId || record.engineId || (type === 'image' ? imageEngines[0]?.id : videoEngines[0]?.id),
|
||||
includeMediaReferences: Boolean(params?.includeMediaReferences ?? record.includeMediaReferences),
|
||||
aspectRatio: params?.aspectRatio,
|
||||
resolution: params?.resolution,
|
||||
imageSize: record.imageSize,
|
||||
});
|
||||
startPolling(record.id);
|
||||
message.success({
|
||||
@@ -4529,14 +4696,31 @@ const GeneratePage: React.FC = () => {
|
||||
)}{" "}
|
||||
{/* {type === "video" &&
|
||||
historyParams[record.id]?.resolution
|
||||
? `(${calcVideoCredits(record.duration || 5, historyParams[record.id].resolution)}积分)`
|
||||
? `(${calcVideoCredits(
|
||||
record.duration || 5,
|
||||
historyParams[record.id].resolution as Resolution,
|
||||
historyParams[record.id].engineId || record.engineId || videoEngines[0]?.id,
|
||||
Boolean(historyParams[record.id].includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)}积分)`
|
||||
: ""} */}
|
||||
{type === "video" &&
|
||||
historyParams[record.id]?.resolution
|
||||
? `(${calcVideoCredits(record.duration || 5, historyParams[record.id].resolution)}积分)`
|
||||
? `(${calcVideoCredits(
|
||||
record.duration || 5,
|
||||
historyParams[record.id].resolution as Resolution,
|
||||
historyParams[record.id].engineId || record.engineId || videoEngines[0]?.id,
|
||||
Boolean(historyParams[record.id].includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)}积分)`
|
||||
: ""}
|
||||
{type === "image" && record.imageSize
|
||||
? `(${getImageCredits(record.imageSize)}积分)`
|
||||
? `(${getImageCreditsFromCimage(
|
||||
record.imageSize,
|
||||
historyParams[record.id]?.engineId || record.engineId || imageEngines[0]?.id,
|
||||
Boolean(historyParams[record.id]?.includeMediaReferences ?? record.includeMediaReferences),
|
||||
record.references,
|
||||
)}积分)`
|
||||
: type === "image"
|
||||
? `(积分)`
|
||||
: ""}
|
||||
@@ -4807,7 +4991,7 @@ const GeneratePage: React.FC = () => {
|
||||
setRecordStates((p) => ({ ...p, [record.id]: 'generating' }));
|
||||
message.loading({ content: `「${projectName}」正在生成视频...`, duration: 0, key: record.id });
|
||||
try {
|
||||
await generateVideo(record.id, { aspectRatio: params.aspectRatio, resolution: params.resolution });
|
||||
await generateVideo(record.id, { engineId: params.engineId || record.engineId || videoEngines[0]?.id, includeMediaReferences: Boolean(params.includeMediaReferences ?? record.includeMediaReferences), aspectRatio: params.aspectRatio, resolution: params.resolution, imageSize: record.imageSize });
|
||||
setRecordStates((p) => ({ ...p, [record.id]: 'done' }));
|
||||
message.success({ content: `「${projectName}」视频生成成功!`, key: record.id, duration: 3 });
|
||||
} catch {
|
||||
|
||||
@@ -901,9 +901,9 @@ const HomePage: React.FC = () => {
|
||||
onClick={() => {
|
||||
const id = video.moduleProjectId;
|
||||
if (video.type === 'hotOpeningReplicate' && id != null) {
|
||||
navigate(`/initial/${id}/initialinfo`);
|
||||
navigate(`/initial/${id}/initialinfo?flow_version=${video.moduleProjectFlowVersion === 'v2' ? 'v2' : 'v1'}`);
|
||||
} else if (video.type === 'shotReplicate' && id != null) {
|
||||
navigate(`/removelens/${id}/removefenbu`);
|
||||
navigate(`/removelens/${id}/removefenbu?flow_version=${video.moduleProjectFlowVersion === 'v2' ? 'v2' : 'v1'}`);
|
||||
} else if (video.type === 'chatAi') {
|
||||
navigate(`/conversation`);
|
||||
} else if (video.type === 'project') {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip } from 'antd';
|
||||
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema, updateImagePrompt, calculateCredits } from '../api/index';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema, updateImagePrompt, calculateCredits, retryHotOpeningVideoPromptV2, updateHotOpeningVideoPromptSchemaV2, generateHotOpeningVideoV2 } from '../api/index';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
import VideoPromptSchemaEditor from '../components/VideoPromptSchemaEditor';
|
||||
import { validateVideoPromptSchemaByConfig } from '../utils/videoPromptSchema';
|
||||
@@ -25,6 +25,8 @@ const buildMediaUrl = (url: string): string => {
|
||||
function InitialInfo() {
|
||||
const navigate = useNavigate();
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
@@ -35,7 +37,10 @@ function InitialInfo() {
|
||||
const [videoSchemaConfigSnapshot, setVideoSchemaConfigSnapshot] = useState<any>(null);
|
||||
const [editingPromptStepId, setEditingPromptStepId] = useState<string>('');
|
||||
const [promptSaving, setPromptSaving] = useState(false);
|
||||
const [pollingTimer, setPollingTimer] = useState<any>(null);
|
||||
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [retryPromptModalVisible, setRetryPromptModalVisible] = useState(false);
|
||||
const [retryPromptStepId, setRetryPromptStepId] = useState<string>('');
|
||||
const [retryPromptSubmitting, setRetryPromptSubmitting] = useState(false);
|
||||
|
||||
// 引擎和视频参数相关状态
|
||||
const [enginesele, setEnginesele] = useState<any>({});
|
||||
@@ -139,14 +144,21 @@ function InitialInfo() {
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
//
|
||||
const baseSteps = [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
const isV2 = flowVersion === 'v2';
|
||||
|
||||
const baseSteps = isV2
|
||||
? [
|
||||
{ id: 1, title: '素材与项目信息', description: '固定素材和项目描述', childId: 1 },
|
||||
{ id: 2, title: '生成视频提示词', description: '生成并确认视频提示词', childId: 4 },
|
||||
{ id: 3, title: '生成最终视频', description: '使用当前视频提示词生成视频', childId: 5 },
|
||||
]
|
||||
: [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
|
||||
// 合并基础步骤和API返回的状态
|
||||
const steps = baseSteps.map((step, index) => ({
|
||||
@@ -295,7 +307,9 @@ function InitialInfo() {
|
||||
total += inputVideoCost;
|
||||
}
|
||||
|
||||
const inputImageCount = taskDetail?.videoGeneration?.inputMedia?.image?.length || 0;
|
||||
const inputImageCount = isV2
|
||||
? (taskDetail?.material?.materialImageUrl ? 1 : 0)
|
||||
: (taskDetail?.videoGeneration?.inputMedia?.image?.length || 0);
|
||||
if (inputImageCount > 0) {
|
||||
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
|
||||
total += inputImageCost;
|
||||
@@ -313,12 +327,12 @@ function InitialInfo() {
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
setPollingTimer(null);
|
||||
if (pollingTimerRef.current) {
|
||||
clearInterval(pollingTimerRef.current);
|
||||
pollingTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [pollingTimer]);
|
||||
}, []);
|
||||
|
||||
// 点击外部关闭弹窗
|
||||
useEffect(() => {
|
||||
@@ -338,39 +352,30 @@ function InitialInfo() {
|
||||
// 获取任务详情数据
|
||||
useEffect(() => {
|
||||
if (creatID) {
|
||||
getReplicationDetail(creatID).then((res: any) => {
|
||||
getReplicationDetail(creatID, flowVersion).then((res: any) => {
|
||||
setTaskDetail(res);
|
||||
if (res.steps) {
|
||||
setApiSteps(res.steps);
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
message.error(error?.message || '加载任务详情失败');
|
||||
});
|
||||
}
|
||||
}, [creatID]);
|
||||
}, [creatID, flowVersion]);
|
||||
|
||||
// 监听合并后的 steps 数据,判断第五步状态并启动/停止轮询
|
||||
// 仅在项目或任一步骤处于 processing 时轮询;waiting_user 必须停止。
|
||||
useEffect(() => {
|
||||
const shouldPoll = taskDetail?.status === 'processing'
|
||||
|| apiSteps.some((step: any) => step?.status === 'processing');
|
||||
|
||||
// 检查第五步的状态(索引 4)
|
||||
const fifthStep = steps[4];
|
||||
|
||||
if (fifthStep && fifthStep.status !== 'completed' && fifthStep.status !== 'failed') {
|
||||
// 第五步未完成,启动轮询
|
||||
if (!pollingTimer) {
|
||||
const timer = setInterval(pollTaskDetail, 15000);
|
||||
setPollingTimer(timer);
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
// 第五步已完成或失败,停止轮询
|
||||
if (fifthStep) {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
setPollingTimer(null);
|
||||
}
|
||||
}
|
||||
if (shouldPoll && !pollingTimerRef.current) {
|
||||
pollingTimerRef.current = setInterval(pollTaskDetail, 15000);
|
||||
} else if (!shouldPoll && pollingTimerRef.current) {
|
||||
clearInterval(pollingTimerRef.current);
|
||||
pollingTimerRef.current = null;
|
||||
}
|
||||
}, [steps]);
|
||||
}, [taskDetail?.status, apiSteps, creatID, flowVersion]);
|
||||
|
||||
|
||||
const handleOpenModal = (prompt?: any, type?: string, stepId?: string | number, schemaConfigSnapshot?: any) => {
|
||||
setCurrentType(type || 'image');
|
||||
@@ -436,7 +441,8 @@ function InitialInfo() {
|
||||
|
||||
setPromptSaving(true);
|
||||
try {
|
||||
await updateHotOpeningVideoPromptSchema(taskDetail.id, editingPromptStepId, {
|
||||
const updateVideoPrompt = isV2 ? updateHotOpeningVideoPromptSchemaV2 : updateHotOpeningVideoPromptSchema;
|
||||
await updateVideoPrompt(taskDetail.id, editingPromptStepId, {
|
||||
prompt_schema: formData,
|
||||
});
|
||||
message.success('视频提示词已保存');
|
||||
@@ -456,12 +462,13 @@ function InitialInfo() {
|
||||
return;
|
||||
}
|
||||
|
||||
getReplicationDetail(creatID).then((res: any) => {
|
||||
getReplicationDetail(creatID, flowVersion).then((res: any) => {
|
||||
setTaskDetail(res);
|
||||
if (res.steps) {
|
||||
setApiSteps(res.steps);
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
console.error('轮询任务详情失败', error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -469,12 +476,13 @@ function InitialInfo() {
|
||||
const refreshTaskDetail = () => {
|
||||
if (!creatID) return;
|
||||
|
||||
getReplicationDetail(creatID).then((res: any) => {
|
||||
getReplicationDetail(creatID, flowVersion).then((res: any) => {
|
||||
setTaskDetail(res);
|
||||
if (res.steps) {
|
||||
setApiSteps(res.steps);
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
message.error(error?.message || '刷新任务详情失败');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -539,35 +547,109 @@ function InitialInfo() {
|
||||
// 这里可以添加下一步的逻辑,比如调用接口等
|
||||
};
|
||||
|
||||
const createvideo = (stepId: number, engineId: string) => {
|
||||
let params = {
|
||||
engine_id: '',
|
||||
}
|
||||
|
||||
|
||||
getfour(taskDetail.id, stepId.toString(), params).then((res: any) => {
|
||||
// 重新获取任务详情以更新数据
|
||||
message.info('正在生成视频,请稍候...');
|
||||
const createvideo = async (stepId: number, engineId: string) => {
|
||||
try {
|
||||
if (isV2) {
|
||||
await generateHotOpeningVideoV2(taskDetail.id, String(stepId));
|
||||
} else {
|
||||
await getfour(taskDetail.id, String(stepId), { engine_id: engineId || '' });
|
||||
}
|
||||
message.info('正在生成视频,请稍候...');
|
||||
refreshTaskDetail();
|
||||
}).catch((error: any) => {
|
||||
const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败';
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败';
|
||||
message.error(errorMsg);
|
||||
});
|
||||
}
|
||||
const agincreatevideo = () => {
|
||||
let params = {
|
||||
engine_id: '',
|
||||
}
|
||||
};
|
||||
|
||||
const agincreatevideo = async () => {
|
||||
const promptStep = isV2 ? steps[1] : steps[3];
|
||||
if (!promptStep?.id) return;
|
||||
await createvideo(promptStep.id, promptStep.engineId || '');
|
||||
};
|
||||
|
||||
getfour(taskDetail.id, steps[3].id.toString(), params).then((res: any) => {
|
||||
// 重新获取任务详情以更新数据
|
||||
message.info('正在生成视频,请稍候...');
|
||||
const applyRetryVideoEngine = (
|
||||
engineId: string,
|
||||
preferred?: { duration?: number; aspectRatio?: string; resolution?: string },
|
||||
) => {
|
||||
const engine = (enginesele.video || []).find((item: any) => String(item.id) === String(engineId));
|
||||
if (!engine) {
|
||||
return false;
|
||||
}
|
||||
const ratios = Array.isArray(engine.supportedRatios) && engine.supportedRatios.length > 0
|
||||
? engine.supportedRatios.map((item: any) => String(item))
|
||||
: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'];
|
||||
const resolutions = Array.isArray(engine.supportedResolutions) && engine.supportedResolutions.length > 0
|
||||
? engine.supportedResolutions.map((item: any) => String(item))
|
||||
: ['480p', '720p', '1080p'];
|
||||
const parsedDurations = Array.isArray(engine.supportedDurations)
|
||||
? engine.supportedDurations
|
||||
.map((item: any) => Number(item))
|
||||
.filter((item: number) => Number.isFinite(item) && item > 0)
|
||||
: [];
|
||||
const durations = parsedDurations.length > 0 ? parsedDurations : [5, 8, 10, 12, 15];
|
||||
|
||||
const preferredDuration = Number(preferred?.duration ?? videoDuration);
|
||||
const preferredRatio = String(preferred?.aspectRatio || videoAspectRatio || '');
|
||||
const preferredResolution = String(preferred?.resolution || videoResolution || '');
|
||||
|
||||
setCountType(String(engine.id));
|
||||
setEngineOptions({ ratios, resolutions, durations });
|
||||
setVideoDuration(durations.includes(preferredDuration) ? preferredDuration : durations[0]);
|
||||
setVideoAspectRatio(ratios.includes(preferredRatio) ? preferredRatio : ratios[0]);
|
||||
setVideoResolution(resolutions.includes(preferredResolution) ? preferredResolution : resolutions[0]);
|
||||
return true;
|
||||
};
|
||||
|
||||
const openRegenerateVideoPrompt = (stepId: number) => {
|
||||
if (!isV2 || !taskDetail?.id) return;
|
||||
const currentConfig = taskDetail?.videoGeneration?.promptParams || taskDetail?.videoGeneration?.params || {};
|
||||
const requestedEngineId = String(
|
||||
currentConfig.engineId
|
||||
|| currentConfig.engine_id
|
||||
|| taskDetail?.videoGeneration?.engineId
|
||||
|| countType
|
||||
|| '',
|
||||
);
|
||||
const currentEngineId = String(
|
||||
(enginesele.video || []).some((engine: any) => String(engine.id) === requestedEngineId)
|
||||
? requestedEngineId
|
||||
: enginesele.video?.[0]?.id || '',
|
||||
);
|
||||
if (!currentEngineId || !applyRetryVideoEngine(currentEngineId, {
|
||||
duration: Number(currentConfig.duration || videoDuration),
|
||||
aspectRatio: String(currentConfig.aspectRatio || currentConfig.aspect_ratio || videoAspectRatio),
|
||||
resolution: String(currentConfig.resolution || videoResolution),
|
||||
})) {
|
||||
message.warning('当前没有可用的视频生成引擎');
|
||||
return;
|
||||
}
|
||||
setRetryPromptStepId(String(stepId));
|
||||
setRetryPromptModalVisible(true);
|
||||
};
|
||||
|
||||
const submitRegenerateVideoPrompt = async () => {
|
||||
if (!isV2 || !taskDetail?.id || !retryPromptStepId || !countType) return;
|
||||
setRetryPromptSubmitting(true);
|
||||
try {
|
||||
await retryHotOpeningVideoPromptV2(taskDetail.id, retryPromptStepId, {
|
||||
video_config: {
|
||||
engine_id: countType,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
},
|
||||
});
|
||||
message.info('正在按新视频参数重新生成视频提示词,请稍候...');
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
refreshTaskDetail();
|
||||
}).catch((error: any) => {
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '重新生成视频提示词失败');
|
||||
} finally {
|
||||
setRetryPromptSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -896,6 +978,7 @@ function InitialInfo() {
|
||||
重新生成
|
||||
</Button>
|
||||
</div>
|
||||
{!isV2 && (<>
|
||||
{/* 引擎选择器和视频参数设置 */}
|
||||
<p style={{marginBottom:6,fontSize: 14, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>视频参数选择:</p>
|
||||
<div style={{ width:'100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
@@ -1287,6 +1370,7 @@ function InitialInfo() {
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>)}
|
||||
</>
|
||||
)}
|
||||
{/* 步骤4: 生成视频提示词 */}
|
||||
@@ -1307,6 +1391,16 @@ function InitialInfo() {
|
||||
>
|
||||
查看/修改视频提词
|
||||
</Button>
|
||||
{isV2 && (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => openRegenerateVideoPrompt(step.id)}
|
||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||
disabled={step.status === 'processing' || steps[2]?.status === 'processing'}
|
||||
>
|
||||
重新生成视频提词
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => { createvideo(step.id, step.engineId); }}
|
||||
type="primary"
|
||||
@@ -1341,9 +1435,9 @@ function InitialInfo() {
|
||||
type="default"
|
||||
icon={<EditOutlined />}
|
||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
||||
disabled={step.status !== 'completed'}
|
||||
disabled={isV2 ? !['completed', 'failed'].includes(step.status) : step.status !== 'completed'}
|
||||
>
|
||||
重新生成
|
||||
{step.status === 'failed' ? '重试生成' : '重新生成'}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -1373,6 +1467,101 @@ function InitialInfo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
title="重新生成视频提词"
|
||||
open={retryPromptModalVisible}
|
||||
onCancel={() => {
|
||||
if (!retryPromptSubmitting) {
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
}
|
||||
}}
|
||||
onOk={submitRegenerateVideoPrompt}
|
||||
confirmLoading={retryPromptSubmitting}
|
||||
okText="按新参数生成提词"
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频引擎</Text>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }}>
|
||||
{(enginesele.video || []).map((engine: any) => (
|
||||
<button
|
||||
key={engine.id}
|
||||
type="button"
|
||||
onClick={() => applyRetryVideoEngine(String(engine.id), {
|
||||
duration: videoDuration,
|
||||
aspectRatio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
})}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: countType === String(engine.id) ? '2px solid #6366f1' : '1px solid #e5e7eb',
|
||||
background: countType === String(engine.id) ? 'rgba(99,102,241,0.08)' : '#fff',
|
||||
color: countType === String(engine.id) ? '#4f46e5' : '#374151',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontWeight: countType === String(engine.id) ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{engine.name || engine.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频时长</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.durations.map((duration) => (
|
||||
<Button
|
||||
key={duration}
|
||||
type={videoDuration === duration ? 'primary' : 'default'}
|
||||
onClick={() => setVideoDuration(duration)}
|
||||
>
|
||||
{duration} 秒
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>画面比例</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.ratios.map((ratio) => (
|
||||
<Button
|
||||
key={ratio}
|
||||
type={videoAspectRatio === ratio ? 'primary' : 'default'}
|
||||
onClick={() => setVideoAspectRatio(ratio)}
|
||||
>
|
||||
{ratio}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>分辨率</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.resolutions.map((resolution) => (
|
||||
<Button
|
||||
key={resolution}
|
||||
type={videoResolution === resolution ? 'primary' : 'default'}
|
||||
onClick={() => setVideoResolution(resolution)}
|
||||
>
|
||||
{resolution}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '10px 12px', borderRadius: 8, background: '#f8fafc', color: '#64748b' }}>
|
||||
当前参数:{enginesele.video?.find((engine: any) => String(engine.id) === countType)?.name || countType}
|
||||
{' · '}{videoDuration} 秒 · {videoAspectRatio} · {videoResolution}
|
||||
<span style={{ marginLeft: 12 }}>最终视频预估积分:{estimatedCredits}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
@@ -1586,7 +1775,7 @@ function InitialInfo() {
|
||||
|
||||
render: (_, record) => (
|
||||
<button
|
||||
onClick={() => navigate(`/initial/${record.id}/initialinfo`)}
|
||||
onClick={() => navigate(`/initial/${record.id}/initialinfo?flow_version=${record.flowVersion === 'v2' ? 'v2' : 'v1'}`)}
|
||||
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'rgba(99, 102, 241, 0.08)', padding: '4px 12px', borderRadius: 8, cursor: 'pointer', transition: 'all 0.2s', fontWeight: 500 }}
|
||||
>
|
||||
查看详情
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Space,
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Select,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -19,8 +20,9 @@ import {
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, getone, getReplicationDetail, deleteHotOpeningReplicationTask } from '../api';
|
||||
import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, deleteHotOpeningReplicationTask, getEngine, calculateCredits } from '../api';
|
||||
import bg1 from '../assets/bg1.png';
|
||||
import UploadSelector from '../components/UploadSelector';
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +31,13 @@ const { TextArea } = Input;
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||||
|
||||
const supportsReferenceImage = (engine: any): boolean => {
|
||||
if (!engine) return false;
|
||||
const supports = engine.supportsUniversalReference ?? engine.supports_universal_reference;
|
||||
const maxImages = engine.maxImageCount ?? engine.max_image_count;
|
||||
return supports !== false && (maxImages === undefined || maxImages === null || Number(maxImages) >= 1);
|
||||
};
|
||||
|
||||
const buildAssetUrl = (url?: string): string => {
|
||||
if (!url) return '';
|
||||
if (/^https?:\/\//i.test(url) || url.startsWith('blob:')) return url;
|
||||
@@ -70,6 +79,12 @@ const GenerateConver: React.FC = () => {
|
||||
const [imageResourceId, setImageResourceId] = useState<string>('');
|
||||
const [videoUploading, setVideoUploading] = useState(false);
|
||||
const [imageUploading, setImageUploading] = useState(false);
|
||||
const [videoEngines, setVideoEngines] = useState<any[]>([]);
|
||||
const [engineId, setEngineId] = useState('');
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
const [videoAspectRatio, setVideoAspectRatio] = useState('16:9');
|
||||
const [videoResolution, setVideoResolution] = useState('480p');
|
||||
const [creditRules, setCreditRules] = useState<any[]>([]);
|
||||
|
||||
// 弹窗状态
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -79,6 +94,35 @@ const GenerateConver: React.FC = () => {
|
||||
|
||||
// 表格轮询定时器(使用 ref 避免闭包问题)
|
||||
const tablePollingTimer = useRef<any>(null);
|
||||
// 同一次创建在网络重试时复用幂等键;成功后再生成新键。
|
||||
const createIdempotencyKeyRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getEngine().then((data: any) => {
|
||||
const engines = data?.engine?.video || [];
|
||||
setVideoEngines(engines);
|
||||
if (engines.length > 0) {
|
||||
const first = engines[0];
|
||||
setEngineId(first.id);
|
||||
setVideoAspectRatio(first.supportedRatios?.[0] || '16:9');
|
||||
setVideoResolution(first.supportedResolutions?.[0] || '480p');
|
||||
setVideoDuration(first.supportedDurations?.[0] || 5);
|
||||
}
|
||||
}).catch(() => message.error('视频引擎加载失败'));
|
||||
calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([]));
|
||||
}, []);
|
||||
|
||||
const selectedEngine = videoEngines.find((item: any) => item.id === engineId);
|
||||
const selectedEngineSupportsImage = !imageUrl || supportsReferenceImage(selectedEngine);
|
||||
const estimatedCredits = (() => {
|
||||
const rule = creditRules.find((item: any) => item.modelConfigId === engineId && item.genType === 'video' && item.resolution === videoResolution);
|
||||
if (!rule) return 0;
|
||||
let total = (Number(rule.baseCredits || 0) + Number(rule.perSecondCredits || 0) * videoDuration) * Number(rule.ratio || 1);
|
||||
if (imageUrl) {
|
||||
total += (Number(rule.inputImageBaseCredits || 0) + Number(rule.inputImagePerImageCredits || 0)) * Number(rule.inputImageRatio || 1);
|
||||
}
|
||||
return Number(total.toFixed(2));
|
||||
})();
|
||||
|
||||
// 获取卡片列表数据(独立于表格)
|
||||
const fetchCardList = (page: number, size: number, isPolling = false) => {
|
||||
@@ -355,90 +399,52 @@ const GenerateConver: React.FC = () => {
|
||||
message.warning('请上传复刻视频');
|
||||
return;
|
||||
}
|
||||
if (!imageUrl) {
|
||||
message.warning('请上传产品图片');
|
||||
if (!engineId || !videoDuration || !videoAspectRatio || !videoResolution) {
|
||||
message.warning('请选择完整的视频生成参数');
|
||||
return;
|
||||
}
|
||||
if (!originalProductName.trim()) {
|
||||
message.warning('请输入原视频产品名称');
|
||||
if (!selectedEngineSupportsImage) {
|
||||
message.warning('当前视频引擎不支持参考图片,请移除素材图片或切换引擎');
|
||||
return;
|
||||
}
|
||||
if (!ownProductName.trim()) {
|
||||
message.warning('请输入自有产品名称');
|
||||
return;
|
||||
}
|
||||
if (!productSellingPoints.trim()) {
|
||||
message.warning('请输入产品卖点');
|
||||
return;
|
||||
}
|
||||
// message.success('正在生成爆款开头复刻视频...');
|
||||
// console.log('生成参数:', {
|
||||
// videoUrl,
|
||||
// imageUrl,
|
||||
// originalProductName,
|
||||
// ownProductName,
|
||||
// productSellingPoints,
|
||||
// });
|
||||
let params = {
|
||||
const idempotencyKey = createIdempotencyKeyRef.current
|
||||
|| `hot_v2_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
createIdempotencyKeyRef.current = idempotencyKey;
|
||||
const params = {
|
||||
material_video_url: videoUrl,
|
||||
material_image_url: imageUrl,
|
||||
material_image_url: imageUrl || undefined,
|
||||
material_video_resource_id: videoResourceId || undefined,
|
||||
material_image_resource_id: imageResourceId || undefined,
|
||||
material_video_duration_seconds: videoDurationSeconds || undefined,
|
||||
source_project_name: originalProductName,
|
||||
target_project_name: ownProductName,
|
||||
core_content_point: productSellingPoints,
|
||||
idempotency_key: Date.now().toString(),
|
||||
}
|
||||
|
||||
// 在这里调用生成视频的API,并传递上述参数
|
||||
generateReplication(params).then((res) => {
|
||||
|
||||
|
||||
let projectId = '';
|
||||
let childId = '';
|
||||
// console.log('生成视频成功:', res);
|
||||
// message.success('任务开始');
|
||||
|
||||
// 清空上传的媒体和文本
|
||||
source_project_name: originalProductName.trim() || undefined,
|
||||
target_project_name: ownProductName.trim() || undefined,
|
||||
project_description: productSellingPoints.trim() || undefined,
|
||||
core_content_point: productSellingPoints.trim() || undefined,
|
||||
video_config: {
|
||||
engine_id: engineId,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
},
|
||||
idempotency_key: idempotencyKey,
|
||||
};
|
||||
generateReplication(params).then((res: any) => {
|
||||
const projectId = res.id || res.projectId || res.detail?.id;
|
||||
createIdempotencyKeyRef.current = null;
|
||||
setVideoUrl('');
|
||||
setVideoResourceId('');
|
||||
setVideoDurationSeconds(null);
|
||||
setImageUrl('');
|
||||
setImageResourceId('');
|
||||
setImageFile(null);
|
||||
setOriginalProductName('');
|
||||
setOwnProductName('');
|
||||
setProductSellingPoints('');
|
||||
|
||||
fetchCardList(1, 8);
|
||||
|
||||
// 获取第一个的id直接进行下一步
|
||||
getReplicationList(1, 20).then((res: any) => {
|
||||
if (res.items) {
|
||||
const firstId = res.items[0].id;
|
||||
setTableData(res.items);
|
||||
getReplicationDetail(res.items[0].id).then((res: any) => {
|
||||
projectId = res.id;
|
||||
childId = res.steps[0].id;
|
||||
getone(projectId, childId).then((res: any) => {
|
||||
message.loading('创建中...', 3);
|
||||
setTimeout(() => {
|
||||
navigate(`/initial/${firstId}/initialinfo`);
|
||||
}, 3000);
|
||||
})
|
||||
}).catch((error: any) => {
|
||||
});
|
||||
}
|
||||
|
||||
}).catch((error: any) => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}).catch((error) => {
|
||||
message.error('视频开头复刻失败');
|
||||
message.success('项目已创建,视频提词正在生成');
|
||||
if (projectId) navigate(`/initial/${projectId}/initialinfo?flow_version=v2`);
|
||||
}).catch((error: any) => {
|
||||
message.error(error?.message || '视频开头复刻失败');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -548,7 +554,7 @@ const GenerateConver: React.FC = () => {
|
||||
transition: 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
// border: '1px solid rgba(99, 102, 241, 0.06)',
|
||||
}}
|
||||
onClick={() => navigate(`/initial/${item.id}/initialinfo`)}
|
||||
onClick={() => navigate(`/initial/${item.id}/initialinfo?flow_version=${item.flowVersion === 'v2' ? 'v2' : 'v1'}`)}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-6px)';
|
||||
e.currentTarget.style.boxShadow = '0 12px 32px rgba(99, 102, 241, 0.12)';
|
||||
@@ -1021,12 +1027,22 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
style={{ width: '100%' }}
|
||||
|
||||
beforeUpload={beforeImageUpload}
|
||||
showUploadList={false}
|
||||
accept="image/jpeg,image/jpg,image/png,.jpg,.jpeg,.png"
|
||||
<UploadSelector
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
mediaType="image"
|
||||
uploading={imageUploading}
|
||||
maxImageCount={1}
|
||||
usedImageCount={0}
|
||||
onLocalSelect={(files) => { if (files[0]) void beforeImageUpload(files[0]); }}
|
||||
onHistorySelect={(items) => {
|
||||
const item = items[0];
|
||||
if (!item) return;
|
||||
setImageFile(null);
|
||||
setImageUrl(item.resourceUrl || item.previewUrl || item.displayUrl || '');
|
||||
setImageResourceId(item.id);
|
||||
message.success('历史图片已选择');
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -1088,7 +1104,7 @@ const GenerateConver: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Upload>
|
||||
</UploadSelector>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1161,12 +1177,38 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 20, display: 'grid', gap: 10 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 600, color: '#475569' }}>视频生成参数(必选)</p>
|
||||
<Select value={engineId || undefined} placeholder="选择视频引擎" onChange={(value) => {
|
||||
setEngineId(value);
|
||||
const engine = videoEngines.find((item: any) => item.id === value);
|
||||
setVideoAspectRatio(engine?.supportedRatios?.[0] || '16:9');
|
||||
setVideoResolution(engine?.supportedResolutions?.[0] || '480p');
|
||||
setVideoDuration(engine?.supportedDurations?.[0] || 5);
|
||||
}} options={videoEngines.map((item: any) => ({
|
||||
value: item.id,
|
||||
label: imageUrl && !supportsReferenceImage(item) ? `${item.name}(不支持参考图)` : item.name,
|
||||
disabled: Boolean(imageUrl) && !supportsReferenceImage(item),
|
||||
}))} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
|
||||
<Select value={videoDuration} onChange={setVideoDuration} options={(selectedEngine?.supportedDurations || [5]).map((value: number) => ({ value, label: `${value}秒` }))} />
|
||||
<Select value={videoAspectRatio} onChange={setVideoAspectRatio} options={(selectedEngine?.supportedRatios || ['16:9']).map((value: string) => ({ value, label: value }))} />
|
||||
<Select value={videoResolution} onChange={setVideoResolution} options={(selectedEngine?.supportedResolutions || ['480p']).map((value: string) => ({ value, label: value }))} />
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: selectedEngineSupportsImage ? '#8b5cf6' : '#ef4444' }}>
|
||||
{selectedEngineSupportsImage
|
||||
? `预估视频积分:${estimatedCredits || '-'};素材图片可不传,最终生成只会携带图片,不会携带参考视频。`
|
||||
: '当前引擎不支持参考图片,请移除素材图片或切换引擎。'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 立即生成按钮 */}
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
onClick={handleGenerate}
|
||||
disabled={!selectedEngineSupportsImage}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
height: 44,
|
||||
@@ -1382,7 +1424,7 @@ const GenerateConver: React.FC = () => {
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<button
|
||||
onClick={() => navigate(`/initial/${record.id}/initialinfo`)}
|
||||
onClick={() => navigate(`/initial/${record.id}/initialinfo?flow_version=${record.flowVersion === 'v2' ? 'v2' : 'v1'}`)}
|
||||
style={{
|
||||
color: '#6366f1',
|
||||
textDecoration: 'none',
|
||||
@@ -1402,7 +1444,7 @@ const GenerateConver: React.FC = () => {
|
||||
title="确认删除这个爆款开头复刻任务吗?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await deleteHotOpeningReplicationTask(record.id);
|
||||
await deleteHotOpeningReplicationTask(record.id, record.flowVersion);
|
||||
message.success('删除成功');
|
||||
fetchList(1, pageSize, false, searchKeyword);
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, Drawer, Input, Modal, Popconfirm, Spin, Table, Tag, Tooltip, Upload, message } from 'antd';
|
||||
import { Button, Drawer, Input, Modal, Popconfirm, Select, Spin, Table, Tag, Tooltip, message } from 'antd';
|
||||
import { ArrowLeftOutlined, DeleteOutlined, PlusOutlined, XOutlined } from '@ant-design/icons';
|
||||
|
||||
import {uploadShotReplicateImage, createRemoveLens, deleteSegment, getShotReplicationDetail, reanalyzeSegment, retrySplit, Removelist, removeCreate, reanalyzeShotReplication, splitCustom, uploadImage } from '../api';
|
||||
import { uploadShotReplicateImage, createRemoveLens, deleteSegment, getShotReplicationDetail, reanalyzeSegment, retrySplit, Removelist, removeCreate, reanalyzeShotReplication, splitCustom, getEngine, calculateCredits } from '../api';
|
||||
|
||||
import VideoTrimPicker from '../components/VideoTrimPicker';
|
||||
import UploadSelector from '../components/UploadSelector';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -13,6 +15,13 @@ const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||||
const MIN_TRIM_SECONDS = 2;
|
||||
const MAX_TRIM_SECONDS = 15;
|
||||
|
||||
const supportsReferenceImage = (engine: any): boolean => {
|
||||
if (!engine) return false;
|
||||
const supports = engine.supportsUniversalReference ?? engine.supports_universal_reference;
|
||||
const maxImages = engine.maxImageCount ?? engine.max_image_count;
|
||||
return supports !== false && (maxImages === undefined || maxImages === null || Number(maxImages) >= 1);
|
||||
};
|
||||
|
||||
function buildAssetUrl(url?: string): string {
|
||||
if (!url) return '';
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
@@ -22,6 +31,7 @@ function buildAssetUrl(url?: string): string {
|
||||
function RemoveInfo() {
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuthStore();
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [trimModalVisible, setTrimModalVisible] = useState(false);
|
||||
const [currentSegment, setCurrentSegment] = useState<string | null>(null);
|
||||
@@ -33,6 +43,12 @@ function RemoveInfo() {
|
||||
const [productSellingPoint, setProductSellingPoint] = useState('');
|
||||
const [productImage, setProductImage] = useState('');
|
||||
const [productImageResourceId, setProductImageResourceId] = useState('');
|
||||
const [videoEngines, setVideoEngines] = useState<any[]>([]);
|
||||
const [engineId, setEngineId] = useState('');
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
const [videoAspectRatio, setVideoAspectRatio] = useState('16:9');
|
||||
const [videoResolution, setVideoResolution] = useState('480p');
|
||||
const [creditRules, setCreditRules] = useState<any[]>([]);
|
||||
const [taskDetail, setTaskDetail] = useState<any>(null);
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -43,6 +59,45 @@ function RemoveInfo() {
|
||||
|
||||
const videoUrl = useMemo(() => buildAssetUrl(taskDetail?.videoUrl), [taskDetail?.videoUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
getEngine().then((data: any) => {
|
||||
const engines = data?.engine?.video || [];
|
||||
setVideoEngines(engines);
|
||||
if (engines.length > 0) {
|
||||
const first = engines[0];
|
||||
setEngineId(first.id);
|
||||
setVideoAspectRatio(first.supportedRatios?.[0] || '16:9');
|
||||
setVideoResolution(first.supportedResolutions?.[0] || '480p');
|
||||
setVideoDuration(first.supportedDurations?.[0] || 5);
|
||||
}
|
||||
}).catch(() => message.error('视频引擎加载失败'));
|
||||
calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([]));
|
||||
}, []);
|
||||
|
||||
const createIdempotencyRef = useRef<{ segmentId: string; key: string } | null>(null);
|
||||
|
||||
const selectedEngine = videoEngines.find((item: any) => item.id === engineId);
|
||||
const selectedEngineSupportsImage = !productImage || supportsReferenceImage(selectedEngine);
|
||||
const estimatedCredits = useMemo(() => {
|
||||
const rule = creditRules.find((item: any) => item.modelConfigId === engineId && item.genType === 'video' && item.resolution === videoResolution);
|
||||
if (!rule) return 0;
|
||||
let total = (Number(rule.baseCredits || 0) + Number(rule.perSecondCredits || 0) * videoDuration) * Number(rule.ratio || 1);
|
||||
if (productImage) {
|
||||
total += (Number(rule.inputImageBaseCredits || 0) + Number(rule.inputImagePerImageCredits || 0)) * Number(rule.inputImageRatio || 1);
|
||||
}
|
||||
return Number(total.toFixed(2));
|
||||
}, [creditRules, engineId, productImage, videoDuration, videoResolution]);
|
||||
const creditsInsufficient = Number(user?.credits || 0) < estimatedCredits;
|
||||
|
||||
const handleEngineChange = (value: string) => {
|
||||
setEngineId(value);
|
||||
const engine = videoEngines.find((item: any) => item.id === value);
|
||||
if (!engine) return;
|
||||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||||
setVideoResolution(engine.supportedResolutions?.[0] || '480p');
|
||||
setVideoDuration(engine.supportedDurations?.[0] || 5);
|
||||
};
|
||||
|
||||
const autoSplitButtonText = useMemo(() => {
|
||||
if (tableData.length === 0) {
|
||||
return 'AI自动拆分';
|
||||
@@ -292,13 +347,6 @@ function RemoveInfo() {
|
||||
setProductImageResourceId('');
|
||||
};
|
||||
|
||||
const handleProductImageChange: any = (info: any) => {
|
||||
if (info.fileList.length === 0) {
|
||||
setProductImage('');
|
||||
setProductImageResourceId('');
|
||||
}
|
||||
};
|
||||
|
||||
const beforeUploadProductImage = async (file: File) => {
|
||||
try {
|
||||
const uploadResult = await uploadShotReplicateImage(file);
|
||||
@@ -316,44 +364,53 @@ function RemoveInfo() {
|
||||
message.warning('请先选择拆镜片段');
|
||||
return;
|
||||
}
|
||||
if (!productImage) {
|
||||
message.warning('请上传产品图');
|
||||
if (!engineId) {
|
||||
message.warning('请选择视频引擎');
|
||||
return;
|
||||
}
|
||||
if (!productName.trim()) {
|
||||
message.warning('请输入产品名称');
|
||||
if (!selectedEngineSupportsImage) {
|
||||
message.warning('当前视频引擎不支持参考图片,请移除素材图片或切换引擎');
|
||||
return;
|
||||
}
|
||||
if (!productSellingPoint.trim()) {
|
||||
message.warning('请输入产品卖点');
|
||||
if (creditsInsufficient) {
|
||||
message.warning('积分不足,请充值后再创建复刻项目');
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIdempotency = createIdempotencyRef.current;
|
||||
const idempotencyKey = existingIdempotency?.segmentId === currentSegment
|
||||
? existingIdempotency.key
|
||||
: `shot_v2_${currentSegment}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
createIdempotencyRef.current = { segmentId: currentSegment, key: idempotencyKey };
|
||||
|
||||
const params = {
|
||||
|
||||
target_project_name: productName.trim(),
|
||||
core_content_point: productSellingPoint.trim(),
|
||||
material_image_url: productImage,
|
||||
project_description: productSellingPoint.trim() || undefined,
|
||||
material_image_url: productImage || undefined,
|
||||
material_image_resource_id: productImageResourceId || undefined,
|
||||
idempotency_key: `replication_${Date.now()}`,
|
||||
video_config: {
|
||||
engine_id: engineId,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
},
|
||||
idempotency_key: idempotencyKey,
|
||||
};
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await removeCreate(currentSegment, params);
|
||||
message.success('视频生成任务创建成功');
|
||||
const result = await removeCreate(currentSegment, params);
|
||||
const projectId = result?.projectId || result?.id || result?.detail?.id;
|
||||
createIdempotencyRef.current = null;
|
||||
message.success('项目已创建,视频提词正在生成');
|
||||
handleCloseDrawer();
|
||||
Removelist(creatID).then((res: any) => {
|
||||
|
||||
const targetItem = res.items.find((item: any) => item.id === currentSegment);
|
||||
message.loading('创建中...', 3);
|
||||
|
||||
setTimeout(() => {
|
||||
navigate(`/removelens/${targetItem.moduleProjectId}/removefenbu`);
|
||||
}, 3000);
|
||||
});
|
||||
await fetchSegments();
|
||||
if (projectId) {
|
||||
navigate(`/removelens/${projectId}/removefenbu?flow_version=v2`);
|
||||
}
|
||||
// fetchSegments().then((res: any) => {
|
||||
// console.log('123123123123',res);
|
||||
|
||||
@@ -677,7 +734,7 @@ function RemoveInfo() {
|
||||
{record.moduleProjectId ? (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => navigate(`/removelens/${record.moduleProjectId}/removefenbu`)}
|
||||
onClick={() => navigate(`/removelens/${record.moduleProjectId}/removefenbu?flow_version=${record.moduleProjectFlowVersion === 'v2' ? 'v2' : 'v1'}`)}
|
||||
style={{ color: '#10b981', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
详情任务
|
||||
@@ -1130,65 +1187,47 @@ function RemoveInfo() {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div style={{ background: '#fff', borderRadius: 12, padding: 16, border: '1px solid rgba(99, 102, 241, 0.1)' }}>
|
||||
<label style={{ fontWeight: 500, color: '#334155', marginBottom: 8, display: 'block', fontSize: 14 }}>
|
||||
产品白底图 <span style={{ color: '#ef4444' }}>*</span>
|
||||
素材图片(可选)
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{ width: 120, height: 120, borderRadius: 8, border: '2px dashed rgba(99, 102, 241, 0.3)', background: 'rgba(99, 102, 241, 0.02)', position: 'relative', overflow: 'hidden' }}>
|
||||
{productImage ? (
|
||||
<>
|
||||
<img
|
||||
src={buildAssetUrl(productImage)}
|
||||
alt="产品图"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setProductImage('');
|
||||
setProductImageResourceId('');
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined style={{ color: '#fff', fontSize: 12 }} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<Upload
|
||||
beforeUpload={beforeUploadProductImage}
|
||||
maxCount={1}
|
||||
accept="image/*"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, width: '100%', height: '100%' }}>
|
||||
<PlusOutlined style={{ fontSize: 20, color: '#6366f1' }} />
|
||||
<span style={{ fontSize: 12, color: '#64748b' }}>上传产品图</span>
|
||||
</div>
|
||||
</Upload>
|
||||
)}
|
||||
{productImage ? (
|
||||
<div style={{ width: 120, height: 120, borderRadius: 8, position: 'relative', overflow: 'hidden' }}>
|
||||
<img src={buildAssetUrl(productImage)} alt="素材图" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
<button onClick={() => { setProductImage(''); setProductImageResourceId(''); }} style={{ position: 'absolute', bottom: 4, right: 4, width: 24, height: 24, borderRadius: '50%', backgroundColor: 'rgba(0,0,0,.5)', border: 'none', cursor: 'pointer' }}>
|
||||
<DeleteOutlined style={{ color: '#fff', fontSize: 12 }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<UploadSelector
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
mediaType="image"
|
||||
maxImageCount={1}
|
||||
usedImageCount={0}
|
||||
onLocalSelect={(files) => { if (files[0]) void beforeUploadProductImage(files[0]); }}
|
||||
onHistorySelect={(items) => {
|
||||
const item = items[0];
|
||||
if (!item) return;
|
||||
setProductImage(item.resourceUrl || item.previewUrl || item.displayUrl || '');
|
||||
setProductImageResourceId(item.id);
|
||||
message.success('历史图片已选择');
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 120, height: 120, borderRadius: 8, border: '2px dashed rgba(99,102,241,.3)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', gap: 6 }}>
|
||||
<PlusOutlined style={{ fontSize: 20, color: '#6366f1' }} />
|
||||
<span style={{ fontSize: 12, color: '#64748b' }}>上传或选择历史图片</span>
|
||||
</div>
|
||||
</UploadSelector>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', borderRadius: 12, padding: 16, border: '1px solid rgba(99, 102, 241, 0.1)' }}>
|
||||
<label style={{ fontWeight: 500, color: '#334155', marginBottom: 8, display: 'block', fontSize: 14 }}>
|
||||
产品名称 <span style={{ color: '#ef4444' }}>*</span>
|
||||
项目名称(可选)
|
||||
</label>
|
||||
<Input
|
||||
value={productName}
|
||||
onChange={(e) => setProductName(e.target.value)}
|
||||
placeholder="请输入产品名称"
|
||||
placeholder="请输入项目名称"
|
||||
style={{ height: 44, borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
||||
maxLength={10}
|
||||
showCount
|
||||
@@ -1197,12 +1236,12 @@ function RemoveInfo() {
|
||||
|
||||
<div style={{ background: '#fff', borderRadius: 12, padding: 16, border: '1px solid rgba(99, 102, 241, 0.1)' }}>
|
||||
<label style={{ fontWeight: 500, color: '#334155', marginBottom: 8, display: 'block', fontSize: 14 }}>
|
||||
产品卖点 <span style={{ color: '#ef4444' }}>*</span>
|
||||
项目描述(可选)
|
||||
</label>
|
||||
<TextArea
|
||||
value={productSellingPoint}
|
||||
onChange={(e) => setProductSellingPoint(e.target.value)}
|
||||
placeholder="请输入产品卖点"
|
||||
placeholder="请输入项目描述"
|
||||
style={{ borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
||||
maxLength={100}
|
||||
showCount
|
||||
@@ -1210,12 +1249,37 @@ function RemoveInfo() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', borderRadius: 12, padding: 16, border: '1px solid rgba(99, 102, 241, 0.1)' }}>
|
||||
<label style={{ fontWeight: 500, color: '#334155', marginBottom: 8, display: 'block', fontSize: 14 }}>视频参数</label>
|
||||
<Select
|
||||
value={engineId || undefined}
|
||||
onChange={handleEngineChange}
|
||||
placeholder="选择视频引擎"
|
||||
style={{ width: '100%', marginBottom: 12 }}
|
||||
options={videoEngines.map((item: any) => ({
|
||||
value: item.id,
|
||||
label: productImage && !supportsReferenceImage(item) ? `${item.name}(不支持参考图)` : item.name,
|
||||
disabled: Boolean(productImage) && !supportsReferenceImage(item),
|
||||
}))}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
<Select value={videoDuration} onChange={setVideoDuration} style={{ flex: 1 }} options={(selectedEngine?.supportedDurations || [5]).map((item: number) => ({ value: item, label: `${item}秒` }))} />
|
||||
<Select value={videoAspectRatio} onChange={setVideoAspectRatio} style={{ flex: 1 }} options={(selectedEngine?.supportedRatios || ['16:9']).map((item: string) => ({ value: item, label: item }))} />
|
||||
<Select value={videoResolution} onChange={setVideoResolution} style={{ flex: 1 }} options={(selectedEngine?.supportedResolutions || ['480p']).map((item: string) => ({ value: item, label: item }))} />
|
||||
</div>
|
||||
<div style={{ color: creditsInsufficient || !selectedEngineSupportsImage ? '#ef4444' : '#6366f1', fontSize: 13 }}>
|
||||
{selectedEngineSupportsImage
|
||||
? `预估积分:${estimatedCredits},当前积分:${Number(user?.credits || 0).toFixed(2)}`
|
||||
: '当前引擎不支持参考图片,请移除素材图片或切换引擎。'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleManualGenerate}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
disabled={loading || creditsInsufficient || !engineId || !selectedEngineSupportsImage}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 48,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip } from 'antd';
|
||||
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { getShotReplicationList, removeDetail, removeone, removetwo, removethree, removefour, getEngine, updateShotImagePrompt, updateShotVideoPromptSchema, calculateCredits } from '../api/index';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { getShotReplicationList, removeDetail, removeone, removetwo, removethree, removefour, getEngine, updateShotImagePrompt, updateShotVideoPromptSchema, calculateCredits, retryShotVideoPromptV2, updateShotVideoPromptSchemaV2, generateShotVideoV2 } from '../api/index';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
import VideoPromptSchemaEditor from '../components/VideoPromptSchemaEditor';
|
||||
import { validateVideoPromptSchemaByConfig } from '../utils/videoPromptSchema';
|
||||
@@ -25,6 +25,8 @@ const buildMediaUrl = (url: string): string => {
|
||||
function InitialInfo() {
|
||||
const navigate = useNavigate();
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
@@ -35,9 +37,12 @@ function InitialInfo() {
|
||||
const [currentType, setCurrentType] = useState<string>('image');
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [videoSchemaConfigSnapshot, setVideoSchemaConfigSnapshot] = useState<any>(null);
|
||||
const [pollingTimer, setPollingTimer] = useState<any>(null);
|
||||
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [editingPromptStepId, setEditingPromptStepId] = useState('');
|
||||
const [promptSaving, setPromptSaving] = useState(false);
|
||||
const [retryPromptModalVisible, setRetryPromptModalVisible] = useState(false);
|
||||
const [retryPromptStepId, setRetryPromptStepId] = useState<string>('');
|
||||
const [retryPromptSubmitting, setRetryPromptSubmitting] = useState(false);
|
||||
|
||||
// 引擎和视频参数相关状态
|
||||
const [enginesele, setEnginesele] = useState<any>({});
|
||||
@@ -111,14 +116,21 @@ function InitialInfo() {
|
||||
}
|
||||
}, [previewVisible, previewType]);
|
||||
|
||||
//
|
||||
const baseSteps = [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
const isV2 = flowVersion === 'v2';
|
||||
|
||||
const baseSteps = isV2
|
||||
? [
|
||||
{ id: 1, title: '素材与项目信息', description: '固定片段素材和项目描述', childId: 1 },
|
||||
{ id: 2, title: '生成视频提示词', description: '生成并确认视频提示词', childId: 4 },
|
||||
{ id: 3, title: '生成最终视频', description: '使用当前视频提示词生成视频', childId: 5 },
|
||||
]
|
||||
: [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
|
||||
// 合并基础步骤和API返回的状态
|
||||
const steps = baseSteps.map((step, index) => ({
|
||||
@@ -223,12 +235,12 @@ function InitialInfo() {
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
setPollingTimer(null);
|
||||
if (pollingTimerRef.current) {
|
||||
clearInterval(pollingTimerRef.current);
|
||||
pollingTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [pollingTimer]);
|
||||
}, []);
|
||||
|
||||
// 点击外部关闭弹窗
|
||||
useEffect(() => {
|
||||
@@ -250,39 +262,30 @@ function InitialInfo() {
|
||||
|
||||
|
||||
if (creatID) {
|
||||
removeDetail(creatID).then((res: any) => {
|
||||
removeDetail(creatID, flowVersion).then((res: any) => {
|
||||
setTaskDetail(res);
|
||||
if (res.steps) {
|
||||
setApiSteps(res.steps);
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
message.error(error?.message || '加载任务详情失败');
|
||||
});
|
||||
}
|
||||
}, [creatID]);
|
||||
}, [creatID, flowVersion]);
|
||||
|
||||
// 监听合并后的 steps 数据,判断第五步状态并启动/停止轮询
|
||||
// 仅在项目或任一步骤处于 processing 时轮询;waiting_user 必须停止。
|
||||
useEffect(() => {
|
||||
const shouldPoll = taskDetail?.status === 'processing'
|
||||
|| apiSteps.some((step: any) => step?.status === 'processing');
|
||||
|
||||
// 检查第五步的状态(索引 4)
|
||||
const fifthStep = steps[4];
|
||||
|
||||
if (fifthStep && fifthStep.status !== 'completed' && fifthStep.status !== 'failed') {
|
||||
// 第五步未完成,启动轮询
|
||||
if (!pollingTimer) {
|
||||
const timer = setInterval(pollTaskDetail, 30000);
|
||||
setPollingTimer(timer);
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
// 第五步已完成或失败,停止轮询
|
||||
if (fifthStep) {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
setPollingTimer(null);
|
||||
}
|
||||
}
|
||||
if (shouldPoll && !pollingTimerRef.current) {
|
||||
pollingTimerRef.current = setInterval(pollTaskDetail, 30000);
|
||||
} else if (!shouldPoll && pollingTimerRef.current) {
|
||||
clearInterval(pollingTimerRef.current);
|
||||
pollingTimerRef.current = null;
|
||||
}
|
||||
}, [steps]);
|
||||
}, [taskDetail?.status, apiSteps, creatID, flowVersion]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
calculateCredits().then((data: any) => {
|
||||
@@ -320,7 +323,9 @@ function InitialInfo() {
|
||||
total += inputVideoCost;
|
||||
}
|
||||
|
||||
const inputImageCount = taskDetail?.videoGeneration?.inputMedia?.image?.length || 0;
|
||||
const inputImageCount = isV2
|
||||
? (taskDetail?.material?.materialImageUrl ? 1 : 0)
|
||||
: (taskDetail?.videoGeneration?.inputMedia?.image?.length || 0);
|
||||
if (inputImageCount > 0) {
|
||||
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
|
||||
total += inputImageCost;
|
||||
@@ -399,7 +404,8 @@ function InitialInfo() {
|
||||
|
||||
setPromptSaving(true);
|
||||
try {
|
||||
await updateShotVideoPromptSchema(taskDetail.id, editingPromptStepId, {
|
||||
const updateVideoPrompt = isV2 ? updateShotVideoPromptSchemaV2 : updateShotVideoPromptSchema;
|
||||
await updateVideoPrompt(taskDetail.id, editingPromptStepId, {
|
||||
prompt_schema: formData,
|
||||
});
|
||||
message.success('视频提示词已保存');
|
||||
@@ -419,12 +425,13 @@ function InitialInfo() {
|
||||
return;
|
||||
}
|
||||
|
||||
removeDetail(creatID).then((res: any) => {
|
||||
removeDetail(creatID, flowVersion).then((res: any) => {
|
||||
setTaskDetail(res);
|
||||
if (res.steps) {
|
||||
setApiSteps(res.steps);
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
console.error('轮询任务详情失败', error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -432,12 +439,13 @@ function InitialInfo() {
|
||||
const refreshTaskDetail = () => {
|
||||
if (!creatID) return;
|
||||
|
||||
removeDetail(creatID).then((res: any) => {
|
||||
removeDetail(creatID, flowVersion).then((res: any) => {
|
||||
setTaskDetail(res);
|
||||
if (res.steps) {
|
||||
setApiSteps(res.steps);
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
message.error(error?.message || '刷新任务详情失败');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -540,34 +548,109 @@ function InitialInfo() {
|
||||
// 这里可以添加下一步的逻辑,比如调用接口等
|
||||
};
|
||||
|
||||
const createvideo = (stepId: number, engineId: string) => {
|
||||
let params = {
|
||||
engine_id: '',
|
||||
}
|
||||
|
||||
|
||||
removefour(taskDetail.id, stepId.toString(), params).then((res: any) => {
|
||||
// 重新获取任务详情以更新数据
|
||||
const createvideo = async (stepId: number, engineId: string) => {
|
||||
try {
|
||||
if (isV2) {
|
||||
await generateShotVideoV2(taskDetail.id, String(stepId));
|
||||
} else {
|
||||
await removefour(taskDetail.id, String(stepId), { engine_id: engineId || '' });
|
||||
}
|
||||
message.info('正在生成视频,请稍候...');
|
||||
refreshTaskDetail();
|
||||
}).catch((error: any) => {
|
||||
const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败';
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败';
|
||||
message.error(errorMsg);
|
||||
|
||||
});
|
||||
}
|
||||
const agincreatevideo = () => {
|
||||
let params = {
|
||||
engine_id: '',
|
||||
}
|
||||
};
|
||||
|
||||
const agincreatevideo = async () => {
|
||||
const promptStep = isV2 ? steps[1] : steps[3];
|
||||
if (!promptStep?.id) return;
|
||||
await createvideo(promptStep.id, promptStep.engineId || '');
|
||||
};
|
||||
|
||||
removefour(taskDetail.id, steps[3].id.toString(), params).then((res: any) => {
|
||||
// 重新获取任务详情以更新数据
|
||||
const applyRetryVideoEngine = (
|
||||
engineId: string,
|
||||
preferred?: { duration?: number; aspectRatio?: string; resolution?: string },
|
||||
) => {
|
||||
const engine = (enginesele.video || []).find((item: any) => String(item.id) === String(engineId));
|
||||
if (!engine) {
|
||||
return false;
|
||||
}
|
||||
const ratios = Array.isArray(engine.supportedRatios) && engine.supportedRatios.length > 0
|
||||
? engine.supportedRatios.map((item: any) => String(item))
|
||||
: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'];
|
||||
const resolutions = Array.isArray(engine.supportedResolutions) && engine.supportedResolutions.length > 0
|
||||
? engine.supportedResolutions.map((item: any) => String(item))
|
||||
: ['480p', '720p', '1080p'];
|
||||
const parsedDurations = Array.isArray(engine.supportedDurations)
|
||||
? engine.supportedDurations
|
||||
.map((item: any) => Number(item))
|
||||
.filter((item: number) => Number.isFinite(item) && item > 0)
|
||||
: [];
|
||||
const durations = parsedDurations.length > 0 ? parsedDurations : [5, 8, 10, 12, 15];
|
||||
|
||||
const preferredDuration = Number(preferred?.duration ?? videoDuration);
|
||||
const preferredRatio = String(preferred?.aspectRatio || videoAspectRatio || '');
|
||||
const preferredResolution = String(preferred?.resolution || videoResolution || '');
|
||||
|
||||
setCountType(String(engine.id));
|
||||
setEngineOptions({ ratios, resolutions, durations });
|
||||
setVideoDuration(durations.includes(preferredDuration) ? preferredDuration : durations[0]);
|
||||
setVideoAspectRatio(ratios.includes(preferredRatio) ? preferredRatio : ratios[0]);
|
||||
setVideoResolution(resolutions.includes(preferredResolution) ? preferredResolution : resolutions[0]);
|
||||
return true;
|
||||
};
|
||||
|
||||
const openRegenerateVideoPrompt = (stepId: number) => {
|
||||
if (!isV2 || !taskDetail?.id) return;
|
||||
const currentConfig = taskDetail?.videoGeneration?.promptParams || taskDetail?.videoGeneration?.params || {};
|
||||
const requestedEngineId = String(
|
||||
currentConfig.engineId
|
||||
|| currentConfig.engine_id
|
||||
|| taskDetail?.videoGeneration?.engineId
|
||||
|| countType
|
||||
|| '',
|
||||
);
|
||||
const currentEngineId = String(
|
||||
(enginesele.video || []).some((engine: any) => String(engine.id) === requestedEngineId)
|
||||
? requestedEngineId
|
||||
: enginesele.video?.[0]?.id || '',
|
||||
);
|
||||
if (!currentEngineId || !applyRetryVideoEngine(currentEngineId, {
|
||||
duration: Number(currentConfig.duration || videoDuration),
|
||||
aspectRatio: String(currentConfig.aspectRatio || currentConfig.aspect_ratio || videoAspectRatio),
|
||||
resolution: String(currentConfig.resolution || videoResolution),
|
||||
})) {
|
||||
message.warning('当前没有可用的视频生成引擎');
|
||||
return;
|
||||
}
|
||||
setRetryPromptStepId(String(stepId));
|
||||
setRetryPromptModalVisible(true);
|
||||
};
|
||||
|
||||
const submitRegenerateVideoPrompt = async () => {
|
||||
if (!isV2 || !taskDetail?.id || !retryPromptStepId || !countType) return;
|
||||
setRetryPromptSubmitting(true);
|
||||
try {
|
||||
await retryShotVideoPromptV2(taskDetail.id, retryPromptStepId, {
|
||||
video_config: {
|
||||
engine_id: countType,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
},
|
||||
});
|
||||
message.info('正在按新视频参数重新生成视频提示词,请稍候...');
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
refreshTaskDetail();
|
||||
}).catch((error: any) => {
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '重新生成视频提示词失败');
|
||||
} finally {
|
||||
setRetryPromptSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1298,6 +1381,11 @@ function InitialInfo() {
|
||||
>
|
||||
查看/修改视频提词
|
||||
</Button>
|
||||
{isV2 && (
|
||||
<Button type="default" onClick={() => openRegenerateVideoPrompt(step.id)} style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }} disabled={step.status === 'processing' || steps[2]?.status === 'processing'}>
|
||||
重新生成视频提词
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => { createvideo(step.id, step.engineId); }} type="primary" style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }} disabled={step.status !== 'completed'}>
|
||||
下一步:生成视频
|
||||
</Button>
|
||||
@@ -1322,8 +1410,8 @@ function InitialInfo() {
|
||||
)}
|
||||
</div>
|
||||
<Space style={{ width: '100%', gap: 12 }}>
|
||||
<Button onClick={() => agincreatevideo()} type="default" icon={<EditOutlined />} style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }} disabled={step.status !== 'completed'}>
|
||||
重新生成
|
||||
<Button onClick={() => agincreatevideo()} type="default" icon={<EditOutlined />} style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }} disabled={isV2 ? !['completed', 'failed'].includes(step.status) : step.status !== 'completed'}>
|
||||
{step.status === 'failed' ? '重试生成' : '重新生成'}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -1353,6 +1441,89 @@ function InitialInfo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
title="重新生成视频提词"
|
||||
open={retryPromptModalVisible}
|
||||
onCancel={() => {
|
||||
if (!retryPromptSubmitting) {
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
}
|
||||
}}
|
||||
onOk={submitRegenerateVideoPrompt}
|
||||
confirmLoading={retryPromptSubmitting}
|
||||
okText="按新参数生成提词"
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频引擎</Text>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }}>
|
||||
{(enginesele.video || []).map((engine: any) => (
|
||||
<button
|
||||
key={engine.id}
|
||||
type="button"
|
||||
onClick={() => applyRetryVideoEngine(String(engine.id), {
|
||||
duration: videoDuration,
|
||||
aspectRatio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
})}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: countType === String(engine.id) ? '2px solid #6366f1' : '1px solid #e5e7eb',
|
||||
background: countType === String(engine.id) ? 'rgba(99,102,241,0.08)' : '#fff',
|
||||
color: countType === String(engine.id) ? '#4f46e5' : '#374151',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontWeight: countType === String(engine.id) ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{engine.name || engine.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频时长</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.durations.map((duration) => (
|
||||
<Button key={duration} type={videoDuration === duration ? 'primary' : 'default'} onClick={() => setVideoDuration(duration)}>
|
||||
{duration} 秒
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>画面比例</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.ratios.map((ratio) => (
|
||||
<Button key={ratio} type={videoAspectRatio === ratio ? 'primary' : 'default'} onClick={() => setVideoAspectRatio(ratio)}>
|
||||
{ratio}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>分辨率</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.resolutions.map((resolution) => (
|
||||
<Button key={resolution} type={videoResolution === resolution ? 'primary' : 'default'} onClick={() => setVideoResolution(resolution)}>
|
||||
{resolution}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '10px 12px', borderRadius: 8, background: '#f8fafc', color: '#64748b' }}>
|
||||
当前参数:{enginesele.video?.find((engine: any) => String(engine.id) === countType)?.name || countType}
|
||||
{' · '}{videoDuration} 秒 · {videoAspectRatio} · {videoResolution}
|
||||
<span style={{ marginLeft: 12 }}>最终视频预估积分:{estimatedCredits}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
|
||||
@@ -169,7 +169,10 @@ export interface GenerationRecord {
|
||||
imageProportion: string;
|
||||
imagePx: string;
|
||||
imageUrl: string;
|
||||
|
||||
engineId?: string;
|
||||
engineName?: string;
|
||||
engineSnapshot?: Record<string, any>;
|
||||
includeMediaReferences?: boolean;
|
||||
}
|
||||
|
||||
export interface OptimizeParams {
|
||||
@@ -189,8 +192,31 @@ export interface OptimizeParams {
|
||||
}
|
||||
|
||||
export interface GenerateParams {
|
||||
engineId?: string;
|
||||
includeMediaReferences?: boolean;
|
||||
aspectRatio?: AspectRatio;
|
||||
resolution?: Resolution;
|
||||
imageSize?: string;
|
||||
}
|
||||
|
||||
export type ModuleGenerationFlowVersion = 'v1' | 'v2';
|
||||
|
||||
export interface ModuleGenerationVideoConfigV2 {
|
||||
engineId: string;
|
||||
duration: number;
|
||||
aspectRatio: string;
|
||||
resolution: string;
|
||||
}
|
||||
|
||||
export interface ModuleGenerationProjectV2Create {
|
||||
materialImageUrl?: string;
|
||||
materialImageResourceId?: string;
|
||||
targetProjectName?: string;
|
||||
coreContentPoint?: string;
|
||||
projectDescription?: string;
|
||||
targetPlatform?: string;
|
||||
videoConfig: ModuleGenerationVideoConfigV2;
|
||||
idempotencyKey?: string;
|
||||
}
|
||||
|
||||
export interface OptimizeResult {
|
||||
|
||||
Reference in New Issue
Block a user