87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
|
|
import { experimental_generateImage as generateImage, generateText } from "ai";
|
||
|
|
import { createOpenAI } from "@ai-sdk/openai";
|
||
|
|
import { createXai } from "@ai-sdk/xai";
|
||
|
|
import type { ImageModelV1 } from "@ai-sdk/provider";
|
||
|
|
import type { ProviderApiKeys } from "./keys";
|
||
|
|
import { resolveApiKey } from "./keys";
|
||
|
|
import { getModel } from "./provider";
|
||
|
|
import { sanitizePromptText } from "@/lib/utils";
|
||
|
|
|
||
|
|
function getImageModel(aiProvider: string, apiKeys: ProviderApiKeys): ImageModelV1 {
|
||
|
|
const xaiKey = resolveApiKey("xai", apiKeys);
|
||
|
|
const openaiKey = resolveApiKey("openai", apiKeys);
|
||
|
|
|
||
|
|
if (aiProvider === "openai" && openaiKey) {
|
||
|
|
return createOpenAI({ apiKey: openaiKey }).image("dall-e-3");
|
||
|
|
}
|
||
|
|
if (xaiKey) {
|
||
|
|
return createXai({ apiKey: xaiKey }).image("grok-2-image");
|
||
|
|
}
|
||
|
|
if (openaiKey) {
|
||
|
|
return createOpenAI({ apiKey: openaiKey }).image("dall-e-3");
|
||
|
|
}
|
||
|
|
throw new Error("請在設定頁填入 xAI 或 OpenAI API key 以使用 AI 生圖");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function buildImagePrompt(input: {
|
||
|
|
text: string;
|
||
|
|
hook?: string | null;
|
||
|
|
imageBrief?: string | null;
|
||
|
|
angle?: string | null;
|
||
|
|
aiProvider: string;
|
||
|
|
aiModel: string;
|
||
|
|
apiKeys: ProviderApiKeys;
|
||
|
|
}): Promise<string> {
|
||
|
|
const model = getModel(input.aiProvider, input.aiModel, input.apiKeys);
|
||
|
|
const context = sanitizePromptText(
|
||
|
|
[
|
||
|
|
input.angle ? `切角:${input.angle}` : "",
|
||
|
|
input.hook ? `開頭:${input.hook}` : "",
|
||
|
|
input.imageBrief ? `配圖說明:${input.imageBrief}` : "",
|
||
|
|
`貼文:${input.text}`,
|
||
|
|
]
|
||
|
|
.filter(Boolean)
|
||
|
|
.join("\n")
|
||
|
|
);
|
||
|
|
|
||
|
|
const { text } = await generateText({
|
||
|
|
model,
|
||
|
|
system:
|
||
|
|
"You write concise English prompts for social media image generation. Output only the prompt, no markdown.",
|
||
|
|
prompt: `Create one image generation prompt for a Threads post visual.
|
||
|
|
Requirements:
|
||
|
|
- Warm, modern, mobile-friendly composition
|
||
|
|
- No real celebrity faces or logos
|
||
|
|
- Minimal readable text on image (prefer no text)
|
||
|
|
- Match the mood of this Traditional Chinese post
|
||
|
|
|
||
|
|
${context}`,
|
||
|
|
});
|
||
|
|
|
||
|
|
const prompt = text.trim();
|
||
|
|
if (!prompt) {
|
||
|
|
throw new Error("無法產生繪圖提示詞");
|
||
|
|
}
|
||
|
|
return prompt.slice(0, 1200);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function generateDraftImage(input: {
|
||
|
|
text: string;
|
||
|
|
hook?: string | null;
|
||
|
|
imageBrief?: string | null;
|
||
|
|
angle?: string | null;
|
||
|
|
aiProvider: string;
|
||
|
|
aiModel: string;
|
||
|
|
apiKeys: ProviderApiKeys;
|
||
|
|
}) {
|
||
|
|
const prompt = await buildImagePrompt(input);
|
||
|
|
const model = getImageModel(input.aiProvider, input.apiKeys);
|
||
|
|
|
||
|
|
const { image } = await generateImage({
|
||
|
|
model,
|
||
|
|
prompt,
|
||
|
|
aspectRatio: "1:1",
|
||
|
|
});
|
||
|
|
|
||
|
|
return { image, prompt };
|
||
|
|
}
|