637 lines
26 KiB
PHP
637 lines
26 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Setting;
|
||
use Illuminate\Support\Facades\Http;
|
||
|
||
class DeepSeekService
|
||
{
|
||
private string $apiKey;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->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 = <<<PROMPT
|
||
Sen bir anime veritabanı asistanısın. Aşağıdaki anime hakkında tüm bilgileri JSON formatında döndür.
|
||
Anime adı: {$title}
|
||
{$titleJp}
|
||
Döndüreceğin JSON şöyle olmalı (bilinmiyorsa null):
|
||
{
|
||
"description": "Türkçe, akıcı, spoiler içermeyen 3-5 cümlelik özet (120-220 kelime)",
|
||
"release_year": 2021,
|
||
"studio": "Stüdyo adı",
|
||
"type": "series|movie|ova|ona|special",
|
||
"status": "completed|ongoing|upcoming",
|
||
"rating": 8.5,
|
||
"title_en": "İngilizce başlık",
|
||
"title_jp": "日本語タイトル",
|
||
"genres": ["Aksiyon", "Drama"]
|
||
}
|
||
Genres listesi için sadece şu kategorileri kullan (uygunları seç): Aksiyon, Macera, Komedi, Drama, Fantezi, Bilim Kurgu, Korku, Romantizm, Spor, Supernatural, Gerilim, Slice of Life, Mecha, Müzik, Tarihsel, Okul, Isekai, Ecchi, Shounen, Shoujo, Seinen, Josei, Dövüş Sanatları.
|
||
SADECE geçerli JSON döndür, hiçbir açıklama veya markdown ekleme.
|
||
PROMPT;
|
||
|
||
$raw = $this->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 = <<<PROMPT
|
||
Sen Animexe platformu için SEO uyumlu Türkçe blog yazıları yazan bir editörsün.
|
||
Animexe, Türkçe altyazılı/dublajlı anime izleme platformudur.
|
||
|
||
Aşağıdaki anime için çok kapsamlı, SEO uyumlu bir blog yazısı yaz:
|
||
- Anime adı: {$anime->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: <h2>, <p>, <ul>, <li>, <strong> etiketleri kullan
|
||
3. Yapı: Giriş (1-2 paragraf) → Ana içerik (2 H2 bölümü) → FAQ (2 soru-cevap, <div class="faq-item"><h3>...</h3><p>...</p></div>)
|
||
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 = <<<SYS
|
||
Sen Animexe için çalışan kıdemli bir SEO danışmanısın. Animexe, Türkçe altyazılı ve dublajlı anime izleme platformudur (animexe.com).
|
||
|
||
Site istatistikleri:
|
||
- Yayında anime sayısı: {$stats['anime_count']}
|
||
- SEO başlığı olan anime: {$stats['seo_covered']} / {$stats['anime_count']}
|
||
- SEO skoru: {$stats['seo_score']}/100
|
||
- Sitemap URL sayısı: {$stats['sitemap_urls']}
|
||
|
||
Uzmanlık alanların:
|
||
- Teknik SEO (Core Web Vitals, canonicals, structured data)
|
||
- İçerik SEO (keyword araştırması, meta etiketler, başlık optimizasyonu)
|
||
- Yerel SEO (Türkiye pazarı için anime arama trendleri)
|
||
- E-A-T (Expertise, Authoritativeness, Trustworthiness)
|
||
- Schema.org (TVSeries, VideoObject, FAQPage, BreadcrumbList, Organization)
|
||
- Backlink stratejisi ve internal linking
|
||
|
||
Kurallar:
|
||
- Türkçe cevap ver, kısa ve aksiyona yönelik ol
|
||
- Spesifik, ölçülebilir öneriler sun
|
||
- Animexe'nin bulunduğu nişe (anime, Türkiye, ücretsiz izleme) odaklan
|
||
- Rakip analizi yapabilirsin: anizm.tv, turkanime.co, animeshark.net
|
||
- Markdown kullanabilirsin (bold, liste, başlık)
|
||
SYS;
|
||
|
||
$apiMessages = array_merge(
|
||
[['role' => '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 = <<<PROMPT
|
||
Sen bir SEO uzmanısın. Aşağıdaki anime için Türkiye pazarına yönelik, tıklanma oranını (CTR) maksimize eden SEO başlığı ve meta açıklaması yaz.
|
||
|
||
Anime bilgileri:
|
||
- Ad: {$anime->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 = <<<PROMPT
|
||
Sen bir keyword araştırma uzmanısın. "{$topic}" konusu için Türkiye pazarında arama yapan kullanıcıların kullandığı anahtar kelimeleri bul.
|
||
|
||
Platform nişi: {$niche} izleme platformu (Türkçe altyazılı, ücretsiz)
|
||
|
||
Şu kategorilerde kelimeler öner:
|
||
1. Head keywords (yüksek hacim, yüksek rekabet) - 3 adet
|
||
2. Body keywords (orta hacim, orta rekabet) - 4 adet
|
||
3. Long-tail keywords (düşük hacim, düşük rekabet, yüksek niyet) - 5 adet
|
||
4. Question keywords (soru formatı, featured snippet fırsatı) - 3 adet
|
||
|
||
JSON formatı:
|
||
{
|
||
"head": [{"keyword":"...", "volume":"Yüksek/Orta/Düşük", "difficulty":75, "intent":"..."}],
|
||
"body": [...],
|
||
"long_tail": [...],
|
||
"questions": [...],
|
||
"content_ideas": ["Bu kelimeler için 3 içerik fikri"]
|
||
}
|
||
SADECE JSON.
|
||
PROMPT;
|
||
|
||
return $this->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 = <<<PROMPT
|
||
Sen bir teknik SEO denetçisisin. Aşağıdaki web sayfasını SEO açısından analiz et.
|
||
|
||
URL: {$url}
|
||
Sayfa Başlığı: {$title}
|
||
Meta Açıklama: {$description}
|
||
İçerik (ilk 500 karakter): {$contentSnippet}
|
||
|
||
Şunları değerlendir ve JSON döndür:
|
||
{
|
||
"overall_score": 75,
|
||
"title_analysis": {"score":80, "issues":["Sorun1"], "suggestions":["Öneri1"]},
|
||
"description_analysis": {"score":70, "issues":[], "suggestions":[]},
|
||
"content_analysis": {"score":65, "keyword_density":"iyi/zayıf/aşırı", "issues":[], "suggestions":[]},
|
||
"technical_issues": ["Liste halinde teknik sorunlar"],
|
||
"quick_wins": ["Hemen yapılabilecek 3-5 iyileştirme"],
|
||
"priority_actions": ["Öncelikli aksiyonlar (sıralı)"]
|
||
}
|
||
SADECE JSON.
|
||
PROMPT;
|
||
|
||
return $this->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 = <<<PROMPT
|
||
Sen bir schema.org uzmanısın. Aşağıdaki anime sayfası için Google'ın featured snippet'larını hedefleyen 5 adet FAQPage sorusu ve cevabı yaz.
|
||
|
||
Anime: {$anime->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 = <<<PROMPT
|
||
Sen Animexe için çalışan bir SEO içerik stratejisti olarak, 90 günlük bir SEO eylem planı hazırla.
|
||
|
||
Mevcut durum:
|
||
- SEO skoru: {$score}/100
|
||
- Toplam anime: {$total}
|
||
- SEO başlığı olan: {$covered}/{$total}
|
||
- Zayıf kelimeler / fırsatlar: {$kwList}
|
||
- Platform: Türkçe anime izleme (animexe.com)
|
||
- Rakipler: anizm.tv, turkanime.co
|
||
|
||
Plan şunları içersin:
|
||
1. **İlk 30 gün — Teknik temel** (hızlı kazançlar)
|
||
2. **31-60 gün — İçerik genişletme** (keyword hedefleme)
|
||
3. **61-90 gün — Otorite inşası** (backlink, sosyal sinyal)
|
||
4. **KPI hedefleri** (organik trafik artışı, sıralama hedefleri)
|
||
5. **Öncelikli içerik türleri** (anime review, top listeler, karşılaştırma yazıları)
|
||
|
||
Markdown formatında yaz. Somut, ölçülebilir ve Türkiye pazarına özgü ol.
|
||
PROMPT;
|
||
|
||
return $this->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;
|
||
}
|
||
}
|
||
}
|