fix: map yuanjie media manifest references

This commit is contained in:
FengLee
2026-05-20 19:51:44 +08:00
parent afd8585882
commit d8619fd9e6
12 changed files with 235 additions and 17 deletions

View File

@@ -176,4 +176,6 @@ Primary SQL tables touched directly in API routes include:
`src/lib/yuanjie-video-model-templates.ts` is the canonical source for built-in 元界 AI video model definitions from the local video docs. It maps each model to a Manifest request body, records whether the model supports 文生视频 and/or 图生视频, and stores aspect ratio, resolution, duration, and quality/mode capability options for the selected system video model. Video templates use the same documented `is_final` plus `state` polling rule.
Yuanjie Manifest references use `$inputImages.urls` for provider-facing JSON fields. For image-to-image, `/api/generate/image` reads the primary `image` plus `extraImages` and sends all references to `src/lib/user-api-manifest-executor.ts`; the executor uploads data URL references into storage before rendering Yuanjie `params.images`, top-level `images`, or `base64Array`. Yuanjie video templates keep documented model-specific fields inside `src/lib/yuanjie-video-model-templates.ts`, including first/last reference fields and mode fields such as `input_reference`, `reference_urls`, `img_url`, `image_tail`, `ratio`, `size`, and `generation_mode`.
Profile naming convention: `profiles.nickname` is the stable login username; `profiles.display_nickname` is the public nickname shown in navbar/gallery/profile UI. APIs return `username` plus `nickname`/`display_nickname` so older clients can keep reading `nickname` as the display name.

View File

@@ -159,6 +159,8 @@ User-level intelligent API imports add a fourth data artifact tied to source 2:
At generation time, `src/lib/server-api-config.ts` returns `manifestPath` for user custom keys and admin system API keys. `src/app/api/generate/image/route.ts` and `src/app/api/generate/video/route.ts` call `src/lib/user-api-manifest-executor.ts` first when that path exists. The executor handles JSON, multipart file fields, `{task_id}` polling, `*` JSON-path extraction, and media persistence handoff. For image Manifest results, the route persists returned result URLs through `src/lib/media-storage.ts`; external result URL downloads use `src/lib/remote-fetch.ts` with browser-like headers and limited retry so provider/CDN-side 403, 429, 5xx, or timeout failures are distinguished from upstream generation failures. If the provider returned a result but MiaoJing cannot download or save the image media, the API should report a platform download/save failure instead of a resolution mismatch. Imported Manifest rows still need the user or admin to edit and save an API Key before they can generate.
Manifest template rendering exposes input images in two forms: `$inputImages.dataUrls` keeps the raw uploaded data for multipart/file manifests, while `$inputImages.urls` is normalized for providers that require URL references. The executor converts data URL references to storage-backed public URLs before rendering JSON templates, using object-storage signed URLs when available or the app public base URL plus `/api/local-storage/<key>` otherwise.
Admin system intelligent API imports live in `src/components/admin/api-management-tab.tsx` and `src/app/api/admin/system-apis/smart-import/route.ts`. The `智能配置 API` section is generic Manifest import only: each imported profile/model becomes one global `system_api_configs` row with its own `manifest_path`, backed by `system-api-manifests/<systemApiId>.json`, and the visible `api_url` is resolved from the Manifest profile/provider. Incomplete configs without a resolvable relay API request URL are rejected. Optional `profile.capabilities` can constrain or hide create-page image/video parameter choices for system models. Provider-specific built-in templates such as 元界 AI are not exposed in this smart import UI; 元界 definitions remain in `src/lib/yuanjie-image-model-templates.ts` and `src/lib/yuanjie-video-model-templates.ts` for the system-default-model management path, where admins configure each model row's Key, pricing/member visibility/polling, `video_usage_modes`, and enablement before it is available to users. Admin Manifest files must remain separate from user-level files and must keep using the system pricing/credit deduction policy for the selected model. System API rows also own `is_default`, `allowed_membership_tiers`, `polling_mode`, and `polling_order`; `/api/model-config` returns only one active platform-default row per allowed media type plus admin display name so the create page shows a single default model label, and image generation expands the selected row back into all allowed supplier candidates with the same display name. The upstream `model_name` can differ between suppliers and is only used as that supplier's request model. Video model billing supports per-use count (`fixed`), per-second duration (`duration_price_per_second`), and token mode. Token billing prices shown in the admin console are credits per 1M tokens for both input and output; older storage/API field names containing `1k` remain compatibility names and must not be shown to admins as per-K pricing. If a system image supplier fails because a stream request idles until Cloudflare 524, `/api/generate/image` retries that candidate once with `stream:false`; 502/503/504 gateway responses are retried once by the shared transport. If every supplier still fails or returns no usable result, the route returns the last actionable upstream error when available, otherwise the generic model-busy message. This polling fallback is only for admin default system models and must not be applied to user custom API keys.
After production migration, app runtime tables in `public` should be owned by the app DB user from `LOCAL_DB_URL`. Runtime compatibility helpers use `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` and index creation; if restored tables remain owned by `postgres`, public routes such as `/api/model-config`, profile refresh, or generation jobs can fail with `must be owner of table ...`.

View File

@@ -98,6 +98,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| 邀请链接不生成、邀请注册未发积分、后台看不到邀请关系 | `src/components/profile/credits-tab.tsx`, `src/app/api/invitations/me/route.ts`, `src/app/auth/register/page.tsx`, `src/app/api/auth/register/route.ts`, `src/app/api/admin/invitations/route.ts`, `src/lib/invitation-service.ts` | `profiles.invite_code` must be unique and stable. Registration links use `/auth/register?invite=...`; successful invited registration writes `invitation_referrals`, sets `profiles.referred_by_user_id`, grants 50 credits to inviter and invitee, and writes `credit_transactions` rows in the registration transaction. |
| New API image endpoint incompatible | `src/app/api/generate/image/route.ts`, `src/lib/custom-api-fetch.ts` | Provider is `newapi`/`new api`, endpoint normalization, model-specific size/count/quality handling. |
| API key leaked in UI/API | `src/app/api/user-api-keys/route.ts`, `src/app/api/admin/system-apis/route.ts`, `src/lib/server-crypto.ts`, `src/lib/server-api-config.ts` | Response mapping must return preview/empty key only. |
| Yuanjie GPT Image 2 image-to-image ignores reference images or behaves like text-to-image | `src/components/create/image-to-image.tsx`, `src/app/api/generate/image/route.ts`, `src/lib/user-api-manifest-executor.ts`, `src/lib/yuanjie-image-model-templates.ts` | Check whether the route reads `extraImages`, normalizes `image + extraImages` into Manifest `inputImages`, and whether Yuanjie templates use `$inputImages.urls` for `params.images`, top-level `images`, and `base64Array`. The executor should upload data URL references into storage and expose public references as `$inputImages.urls`; do not fix this by changing mozheAPI or generic OpenAI-compatible fallbacks. |
## Gallery And Creation History

View File

@@ -22,6 +22,10 @@ Use this document before changing non-generic provider/platform behavior. If a u
- Admin default models must use `system_api_configs.name` as the frontend display name, while `model_name` remains the upstream request model.
- When 元界 is used as a system default model, credit deduction must still follow the selected `system_api_configs` row's pricing through the generation job backend. New-job balance preflight should include same-user queued/running system-default jobs. Image and video create UI should display only the completed job's returned `creditsCost` and refresh the profile balance from `creditsBalance`, not a separate predicted button cost. Failed jobs must not write consume transactions.
- Yuanjie image and video templates that accept reference media should render provider-facing references with `$inputImages.urls`, not `$inputImages.dataUrls`. The Manifest executor uploads data URL references into storage first, then exposes object/local-storage public URLs as `$inputImages.urls` while keeping raw data URLs available as `$inputImages.dataUrls` for multipart/file-upload manifests.
- Yuanjie GPT Image 2 / GPT Image 2 official-transfer image-to-image must pass all frontend references. `src/components/create/image-to-image.tsx` sends the primary image as `image` and additional references as `extraImages`; `src/app/api/generate/image/route.ts` must normalize them into the Manifest `inputImages` array before calling `src/lib/user-api-manifest-executor.ts`.
- Yuanjie video templates must stay in `src/lib/yuanjie-video-model-templates.ts` and map documented model-specific fields there instead of changing generic mozheAPI/custom-API request builders. Current documented special mappings include `sora-2.params.input_reference`, `wan2.6-cankaosheng.params.reference_urls`, `wan2.6-shouzheng.params.img_url`, `kling-v3-omni-shouweizhen.params.image/image_tail`, `happyhorse-r2v.params.ratio`, `grok-video-3.params.size`, and `veo3.1.params.generation_mode/enhance_prompt/enable_upsample`.
## mozheAPI
- Start with `src/proxy.ts` for iframe/embed failures before changing page components.

View File

@@ -80,6 +80,7 @@ Use this document to jump directly to code before broad searching.
| User API smart import | `src/components/profile/api-key-manager.tsx`, `src/app/api/user-api-keys/smart-import/route.ts`, `src/lib/user-api-manifest.ts`, `src/lib/user-api-manifest-executor.ts`, `src/lib/model-capabilities.ts`, `src/lib/model-display.ts` | The profile API settings page has an `智能配置 API` button next to `添加 API 密钥`. It opens a wide viewport-capped Manifest editor, can copy the LLM prompt, shows guidance under the prompt button explaining the copy-to-chat-AI and paste-and-import flow, can paste clipboard JSON without importing, and can paste-and-import in one action. The prompt instructs the LLM to stop and ask the user for the relay API Base URL when the docs do not contain it. Imports create each profile/model as an independent `user_api_keys` row plus a separate `user-api-manifests/<userId>/<keyId>.json` file and reject incomplete configs without a resolvable request URL. Imported rows should store a human-readable provider name in the editable provider/supplier fields and resolve the visible API request URL from `profile.baseUrl + submit.path` for synchronous endpoints. Generic placeholder notes such as `导入的 API Key` must not be used as model labels; creation/profile UI should prefer a real note plus model, or provider plus model. Optional `profile.capabilities` filters or hides create-page aspect ratio, resolution, image format, and quality controls for the selected model. Polling Manifest query values can include `{task_id}` so task IDs are sent as real query parameters rather than being embedded into pathname strings. Generation routes must use the selected model key's `manifest_path`; do not merge different request configs under one user-level file. |
| Admin system API smart import | `src/components/admin/api-management-tab.tsx`, `src/app/api/admin/system-apis/smart-import/route.ts`, `src/app/api/admin/system-apis/route.ts`, `src/lib/server-api-config.ts`, `src/lib/user-api-manifest.ts`, `src/lib/user-api-manifest-executor.ts`, `src/lib/model-capabilities.ts` | The console API management page has a separate `智能配置 API` section for admins, but this section is generic Manifest import only. It supports copy-to-chat-AI and paste-and-import Manifest flow, then creates one independent system API row and `system-api-manifests/<systemApiId>.json` file per imported profile/model. Imported rows resolve the visible API request URL from the Manifest profile/provider before save, and optional `profile.capabilities` can constrain or hide create-page image/video parameter choices for the selected system model. Provider-specific built-in template management, including 元界 AI, belongs in the `系统默认模型` management flow and should not be exposed in the smart import UI. |
| Admin console active page persistence | `src/modules/console/pages/console-dashboard-page.tsx` | The console active view is stored in `sessionStorage`, so browser refresh keeps the current admin page/tab. Logout clears the value, and closing/reopening the console starts from the dashboard because `sessionStorage` is tab-scoped. |
| Manifest input image URLs | `src/lib/user-api-manifest-executor.ts`, `src/app/api/generate/image/route.ts`, `src/app/api/generate/video/route.ts` | Manifest templates can use `$inputImages.dataUrls` for raw uploaded data and `$inputImages.urls` for provider-facing public references. The executor converts data URL input images into storage-backed URLs before rendering templates. Image-to-image generation normalizes the primary `image` plus `extraImages` into Manifest `inputImages`, so multi-reference providers such as Yuanjie GPT Image 2 receive all references. |
## Models And Providers

View File

@@ -18,6 +18,7 @@
"test:custom-image-fallback": "tsx ./scripts/test-custom-image-fallback.mjs",
"test:generation-credit-policy": "tsx ./scripts/test-generation-credit-policy.mjs",
"test:gallery-response": "node --no-warnings ./scripts/test-gallery-response.mjs",
"test:yuanjie-media-manifest-mapping": "tsx ./scripts/test-yuanjie-media-manifest-mapping.mjs",
"test:yuanjie-image2-persistence": "tsx ./scripts/test-yuanjie-image2-persistence.mjs",
"test:ops-hardening": "node --no-warnings ./scripts/test-ops-hardening.mjs",
"pm2:restart": "pm2 startOrReload ecosystem.config.cjs --update-env",

View File

@@ -0,0 +1,86 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
const repoRoot = path.resolve(import.meta.dirname, '..');
const {
YUANJIE_IMAGE_MODEL_TEMPLATES,
buildYuanjieSubmit,
} = await import('../src/lib/yuanjie-image-model-templates.ts');
const {
YUANJIE_VIDEO_MODEL_TEMPLATES,
buildYuanjieVideoSubmit,
} = await import('../src/lib/yuanjie-video-model-templates.ts');
async function runTest(name, fn) {
try {
await fn();
console.log(`PASS ${name}`);
} catch (error) {
console.error(`FAIL ${name}`);
console.error(error);
process.exitCode = 1;
}
}
function read(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
}
function imageTemplate(modelName) {
const template = YUANJIE_IMAGE_MODEL_TEMPLATES.find(item => item.modelName === modelName);
assert.ok(template, `missing image template ${modelName}`);
return buildYuanjieSubmit(template);
}
function videoTemplate(modelName) {
const template = YUANJIE_VIDEO_MODEL_TEMPLATES.find(item => item.modelName === modelName);
assert.ok(template, `missing video template ${modelName}`);
return buildYuanjieVideoSubmit(template);
}
await runTest('image-to-image route passes all reference images into Manifest execution', () => {
const source = read('src/app/api/generate/image/route.ts');
assert.match(source, /extraImages/);
assert.match(source, /const referenceImages = normalizeReferenceImages\(image,\s*undefined,\s*extraImages\)/s);
assert.match(source, /inputImages:\s*referenceImages,/);
assert.doesNotMatch(source, /inputImages:\s*image \? \[image\] : \[\]/);
});
await runTest('yuanjie GPT Image 2 uses public image reference URLs for edit/image-to-image', () => {
const submit = imageTemplate('gpt-image-2');
assert.equal(submit.body?.params?.images, '$inputImages.urls');
assert.equal(submit.body?.images, '$inputImages.urls');
assert.equal(submit.body?.base64Array, '$inputImages.urls');
});
await runTest('manifest executor exposes normalized public input image URLs to templates', () => {
const source = read('src/lib/user-api-manifest-executor.ts');
assert.match(source, /inputImageUrls\?: string\[\]/);
assert.match(source, /inputImages:\s*\{\s*dataUrls:\s*input\.inputImages \|\| \[\],\s*urls:\s*input\.inputImageUrls \|\| input\.inputImages \|\| \[\],/s);
assert.match(source, /resolveManifestInputImageReferences\(input\.inputImages \|\| \[\]\)/);
});
await runTest('yuanjie video templates map documented reference fields and mode fields', () => {
assert.equal(videoTemplate('sora-2').body?.params?.input_reference, '$inputImages.urls.0');
assert.equal(videoTemplate('wan2.6-cankaosheng').body?.params?.reference_urls, '$inputImages.urls');
assert.equal(videoTemplate('wan2.6-shouzheng').body?.params?.img_url, '$inputImages.urls.0');
assert.equal(videoTemplate('kling-v3-omni-shouweizhen').body?.params?.image, '$inputImages.urls.0');
assert.equal(videoTemplate('kling-v3-omni-shouweizhen').body?.params?.image_tail, '$inputImages.urls.1');
assert.equal(videoTemplate('happyhorse-r2v').body?.params?.ratio, '$params.aspect_ratio');
assert.equal(videoTemplate('grok-video-3').body?.params?.size, '$params.resolution');
assert.equal(videoTemplate('veo3.1').body?.params?.generation_mode, '$params.quality');
assert.equal(videoTemplate('veo3.1').body?.params?.enhance_prompt, true);
assert.equal(videoTemplate('veo3.1').body?.params?.enable_upsample, false);
});
await runTest('video route passes negative prompt through Manifest params for providers that document it', () => {
const source = read('src/app/api/generate/video/route.ts');
assert.match(source, /negative_prompt:\s*negativePrompt,/);
});
if (process.exitCode) process.exit(process.exitCode);

View File

@@ -135,6 +135,22 @@ function normalizeImageCount(value: unknown): number | undefined {
return Math.min(10, Math.max(1, Math.floor(parsed)));
}
function normalizeReferenceImages(image?: string, images?: unknown, extraImages?: unknown): string[] {
const refs: string[] = [];
if (image) refs.push(image);
if (Array.isArray(images)) {
for (const item of images) {
if (typeof item === 'string' && item.trim()) refs.push(item);
}
}
if (Array.isArray(extraImages)) {
for (const item of extraImages) {
if (typeof item === 'string' && item.trim()) refs.push(item);
}
}
return Array.from(new Set(refs));
}
function resolveAutoImageRequestParams(input: {
prompt: string;
aspectRatio: unknown;
@@ -921,6 +937,7 @@ export async function POST(request: NextRequest) {
guidanceScale = 7,
stream,
image,
extraImages,
strength,
customApiConfig,
} = body as {
@@ -940,6 +957,7 @@ export async function POST(request: NextRequest) {
guidanceScale?: number;
stream?: boolean;
image?: string;
extraImages?: string[];
strength?: number;
customApiConfig?: CustomApiConfig;
};
@@ -952,12 +970,13 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: '创作描述过短,请输入更详细的描述' }, { status: 400 });
}
const referenceImages = normalizeReferenceImages(image, undefined, extraImages);
const resolvedAutoParams = resolveAutoImageRequestParams({
prompt,
aspectRatio,
resolution,
count,
hasReferenceImage: !!image,
hasReferenceImage: referenceImages.length > 0,
});
if (!resolvedAutoParams.ok) {
return NextResponse.json({ error: resolvedAutoParams.message }, { status: 400 });
@@ -1010,7 +1029,7 @@ export async function POST(request: NextRequest) {
hasCustomApi: !!resolvedCustomApiConfig,
customApiUrl: resolvedCustomApiConfig?.apiUrl,
customApiModel: resolvedCustomApiConfig?.modelName,
hasImage: !!image,
hasImage: referenceImages.length > 0,
strength,
promptLength: prompt.length,
stream: stream !== false,
@@ -1036,8 +1055,8 @@ export async function POST(request: NextRequest) {
aspect_ratio: resolvedAutoParams.aspectRatio,
resolution: resolvedAutoParams.resolution,
},
inputImages: image ? [image] : [],
preferEdit: !!image,
inputImages: referenceImages,
preferEdit: referenceImages.length > 0,
timeoutMs: GENERATION_TIMEOUT,
onProgress: handleUpstreamProgress,
});

View File

@@ -495,6 +495,8 @@ export async function POST(request: NextRequest) {
aspectRatio = '16:9',
duration = 5,
resolution = '720p',
quality,
mode,
fps = 30,
image,
images,
@@ -507,6 +509,8 @@ export async function POST(request: NextRequest) {
aspectRatio?: string;
duration?: number | string;
resolution?: string;
quality?: string;
mode?: string;
fps?: number;
image?: string;
images?: string[];
@@ -553,7 +557,10 @@ export async function POST(request: NextRequest) {
aspect_ratio: aspectRatio,
duration,
resolution,
quality: quality || mode,
mode: mode || quality,
fps,
negative_prompt: negativePrompt,
},
inputImages: referenceImages,
preferEdit: referenceImages.length > 0,

View File

@@ -1,4 +1,5 @@
import { fetchPublicHttpUrl } from '@/lib/remote-fetch';
import { localStorage } from '@/lib/local-storage';
import {
buildCustomApiHeaders,
fetchWithRetry,
@@ -32,6 +33,7 @@ export type UserApiManifestExecutionInput = {
prompt: string;
params?: ManifestParams;
inputImages?: string[];
inputImageUrls?: string[];
mask?: string;
preferEdit?: boolean;
timeoutMs: number;
@@ -46,6 +48,16 @@ export type UserApiManifestExecutionResult = {
const OMIT = Symbol('omit');
function publicAppBaseUrl(): string {
return (process.env.APP_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || '').trim().replace(/\/+$/, '');
}
function toAbsolutePublicUrl(url: string): string {
if (/^https?:\/\//i.test(url)) return url;
const baseUrl = publicAppBaseUrl();
return baseUrl && url.startsWith('/') ? `${baseUrl}${url}` : url;
}
function stripSlashes(value: string): string {
return value.replace(/^\/+|\/+$/g, '');
}
@@ -140,6 +152,7 @@ function getTemplateVariable(path: string, input: UserApiManifestExecutionInput)
params: input.params || {},
inputImages: {
dataUrls: input.inputImages || [],
urls: input.inputImageUrls || input.inputImages || [],
},
mask: {
dataUrl: input.mask,
@@ -148,6 +161,50 @@ function getTemplateVariable(path: string, input: UserApiManifestExecutionInput)
return getPathValue(context, path);
}
function parseDataUrlForUpload(value: string): { buffer: Buffer; mimeType: string; ext: string } | null {
const match = value.match(/^data:([^;]+);base64,([\s\S]+)$/);
if (!match) return null;
const mimeType = match[1].split(';')[0] || 'application/octet-stream';
const ext = mimeType.split('/')[1] || 'bin';
return {
buffer: Buffer.from(match[2], 'base64'),
mimeType,
ext,
};
}
async function uploadManifestInputDataUrl(value: string, index: number): Promise<string | null> {
const parsed = parseDataUrlForUpload(value);
if (!parsed) return null;
const fileKey = await localStorage.uploadFileObjectOnly({
fileContent: parsed.buffer,
fileName: `manifest-reference-images/${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}.${parsed.ext}`,
contentType: parsed.mimeType,
});
const objectReadUrl = localStorage.generateObjectReadUrl(fileKey, 3600);
if (objectReadUrl) return objectReadUrl;
const publicUrl = await localStorage.generatePresignedUrl({ key: fileKey, expireTime: 3600 });
return publicUrl ? toAbsolutePublicUrl(publicUrl) : null;
}
async function resolveManifestInputImageReferences(inputImages: string[]): Promise<string[]> {
const resolved: string[] = [];
for (let index = 0; index < inputImages.length; index += 1) {
const value = inputImages[index]?.trim();
if (!value) continue;
if (value.startsWith('data:')) {
const publicUrl = await uploadManifestInputDataUrl(value, index);
resolved.push(publicUrl || value);
continue;
}
resolved.push(toAbsolutePublicUrl(value));
}
return Array.from(new Set(resolved));
}
function renderTemplate(value: unknown, input: UserApiManifestExecutionInput): unknown | typeof OMIT {
if (typeof value === 'string') {
const exact = value.match(/^\$([a-zA-Z0-9_.]+)$/);
@@ -193,13 +250,15 @@ function replaceTaskIdPlaceholders(value: unknown, taskId?: string): unknown {
}
function dataUrlToBlob(value: string): { blob: Blob; fileName: string } | null {
const match = value.match(/^data:([^;]+);base64,([\s\S]+)$/);
if (!match) return null;
const mimeType = match[1];
const ext = mimeType.split('/')[1] || 'bin';
const parsed = parseDataUrlForUpload(value);
if (!parsed) return null;
const arrayBuffer = parsed.buffer.buffer.slice(
parsed.buffer.byteOffset,
parsed.buffer.byteOffset + parsed.buffer.byteLength,
) as ArrayBuffer;
return {
blob: new Blob([Buffer.from(match[2], 'base64')], { type: mimeType }),
fileName: `image.${ext}`,
blob: new Blob([arrayBuffer], { type: parsed.mimeType }),
fileName: `image.${parsed.ext}`,
};
}
@@ -344,6 +403,7 @@ export async function executeUserApiManifest(input: UserApiManifestExecutionInpu
...input,
apiUrl: input.apiUrl || stored.profile.baseUrl || '',
modelName: input.modelName || stored.profile.model || '',
inputImageUrls: input.inputImageUrls || await resolveManifestInputImageReferences(input.inputImages || []),
};
const submitRaw = await requestManifestEndpoint(endpoint, executionInput);
const submitMedia = extractMediaFromResult(submitRaw, endpoint);

View File

@@ -284,7 +284,7 @@ export function buildYuanjieParams(template: YuanjieImageModelTemplate): Record<
if (template.capabilities.qualities && template.paramKind !== 'midjourney') {
params.quality = '$params.quality';
}
params.images = '$inputImages.dataUrls';
params.images = '$inputImages.urls';
return params;
}
@@ -305,8 +305,8 @@ export function buildYuanjieSubmit(template: YuanjieImageModelTemplate): Manifes
aspectRatio: params.aspectRatio,
imageSize: params.imageSize,
resolution: params.resolution,
images: '$inputImages.dataUrls',
base64Array: '$inputImages.dataUrls',
images: '$inputImages.urls',
base64Array: '$inputImages.urls',
},
taskIdPath: 'task_id|taskId|data.task_id|data.taskId|data.id|id|result.task_id|result.taskId|result.id|result',
result: {

View File

@@ -96,6 +96,9 @@ function firstCapabilityValue(capabilities: ModelCapabilityConfig, key: 'aspectR
function buildYuanjieVideoParams(template: YuanjieVideoModelTemplate): Record<string, unknown> {
const params: Record<string, unknown> = { ...(template.defaultParams || {}) };
const referenceUrls = '$inputImages.urls';
const firstReferenceUrl = '$inputImages.urls.0';
const secondReferenceUrl = '$inputImages.urls.1';
const defaultAspectRatio = firstCapabilityValue(template.capabilities, 'aspectRatios');
const defaultResolution = firstCapabilityValue(template.capabilities, 'resolutions');
const defaultDuration = firstCapabilityValue(template.capabilities, 'durations');
@@ -121,8 +124,40 @@ function buildYuanjieVideoParams(template: YuanjieVideoModelTemplate): Record<st
if (!params.resolution && defaultResolution) params.resolution = defaultResolution;
if (!params.duration && defaultDuration) params.duration = defaultDuration;
if (!params.quality && defaultQuality) params.quality = defaultQuality;
params.images = '$inputImages.dataUrls';
params.image = '$inputImages.dataUrls';
params.images = referenceUrls;
params.image = firstReferenceUrl;
if (template.modelName === 'sora-2') {
params.input_reference = firstReferenceUrl;
}
if (template.modelName === 'wan2.6-cankaosheng') {
params.reference_urls = referenceUrls;
params.reference_images = referenceUrls;
}
if (template.modelName === 'wan2.6-shouzheng') {
params.img_url = firstReferenceUrl;
}
if (template.modelName === 'wan2.7-shouweizhen') {
params.input = {
first_frame_url: firstReferenceUrl,
last_frame_url: secondReferenceUrl,
};
}
if (template.modelName === 'kling-v3-omni-shouweizhen') {
params.image = firstReferenceUrl;
params.image_tail = secondReferenceUrl;
}
if (template.modelName === 'happyhorse-r2v') {
params.ratio = '$params.aspect_ratio';
}
if (template.modelName === 'grok-video-3') {
params.size = '$params.resolution';
}
if (template.modelName === 'veo3.1') {
params.generation_mode = '$params.quality';
params.enhance_prompt = true;
params.enable_upsample = false;
}
return params;
}
@@ -143,8 +178,8 @@ export function buildYuanjieVideoSubmit(template: YuanjieVideoModelTemplate): Ma
size: params.size,
duration: params.duration,
quality: params.quality,
images: '$inputImages.dataUrls',
image: '$inputImages.dataUrls',
images: '$inputImages.urls',
image: '$inputImages.urls.0',
},
taskIdPath: 'task_id|taskId|data.task_id|data.taskId|data.id|id|result.task_id|result.taskId|result.id|result',
result: {