apiKey = Setting::get('deepseek_api_key', ''); } public function isConfigured(): bool { return !empty($this->apiKey); } /** * Anime için Türkçe özet/açıklama üret. */ public function generateAnimeDescription(string $title, string $titleJp = '', string $genres = ''): ?string { $prompt = "Sen bir anime veritabanı editörüsün. Aşağıdaki anime için Türkçe, akıcı ve bilgilendirici bir özet/açıklama yaz (3-5 cümle, 120-220 kelime arası). Spoiler verme, merak uyandır.\n\n" . "Anime adı: {$title}" . ($titleJp ? " ({$titleJp})" : '') . "\n" . ($genres ? "Türler: {$genres}\n" : '') . "\nSadece açıklama metnini yaz, başka hiçbir şey ekleme."; return $this->call($prompt); } /** * Bölüm için Türkçe açıklama üret. */ public function generateEpisodeDescription(string $animeTitle, int $episodeNumber, string $episodeTitle = ''): ?string { $prompt = "Sen bir anime veritabanı editörüsün. Aşağıdaki anime bölümü için kısa, akıcı ve spoiler içermeyen Türkçe bir açıklama yaz (2-4 cümle, 80-160 kelime arası).\n\n" . "Anime: {$animeTitle}\n" . "Bölüm: {$episodeNumber}. Bölüm" . ($episodeTitle ? " — {$episodeTitle}" : '') . "\n\n" . "Sadece açıklama metnini yaz, başka hiçbir şey ekleme."; return $this->call($prompt); } /** * Anime için tüm meta verileri JSON olarak döndür. * Dönen alanlar: description, release_year, studio, type, status, rating, title_en, title_jp, genres[] */ public function generateAnimeMeta(string $title, string $titleJp = ''): ?array { $prompt = <<callJson($prompt); return $raw; } public function checkSpoiler(string $text): ?array { $result = $this->moderateComment($text); return ['is_spoiler' => $result['is_spoiler'], 'score' => $result['spoiler_score']]; } /** * Yorum moderasyonu: spoiler + küfür/hakaret kontrolü. * Döner: ['is_spoiler'=>bool, 'spoiler_score'=>int, 'is_rude'=>bool, 'rude_score'=>int] */ public function moderateComment(string $text): array { $default = ['is_spoiler' => false, 'spoiler_score' => 0, 'is_rude' => false, 'rude_score' => 0]; $prompt = "Aşağıdaki metin bir anime platformuna yazılmış kullanıcı yorumudur. İki şeyi kontrol et:\n" . "1) Anime bölümüne ait SPOILER içeriyor mu? (olay örgüsü açıklama, karakter ölümü, sürpriz sahne ifşası vb.)\n" . "2) KABA/HAKARET içeriyor mu? (küfür, nefret söylemi, ağır hakaret, cinsel içerik)\n\n" . "Sadece JSON döndür:\n" . "{\"is_spoiler\": false, \"spoiler_score\": 10, \"is_rude\": false, \"rude_score\": 5}\n" . "score değerleri 0-100 arası olasılık.\n\n" . "Metin: " . mb_substr($text, 0, 400); $raw = $this->callJson($prompt, 80); if (!$raw) return $default; return [ 'is_spoiler' => (bool)($raw['is_spoiler'] ?? false), 'spoiler_score' => (int)($raw['spoiler_score'] ?? $raw['score'] ?? 0), 'is_rude' => (bool)($raw['is_rude'] ?? false), 'rude_score' => (int)($raw['rude_score'] ?? 0), ]; } public string $lastError = ''; private function callJson(string $prompt, int $maxTokens = 600): ?array { if (!$this->isConfigured()) { $this->lastError = 'API anahtarı ayarlanmamış'; return null; } try { $response = Http::withToken($this->apiKey) ->timeout(90) ->post('https://api.deepseek.com/chat/completions', [ 'model' => 'deepseek-chat', 'messages' => [['role' => 'user', 'content' => $prompt]], 'max_tokens' => $maxTokens, 'temperature' => 0.3, 'response_format' => ['type' => 'json_object'], ]); if (!$response->successful()) { $this->lastError = 'HTTP ' . $response->status() . ': ' . $response->json('error.message', $response->body()); \Log::error('DeepSeek API hatası', ['status' => $response->status(), 'body' => $response->body()]); return null; } $content = trim($response->json('choices.0.message.content', '')); if (!$content) { $this->lastError = 'API boş yanıt döndürdü'; return null; } $content = preg_replace('/^```json\s*/i', '', $content); $content = preg_replace('/\s*```$/i', '', $content); $data = json_decode($content, true); if (!is_array($data)) { $this->lastError = 'JSON parse hatası: ' . substr($content, 0, 200); return null; } return $data; } catch (\Exception $e) { $this->lastError = $e->getMessage(); \Log::error('DeepSeek exception', ['message' => $e->getMessage()]); return null; } } // ── Frontend AI methods ────────────────────────────────────────────────── /** * Anime kataloğunu AI context string olarak döndür (1 saat önbellek). */ public function getAnimeContext(): string { return \Illuminate\Support\Facades\Cache::remember('ai_anime_context', 3600, function () { $animes = \App\Models\Anime::where('is_published', true) ->with('genres:id,name') ->get(['id', 'title', 'type', 'status', 'rating', 'release_year']); return $animes->map(function ($a) { $genres = $a->genres->pluck('name')->join(', '); $type = $a->type === 'movie' ? 'Film' : 'Dizi'; return "ID:{$a->id}|{$a->title}|{$type}|{$a->release_year}|{$a->rating}" . ($genres ? "|{$genres}" : ''); })->join("\n"); }); } /** * Çok turlu sohbet (sistem mesajı + anime kataloğu ile). * $messages = [['role'=>'user','content'=>'...'], ...] */ public function chat(array $messages, string $animeContext = ''): ?string { $sys = "Sen Animexe'nin AI anime asistanısın. Animexe, Türkçe altyazılı/dublajlı ücretsiz anime izleme platformudur (animexe.com).\n\n"; $sys .= "== PLATFORM BİLGİLERİ (kullanıcı sorarsa bunları kullan) ==\n" . "- Kayıt: Ücretsiz, e-posta ile. Kayıt olmadan bazı içerikler kısıtlı.\n" . "- Premium üyelik: Aylık ücretli. Avantajları: reklamsız izleme, 1080p HD, erken bölüm erişimi.\n" . "- Altyazı/Dublaj: Türkçe altyazı ve Türkçe dublaj seçenekleri mevcuttur. Player'da seçilebilir.\n" . "- Takip/Favori: Anime sayfasında kalp veya 'Takip' butonuna tıkla. Yeni bölüm bildirimi gelir.\n" . "- İzleme geçmişi: Otomatik kaydedilir. Profil > Geçmiş kısmından görebilirsin.\n" . "- Arama: Üst menüdeki arama kutusuna anime adını yaz.\n" . "- Anime isteği: 'Anime İste' sayfasından eksik animeleri talep edebilirsin.\n" . "- Mobil: Tarayıcıdan tam destek. Android uygulaması da mevcut.\n" . "- Dil seçimi: Player'da ses ve altyazı dili değiştirilebilir.\n" . "- Yorumlar: Her bölümün altında yorum yapılabilir, spoiler işaretlenebilir.\n\n"; if ($animeContext) { $sys .= "== PLATFORM KATALOĞU (ID|Başlık|Tip|Yıl|Puan|Türler) ==\n{$animeContext}\n\n"; } $sys .= "== KURALLAR ==\n" . "- Türkçe, samimi, kısa ve net cevap ver. Emoji kullanabilirsin.\n" . "- Sadece platformdaki animeleri öner (katalogdan ID'si olan).\n" . "- Spoiler verme. Merak uyandır.\n" . "- Anime önerirken cevabının en sonuna şu formatı ekle (başka yere koyma): [SUGGEST:id1,id2,id3]\n" . " Örnek: 'Attack on Titan harika! [SUGGEST:42]' — max 5 anime ID.\n" . "- Eğer anime önermiyorsan [SUGGEST:...] satırını HİÇ EKLEME.\n" . "- Site hakkında soruları yukarıdaki platform bilgilerini kullanarak cevapla.\n"; $apiMessages = array_merge( [['role' => 'system', 'content' => $sys]], array_slice($messages, -12) ); return $this->callMessages($apiMessages, 700); } /** * Kullanıcı tercihlerine göre 6 anime öner. * Döner: [['id'=>1,'reason'=>'...'], ...] */ public function recommend(string $preferences, array $animes): ?array { $list = implode("\n", array_map(function ($a) { $genres = isset($a['genres']) ? implode(', ', array_column($a['genres'], 'name')) : ''; return "ID:{$a['id']}|{$a['title']}|" . ($a['type'] === 'movie' ? 'Film' : 'Dizi') . "|{$a['release_year']}|{$a['rating']}" . ($genres ? "|{$genres}" : ''); }, array_slice($animes, 0, 250))); $prompt = "Anime öneri sistemi: Kullanıcı tercihlerine göre listeden EN İYİ 6 animeyi seç.\n\n" . "Tercihler:\n{$preferences}\n\n" . "Animeler:\n{$list}\n\n" . "Yanıt: {\"recommendations\":[{\"id\":1,\"reason\":\"Kısa Türkçe neden (max 12 kelime)\"}]}\n" . "SADECE JSON."; $result = $this->callJson($prompt, 500); if (!is_array($result)) return null; if (isset($result['recommendations']) && is_array($result['recommendations'])) { return $result['recommendations']; } if (isset($result[0]['id'])) return $result; return null; } /** * Doğal dil sorgusu ile anime ara. * Döner: [id1, id2, ...] */ public function naturalSearch(string $query, array $animes): ?array { $list = implode("\n", array_map(function ($a) { $genres = isset($a['genres']) ? implode(', ', array_column($a['genres'], 'name')) : ''; return "ID:{$a['id']}|{$a['title']}" . ($genres ? "|{$genres}" : ''); }, array_slice($animes, 0, 300))); $prompt = "Kullanıcı sorgusu: \"{$query}\"\n\nAnimeler:\n{$list}\n\n" . "En uygun max 12 animeyi bul: {\"ids\":[1,5,12]}\nSADECE JSON."; $result = $this->callJson($prompt, 150); if (!is_array($result)) return null; if (isset($result['ids']) && is_array($result['ids'])) return array_map('intval', $result['ids']); return null; } /** * Bölüm hakkında spoilersız AI analizi. */ public function episodeInfo(string $animeTitle, int $episodeNumber, string $episodeTitle = '', string $description = ''): ?string { $prompt = "Sen bir anime uzmanısın. Aşağıdaki bölüm hakkında Türkçe, kısa ve ilgi çekici bir analiz yaz (3-4 cümle). " . "Spoiler içerme. Bölümün atmosferini, önemini ve izleyiciyi neden heyecanlandırabileceğini anlat.\n\n" . "Anime: {$animeTitle}\n" . "Bölüm: {$episodeNumber}." . ($episodeTitle ? " — {$episodeTitle}" : '') . "\n" . ($description ? "Açıklama: {$description}\n" : '') . "\nSadece analiz metnini yaz."; return $this->call($prompt, 350); } // ── Blog Generation ────────────────────────────────────────────────────── /** * Bir anime için SEO blog yazısı üret. * Döner: ['title','slug','excerpt','content','focus_keyword','meta_description','faq','linked_slugs'] */ public function generateBlogPost(\App\Models\Anime $anime, array $relatedAnimes = []): ?array { $genreList = $anime->genres->pluck('name')->join(', '); $type = $anime->type === 'movie' ? 'anime film' : 'anime dizi'; $year = $anime->release_year ?? ''; $status = match($anime->status ?? '') { 'ongoing' => 'devam ediyor', 'completed' => 'tamamlandı', 'upcoming' => 'yakında çıkacak', default => '', }; $relatedList = ''; if (!empty($relatedAnimes)) { $relatedList = "\nİlgili animeler (içerik içinde bunlara link ver, format: [LINK:slug]Anime Adı[/LINK]):\n"; foreach (array_slice($relatedAnimes, 0, 5) as $r) { $relatedList .= "- {$r['slug']}: {$r['title']}\n"; } } $prompt = <<title} - Tür: {$type} - Yıl: {$year} - Türler: {$genreList} - Durum: {$status} - Açıklama: {$anime->description} {$relatedList} Blog yazısı gereksinimleri: 1. 400-600 kelime, sade ve akıcı Türkçe 2. HTML formatında yaz:

,

,

    ,
  • , etiketleri kullan 3. Yapı: Giriş (1-2 paragraf) → Ana içerik (2 H2 bölümü) → FAQ (2 soru-cevap,

    ...

    ...

    ) 4. {$anime->title} anahtar kelimesini doğal şekilde 3-5 kez kullan 5. İlgili animelere [LINK:slug]Anime Adı[/LINK] formatında link ekle (varsa, max 2) 6. Spoiler verme, merak uyandır 7. Sonunda kısa bir CTA ekle Yanıt JSON formatında: {{ "title": "SEO başlığı (50-60 karakter, anime adını içermeli)", "excerpt": "Meta description (150-160 karakter)", "focus_keyword": "Ana anahtar kelime", "meta_description": "SEO meta açıklaması (150-160 karakter)", "content": "Tam HTML blog içeriği", "faq": [ {{"q": "Soru", "a": "Cevap"}}, {{"q": "Soru", "a": "Cevap"}}, {{"q": "Soru", "a": "Cevap"}} ], "linked_slugs": ["slug1", "slug2"] }} SADECE geçerli JSON döndür. PROMPT; return $this->callJson($prompt, 4096); } // ── SEO AI Methods ─────────────────────────────────────────────────────── /** * SEO danışman sohbeti — sitenin kontekstini bilen uzman */ public function seoChat(array $messages, array $siteContext = []): ?string { $stats = $siteContext; $sys = << 'system', 'content' => $sys]], array_slice($messages, -16) ); return $this->callMessages($apiMessages, 800); } /** * Anime için AI destekli SEO başlığı + meta açıklama üret */ public function generateAnimeSeoMeta(\App\Models\Anime $anime): ?array { $genres = $anime->genres?->pluck('name')->join(', ') ?? ''; $type = $anime->type === 'movie' ? 'anime film' : ($anime->type === 'ova' ? 'OVA' : 'anime dizi'); $year = $anime->release_year ?? ''; $desc = $anime->description ? mb_substr(strip_tags($anime->description), 0, 300) : ''; $prompt = <<title} - Tür: {$type} - Yıl: {$year} - Kategoriler: {$genres} - Açıklama: {$desc} Kurallar: - SEO Başlığı: 50-65 karakter, anahtar kelimeyi başa al, duygusal tetikleyici ekle, "Türkçe" kelimesi kullan - Meta Açıklama: 145-158 karakter, aksiyon çağrısı içersin, "ücretsiz", "HD" gibi değer önerileri ekle - Anahtar Kelimeler: 3-5 adet, virgülle ayrılmış, Türkçe arama trendlerine uygun - Kullanıcı niyeti: anime izlemek isteyen Türkçe konuşan kullanıcılar JSON formatında yanıt ver: { "seo_title": "...", "seo_meta_desc": "...", "seo_keywords": "...", "primary_keyword": "...", "search_intent": "transactional|informational|navigational" } SADECE JSON. PROMPT; return $this->callJson($prompt, 400); } /** * Belirli bir konu için anahtar kelime önerileri */ public function suggestKeywords(string $topic, string $niche = 'anime'): ?array { $prompt = <<callJson($prompt, 1200); } /** * URL'nin SEO sorunlarını AI ile analiz et */ public function analyzePageSeo(string $url, string $title, string $description, string $content): ?array { $contentSnippet = mb_substr(strip_tags($content), 0, 500); $prompt = <<callJson($prompt, 800); } /** * Anime için FAQ Schema (JSON-LD) üret */ public function generateFaqSchema(\App\Models\Anime $anime): ?array { $type = $anime->type === 'movie' ? 'film' : 'dizi'; $desc = $anime->description ? mb_substr(strip_tags($anime->description), 0, 200) : $anime->title; $genres = $anime->genres?->pluck('name')->join(', ') ?? ''; $prompt = <<title} ({$type}) Kategoriler: {$genres} Açıklama: {$desc} Kurallar: - Kullanıcıların gerçekten sorduğu sorular ("nerede izlenir", "kaç bölüm", "türkçe var mı" gibi) - Cevaplar 1-3 cümle, net ve bilgilendirici - Türkçe - Animexe platformuna yönlendiren cevaplar (animexe.com'da izleyebilirsiniz) JSON formatı: { "faqs": [ {"question": "...", "answer": "..."}, ... ] } SADECE JSON. PROMPT; return $this->callJson($prompt, 600); } /** * Site için genel içerik stratejisi öner */ public function generateContentStrategy(array $siteStats, array $weakKeywords = []): ?string { $kwList = !empty($weakKeywords) ? implode(', ', array_slice($weakKeywords, 0, 10)) : 'genel anime'; $score = $siteStats['seo_score'] ?? 0; $total = $siteStats['anime_count'] ?? 0; $covered = $siteStats['seo_covered'] ?? 0; $prompt = <<call($prompt, 1200); } /** * Robots.txt için AI önerisi */ public function generateRobotsTxt(string $domain, array $paths = []): ?string { $pathList = !empty($paths) ? implode(', ', $paths) : '/admin, /api, /storage, /profile'; $prompt = "Sen bir teknik SEO uzmanısın. {$domain} domaini için optimal robots.txt içeriği oluştur. " . "Platform bir anime izleme sitesi. Korunacak dizinler: {$pathList}. " . "Sadece robots.txt içeriğini döndür, açıklama ekleme."; return $this->call($prompt, 300); } /** * Keşfet sayfası için anime başına kısa, çekici hook metni üret (2 cümle, spoiler yok). */ public function generateDiscoveryHook(\App\Models\Anime $anime): ?string { $genres = $anime->relationLoaded('genres') ? $anime->genres->pluck('name')->join(', ') : ''; $info = $anime->title; if ($anime->release_year) $info .= " ({$anime->release_year})"; if ($genres) $info .= ", {$genres}"; if ($anime->description) $info .= ". " . \Illuminate\Support\Str::limit(strip_tags($anime->description), 180); $prompt = "Sen bir anime tanıtım yazarısın. Aşağıdaki anime için 1-2 cümlelik, merak uyandırıcı, spoiler içermeyen Türkçe bir tanıtım yaz. Emoji kullanma. Sadece tanıtım metnini yaz.\n\n{$info}"; return $this->call($prompt, 80); } // ── Private helpers ────────────────────────────────────────────────────── private function callMessages(array $messages, int $maxTokens = 400): ?string { if (!$this->isConfigured()) return null; try { $r = Http::withToken($this->apiKey)->timeout(35) ->post('https://api.deepseek.com/chat/completions', [ 'model' => 'deepseek-chat', 'messages' => $messages, 'max_tokens' => $maxTokens, 'temperature' => 0.75, ]); if (!$r->successful()) return null; return trim($r->json('choices.0.message.content', '')) ?: null; } catch (\Exception $e) { return null; } } private function call(string $prompt, int $maxTokens = 400): ?string { if (!$this->isConfigured()) { return null; } try { $response = Http::withToken($this->apiKey) ->timeout(30) ->post('https://api.deepseek.com/chat/completions', [ 'model' => 'deepseek-chat', 'messages' => [['role' => 'user', 'content' => $prompt]], 'max_tokens' => $maxTokens, 'temperature' => 0.7, ]); if (!$response->successful()) { return null; } return trim($response->json('choices.0.message.content', '')) ?: null; } catch (\Exception $e) { return null; } } }