This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { getPricing } from "@/lib/services/pricing-service";
|
||||
import { GoogleGenAI, Modality } from "@google/genai";
|
||||
|
||||
/**
|
||||
* Merkezi yapay zeka istemci örneği
|
||||
* Tüm yapay zeka istekleri bu örneği kullanmalıdır
|
||||
*/
|
||||
export const ai = new GoogleGenAI({
|
||||
vertexai: true,
|
||||
|
||||
apiKey: process.env.GOOGLE_API_KEY,
|
||||
});
|
||||
|
||||
// Kalite artırıcı anahtar kelimeler
|
||||
const QUALITY_BOOSTERS = [
|
||||
"highly detailed",
|
||||
"8k resolution",
|
||||
"professional photography",
|
||||
"studio lighting",
|
||||
"sharp focus",
|
||||
"cinematic composition",
|
||||
"vibrant colors",
|
||||
"masterpiece",
|
||||
"masterpiece",
|
||||
];
|
||||
|
||||
/**
|
||||
* Yapay Zeka Model yapılandırmaları
|
||||
*/
|
||||
export const AI_MODELS = {
|
||||
FLASH_LITE: "gemini-2.5-flash-lite",
|
||||
FLASH: "gemini-2.5-flash",
|
||||
FLASH_IMAGE: "gemini-2.5-flash-image",
|
||||
} as const;
|
||||
|
||||
// ——————————————————————————————————————
|
||||
// Type definitions for Gemini API responses
|
||||
// ——————————————————————————————————————
|
||||
|
||||
/** Token usage metadata from Gemini API */
|
||||
interface UsageMetadata {
|
||||
promptTokenCount?: number;
|
||||
candidatesTokenCount?: number;
|
||||
totalTokenCount?: number;
|
||||
}
|
||||
|
||||
/** Gemini content part (text or inline data) */
|
||||
interface ContentPart {
|
||||
text?: string;
|
||||
inlineData?: { mimeType: string; data: string };
|
||||
}
|
||||
|
||||
/** Generation config passed to Gemini */
|
||||
interface GenerationConfig {
|
||||
responseModalities?: Modality[];
|
||||
temperature?: number;
|
||||
maxOutputTokens?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Prompt content — can be a string, structured content array, or object */
|
||||
type PromptContent =
|
||||
| string
|
||||
| Array<{ role: string; parts: ContentPart[] }>
|
||||
| Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Yapay zeka ile içerik oluştur
|
||||
* Tüm yapay zeka içerik oluşturma işlemleri için merkezi fonksiyon
|
||||
*
|
||||
* @param model - Kullanılacak yapay zeka modeli
|
||||
* @param prompt - Gönderilecek istem veya içerikler
|
||||
* @param config - İsteğe bağlı yapılandırma
|
||||
* @returns Yapay zeka yanıtı
|
||||
*/
|
||||
export async function generateAIContent(
|
||||
model: string,
|
||||
prompt: PromptContent,
|
||||
config?: GenerationConfig
|
||||
) {
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: prompt,
|
||||
...(config && { config }),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("AI generation error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Yapay zeka ile metin içeriği oluştur
|
||||
* @param model - Kullanılacak yapay zeka modeli
|
||||
* @param prompt - Metin istemi
|
||||
* @returns Oluşturulan metin
|
||||
*/
|
||||
export async function generateText(
|
||||
model: string,
|
||||
prompt: string
|
||||
): Promise<{ text: string; usage?: UsageMetadata }> {
|
||||
const response = await generateAIContent(model, prompt);
|
||||
return {
|
||||
text: (response.text || "").trim(),
|
||||
usage: response.usageMetadata as UsageMetadata | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Görüntü girdisi ile içerik oluştur
|
||||
* @param model - Kullanılacak yapay zeka modeli
|
||||
* @param imageBase64 - Base64 kodlanmış görüntü
|
||||
* @param textPrompt - Metin istemi
|
||||
* @returns Yapay zeka yanıtı
|
||||
*/
|
||||
export async function generateWithImage(
|
||||
model: string,
|
||||
imageBase64: string,
|
||||
textPrompt: string
|
||||
) {
|
||||
const response = await generateAIContent(model, [
|
||||
{
|
||||
role: "user",
|
||||
parts: [
|
||||
{ inlineData: { mimeType: "image/jpeg", data: imageBase64 } },
|
||||
{ text: textPrompt },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
text: (response.text || "").trim(),
|
||||
usage: response.usageMetadata as UsageMetadata | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* İstemden görüntü oluştur
|
||||
* @param prompt - Görüntü oluşturma için metin istemi
|
||||
* @param imageBase64 - Düzenleme için isteğe bağlı base64 görüntü
|
||||
* @param suggestions - Görsel iyileştirme önerileri (Nano Banana için)
|
||||
* @param model - Kullanılacak model (varsayılan: FLASH_IMAGE)
|
||||
* @param aspectRatio - Hedef en boy oranı (örn: "16:9", "1:1")
|
||||
* @returns Oluşturulan görüntü URL'si (base64)
|
||||
*/
|
||||
export async function generateImage(
|
||||
prompt: string,
|
||||
imageBase64?: string,
|
||||
suggestions?: string[],
|
||||
model: string = AI_MODELS.FLASH_IMAGE,
|
||||
aspectRatio?: string
|
||||
): Promise<{ imageUrl: string | null; usage?: UsageMetadata }> {
|
||||
let parts: Array<
|
||||
{ text: string } | { inlineData: { mimeType: string; data: string } }
|
||||
> = [];
|
||||
|
||||
// En boy oranı talimatı oluştur
|
||||
const ratioInstruction = aspectRatio
|
||||
? `\n\nTarget Aspect Ratio: ${aspectRatio}\nEnsure the image strictly follows the ${aspectRatio} aspect ratio format.`
|
||||
: "";
|
||||
|
||||
if (imageBase64) {
|
||||
// Görüntü düzenleme modu
|
||||
const cleanBase64 = imageBase64.replace(
|
||||
/^data:image\/(png|jpeg|webp|jpg);base64,/,
|
||||
""
|
||||
);
|
||||
|
||||
// Eğer Nano Banana modeli ve öneriler varsa, prompt'u zenginleştir
|
||||
let finalPrompt = prompt;
|
||||
if (model === "nano-banana") {
|
||||
let improvementInstructions = "";
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
improvementInstructions = `\n\nApply these specific improvements based on platform analysis:\n${suggestions.map((s) => `- ${s}`).join("\n")}`;
|
||||
}
|
||||
|
||||
finalPrompt = `${prompt}${improvementInstructions}\n\nStyle & Quality Instructions:\nRender in ${QUALITY_BOOSTERS.join(", ")}.\n\nCRITICAL OBJECTIVE: The result MUST achieve a perfect 10/10 score.\n- Clarity: 10/10 (Ultra-sharp, no blur)\n- Professionalism: 10/10 (High-end commercial look)\n- Engagement: 10/10 (Eye-catching contrast and lighting)\n- Platform Fit: 10/10 (Perfect aspect ratio and framing)\nEnsure the image is visually stunning and flawless.`;
|
||||
}
|
||||
|
||||
parts.push({ inlineData: { mimeType: "image/png", data: cleanBase64 } });
|
||||
parts.push({ text: finalPrompt + ratioInstruction });
|
||||
} else {
|
||||
parts.push({ text: prompt + ratioInstruction });
|
||||
}
|
||||
|
||||
console.log("[AI-Helper] generateImage calling Gemini model", {
|
||||
model: AI_MODELS.FLASH_IMAGE,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: [{ role: "user", parts }],
|
||||
config: {
|
||||
responseModalities: [Modality.IMAGE],
|
||||
},
|
||||
});
|
||||
|
||||
const candidate = response.candidates?.[0];
|
||||
const imagePart = candidate?.content?.parts?.find(
|
||||
(p) => p.inlineData?.mimeType?.startsWith("image/")
|
||||
);
|
||||
|
||||
if (imagePart?.inlineData?.data) {
|
||||
return {
|
||||
imageUrl: `data:${imagePart.inlineData.mimeType};base64,${imagePart.inlineData.data}`,
|
||||
usage: response.usageMetadata as UsageMetadata | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn("No image part found in response");
|
||||
return { imageUrl: null, usage: response.usageMetadata as UsageMetadata | undefined };
|
||||
} catch (error) {
|
||||
console.error("generateImage error:", error);
|
||||
return { imageUrl: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token kullanımına dayalı Yapay Zeka Maliyetini hesapla
|
||||
* Giriş Maliyeti = (Giriş Tokenları / 1.000.000) * Giriş Fiyatı
|
||||
* Çıkış Maliyeti = (Çıkış Tokenları / 1.000.000) * Çıkış Fiyatı
|
||||
* @param usage - Yapay zeka yanıtından kullanım meta verileri
|
||||
* @returns Para birimi cinsinden toplam maliyet
|
||||
*/
|
||||
export async function calculateAICost(usage: UsageMetadata | null | undefined): Promise<number> {
|
||||
if (!usage) return 0;
|
||||
|
||||
const pricing = await getPricing();
|
||||
const inputTokens = usage.promptTokenCount || 0;
|
||||
const outputTokens = usage.candidatesTokenCount || 0;
|
||||
|
||||
const inputCost = (inputTokens / 1_000_000) * pricing.inputCost;
|
||||
const outputCost = (outputTokens / 1_000_000) * pricing.outputCost;
|
||||
|
||||
return inputCost + outputCost;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Language mapping utility for AI prompts
|
||||
* Maps locale codes to full language names for AI instructions
|
||||
*/
|
||||
|
||||
export const SUPPORTED_LOCALES = {
|
||||
tr: 'Turkish',
|
||||
en: 'English',
|
||||
de: 'German',
|
||||
ar: 'Arabic',
|
||||
zh: 'Chinese',
|
||||
es: 'Spanish',
|
||||
fr: 'French',
|
||||
it: 'Italian',
|
||||
ja: 'Japanese',
|
||||
ko: 'Korean',
|
||||
pt: 'Portuguese',
|
||||
ru: 'Russian',
|
||||
} as const;
|
||||
|
||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES;
|
||||
|
||||
/**
|
||||
* Get the full language name for AI prompts
|
||||
* @param locale - Locale code (e.g., 'tr', 'en')
|
||||
* @returns Full language name (e.g., 'Turkish', 'English')
|
||||
*/
|
||||
export function getLanguageName(locale?: string): string {
|
||||
if (!locale) return SUPPORTED_LOCALES.tr; // Default to Turkish
|
||||
|
||||
const normalizedLocale = locale.toLowerCase() as SupportedLocale;
|
||||
return SUPPORTED_LOCALES[normalizedLocale] || SUPPORTED_LOCALES.en; // Fallback to English
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language instruction for AI prompts
|
||||
* @param locale - Locale code
|
||||
* @returns Formatted instruction for AI (e.g., "Write in Turkish")
|
||||
*/
|
||||
export function getLanguageInstruction(locale?: string): string {
|
||||
const language = getLanguageName(locale);
|
||||
return `Write in ${language}.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a locale is supported
|
||||
* @param locale - Locale code to check
|
||||
* @returns True if supported, false otherwise
|
||||
*/
|
||||
export function isSupportedLocale(locale: string): locale is SupportedLocale {
|
||||
return locale.toLowerCase() in SUPPORTED_LOCALES;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Gemini için Fotoğrafçılık Taslağı (JSON Blueprint)
|
||||
* Bu yapı modeli profesyonel bir fotoğrafçı gibi düşünmeye zorlar.
|
||||
* Client-side güvenlidir.
|
||||
*/
|
||||
export const buildPhotorealisticPrompt = (corePrompt: string): string => {
|
||||
const blueprint = {
|
||||
subject_context: corePrompt,
|
||||
style: {
|
||||
direction: 'Professional Lifestyle Photography / Authentic Brand Content',
|
||||
aesthetic: 'Modern, bright, engaging, premium but approachable',
|
||||
lighting: 'Natural ambient daylight mixed with soft studio fill, warm tones',
|
||||
},
|
||||
camera_settings: {
|
||||
sensor: 'Full-frame Digital Sensor (Sony Alpha / Canon R5)',
|
||||
lens: '35mm or 50mm Prime (Standard Social Media View)',
|
||||
depth_of_field: 'Moderate depth of field (sharp subject, slightly softened background)',
|
||||
shutter: '1/125s natural motion freeze',
|
||||
},
|
||||
composition_rules: {
|
||||
framing: 'Rule of thirds, center-weighted for social media engagement',
|
||||
angle: 'Eye-level or 45-degree isometric (depending on subject)',
|
||||
aspect_ratio_fit: 'Optimized for Instagram/LinkedIn',
|
||||
},
|
||||
quality_assurance: {
|
||||
clarity: 'Perfect Focus',
|
||||
noise_level: 'Minimal natural grain allowed for authenticity',
|
||||
texture_detail: 'High fidelity materials',
|
||||
render_engine: 'Photorealistic Photography Style (Not CGI looking)',
|
||||
},
|
||||
prohibited_elements: ['text watermarks', 'blurry', 'distorted', 'ugly', 'low resolution'],
|
||||
};
|
||||
|
||||
return JSON.stringify(blueprint, null, 2);
|
||||
};
|
||||
Reference in New Issue
Block a user