Sync latest TrueGrowth updates
This commit is contained in:
@@ -25,8 +25,8 @@ import {
|
||||
ImagePlus,
|
||||
MoreHorizontal,
|
||||
Music2,
|
||||
PlayCircle,
|
||||
RefreshCcw,
|
||||
Scissors,
|
||||
Send,
|
||||
Trash2,
|
||||
Video as VideoIcon,
|
||||
@@ -62,6 +62,9 @@ import './dialog-task-loading-final.scss';
|
||||
|
||||
const TRUEGROWTH_PUBLISH_HANDOFF_EVENT = 'truegrowth:publish-asset';
|
||||
const TRUEGROWTH_PUBLISH_HANDOFF_STORAGE_KEY = 'truegrowth-publish-handoff-v1';
|
||||
const TRUEGROWTH_TIMELINE_HANDOFF_EVENT = 'truegrowth:timeline-asset';
|
||||
const TRUEGROWTH_TIMELINE_HANDOFF_STORAGE_KEY =
|
||||
'truegrowth-timeline-handoff-v1';
|
||||
const FALLBACK_TASK_FAILURE_REASON = '生成失败,请检查模型服务或参数配置。';
|
||||
const TECHNICAL_FAILURE_PATTERNS = [
|
||||
/\b(seedance|volcengine|comfyui|api|endpoint|fetch|http|https|json|request|response|submit|xhr|network|timeout|exception|stack|trace)\b/i,
|
||||
@@ -456,70 +459,72 @@ export const DialogTaskList: React.FC<DialogTaskListProps> = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const items: InsertionItem[] = insertableTasks.flatMap<InsertionItem>((task) => {
|
||||
if (task.type === TaskType.IMAGE) {
|
||||
const urls = task.result!.urls?.length
|
||||
? task.result!.urls
|
||||
: [task.result!.url];
|
||||
return urls.map((url) => ({
|
||||
type: 'image' as const,
|
||||
content: normalizeImageDataUrl(url),
|
||||
groupId: `dialog-batch-image-${task.id}`,
|
||||
}));
|
||||
}
|
||||
if (task.type === TaskType.VIDEO && task.result?.url) {
|
||||
return [
|
||||
{
|
||||
type: 'video' as const,
|
||||
content: task.result.url,
|
||||
groupId: `dialog-batch-video-${task.id}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (task.type === TaskType.AUDIO) {
|
||||
const urls = resolveAudioResultUrls(task);
|
||||
return urls.map((url, index) => {
|
||||
const clip = task.result?.clips?.[index];
|
||||
return {
|
||||
type: 'audio' as const,
|
||||
content: url,
|
||||
groupId: `dialog-batch-audio-${task.id}`,
|
||||
metadata: {
|
||||
title:
|
||||
clip?.title ||
|
||||
task.result?.title ||
|
||||
task.params.title ||
|
||||
task.params.prompt,
|
||||
duration:
|
||||
typeof clip?.duration === 'number'
|
||||
? clip.duration || undefined
|
||||
: task.result?.duration,
|
||||
previewImageUrl:
|
||||
clip?.imageLargeUrl ||
|
||||
clip?.imageUrl ||
|
||||
task.result?.previewImageUrl,
|
||||
tags:
|
||||
typeof task.params.tags === 'string'
|
||||
? task.params.tags
|
||||
: undefined,
|
||||
mv:
|
||||
typeof task.params.mv === 'string'
|
||||
? task.params.mv
|
||||
: undefined,
|
||||
prompt: task.params.prompt,
|
||||
providerTaskId: task.result?.providerTaskId || task.remoteId,
|
||||
clipId:
|
||||
clip?.clipId ||
|
||||
clip?.id ||
|
||||
task.result?.clipIds?.[index] ||
|
||||
task.result?.primaryClipId,
|
||||
clipIds: task.result?.clipIds,
|
||||
const items: InsertionItem[] = insertableTasks.flatMap<InsertionItem>(
|
||||
(task) => {
|
||||
if (task.type === TaskType.IMAGE) {
|
||||
const urls = task.result!.urls?.length
|
||||
? task.result!.urls
|
||||
: [task.result!.url];
|
||||
return urls.map((url) => ({
|
||||
type: 'image' as const,
|
||||
content: normalizeImageDataUrl(url),
|
||||
groupId: `dialog-batch-image-${task.id}`,
|
||||
}));
|
||||
}
|
||||
if (task.type === TaskType.VIDEO && task.result?.url) {
|
||||
return [
|
||||
{
|
||||
type: 'video' as const,
|
||||
content: task.result.url,
|
||||
groupId: `dialog-batch-video-${task.id}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
];
|
||||
}
|
||||
if (task.type === TaskType.AUDIO) {
|
||||
const urls = resolveAudioResultUrls(task);
|
||||
return urls.map((url, index) => {
|
||||
const clip = task.result?.clips?.[index];
|
||||
return {
|
||||
type: 'audio' as const,
|
||||
content: url,
|
||||
groupId: `dialog-batch-audio-${task.id}`,
|
||||
metadata: {
|
||||
title:
|
||||
clip?.title ||
|
||||
task.result?.title ||
|
||||
task.params.title ||
|
||||
task.params.prompt,
|
||||
duration:
|
||||
typeof clip?.duration === 'number'
|
||||
? clip.duration || undefined
|
||||
: task.result?.duration,
|
||||
previewImageUrl:
|
||||
clip?.imageLargeUrl ||
|
||||
clip?.imageUrl ||
|
||||
task.result?.previewImageUrl,
|
||||
tags:
|
||||
typeof task.params.tags === 'string'
|
||||
? task.params.tags
|
||||
: undefined,
|
||||
mv:
|
||||
typeof task.params.mv === 'string'
|
||||
? task.params.mv
|
||||
: undefined,
|
||||
prompt: task.params.prompt,
|
||||
providerTaskId: task.result?.providerTaskId || task.remoteId,
|
||||
clipId:
|
||||
clip?.clipId ||
|
||||
clip?.id ||
|
||||
task.result?.clipIds?.[index] ||
|
||||
task.result?.primaryClipId,
|
||||
clipIds: task.result?.clipIds,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
);
|
||||
await executeCanvasInsertion({ items });
|
||||
MessagePlugin.success(`已插入 ${items.length} 个作品到画布`);
|
||||
} catch (error) {
|
||||
@@ -615,6 +620,79 @@ export const DialogTaskList: React.FC<DialogTaskListProps> = ({
|
||||
MessagePlugin.success(`已带入 ${assets.length} 个作品到发布中心`);
|
||||
};
|
||||
|
||||
const handleClip = async (taskId: string) => {
|
||||
const task = await resolveTask(taskId);
|
||||
if (
|
||||
!task ||
|
||||
task.type !== TaskType.VIDEO ||
|
||||
task.status !== TaskStatus.COMPLETED ||
|
||||
!task.result
|
||||
) {
|
||||
MessagePlugin.warning('当前视频还不能剪辑');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = task.result.urls?.[0] || task.result.url;
|
||||
if (!url) {
|
||||
MessagePlugin.warning('当前视频没有可剪辑的视频源');
|
||||
return;
|
||||
}
|
||||
|
||||
const title = getFeedTitle(task);
|
||||
const detail = {
|
||||
asset: {
|
||||
id: `task-${task.id}`,
|
||||
assetId: `task-${task.id}`,
|
||||
type: 'video',
|
||||
name: title,
|
||||
source: 'ai-video',
|
||||
provider: task.params.model || 'TrueGrowth',
|
||||
url,
|
||||
thumbnailUrl:
|
||||
task.result.thumbnailUrls?.[0] ||
|
||||
task.result.previewImageUrl ||
|
||||
undefined,
|
||||
durationMs:
|
||||
typeof task.result.duration === 'number'
|
||||
? task.result.duration * 1000
|
||||
: undefined,
|
||||
width: task.result.width,
|
||||
height: task.result.height,
|
||||
createdAt: task.completedAt || task.createdAt || Date.now(),
|
||||
publishState: 'unpublished',
|
||||
sourceRefs: [
|
||||
{
|
||||
type: 'task',
|
||||
id: task.id,
|
||||
label: title,
|
||||
},
|
||||
],
|
||||
},
|
||||
route: '/timeline',
|
||||
intent: 'clip',
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
TRUEGROWTH_TIMELINE_HANDOFF_STORAGE_KEY,
|
||||
JSON.stringify(detail)
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[TrueGrowth] Failed to store timeline handoff:', error);
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(TRUEGROWTH_TIMELINE_HANDOFF_EVENT, { detail })
|
||||
);
|
||||
if (window.location.pathname !== '/timeline') {
|
||||
window.history.pushState(null, '', '/timeline');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}
|
||||
}
|
||||
MessagePlugin.success('已带入剪辑工作台,请选择剪辑能力');
|
||||
};
|
||||
|
||||
const confirmOverwriteDraftIfNeeded = useCallback(async () => {
|
||||
if (!hasAIImageDraftContent()) {
|
||||
return true;
|
||||
@@ -956,6 +1034,7 @@ export const DialogTaskList: React.FC<DialogTaskListProps> = ({
|
||||
onDelete={handleDelete}
|
||||
onDeleteBatch={handleDeleteBatch}
|
||||
onPublishBatch={handlePublishBatch}
|
||||
onClip={handleClip}
|
||||
onPreviewOpen={handlePreviewOpen}
|
||||
/>
|
||||
</React.Fragment>
|
||||
@@ -975,11 +1054,7 @@ export const DialogTaskList: React.FC<DialogTaskListProps> = ({
|
||||
</>
|
||||
) : (
|
||||
<div className="dialog-task-list__empty">
|
||||
{isLoading ? (
|
||||
<p>加载中...</p>
|
||||
) : (
|
||||
<p>{feedEmptyText}</p>
|
||||
)}
|
||||
{isLoading ? <p>加载中...</p> : <p>{feedEmptyText}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1321,9 +1396,11 @@ function getTaskMediaItems(task: Task): CreationBatchMedia[] {
|
||||
taskId: task.id,
|
||||
url: task.type === TaskType.IMAGE ? normalizeImageDataUrl(url) : url,
|
||||
thumbnailUrl:
|
||||
task.result?.thumbnailUrls?.[index] ||
|
||||
task.result?.previewImageUrl ||
|
||||
(task.type === TaskType.IMAGE ? normalizeImageDataUrl(url) : url),
|
||||
task.type === TaskType.VIDEO
|
||||
? task.result?.thumbnailUrls?.[index] || task.result?.previewImageUrl
|
||||
: task.result?.thumbnailUrls?.[index] ||
|
||||
task.result?.previewImageUrl ||
|
||||
normalizeImageDataUrl(url),
|
||||
duration:
|
||||
task.type === TaskType.AUDIO && typeof task.result?.duration === 'number'
|
||||
? task.result.duration
|
||||
@@ -1723,6 +1800,7 @@ function CreationBatchCard({
|
||||
onDelete,
|
||||
onDeleteBatch,
|
||||
onPublishBatch,
|
||||
onClip,
|
||||
onPreviewOpen,
|
||||
}: {
|
||||
batch: CreationBatch;
|
||||
@@ -1734,6 +1812,7 @@ function CreationBatchCard({
|
||||
onDelete?: (taskId: string) => void;
|
||||
onDeleteBatch?: (taskIds: string[]) => void;
|
||||
onPublishBatch?: (taskIds: string[]) => void;
|
||||
onClip?: (taskId: string) => void;
|
||||
onPreviewOpen?: (previewKey: string) => void;
|
||||
}) {
|
||||
const mediaItems = getBatchMediaItems(batch);
|
||||
@@ -1755,6 +1834,12 @@ function CreationBatchCard({
|
||||
(task.type === TaskType.IMAGE || task.type === TaskType.VIDEO) &&
|
||||
hasTaskResultMedia(task)
|
||||
);
|
||||
const firstCompletedVideoTask = batch.tasks.find(
|
||||
(task) =>
|
||||
task.type === TaskType.VIDEO &&
|
||||
task.status === TaskStatus.COMPLETED &&
|
||||
hasTaskResultMedia(task)
|
||||
);
|
||||
const hasFailedTask = batch.tasks.some(
|
||||
(task) => task.status === TaskStatus.FAILED
|
||||
);
|
||||
@@ -1830,16 +1915,22 @@ function CreationBatchCard({
|
||||
const placeholderStyle = !isCompleted
|
||||
? getTaskPlaceholderStyle(media.task)
|
||||
: undefined;
|
||||
const mediaStyle =
|
||||
isCompleted && isVideo
|
||||
? getTaskPlaceholderStyle(media.task)
|
||||
: placeholderStyle;
|
||||
const mediaTaskIds = [media.taskId];
|
||||
return (
|
||||
<div className="tg-creation-batch__item" key={media.id}>
|
||||
<div
|
||||
className={`tg-creation-batch__media${
|
||||
isVideo ? ' tg-creation-batch__media--video' : ''
|
||||
}${
|
||||
mediaFailureReason ? ' tg-creation-batch__media--failed' : ''
|
||||
}${
|
||||
isProcessing ? ' tg-creation-batch__media--processing' : ''
|
||||
}`}
|
||||
style={placeholderStyle}
|
||||
style={mediaStyle}
|
||||
>
|
||||
<button
|
||||
className="tg-creation-batch__media-preview"
|
||||
@@ -1863,11 +1954,15 @@ function CreationBatchCard({
|
||||
src={media.url!}
|
||||
poster={media.thumbnailUrl}
|
||||
alt={media.title}
|
||||
fit="contain"
|
||||
thumbnailSize="small"
|
||||
playOnHover={true}
|
||||
videoProps={{
|
||||
muted: true,
|
||||
loop: true,
|
||||
playsInline: true,
|
||||
preload: 'metadata',
|
||||
controls: false,
|
||||
}}
|
||||
/>
|
||||
) : isCompleted && isAudio ? (
|
||||
@@ -1943,9 +2038,9 @@ function CreationBatchCard({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{(isVideo || isAudio) && isCompleted ? (
|
||||
{isAudio && isCompleted ? (
|
||||
<span className="tg-creation-batch__play" aria-hidden="true">
|
||||
{isAudio ? <Music2 size={22} /> : <PlayCircle size={28} />}
|
||||
<Music2 size={22} />
|
||||
</span>
|
||||
) : null}
|
||||
{isCompleted ? (
|
||||
@@ -1963,6 +2058,21 @@ function CreationBatchCard({
|
||||
<Eye size={15} />
|
||||
</button>
|
||||
</HoverTip>
|
||||
{isVideo ? (
|
||||
<HoverTip content="去剪辑">
|
||||
<button
|
||||
className="tg-creation-batch__icon"
|
||||
type="button"
|
||||
aria-label="去剪辑"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onClip?.(media.taskId);
|
||||
}}
|
||||
>
|
||||
<Scissors size={15} />
|
||||
</button>
|
||||
</HoverTip>
|
||||
) : null}
|
||||
<HoverTip content="插入画布">
|
||||
<button
|
||||
className="tg-creation-batch__icon"
|
||||
@@ -2049,6 +2159,15 @@ function CreationBatchCard({
|
||||
<span>插入画布</span>
|
||||
</button>
|
||||
) : null}
|
||||
{firstCompletedVideoTask ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onClip?.(firstCompletedVideoTask.id)}
|
||||
>
|
||||
<Scissors size={15} />
|
||||
<span>去剪辑</span>
|
||||
</button>
|
||||
) : null}
|
||||
{hasPublishableMedia ? (
|
||||
<details className="tg-creation-batch__more">
|
||||
<summary>
|
||||
|
||||
Reference in New Issue
Block a user