Files
iddaai-be/src/modules/social-poster/caption-generator.service.ts
T
fahricansecer 5645b38f20
Deploy Iddaai Backend / build-and-deploy (push) Successful in 32s
main
2026-05-05 17:09:11 +03:00

170 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string>("OLLAMA_BASE_URL") ||
"http://localhost:11434";
this.ollamaModel =
this.configService.get<string>("OLLAMA_MODEL") ||
this.configService.get<string>("SOCIAL_POSTER_OLLAMA_MODEL") ||
"";
}
/**
* Generate a social media caption for a match prediction using Gemini AI.
*/
async generateCaption(card: PredictionCardDto): Promise<string> {
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<string> {
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();
}
}