import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { GeminiService } from "../gemini/gemini.service"; import { PredictionCardDto } from "./dto/prediction-card.dto"; import axios from "axios"; const SYSTEM_PROMPT = `Sen profesyonel bir spor analisti ve sosyal medya içerik üreticisisin. Verilen maç tahmin verisini kullanarak kısa, etkili ve ilgi çekici sosyal medya postları yazıyorsun. KURALLAR: - Türkçe yaz - Maximum 250 karakter (X/Twitter uyumlu) - Emoji kullan ama abartma (2-4 emoji yeterli) - Skor tahminini vurgula - Güven yüzdesini belirt - Lig, ülke, takım adları ve ana tahminleri SEO için doğal şekilde geçir - İlgili hashtag'leri ekle (#PremierLeague, #SüperLig vb.) - KESİNLİKLE "kesin kazanır", "garanti" gibi ifadeler KULLANMA - "Tahminimiz", "Beklentimiz", "Analizimiz" gibi ifadeler kullan - Farklı maçlar için farklı tarzda yaz, tekdüze olma - Son satıra her zaman hashtag'leri koy`; @Injectable() export class CaptionGeneratorService { private readonly logger = new Logger(CaptionGeneratorService.name); private readonly ollamaBaseUrl: string; private readonly ollamaModel: string; constructor( private readonly geminiService: GeminiService, private readonly configService: ConfigService, ) { this.ollamaBaseUrl = this.configService.get("OLLAMA_BASE_URL") || "http://localhost:11434"; this.ollamaModel = this.configService.get("OLLAMA_MODEL") || this.configService.get("SOCIAL_POSTER_OLLAMA_MODEL") || ""; } /** * Generate a social media caption for a match prediction using Gemini AI. */ async generateCaption(card: PredictionCardDto): Promise { if (this.ollamaModel) { const caption = await this.generateWithOllama(card); if (caption) return caption; } if (!this.geminiService.isAvailable()) { this.logger.warn("Gemini not available, using template caption"); return this.generateFallbackCaption(card); } const prompt = this.buildPrompt(card); try { const { text } = await this.geminiService.generateText(prompt, { systemPrompt: SYSTEM_PROMPT, temperature: 0.8, maxTokens: 300, }); // Ensure hashtags are present const caption = this.ensureHashtags(text, card); this.logger.log( `Caption generated for ${card.homeTeam} vs ${card.awayTeam}`, ); return caption; } catch (error) { this.logger.error("Gemini caption generation failed", error); return this.generateFallbackCaption(card); } } private async generateWithOllama(card: PredictionCardDto): Promise { const prompt = `${SYSTEM_PROMPT} ${this.buildPrompt(card)}`; try { const response = await axios.post( `${this.ollamaBaseUrl.replace(/\/$/, "")}/api/generate`, { model: this.ollamaModel, prompt, stream: false, options: { temperature: 0.7, num_predict: 260, }, }, { timeout: 20000 }, ); const text = String(response.data?.response || "").trim(); if (!text) return ""; this.logger.log( `Ollama caption generated for ${card.homeTeam} vs ${card.awayTeam}`, ); return this.ensureHashtags(text, card); } catch (error) { this.logger.warn(`Ollama caption generation failed: ${error.message}`); return ""; } } private buildPrompt(card: PredictionCardDto): string { const topPicksText = card.topPicks .map( (p, i) => `${i + 1}. ${p.market} (${p.marketEn}) — ${p.pick} — Güven: %${p.confidence} — Oran: ${p.odds}`, ) .join("\n"); return `Aşağıdaki maç tahmin verisini kullanarak bir sosyal medya postu oluştur: MAÇ: ${card.homeTeam} vs ${card.awayTeam} SPOR: ${card.sport === "basketball" ? "Basketbol" : "Futbol"} LİG: ${card.leagueName} ÜLKE/BÖLGE: ${card.countryName || "-"} TARİH: ${card.matchDate} ${card.sport === "basketball" ? "İLK DEVRE" : "İLK YARI"} SKOR TAHMİNİ: ${card.htScore} MAÇ SONU SKOR TAHMİNİ: ${card.ftScore} SKOR GÜVEN: %${card.scoreConfidence} RİSK SEVİYESİ: ${card.riskLevel} EN İYİ TAHMİNLER: ${topPicksText} Sadece post metnini yaz, başka hiçbir şey ekleme.`; } private ensureHashtags(text: string, card: PredictionCardDto): string { // If no hashtags in text, add them if (!text.includes("#")) { const leagueTag = card.leagueName .replace(/\s+/g, "") .replace(/[^a-zA-Z0-9üöçşğıİÜÖÇŞĞ]/g, ""); const homeTag = card.homeTeam.replace(/\s+/g, ""); const awayTag = card.awayTeam.replace(/\s+/g, ""); const sportTag = card.sport === "basketball" ? "Basketbol" : "Futbol"; text += `\n\n#${leagueTag} #${homeTag} #${awayTag} #${sportTag}`; } return text.trim(); } /** * Fallback caption when Gemini is not available. */ private generateFallbackCaption(card: PredictionCardDto): string { const topPick = card.topPicks[0]; const leagueTag = card.leagueName .replace(/\s+/g, "") .replace(/[^a-zA-Z0-9üöçşğıİÜÖÇŞĞ]/g, ""); const sportLabel = card.sport === "basketball" ? "Basketbol" : "Futbol"; const halfLabel = card.sport === "basketball" ? "İD" : "İY"; return `⚡ ${card.leagueName}${card.countryName ? ` (${card.countryName})` : ""}: ${card.homeTeam} vs ${card.awayTeam} 🎯 ${sportLabel} tahminimiz: ${card.ftScore} (${halfLabel}: ${card.htScore}) 📊 Güven: %${card.scoreConfidence} ${topPick ? `🔥 ${topPick.market}: ${topPick.pick} (%${topPick.confidence})` : ""} #${leagueTag} #${sportLabel} #MaçTahmini #iddaai`.trim(); } }