fix: align model provider configuration
This commit is contained in:
@@ -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
|
||||||
|
|||||||
4
RULES.md
4
RULES.md
@@ -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 时回退 mock(SVG 占位图),视频生成不 mock,必须配置 Seedance Key
|
- 图片生成未配置 GPT Key 时回退 mock(SVG 占位图),视频生成不 mock,必须配置 Seedance Key
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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()}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user