Sync latest TrueGrowth updates
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled

This commit is contained in:
jiam
2026-07-08 02:03:18 +08:00
parent 52636c91ae
commit 5119ac0ef8
102 changed files with 20985 additions and 1760 deletions

View File

@@ -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>