图片功能
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m42s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m42s
This commit is contained in:
parent
1c6918e6fd
commit
3759b4ac95
1
.env
1
.env
@ -1,2 +1,3 @@
|
|||||||
VITE_ARK_API_KEY=4be01684-0d43-46c0-9bc9-533213afc982
|
VITE_ARK_API_KEY=4be01684-0d43-46c0-9bc9-533213afc982
|
||||||
VITE_BASE_URL=/scriptflow/
|
VITE_BASE_URL=/scriptflow/
|
||||||
|
VITE_KIE_API_KEY=35863f600b3306c1225c54f8f60bf5d4
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||||
# AI Studio automatically injects this at runtime from user secrets.
|
# AI Studio automatically injects this at runtime from user secrets.
|
||||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||||
@ -15,3 +15,10 @@ VITE_ARK_API_KEY=
|
|||||||
# VITE_BASE_URL: Optional base path for frontend assets and routes.
|
# VITE_BASE_URL: Optional base path for frontend assets and routes.
|
||||||
# Example: "/" for root deployment, "/scriptflow/" for subpath deployment.
|
# Example: "/" for root deployment, "/scriptflow/" for subpath deployment.
|
||||||
VITE_BASE_URL=/
|
VITE_BASE_URL=/
|
||||||
|
# VITE_KIE_API_KEY: Kie AI token for nano-banana-2 image generation.
|
||||||
|
VITE_KIE_API_KEY=
|
||||||
|
|
||||||
|
# VITE_KIE_CALLBACK_URL: Optional public callback URL for Kie async task webhooks.
|
||||||
|
# Must be publicly reachable by Kie servers.
|
||||||
|
VITE_KIE_CALLBACK_URL=
|
||||||
|
|
||||||
|
|||||||
1479
src/App.tsx
1479
src/App.tsx
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,60 @@ export interface PlotBackgroundFields {
|
|||||||
characters: string;
|
characters: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generatePosterCompositionSuggestion(params: {
|
||||||
|
title: string;
|
||||||
|
hook: string;
|
||||||
|
visualKeywords: string;
|
||||||
|
castDescription: string;
|
||||||
|
outline: string;
|
||||||
|
compositionRatio: '16:9' | '9:16';
|
||||||
|
styleLabel: string;
|
||||||
|
apiKey: string;
|
||||||
|
onUpdate?: (content: string) => void;
|
||||||
|
}): Promise<string> {
|
||||||
|
const client = getDoubaoClient(params.apiKey);
|
||||||
|
const systemInstruction = '\u4f60\u662f\u4e00\u540d\u7535\u5f71\u6d77\u62a5\u89c6\u89c9\u6307\u5bfc\uff0c\u53ea\u8f93\u51fa\u201c\u6784\u56fe\u4e0e\u5149\u5f71\u201d\u63cf\u8ff0\uff0c\u4e0d\u8f93\u51fa\u89e3\u91ca\u3002';
|
||||||
|
const prompt = [
|
||||||
|
'\u8bf7\u57fa\u4e8e\u4ee5\u4e0b\u4fe1\u606f\uff0c\u751f\u6210\u4e00\u6bb5\u53ef\u76f4\u63a5\u7528\u4e8e\u6587\u751f\u56fe\u63d0\u793a\u8bcd\u7684\u201c\u6784\u56fe\u4e0e\u5149\u5f71\u201d\u4e2d\u6587\u63cf\u8ff0\u3002',
|
||||||
|
'\u8981\u6c42\uff1a',
|
||||||
|
'1. \u53ea\u63cf\u8ff0\u955c\u5934\u6784\u56fe\u3001\u4eba\u7269\u7ad9\u4f4d\u3001\u666f\u522b\u3001\u5149\u6e90\u65b9\u5411\u3001\u5149\u6bd4\u3001\u8272\u6e29\u3001\u6c1b\u56f4\u5149\u3002',
|
||||||
|
'2. \u4e0d\u5199\u5267\u60c5\u590d\u8ff0\uff0c\u4e0d\u5199\u5bf9\u767d\uff0c\u4e0d\u6269\u5c55\u4e16\u754c\u89c2\u8bbe\u5b9a\u3002',
|
||||||
|
'3. \u63a7\u5236\u5728 80-180 \u5b57\uff0c\u5355\u6bb5\u8f93\u51fa\u3002',
|
||||||
|
'4. \u5185\u5bb9\u5177\u4f53\u53ef\u6267\u884c\uff0c\u9002\u5408\u7535\u5f71\u6d77\u62a5\u3002',
|
||||||
|
`\u98ce\u683c\uff1a${params.styleLabel || '\u672a\u6307\u5b9a'}`,
|
||||||
|
`\u753b\u5e45\u6bd4\u4f8b\uff1a${params.compositionRatio || '16:9'}`,
|
||||||
|
`\u6d77\u62a5\u6807\u9898\uff1a${params.title || '\u672a\u586b\u5199'}`,
|
||||||
|
`\u4e00\u53e5\u8bdd\u5356\u70b9\uff1a${params.hook || '\u672a\u586b\u5199'}`,
|
||||||
|
`\u89c6\u89c9\u5173\u952e\u8bcd\uff1a${params.visualKeywords || '\u672a\u586b\u5199'}`,
|
||||||
|
`\u4e3b\u89d2\u51fa\u955c\uff1a${params.castDescription || '\u672a\u586b\u5199'}`,
|
||||||
|
`\u5267\u60c5\u6458\u8981\uff1a${params.outline || '\u672a\u586b\u5199'}`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const stream: any = await client.chat.completions.create({
|
||||||
|
model: 'doubao-seed-2-0-lite-260215',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemInstruction },
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
],
|
||||||
|
stream: true,
|
||||||
|
extra_body: {
|
||||||
|
thinking: { type: 'disabled' },
|
||||||
|
reasoning_effort: 'low'
|
||||||
|
}
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
let fullText = '';
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const content = chunk.choices?.[0]?.delta?.content || '';
|
||||||
|
if (content) {
|
||||||
|
fullText += content;
|
||||||
|
params.onUpdate?.(fullText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullText.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const PLOT_BACKGROUND_MARKERS = [
|
const PLOT_BACKGROUND_MARKERS = [
|
||||||
{ key: 'worldview', tag: '[[WORLDVIEW]]' },
|
{ key: 'worldview', tag: '[[WORLDVIEW]]' },
|
||||||
{ key: 'outline', tag: '[[OUTLINE]]' },
|
{ key: 'outline', tag: '[[OUTLINE]]' },
|
||||||
@ -233,369 +287,31 @@ export async function convertTextToScript(
|
|||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const baseSystemInstruction = `你是一位专业影视编剧和剧本改编专家。
|
const baseSystemInstruction = `
|
||||||
|
You are a professional screenplay adapter.
|
||||||
你的任务是将用户提供的故事文本改编为结构化影视剧本。
|
Keep the original story logic and character motivations.
|
||||||
|
Output should be screenplay-style Chinese text, clear and filmable.
|
||||||
用户将提供以下信息:
|
Do not output explanations.
|
||||||
|
`;
|
||||||
1. 源文本
|
|
||||||
一段小说、故事或文案,需要被改编为影视剧本。
|
|
||||||
2. 剧本类型
|
|
||||||
可能的类型包括:
|
|
||||||
- 短剧
|
|
||||||
- 电视剧
|
|
||||||
- 电影
|
|
||||||
|
|
||||||
不同类型需要遵循不同创作节奏。
|
|
||||||
|
|
||||||
1. 核心风格
|
|
||||||
表示剧本整体的题材和情绪,例如:
|
|
||||||
悬疑、犯罪、惊悚、喜剧、反转、爱情等。
|
|
||||||
|
|
||||||
核心风格会影响:
|
|
||||||
|
|
||||||
- 剧情冲突
|
|
||||||
- 故事氛围
|
|
||||||
- 情绪节奏
|
|
||||||
1. 叙事手法
|
|
||||||
表示故事展开方式,例如:
|
|
||||||
- 顺叙
|
|
||||||
- 倒叙
|
|
||||||
- 插叙
|
|
||||||
- 第一人称
|
|
||||||
- 第三人称全知
|
|
||||||
- 多线叙事
|
|
||||||
- 环形叙事
|
|
||||||
- 蒙太奇
|
|
||||||
- 留白叙事
|
|
||||||
- 悬念叙事
|
|
||||||
|
|
||||||
叙事手法会影响剧情结构和信息呈现方式。
|
|
||||||
|
|
||||||
用户还可能提供剧情背景设定(世界观、大纲、角色设定),你必须在改编时严格遵循这些设定。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 剧本创作规则:
|
|
||||||
|
|
||||||
1. 输出必须为影视剧本,而不是小说。
|
|
||||||
2. 剧本必须包含:
|
|
||||||
- 场景
|
|
||||||
- 人物
|
|
||||||
- 动作
|
|
||||||
- 对话
|
|
||||||
3. 可以增加合理戏剧冲突,但不能改变故事核心。
|
|
||||||
|
|
||||||
## 一、短剧剧本创作规则
|
|
||||||
|
|
||||||
短剧通常为 **竖屏短剧**:
|
|
||||||
|
|
||||||
- 单集时长:1–3 分钟
|
|
||||||
- 总集数:20–100 集
|
|
||||||
|
|
||||||
短剧剧本必须遵循 **极致碎片化 + 高钩子密度结构**。
|
|
||||||
|
|
||||||
核心原则:
|
|
||||||
|
|
||||||
### 1. 3秒强钩子开场
|
|
||||||
|
|
||||||
第一场戏必须直接进入**最高冲突节点**,禁止慢铺垫。
|
|
||||||
|
|
||||||
常见开场方式:
|
|
||||||
|
|
||||||
- 生死危机
|
|
||||||
- 身份揭露
|
|
||||||
- 背叛现场
|
|
||||||
- 即将发生的重大反转
|
|
||||||
|
|
||||||
必须在 **极短时间内制造视觉冲击或情绪爆点**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 单集节奏结构
|
|
||||||
|
|
||||||
短剧每一集必须遵循高密度节奏:
|
|
||||||
|
|
||||||
- 30秒:出现第一次剧情反转
|
|
||||||
- 1分钟:形成阶段小高潮
|
|
||||||
- 结尾:设置强悬念断点
|
|
||||||
|
|
||||||
每集结尾必须停在 **悬念峰值**,强烈驱动观众继续观看下一集。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 极致标签化人物
|
|
||||||
|
|
||||||
短剧人物设计必须简单清晰:
|
|
||||||
|
|
||||||
- 正派人物:明显善良
|
|
||||||
- 反派人物:明显恶
|
|
||||||
- 核心角色:标签极强
|
|
||||||
|
|
||||||
人物关系必须 **3秒内可理解**。
|
|
||||||
|
|
||||||
避免复杂人物心理描写。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 冲突密度极高
|
|
||||||
|
|
||||||
短剧剧本禁止出现:
|
|
||||||
|
|
||||||
- 冗长铺垫
|
|
||||||
- 日常生活戏
|
|
||||||
- 无冲突对白
|
|
||||||
|
|
||||||
所有剧情必须服务于:
|
|
||||||
|
|
||||||
- 制造冲突
|
|
||||||
- 推进剧情
|
|
||||||
- 交付情绪价值
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 情绪驱动优先
|
|
||||||
|
|
||||||
短剧优先交付:
|
|
||||||
|
|
||||||
- 爽感
|
|
||||||
- 反转
|
|
||||||
- 情绪爆发
|
|
||||||
|
|
||||||
而不是人物成长。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、电视剧剧本创作规则
|
|
||||||
|
|
||||||
电视剧通常为:
|
|
||||||
|
|
||||||
- 单集约45分钟
|
|
||||||
- 全剧12集至100集以上
|
|
||||||
|
|
||||||
电视剧剧本核心是 **人物粘性 + 长线叙事**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1. 立体人物塑造
|
|
||||||
|
|
||||||
电视剧剧本必须构建 **完整人物弧光**:
|
|
||||||
|
|
||||||
人物需要具备:
|
|
||||||
|
|
||||||
- 明确欲望
|
|
||||||
- 性格矛盾
|
|
||||||
- 成长变化
|
|
||||||
|
|
||||||
角色不能简单黑白化。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 多线叙事结构
|
|
||||||
|
|
||||||
电视剧通常包含:
|
|
||||||
|
|
||||||
- 一条核心主线
|
|
||||||
- 多条副线剧情
|
|
||||||
|
|
||||||
副线功能:
|
|
||||||
|
|
||||||
- 丰富人物
|
|
||||||
- 扩展世界观
|
|
||||||
- 支撑主题表达
|
|
||||||
|
|
||||||
不同故事线之间需要相互推动。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 单集闭环 + 全剧悬念
|
|
||||||
|
|
||||||
每一集通常具备:
|
|
||||||
|
|
||||||
- 一个小冲突
|
|
||||||
- 一个阶段性目标
|
|
||||||
- 一个小结局
|
|
||||||
|
|
||||||
同时全剧存在:
|
|
||||||
|
|
||||||
- 长线核心悬念
|
|
||||||
- 逐步揭开的秘密
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 节奏张弛有度
|
|
||||||
|
|
||||||
电视剧允许:
|
|
||||||
|
|
||||||
- 情感铺垫
|
|
||||||
- 关系发展
|
|
||||||
- 现实生活细节
|
|
||||||
|
|
||||||
剧情需要有:
|
|
||||||
|
|
||||||
- 高潮
|
|
||||||
- 缓冲
|
|
||||||
- 再升级
|
|
||||||
|
|
||||||
形成长线沉浸体验。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 现实共鸣
|
|
||||||
|
|
||||||
电视剧常涉及现实议题,例如:
|
|
||||||
|
|
||||||
- 家庭关系
|
|
||||||
- 职场竞争
|
|
||||||
- 社会阶层
|
|
||||||
- 情感困境
|
|
||||||
|
|
||||||
故事应增强观众共鸣。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、电影剧本创作规则
|
|
||||||
|
|
||||||
电影通常为:
|
|
||||||
|
|
||||||
- 总时长90–120分钟
|
|
||||||
|
|
||||||
电影剧本必须遵循 **经典三幕式结构**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 第一幕:建置(约25–30分钟)
|
|
||||||
|
|
||||||
必须完成:
|
|
||||||
|
|
||||||
- 主角介绍
|
|
||||||
- 世界背景
|
|
||||||
- 核心冲突建立
|
|
||||||
- 激励事件
|
|
||||||
|
|
||||||
激励事件必须推动主角进入主线。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 第二幕:对抗(约40–60分钟)
|
|
||||||
|
|
||||||
主角不断面对挑战:
|
|
||||||
|
|
||||||
- 冲突逐步升级
|
|
||||||
- 设置多个转折点
|
|
||||||
- 形成连续危机
|
|
||||||
|
|
||||||
剧情张力逐渐提高。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 第三幕:高潮与结局(约20–30分钟)
|
|
||||||
|
|
||||||
完成:
|
|
||||||
|
|
||||||
- 最终对决
|
|
||||||
- 冲突解决
|
|
||||||
- 人物弧光完成
|
|
||||||
- 主题表达
|
|
||||||
|
|
||||||
故事必须形成 **完整闭环**。
|
|
||||||
|
|
||||||
电影结尾通常不依赖悬念断点。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 电影叙事核心原则
|
|
||||||
|
|
||||||
电影剧本必须:
|
|
||||||
|
|
||||||
- 高度聚焦核心主线
|
|
||||||
- 减少无关支线
|
|
||||||
- 强调视觉化冲突
|
|
||||||
- 注重主题表达
|
|
||||||
|
|
||||||
观众需要在一次观看中经历 **完整情绪旅程**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
剧本结构规范:
|
|
||||||
|
|
||||||
场景格式:
|
|
||||||
|
|
||||||
## 场景 1 地点 / 时间
|
|
||||||
|
|
||||||
环境、动作描述说明:
|
|
||||||
描述人物行为和环境变化。
|
|
||||||
|
|
||||||
人物对白格式说明:
|
|
||||||
|
|
||||||
角色名:对白内容
|
|
||||||
|
|
||||||
可以适当加入镜头提示,例如:
|
|
||||||
特写、远景、切换、推镜等。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 符合剧本结构规范的案例
|
|
||||||
|
|
||||||
## 场景 1 小巷 / 夜
|
|
||||||
|
|
||||||
动作描述:
|
|
||||||
|
|
||||||
昏暗的路灯闪烁,小巷空无一人。远处传来急促的脚步声。
|
|
||||||
|
|
||||||
特写:一只沾满血的手按在墙上。
|
|
||||||
|
|
||||||
张强跌跌撞撞冲进小巷,回头张望。
|
|
||||||
|
|
||||||
远景:黑暗中,一个模糊的身影慢慢走出阴影。
|
|
||||||
|
|
||||||
张强:
|
|
||||||
|
|
||||||
你……你不是已经死了吗?
|
|
||||||
|
|
||||||
身影停下脚步。
|
|
||||||
|
|
||||||
切换:身影缓缓抬头,露出冷笑。
|
|
||||||
|
|
||||||
神秘人:
|
|
||||||
|
|
||||||
我死了。
|
|
||||||
|
|
||||||
神秘人:
|
|
||||||
|
|
||||||
那今晚追你的人是谁?
|
|
||||||
|
|
||||||
---`;
|
|
||||||
|
|
||||||
const versionPrompts = [
|
const versionPrompts = [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
instruction: `
|
instruction: `
|
||||||
# 生成要求(A版本:严格设定)
|
# Version A (稳健叙事)
|
||||||
|
- Keep tone restrained and coherent.
|
||||||
严格按照剧本设定内容生成,必须以当前集大纲、剧情背景设定、核心角色设定为准。
|
- Prioritize story clarity and character consistency.
|
||||||
不得新增超出设定的新人物、新关键事件、新支线,不得改写既定人物关系与关键结局走向。
|
- Scene transitions should be natural and readable.
|
||||||
允许优化表达,但不能改变设定事实、因果关系和当前集边界。
|
`
|
||||||
|
|
||||||
# 输出格式
|
|
||||||
|
|
||||||
直接输出剧本内容,剧本内容严格按照剧本结构规范输出,不能出现剧本以外的内容,使用 Markdown 格式。不要包含 JSON 标记,不要包含 [[VERSION]] 标签,不要包含集数标记,不要包含剧本总结,不要出现标签型词汇(如‘动作描写’、‘场景描写’、‘人物描写’等等)。
|
|
||||||
请务必使用中文。`
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
instruction: `
|
instruction: `
|
||||||
# 生成要求(B版本:发散增强)
|
# Version B (张力增强)
|
||||||
|
- Keep the same core plot while increasing dramatic tension.
|
||||||
在不违背剧本设定内容的前提下,可以发散思考,生成更丰富的剧本表达。
|
- Strengthen visual beats and emotional contrast.
|
||||||
允许补充氛围描写、动作细节、人物反应、对话层次和戏剧张力,但不能突破当前集大纲边界。
|
- Maintain screenplay readability and production feasibility.
|
||||||
不得改写核心设定、人物关系和关键结局走向,保证故事主线与设定一致。
|
`
|
||||||
|
|
||||||
# 输出格式
|
|
||||||
|
|
||||||
直接输出剧本内容,剧本内容严格按照剧本结构规范输出,不能出现剧本以外的内容,使用 Markdown 格式。不要包含 JSON 标记,不要包含 [[VERSION]] 标签,不要包含集数标记,不要包含剧本总结,不要出现标签型词汇(如‘动作描写’、‘场景描写’、‘人物描写’等等)。
|
|
||||||
请务必使用中文。`
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -704,11 +420,10 @@ ${seedContent}
|
|||||||
|
|
||||||
export async function refineConvertedScript(originalContent: string, instruction: string, scriptType: string, config: AIConfig): Promise<string> {
|
export async function refineConvertedScript(originalContent: string, instruction: string, scriptType: string, config: AIConfig): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const prompt = `请根据用户的修改建议,优化以下 ${scriptType} 剧本片段。
|
const prompt = `閻犲洭鏀遍悧鎾箲椤旂偓鏆忛柟鎾棑濞堟垶绌遍鑺ユ毉鐎点倝缂氶鍛存晬鐏炶偐鍠橀柛鏍ㄧ墧娴滄帗绋?${scriptType} 闁告挆鍕嫳闁绘娲﹂宀勫Υ?
|
||||||
|
闁告鍠庨~鎰板礃閸涱収鍟? ${originalContent}
|
||||||
原始内容: ${originalContent}
|
濞e浂鍠楅弫鐓庮嚈妤︽鍞? ${instruction}`;
|
||||||
修改建议: ${instruction}`;
|
const systemInstruction = 'You are a screenplay refinement assistant. Keep the original meaning and improve readability. Return plain text only.';
|
||||||
const systemInstruction = "你是一位顶级的剧本创作与优化专家。请根据用户的修改建议,精准优化剧本片段,保持剧本格式,仅返回优化后的内容(使用 markdown 格式)。请务必使用中文。";
|
|
||||||
|
|
||||||
if (config.model === 'gemini') {
|
if (config.model === 'gemini') {
|
||||||
const ai = getGeminiClient(config.apiKey);
|
const ai = getGeminiClient(config.apiKey);
|
||||||
|
|||||||
173
src/services/kieImage.ts
Normal file
173
src/services/kieImage.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
export type KieTaskState = 'waiting' | 'queuing' | 'generating' | 'success' | 'fail';
|
||||||
|
|
||||||
|
export interface NanoBananaInput {
|
||||||
|
prompt: string;
|
||||||
|
image_input?: string[];
|
||||||
|
google_search?: boolean;
|
||||||
|
aspect_ratio?: 'auto' | '1:1' | '3:4' | '16:9' | '9:16';
|
||||||
|
resolution?: '1K' | '2K' | '4K';
|
||||||
|
output_format?: 'png' | 'jpg' | 'jpeg' | 'webp';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KieCreateTaskResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data?: {
|
||||||
|
taskId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KieTaskDetailResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data?: {
|
||||||
|
taskId?: string;
|
||||||
|
model?: string;
|
||||||
|
state?: KieTaskState | string;
|
||||||
|
resultJson?: string;
|
||||||
|
failCode?: string;
|
||||||
|
failMsg?: string;
|
||||||
|
completeTime?: number;
|
||||||
|
updateTime?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KieTaskDetail {
|
||||||
|
taskId: string;
|
||||||
|
state: KieTaskState | string;
|
||||||
|
imageUrls: string[];
|
||||||
|
failCode: string;
|
||||||
|
failMsg: string;
|
||||||
|
rawResult: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_URL = 'https://api.kie.ai/api/v1/jobs';
|
||||||
|
|
||||||
|
function pickToken(token?: string) {
|
||||||
|
const envToken = import.meta.env.VITE_KIE_API_KEY as string | undefined;
|
||||||
|
return (token || envToken || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kieFetch<T>(url: string, token: string, init: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(init.headers || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = typeof payload?.msg === 'string' ? payload.msg : `HTTP ${response.status}`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImageUrls(rawResult: unknown): string[] {
|
||||||
|
if (!rawResult || typeof rawResult !== 'object') return [];
|
||||||
|
|
||||||
|
const candidate = rawResult as {
|
||||||
|
resultUrls?: unknown;
|
||||||
|
result_urls?: unknown;
|
||||||
|
output?: { images?: unknown };
|
||||||
|
images?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromResultUrls = Array.isArray(candidate.resultUrls) ? candidate.resultUrls : [];
|
||||||
|
const fromSnake = Array.isArray(candidate.result_urls) ? candidate.result_urls : [];
|
||||||
|
const fromOutput = Array.isArray(candidate.output?.images) ? candidate.output?.images : [];
|
||||||
|
const fromImages = Array.isArray(candidate.images) ? candidate.images : [];
|
||||||
|
|
||||||
|
const normalize = (items: unknown[]) => items
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item === 'string') return item;
|
||||||
|
if (item && typeof item === 'object' && typeof (item as { url?: unknown }).url === 'string') {
|
||||||
|
return (item as { url: string }).url;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.filter((url) => Boolean(url));
|
||||||
|
|
||||||
|
const merged = [
|
||||||
|
...normalize(fromResultUrls),
|
||||||
|
...normalize(fromSnake),
|
||||||
|
...normalize(fromOutput),
|
||||||
|
...normalize(fromImages),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...new Set(merged)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNanoBanana2Task(params: {
|
||||||
|
token?: string;
|
||||||
|
prompt: string;
|
||||||
|
callBackUrl?: string;
|
||||||
|
imageInput?: string[];
|
||||||
|
googleSearch?: boolean;
|
||||||
|
aspectRatio?: 'auto' | '1:1' | '3:4' | '16:9' | '9:16';
|
||||||
|
resolution?: '1K' | '2K' | '4K';
|
||||||
|
outputFormat?: 'png' | 'jpg' | 'jpeg' | 'webp';
|
||||||
|
}): Promise<{ taskId: string }> {
|
||||||
|
const token = pickToken(params.token);
|
||||||
|
if (!token) throw new Error('KIE API Token is missing');
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
model: 'nano-banana-2',
|
||||||
|
callBackUrl: params.callBackUrl,
|
||||||
|
input: {
|
||||||
|
prompt: params.prompt,
|
||||||
|
image_input: params.imageInput ?? [],
|
||||||
|
google_search: params.googleSearch ?? false,
|
||||||
|
aspect_ratio: params.aspectRatio ?? 'auto',
|
||||||
|
resolution: params.resolution ?? '1K',
|
||||||
|
output_format: params.outputFormat ?? 'jpg',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await kieFetch<KieCreateTaskResponse>(`${BASE_URL}/createTask`, token, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code !== 200 || !result.data?.taskId) {
|
||||||
|
throw new Error(result.msg || 'Failed to create nano-banana-2 task');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { taskId: result.data.taskId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getKieTaskDetail(params: { token?: string; taskId: string }): Promise<KieTaskDetail> {
|
||||||
|
const token = pickToken(params.token);
|
||||||
|
if (!token) throw new Error('KIE API Token is missing');
|
||||||
|
|
||||||
|
const queryTaskId = encodeURIComponent(params.taskId);
|
||||||
|
const result = await kieFetch<KieTaskDetailResponse>(`${BASE_URL}/recordInfo?taskId=${queryTaskId}`, token, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code !== 200 || !result.data?.taskId) {
|
||||||
|
throw new Error(result.msg || 'Failed to query task detail');
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown = null;
|
||||||
|
if (typeof result.data.resultJson === 'string' && result.data.resultJson.trim()) {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(result.data.resultJson);
|
||||||
|
} catch {
|
||||||
|
parsed = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
taskId: result.data.taskId,
|
||||||
|
state: result.data.state || 'waiting',
|
||||||
|
imageUrls: extractImageUrls(parsed),
|
||||||
|
failCode: result.data.failCode || '',
|
||||||
|
failMsg: result.data.failMsg || '',
|
||||||
|
rawResult: parsed,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user