图片功能
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m42s

This commit is contained in:
Song367 2026-03-13 21:01:28 +08:00
parent 1c6918e6fd
commit 3759b4ac95
5 changed files with 1613 additions and 484 deletions

1
.env
View File

@ -1,2 +1,3 @@
VITE_ARK_API_KEY=4be01684-0d43-46c0-9bc9-533213afc982
VITE_BASE_URL=/scriptflow/
VITE_KIE_API_KEY=35863f600b3306c1225c54f8f60bf5d4

View File

@ -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.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
@ -14,4 +14,11 @@ VITE_ARK_API_KEY=
# VITE_BASE_URL: Optional base path for frontend assets and routes.
# 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=

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,60 @@ export interface PlotBackgroundFields {
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 = [
{ key: 'worldview', tag: '[[WORLDVIEW]]' },
{ key: 'outline', tag: '[[OUTLINE]]' },
@ -233,369 +287,31 @@ export async function convertTextToScript(
}
): Promise<void> {
try {
const baseSystemInstruction = `你是一位专业影视编剧和剧本改编专家。
1.
2.
-
-
-
1.
-
-
-
1.
-
-
-
-
-
- 线
-
-
-
-
---
#
1.
2.
-
-
-
-
3.
##
****
- 13
- 20100
** + **
### 1. 3
****
-
-
-
-
****
---
### 2.
- 30
- 1
-
****
---
### 3.
-
-
-
**3**
---
### 4.
-
-
-
-
-
-
---
### 5.
-
-
-
---
##
- 45
- 12100
** + 线**
---
### 1.
****
-
-
-
---
### 2. 线
- 线
- 线
线
-
-
-
线
---
### 3. +
-
-
-
- 线
-
---
### 4.
-
-
-
-
-
-
线
---
### 5.
-
-
-
-
---
##
- 90120
****
---
### 2530
-
-
-
-
线
---
### 4060
-
-
-
---
### 2030
-
-
-
-
****
---
###
- 线
- 线
-
-
****
---
## 1 /
---
#
## 1 /
---`;
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.
`;
const versionPrompts = [
{
id: 0,
instruction: `
# A版本
线
#
使 Markdown JSON [[VERSION]]
使`
# Version A ()
- Keep tone restrained and coherent.
- Prioritize story clarity and character consistency.
- Scene transitions should be natural and readable.
`
},
{
id: 1,
instruction: `
# B版本
线
#
使 Markdown JSON [[VERSION]]
使`
# Version B ()
- Keep the same core plot while increasing dramatic tension.
- Strengthen visual beats and emotional contrast.
- Maintain screenplay readability and production feasibility.
`
}
];
@ -704,11 +420,10 @@ ${seedContent}
export async function refineConvertedScript(originalContent: string, instruction: string, scriptType: string, config: AIConfig): Promise<string> {
try {
const prompt = `请根据用户的修改建议,优化以下 ${scriptType} 剧本片段。
原始内容: ${originalContent}
修改建议: ${instruction}`;
const systemInstruction = "你是一位顶级的剧本创作与优化专家。请根据用户的修改建议,精准优化剧本片段,保持剧本格式,仅返回优化后的内容(使用 markdown 格式)。请务必使用中文。";
const prompt = `閻犲洭鏀遍悧鎾箲椤旂偓鏆忛柟鎾棑濞堟垶绌遍鑺ユ毉鐎点倝缂氶鍛存晬鐏炶偐鍠橀柛鏍ㄧ墧娴滄帗绋?${scriptType} 闁告挆鍕嫳闁绘娲﹂宀勫Υ?
? ${originalContent}
? ${instruction}`;
const systemInstruction = 'You are a screenplay refinement assistant. Keep the original meaning and improve readability. Return plain text only.';
if (config.model === 'gemini') {
const ai = getGeminiClient(config.apiKey);

173
src/services/kieImage.ts Normal file
View 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,
};
}