Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user