Fix Seedance runtime model handling
This commit is contained in:
@@ -2748,9 +2748,7 @@ function PlayableVideo({
|
|||||||
autoPlay={autoPlay}
|
autoPlay={autoPlay}
|
||||||
playsInline
|
playsInline
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
style={
|
style={aspectRatio ? ({ aspectRatio } as CSSProperties) : undefined}
|
||||||
aspectRatio ? ({ aspectRatio } as CSSProperties) : undefined
|
|
||||||
}
|
|
||||||
onLoadedMetadata={(event) => {
|
onLoadedMetadata={(event) => {
|
||||||
const video = event.currentTarget;
|
const video = event.currentTarget;
|
||||||
if (video.videoWidth > 0 && video.videoHeight > 0) {
|
if (video.videoWidth > 0 && video.videoHeight > 0) {
|
||||||
@@ -9331,14 +9329,30 @@ function OpenTuGenerationPage({
|
|||||||
kind === 'image' ? settings.imageModel : settings.videoModel;
|
kind === 'image' ? settings.imageModel : settings.videoModel;
|
||||||
const selectableModels = useSelectableModels(kind);
|
const selectableModels = useSelectableModels(kind);
|
||||||
const [selectedModel, setSelectedModel] = useState(
|
const [selectedModel, setSelectedModel] = useState(
|
||||||
isConfiguredSettingsValue(initialModel) ? initialModel : undefined
|
|
||||||
);
|
|
||||||
const [selectedModelRef, setSelectedModelRef] = useState<ModelRef | null>(
|
|
||||||
() =>
|
() =>
|
||||||
|
resolveSelectableModelFallback(
|
||||||
|
selectableModels,
|
||||||
|
isConfiguredSettingsValue(initialModel) ? initialModel : null,
|
||||||
createModelRef(
|
createModelRef(
|
||||||
activeRoute?.profileId || null,
|
activeRoute?.profileId || null,
|
||||||
activeRoute?.modelId || initialModel || null
|
activeRoute?.modelId || initialModel || null
|
||||||
)
|
)
|
||||||
|
)?.modelId
|
||||||
|
);
|
||||||
|
const [selectedModelRef, setSelectedModelRef] = useState<ModelRef | null>(
|
||||||
|
() => {
|
||||||
|
const initialModelRef = createModelRef(
|
||||||
|
activeRoute?.profileId || null,
|
||||||
|
activeRoute?.modelId || initialModel || null
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
resolveSelectableModelFallback(
|
||||||
|
selectableModels,
|
||||||
|
isConfiguredSettingsValue(initialModel) ? initialModel : null,
|
||||||
|
initialModelRef
|
||||||
|
)?.modelRef || initialModelRef
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const [hasUserSelectedModel, setHasUserSelectedModel] = useState(false);
|
const [hasUserSelectedModel, setHasUserSelectedModel] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -10370,7 +10384,9 @@ function FunClipEditingPage({
|
|||||||
pendingHandoffNoticeRef.current !== detail.createdAt
|
pendingHandoffNoticeRef.current !== detail.createdAt
|
||||||
) {
|
) {
|
||||||
pendingHandoffNoticeRef.current = detail.createdAt || Date.now();
|
pendingHandoffNoticeRef.current = detail.createdAt || Date.now();
|
||||||
MessagePlugin.info('已打开口播精修,正在等待这个数字人视频同步到素材列表。');
|
MessagePlugin.info(
|
||||||
|
'已打开口播精修,正在等待这个数字人视频同步到素材列表。'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2227,6 +2227,7 @@ body
|
|||||||
.tg-avatar-page--compose
|
.tg-avatar-page--compose
|
||||||
.tg-task-row--aigcpanel.tg-task-row--with-media {
|
.tg-task-row--aigcpanel.tg-task-row--with-media {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
|
container-type: inline-size !important;
|
||||||
grid-template-columns: 32px minmax(0, 1fr) auto !important;
|
grid-template-columns: 32px minmax(0, 1fr) auto !important;
|
||||||
gap: 10px 14px !important;
|
gap: 10px 14px !important;
|
||||||
padding: 18px 20px !important;
|
padding: 18px 20px !important;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
resolveSeedanceActualModel,
|
||||||
|
seedanceVideoAdapter,
|
||||||
|
} from '../model-adapters/seedance-adapter';
|
||||||
|
import type { AdapterContext } from '../model-adapters/types';
|
||||||
|
|
||||||
|
describe('seedance video adapter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps runtime Doubao Seedance model ids unchanged', () => {
|
||||||
|
expect(
|
||||||
|
resolveSeedanceActualModel('doubao-seedance-2-0-fast-260128', '720p')
|
||||||
|
).toBe('doubao-seedance-2-0-fast-260128');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps built-in Seedance aliases to concrete API model ids', () => {
|
||||||
|
expect(resolveSeedanceActualModel('seedance-1.5-pro', '720p')).toBe(
|
||||||
|
'doubao-seedance-1-5-pro_720p'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits runtime Doubao Seedance model ids without adding a second prefix', async () => {
|
||||||
|
const requests: Array<{ url: string; init: RequestInit }> = [];
|
||||||
|
const fetcher = vi.fn(
|
||||||
|
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
requests.push({ url: String(input), init: init || {} });
|
||||||
|
if ((init?.method || 'GET') === 'POST') {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
id: 'task-1',
|
||||||
|
status: 'queued',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
id: 'task-1',
|
||||||
|
status: 'succeeded',
|
||||||
|
progress: 100,
|
||||||
|
url: 'https://cdn.example.com/seedance.mp4',
|
||||||
|
seconds: '5',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const context: AdapterContext = {
|
||||||
|
baseUrl: 'https://truemodel.benchu.cloud/v1',
|
||||||
|
apiKey: 'sk-test',
|
||||||
|
authType: 'bearer',
|
||||||
|
operation: 'video',
|
||||||
|
fetcher,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultPromise = seedanceVideoAdapter.generateVideo(context, {
|
||||||
|
model: 'doubao-seedance-2-0-fast-260128',
|
||||||
|
prompt: 'tiny city at night',
|
||||||
|
size: '720p@16:9',
|
||||||
|
duration: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
const result = await resultPromise;
|
||||||
|
|
||||||
|
const body = requests[0]?.init.body as FormData;
|
||||||
|
expect(result.url).toBe('https://cdn.example.com/seedance.mp4');
|
||||||
|
expect(requests[0]?.url).toBe('/api/truemodel/v1/videos');
|
||||||
|
expect(body.get('model')).toBe('doubao-seedance-2-0-fast-260128');
|
||||||
|
expect(body.get('model')).not.toBe(
|
||||||
|
'doubao-doubao-seedance-2-0-fast-260128_720p'
|
||||||
|
);
|
||||||
|
expect(body.get('size')).toBe('16:9');
|
||||||
|
expect(requests[1]?.url).toBe('/api/truemodel/v1/videos/task-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,14 +15,28 @@ const SEEDANCE_MODELS = [
|
|||||||
|
|
||||||
type SeedanceSubmitResponse = {
|
type SeedanceSubmitResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
status: 'queued' | 'processing' | 'completed' | 'failed';
|
status:
|
||||||
|
| 'queued'
|
||||||
|
| 'processing'
|
||||||
|
| 'in_progress'
|
||||||
|
| 'completed'
|
||||||
|
| 'succeeded'
|
||||||
|
| 'failed'
|
||||||
|
| 'error';
|
||||||
created_at?: number;
|
created_at?: number;
|
||||||
error?: string | { code: string; message: string };
|
error?: string | { code: string; message: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
type SeedanceQueryResponse = {
|
type SeedanceQueryResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
status: 'queued' | 'processing' | 'completed' | 'failed';
|
status:
|
||||||
|
| 'queued'
|
||||||
|
| 'processing'
|
||||||
|
| 'in_progress'
|
||||||
|
| 'completed'
|
||||||
|
| 'succeeded'
|
||||||
|
| 'failed'
|
||||||
|
| 'error';
|
||||||
progress?: number;
|
progress?: number;
|
||||||
video_url?: string;
|
video_url?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -36,10 +50,20 @@ const DEFAULT_POLL_MAX_ATTEMPTS = 1080; // ~90 min
|
|||||||
/**
|
/**
|
||||||
* 将逻辑模型 ID + 分辨率拼接为 API 实际模型名
|
* 将逻辑模型 ID + 分辨率拼接为 API 实际模型名
|
||||||
* seedance-1.5-pro + 720p → doubao-seedance-1-5-pro_720p
|
* seedance-1.5-pro + 720p → doubao-seedance-1-5-pro_720p
|
||||||
|
* 运行时发现的 doubao-seedance-* 已经是真实供应商模型名,必须原样发送。
|
||||||
*/
|
*/
|
||||||
const resolveActualModel = (logicalId: string, resolution: string): string => {
|
export const resolveSeedanceActualModel = (
|
||||||
|
logicalId: string,
|
||||||
|
resolution: string
|
||||||
|
): string => {
|
||||||
|
const normalizedLogicalId = logicalId.trim();
|
||||||
|
const lowerId = normalizedLogicalId.toLowerCase();
|
||||||
|
if (lowerId.startsWith('doubao-seedance-')) {
|
||||||
|
return normalizedLogicalId;
|
||||||
|
}
|
||||||
|
|
||||||
// 将 "." 替换为 "-":seedance-1.5-pro → seedance-1-5-pro
|
// 将 "." 替换为 "-":seedance-1.5-pro → seedance-1-5-pro
|
||||||
const normalized = logicalId.replace(/\./g, '-');
|
const normalized = normalizedLogicalId.replace(/\./g, '-');
|
||||||
return `doubao-${normalized}_${resolution}`;
|
return `doubao-${normalized}_${resolution}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -208,12 +232,14 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
|||||||
async generateVideo(context, request: VideoGenerationRequest) {
|
async generateVideo(context, request: VideoGenerationRequest) {
|
||||||
const logicalModel = request.model || 'seedance-1.5-pro';
|
const logicalModel = request.model || 'seedance-1.5-pro';
|
||||||
const resolution = extractResolution(request.size);
|
const resolution = extractResolution(request.size);
|
||||||
const actualModel = resolveActualModel(logicalModel, resolution);
|
const actualModel = resolveSeedanceActualModel(logicalModel, resolution);
|
||||||
const parsedSize = parseSeedanceSize(request.size);
|
const parsedSize = parseSeedanceSize(request.size);
|
||||||
|
|
||||||
// 宽高比优先取显式参数,其次回退到 size 中的组合值
|
// 宽高比优先取显式参数,其次回退到 size 中的组合值
|
||||||
const aspectRatio =
|
const aspectRatio =
|
||||||
normalizeAspectRatio(request.params?.aspect_ratio as string | undefined) ||
|
normalizeAspectRatio(
|
||||||
|
request.params?.aspect_ratio as string | undefined
|
||||||
|
) ||
|
||||||
normalizeAspectRatio(request.params?.aspectRatio as string | undefined) ||
|
normalizeAspectRatio(request.params?.aspectRatio as string | undefined) ||
|
||||||
parsedSize.aspectRatio ||
|
parsedSize.aspectRatio ||
|
||||||
'16:9';
|
'16:9';
|
||||||
@@ -253,7 +279,7 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
|||||||
onSubmitted?.(taskId);
|
onSubmitted?.(taskId);
|
||||||
|
|
||||||
// 提交时已失败
|
// 提交时已失败
|
||||||
if (submitResult.status === 'failed') {
|
if (submitResult.status === 'failed' || submitResult.status === 'error') {
|
||||||
throw new Error(extractErrorMessage(submitResult.error));
|
throw new Error(extractErrorMessage(submitResult.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +311,7 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
|||||||
: 0);
|
: 0);
|
||||||
onProgress?.(progress, status.status);
|
onProgress?.(progress, status.status);
|
||||||
|
|
||||||
if (status.status === 'completed') {
|
if (status.status === 'completed' || status.status === 'succeeded') {
|
||||||
const url = status.video_url || status.url;
|
const url = status.video_url || status.url;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
throw new Error('Seedance 结果缺少视频 URL');
|
throw new Error('Seedance 结果缺少视频 URL');
|
||||||
@@ -298,7 +324,7 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.status === 'failed') {
|
if (status.status === 'failed' || status.status === 'error') {
|
||||||
isBusinessFailure = true;
|
isBusinessFailure = true;
|
||||||
throw new Error(extractErrorMessage(status.error));
|
throw new Error(extractErrorMessage(status.error));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user