fix: align model provider configuration

This commit is contained in:
2026-05-19 09:07:59 +08:00
parent 12e3b97c06
commit ffa6b2efdf
7 changed files with 64 additions and 26 deletions

View File

@@ -1,10 +1,10 @@
# GPT 最高规格 API。没填 OPENAI_API_KEY 时图片/素材包生成走 mock 占位图。 # GPT 最高规格 API。没填 OPENAI_API_KEY 时图片/素材包生成走 mock 占位图。
OPENAI_API_KEY= OPENAI_API_KEY=
GPT_TEXT_MODEL=gpt-5.5 GPT_TEXT_MODEL=gpt-5.5
GPT_IMAGE_MODEL=image-2 GPT_IMAGE_MODEL=gpt-image-2
GPT_API_BASE=https://api.openai.com/v1 GPT_API_BASE=https://api.openai.com/v1
# 视频生成固定走 Seedance。未配置 Key 时 /api/video/generate 返回 503。 # 视频生成固定走 Seedance。未配置 Key 时 /api/video/generate 返回 503。
SEEDANCE_API_KEY= SEEDANCE_API_KEY=
SEEDANCE_MODEL=seedance-1-0-pro SEEDANCE_MODEL=doubao-seedance-2-0-260128
SEEDANCE_API_BASE=https://ark.cn-beijing.volces.com/api/v3 SEEDANCE_API_BASE=https://ark.cn-beijing.volces.com/api/v3

View File

@@ -23,10 +23,10 @@
## 环境变量 ## 环境变量
- `OPENAI_API_KEY` — GPT API Key文本/结构化/图片生成统一走 GPT 最高规格配置 - `OPENAI_API_KEY` — GPT API Key文本/结构化/图片生成统一走 GPT 最高规格配置
- `GPT_TEXT_MODEL` — 默认 `gpt-5.5`,用于角色设定等结构化输出 - `GPT_TEXT_MODEL` — 默认 `gpt-5.5`,用于角色设定等结构化输出
- `GPT_IMAGE_MODEL` — 默认 `image-2`,用于意向图和三类素材包图片生成 - `GPT_IMAGE_MODEL` — 默认 `gpt-image-2`,用于意向图和三类素材包图片生成
- `GPT_API_BASE` — 默认 `https://api.openai.com/v1` - `GPT_API_BASE` — 默认 `https://api.openai.com/v1`
- `SEEDANCE_API_KEY` — Seedance 视频生成 Key未配置时视频接口返回 503 - `SEEDANCE_API_KEY` — Seedance 视频生成 Key未配置时视频接口返回 503
- `SEEDANCE_MODEL` — 默认 `seedance-1-0-pro` - `SEEDANCE_MODEL` — 默认 `doubao-seedance-2-0-260128`
- `SEEDANCE_API_BASE` — 默认 `https://ark.cn-beijing.volces.com/api/v3` - `SEEDANCE_API_BASE` — 默认 `https://ark.cn-beijing.volces.com/api/v3`
- 配置位置:`.env.local`gitignored参考 `.env.local.example` - 配置位置:`.env.local`gitignored参考 `.env.local.example`
- 图片生成未配置 GPT Key 时回退 mockSVG 占位图),视频生成不 mock必须配置 Seedance Key - 图片生成未配置 GPT Key 时回退 mockSVG 占位图),视频生成不 mock必须配置 Seedance Key

View File

@@ -15,6 +15,7 @@ import type {
PackKind, PackKind,
VideoGenerationResponse, VideoGenerationResponse,
} from '@/lib/types'; } from '@/lib/types';
import type { VIDEO_TEMPLATES } from '@/lib/templates';
export default function Home() { export default function Home() {
const [sessions, setSessions] = useState<GenSession[]>([]); const [sessions, setSessions] = useState<GenSession[]>([]);
@@ -146,22 +147,24 @@ export default function Home() {
} }
} }
async function handleGenerateVideo(image: GenImage, promptTemplate: string) { async function handleGenerateVideo(image: GenImage, template: typeof VIDEO_TEMPLATES[number]) {
if (!current || videoLoading) return; if (!current || videoLoading) return;
setVideoLoading(true); setVideoLoading(true);
try { try {
const character = current.characterSpec const character = current.characterSpec
? `${current.characterSpec.name}${current.characterSpec.oneLiner}` ? `${current.characterSpec.name}${current.characterSpec.oneLiner}`
: current.prompt; : current.prompt;
const prompt = template.promptTemplate.replace('{character}', character);
const r = await fetch('/api/video/generate', { const r = await fetch('/api/video/generate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
prompt: promptTemplate.replace('{character}', character), prompt,
imageUrl: image.url, imageUrl: image.url,
duration: 6, duration: template.duration,
ratio: '16:9', ratio: template.ratio,
resolution: '1080p', generateAudio: true,
watermark: false,
}), }),
}); });
if (!r.ok) { if (!r.ok) {

View File

@@ -266,7 +266,7 @@ function TextTemplateSection() {
function VideoSection({ videoLoading, primaryImage, onGenerateVideo }: { function VideoSection({ videoLoading, primaryImage, onGenerateVideo }: {
videoLoading: boolean; videoLoading: boolean;
primaryImage: GenImage; primaryImage: GenImage;
onGenerateVideo: (image: GenImage, promptTemplate: string) => void; onGenerateVideo: (image: GenImage, template: typeof VIDEO_TEMPLATES[number]) => void;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [showPromptId, setShowPromptId] = useState<string | null>(null); const [showPromptId, setShowPromptId] = useState<string | null>(null);
@@ -331,7 +331,7 @@ function VideoSection({ videoLoading, primaryImage, onGenerateVideo }: {
)} )}
</div> </div>
<button <button
onClick={() => onGenerateVideo(primaryImage, template.promptTemplate)} onClick={() => onGenerateVideo(primaryImage, template)}
disabled={videoLoading} disabled={videoLoading}
className="self-center btn btn-primary text-[11px] px-3 py-1.5 disabled:opacity-40" className="self-center btn btn-primary text-[11px] px-3 py-1.5 disabled:opacity-40"
> >
@@ -401,7 +401,7 @@ export default function PackPanel({
onGenerate: (image: GenImage, kind: PackKind) => void; onGenerate: (image: GenImage, kind: PackKind) => void;
onGenerateAll: (image: GenImage) => void; onGenerateAll: (image: GenImage) => void;
onLockCharacter: (image: GenImage) => void; onLockCharacter: (image: GenImage) => void;
onGenerateVideo: (image: GenImage, prompt: string) => void; onGenerateVideo: (image: GenImage, template: typeof VIDEO_TEMPLATES[number]) => void;
}) { }) {
const [activeNav, setActiveNav] = useState('pack-patent'); const [activeNav, setActiveNav] = useState('pack-patent');
const selectedImages = session.images.filter(image => image.status === 'selected'); const selectedImages = session.images.filter(image => image.status === 'selected');

View File

@@ -3,7 +3,7 @@ import type { GenImage } from './types';
export type Provider = 'mock' | 'gpt'; export type Provider = 'mock' | 'gpt';
export const GPT_TEXT_MODEL = process.env.GPT_TEXT_MODEL || 'gpt-5.5'; export const GPT_TEXT_MODEL = process.env.GPT_TEXT_MODEL || 'gpt-5.5';
export const GPT_IMAGE_MODEL = process.env.GPT_IMAGE_MODEL || 'image-2'; export const GPT_IMAGE_MODEL = process.env.GPT_IMAGE_MODEL || 'gpt-image-2';
const GPT_API_BASE = process.env.GPT_API_BASE || 'https://api.openai.com/v1'; const GPT_API_BASE = process.env.GPT_API_BASE || 'https://api.openai.com/v1';
export function detectProvider(): Provider { export function detectProvider(): Provider {

View File

@@ -179,9 +179,16 @@ export type LockCharacterResponse = {
export type VideoGenerationRequest = { export type VideoGenerationRequest = {
prompt: string; prompt: string;
imageUrl?: string; imageUrl?: string;
references?: Array<{
type: 'image_url' | 'video_url' | 'audio_url';
url: string;
role?: 'reference_image' | 'reference_video' | 'reference_audio';
}>;
duration?: number; duration?: number;
ratio?: '16:9' | '9:16' | '1:1' | '4:3' | '3:4'; ratio?: '16:9' | '9:16' | '1:1' | '4:3' | '3:4';
resolution?: '720p' | '1080p'; resolution?: '720p' | '1080p';
generateAudio?: boolean;
watermark?: boolean;
}; };
export type VideoGenerationResponse = { export type VideoGenerationResponse = {

View File

@@ -1,10 +1,10 @@
import type { VideoGenerationRequest, VideoGenerationResponse } from './types'; import type { VideoGenerationRequest, VideoGenerationResponse } from './types';
export const SEEDANCE_MODEL = process.env.SEEDANCE_MODEL || 'seedance-1-0-pro'; export const SEEDANCE_MODEL = process.env.SEEDANCE_MODEL || 'doubao-seedance-2-0-260128';
const SEEDANCE_API_BASE = process.env.SEEDANCE_API_BASE || 'https://ark.cn-beijing.volces.com/api/v3'; const SEEDANCE_API_BASE = process.env.SEEDANCE_API_BASE || 'https://ark.cn-beijing.volces.com/api/v3';
function durationOrDefault(duration?: number): number { function durationOrDefault(duration?: number): number {
return Math.min(Math.max(duration ?? 6, 3), 10); return Math.min(Math.max(duration ?? 6, 3), 15);
} }
function normalizeStatus(status?: string): VideoGenerationResponse['status'] { function normalizeStatus(status?: string): VideoGenerationResponse['status'] {
@@ -14,15 +14,49 @@ function normalizeStatus(status?: string): VideoGenerationResponse['status'] {
return 'submitted'; return 'submitted';
} }
function publicUrlOrUndefined(url?: string): string | undefined {
if (!url) return undefined;
if (!/^https?:\/\//i.test(url)) return undefined;
if (/^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])/i.test(url)) return undefined;
return url;
}
function buildContent(opts: VideoGenerationRequest): Array<Record<string, unknown>> {
const content: Array<Record<string, unknown>> = [{ type: 'text', text: opts.prompt.trim() }];
const refs = [...(opts.references ?? [])];
if (opts.imageUrl) refs.unshift({ type: 'image_url', url: opts.imageUrl, role: 'reference_image' });
for (const ref of refs) {
const url = publicUrlOrUndefined(ref.url);
if (!url) continue;
if (ref.type === 'image_url') {
content.push({ type: 'image_url', image_url: { url }, role: ref.role ?? 'reference_image' });
}
if (ref.type === 'video_url') {
content.push({ type: 'video_url', video_url: { url }, role: ref.role ?? 'reference_video' });
}
if (ref.type === 'audio_url') {
content.push({ type: 'audio_url', audio_url: { url }, role: ref.role ?? 'reference_audio' });
}
}
return content;
}
export async function generateSeedanceVideo(opts: VideoGenerationRequest): Promise<VideoGenerationResponse> { export async function generateSeedanceVideo(opts: VideoGenerationRequest): Promise<VideoGenerationResponse> {
const key = process.env.SEEDANCE_API_KEY; const key = process.env.SEEDANCE_API_KEY;
if (!key) throw new Error('SEEDANCE_API_KEY missing'); if (!key) throw new Error('SEEDANCE_API_KEY missing');
if (!opts.prompt?.trim()) throw new Error('prompt required'); if (!opts.prompt?.trim()) throw new Error('prompt required');
const content: Array<Record<string, unknown>> = [{ type: 'text', text: opts.prompt.trim() }]; const content = buildContent(opts);
if (opts.imageUrl) { const body: Record<string, unknown> = {
content.push({ type: 'image_url', image_url: { url: opts.imageUrl } }); model: SEEDANCE_MODEL,
} content,
generate_audio: opts.generateAudio ?? true,
ratio: opts.ratio || '16:9',
duration: durationOrDefault(opts.duration),
watermark: opts.watermark ?? false,
};
if (opts.resolution) body.resolution = opts.resolution;
const res = await fetch(`${SEEDANCE_API_BASE}/contents/generations/tasks`, { const res = await fetch(`${SEEDANCE_API_BASE}/contents/generations/tasks`, {
method: 'POST', method: 'POST',
@@ -30,13 +64,7 @@ export async function generateSeedanceVideo(opts: VideoGenerationRequest): Promi
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${key}`, Authorization: `Bearer ${key}`,
}, },
body: JSON.stringify({ body: JSON.stringify(body),
model: SEEDANCE_MODEL,
content,
duration: durationOrDefault(opts.duration),
ratio: opts.ratio || '16:9',
resolution: opts.resolution || '1080p',
}),
}); });
if (!res.ok) throw new Error(`Seedance ${res.status}: ${await res.text()}`); if (!res.ok) throw new Error(`Seedance ${res.status}: ${await res.text()}`);