feat: SEO Power Engine backend updates and remove temp media files
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-04-30 16:57:46 +02:00
parent 7745102584
commit 35bfc311e7
14 changed files with 828 additions and 88 deletions
+205 -63
View File
@@ -1,6 +1,7 @@
import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GoogleGenAI } from '@google/genai';
import { jsonrepair } from 'jsonrepair';
export interface ScriptGenerationInput {
topic: string;
@@ -105,6 +106,8 @@ export interface SeoMetadata {
description: string;
keywords: string[];
hashtags: string[];
trendingHashtags?: string[]; // Trend hashtag'ler (AI tahmini)
estimatedSearchVolume?: string; // Anahtar kelimenin tahmini arama hacmi (AI tahmini)
schemaMarkup: Record<string, unknown>;
}
@@ -117,6 +120,8 @@ export interface GeneratedScript {
hashtags: string[];
};
seo: SeoMetadata;
seoTitleAlternatives: string[]; // 5 alternatif SEO başlığı (CTR sıralı)
seoScore: number; // 0-100 arası SEO güç skoru
scenes: GeneratedScene[];
musicPrompt: string;
musicStyle: string; // AudioCraft: genre/mood tanımı
@@ -170,13 +175,45 @@ HUMAN WRITING (anti-AI detection):
- Have opinions. React to facts, don't just report them
- Acknowledge uncertainty: "I'm not sure how to feel about this" is more human than listing pros/cons neutrally
SEO OPTIMIZATION:
- Video title: Primary keyword within first 3 words, under 60 characters
- Description: 2-3 secondary keywords naturally woven in, 150-200 chars
- Keywords: 8-12 LSI keywords related to the main topic
- Hashtags: 5-8 hashtags, mix of broad (#Shorts) and niche-specific
SEO OPTIMIZATION (CRITICAL — REVENUE DRIVER):
SEO TITLE STRATEGY — Generate exactly 5 alternative SEO-optimized titles:
- Each title MUST use a DIFFERENT hook strategy:
1. Curiosity Gap: "The [Topic] Secret Nobody Tells You 🤯"
2. Data-Driven: "[Number] [Topic] Facts That Will Blow Your Mind 🔥"
3. How-To/Listicle: "How [Topic] Actually Works (Explained) ⚡"
4. Emotional: "Why [Topic] Changes Everything You Thought You Knew 😱"
5. Contrarian: "[Common Belief About Topic] Is Dead Wrong 💀"
- Primary keyword within the FIRST 3 WORDS of every title
- Maximum 60 characters per title (YouTube optimal cut-off)
- EMOJI STRATEGY: Each title MUST contain 1-2 strategic emojis (🔥⚡🤯😱💀🚀💡🎯✅❌) placed at the end or mid-title to boost CTR by 15-20%
- Rank them by estimated CTR (Click-Through Rate) — BEST title FIRST
- Titles must be in the TARGET LANGUAGE
- The BEST title (index 0) will automatically become the project title
SEO SCORE — Calculate an seoScore (0-100) based on:
- Keyword placement in title first 3 words: 30 points
- Curiosity/click-bait factor: 25 points
- Specificity (numbers, names, data): 20 points
- Emotional trigger words: 15 points
- Title length optimization (40-60 chars): 10 points
SEO KEYWORDS & SEARCH VOLUME:
- Generate 12-15 LSI (Latent Semantic Indexing) keywords
- For each keyword, estimate relative search volume as: "HIGH", "MEDIUM", or "LOW"
- Provide this as the seo.estimatedSearchVolume field: a brief summary like "Primary keyword 'elon musk' has HIGH search volume (~500K monthly). Secondary keywords average MEDIUM volume."
- Include both short-tail (1-2 words) and long-tail (3-5 words) keywords
HASHTAG STRATEGY:
- Generate 5-8 standard hashtags (mix of broad like #Shorts and niche-specific)
- Additionally generate 3-5 trendingHashtags: hashtags you estimate to be currently trending on YouTube/TikTok/Instagram based on the topic's virality, seasonality, and cultural relevance
- Mark trending hashtags separately in seo.trendingHashtags array
SEO DESCRIPTION:
- Meta description: 150-200 chars, includes 2-3 secondary keywords naturally woven in
- Schema markup hint for VideoObject structured data
MEASUREMENTS:
- ALWAYS use the METRIC SYSTEM for any measurements (e.g. centimeters, meters, kilograms, liters) in the generated content.
- NEVER use imperial units like feet or inches. Translate them to metric if present in the source.
@@ -237,28 +274,29 @@ Mood reference options (use 2-3 per scene combined):
CRITICAL RULE: Once you establish a visual universe in Scene 1 (based on the provided Style DNA), ALL subsequent scenes MUST stay within that EXACT same visual world. Do not randomly switch styles between scenes.
━━━ LAYER 3: LIGHTING (Source, Direction, Quality) ━━━
Lighting is the single most important factor in image quality. Never leave it to chance.
Lighting is the single most important factor in image quality. Never leave it to chance. You MUST explicitly write "Lighting: [details]" in your prompt.
You must specify THREE lighting properties:
You must specify THREE lighting properties explicitly:
A) SOURCE — Where is the light coming from? (sun, neon signs, candle, spotlight, overcast sky, monitor glow)
B) DIRECTION — From which side relative to camera? (from camera-left, backlighting from behind subject, overhead, from below, rim light from behind-right)
C) QUALITY — How does the light feel? (soft and diffused through curtains, harsh and directional, dappled through tree leaves, warm golden, cold blue-white clinical)
❌ BAD: "good lighting" or just "golden hour"
✅ GOOD: "Late golden hour sunlight raking across the scene from camera-right at a low 15-degree angle, casting long dramatic shadows to the left, warm amber (3200K) backlighting creating a bright rim-light halo around the subject's silhouette, fill light bouncing softly off a nearby white wall on camera-left"
✅ GOOD: "Harsh overhead fluorescent tubes casting unflattering blue-white (6500K) light with hard-edged shadows directly below every object, a single warm desk lamp in the foreground creating a small pool of amber light that contrasts with the cold clinical environment"
✅ GOOD: "Diffused overcast daylight filtering through floor-to-ceiling frosted glass panels, creating even, shadowless illumination with a soft pearl-gray quality, punctuated by a single beam of direct sunlight breaking through a gap in the clouds and hitting the subject's hands"
✅ GOOD: "Lighting: Late golden hour sunlight raking across the scene from camera-right at a low 15-degree angle, casting long dramatic shadows to the left, warm amber (3200K) backlighting creating a bright rim-light halo around the subject's silhouette, fill light bouncing softly off a nearby white wall on camera-left"
✅ GOOD: "Lighting: Harsh overhead fluorescent tubes casting unflattering blue-white (6500K) light with hard-edged shadows directly below every object, a single warm desk lamp in the foreground creating a small pool of amber light that contrasts with the cold clinical environment"
✅ GOOD: "Lighting: Diffused overcast daylight filtering through floor-to-ceiling frosted glass panels, creating even, shadowless illumination with a soft pearl-gray quality, punctuated by a single beam of direct sunlight breaking through a gap in the clouds and hitting the subject's hands"
━━━ LAYER 4: COMPOSITION & CAMERA ━━━
Tell exactly where the camera is, what lens is being used, and how the frame is organized.
━━━ LAYER 4: COMPOSITION, CAMERA & LENS (Movement, Angle, Lens Type, Continuity) ━━━
Tell exactly where the camera is, what explicit lens is being used, how the camera moves, and ensure it connects logically with the previous/next scenes. You MUST explicitly write "Camera & Lens: [details]" in your prompt.
A) CAMERA POSITION: "eye-level", "low angle looking up (worm's eye)", "high angle looking down (bird's eye)", "overhead flat lay", "Dutch angle 15-degree tilt", "POV through character's eyes"
B) CAMERA DISTANCE: "extreme close-up on eyes", "medium close-up chest and up", "medium shot waist up", "full body shot", "wide establishing shot", "extreme wide showing entire landscape"
C) CAMERA MOVEMENT: "static locked-off tripod", "slow push-in towards subject", "smooth dolly tracking left-to-right", "orbiting 360° around subject", "crane rising up", "handheld with subtle shake"
D) FRAMING: "rule of thirds with subject on left intersect", "perfectly centered symmetrical", "framed through doorway", "leading lines converging to vanishing point", "negative space in upper third for text overlay"
A) LENS DETAILS (MANDATORY): Specify the exact focal length and lens type. Examples: "24mm wide-angle lens", "50mm standard prime", "85mm portrait lens", "200mm telephoto compression", "vintage anamorphic lens", "macro lens", "fisheye lens".
B) CAMERA POSITION/ANGLE: "eye-level", "low angle looking up (worm's eye)", "high angle looking down (bird's eye)", "overhead flat lay", "Dutch angle 15-degree tilt", "POV through character's eyes"
C) CAMERA DISTANCE: "extreme close-up on eyes", "medium close-up chest and up", "medium shot waist up", "full body shot", "wide establishing shot", "extreme wide showing entire landscape"
D) CAMERA MOVEMENT (MANDATORY): Specify EXACTLY how the camera moves during the scene. Examples: "slide in from left", "slow push-in (zoom in) towards subject's face", "arch in/orbiting 360° around subject", "smooth dolly tracking right", "crane rising up", "static locked-off tripod", "handheld with subtle shake"
E) CONTINUITY: Camera movements must make sense across scenes. If Scene 1 ends with a zoom-in, Scene 2 could start with an extreme close-up. If Scene 1 is an establishing wide shot panning right, Scene 2 could be a medium tracking shot moving right.
❌ BAD: "wide shot of the city"
✅ GOOD: "Extreme wide establishing shot from a drone at 300 meters altitude, camera slowly descending at 45-degree angle, the ancient temple complex positioned on the lower-right third of frame, leading lines of the river drawing the eye from lower-left foreground to the temple, vast jungle canopy filling the upper two-thirds creating a sense of overwhelming scale, a thin mist layer at the treeline adding depth separation between foreground and background"
✅ GOOD: "Camera & Lens: Shot on a 24mm wide-angle lens. Extreme wide establishing shot from a drone at 300 meters altitude, smooth crane descending at 45-degree angle while slowly panning right. The ancient temple complex positioned on the lower-right third of frame, leading lines of the river drawing the eye to the temple."
ASPECT RATIO COMPOSITION GUIDE:
• 9:16 (PORTRAIT — Shorts/Reels): Vertical framing, subject fills center-frame, use foreground-to-background depth, create visual interest through vertical stacking of elements, leave negative space in top or bottom third for text/subtitles
@@ -407,7 +445,7 @@ NARRATION TEXT (IN TARGET LANGUAGE)
• Scene 1: powerful hook creating instant curiosity
• Build escalating intrigue through middle scenes
• End with a thought-provoking statement
• Word count: targetDuration × 2.5 words/second
• Word count: targetDuration × 2 words/second (e.g., 120 words for 60s video)
• Conversational, not academic — like explaining to a smart friend
• Use rhetorical questions, surprising facts, emotional language
@@ -420,14 +458,17 @@ SUBTITLE TEXT (IN TARGET LANGUAGE)
• Simplify complex narration into punchy visual text
═══════════════════════════════════
SCENE STRUCTURE
SCENE STRUCTURE & CONTINUITY
═══════════════════════════════════
Min 4 scenes, max 10 scenes
Scene 1 (HOOK): 2-4 seconds — instant attention
Middle scenes: 5-12 seconds each — build the story
Final scene (CLOSER): 3-6 seconds — memorable conclusion
Total duration: within ±5 seconds of targetDuration
AI video generators produce best results in 4-6 second bursts.
You MUST generate enough scenes to cover the targetDuration (targetDuration / 5 = approximate scene count).
For a 60-second video, you must generate 10 to 15 scenes.
Scene 1 (HOOK): 3-5 seconds — instant attention
Middle scenes: 4-6 seconds each — continuous visual flow
• Final scene (CLOSER): 3-5 seconds — memorable conclusion
• Total duration: within ±2 seconds of targetDuration
• CONTINUITY: Ensure consecutive visual prompts maintain strict subject, lighting, and camera movement continuity so they blend seamlessly.
TRANSITION TYPES:
• CUT — Quick, impactful. Most scene changes
@@ -486,12 +527,17 @@ Describe ideal TTS voice with precision for ElevenLabs:
SOCIAL MEDIA CONTENT
═══════════════════════════════════
Generate platform-specific text:
- youtubeTitle: Primary keyword first, under 60 chars, curiosity-driven
- youtubeDescription: 500+ chars, include CTA, 2-3 secondary keywords, link placeholder
- tiktokCaption: Under 150 chars, trending format, 3-5 hashtags
- instagramCaption: Under 300 chars, emotional hook, 5 hashtags
- twitterText: Under 280 chars, hot take format, 2 hashtags
Generate platform-specific text (CTA-POWERED):
- youtubeTitle: BEST title from the 5 seoTitleAlternatives, under 60 chars, with emoji
- youtubeDescription: 500+ chars, structured EXACTLY as:
→ First 2 lines: Hook text (this is what shows before "...more")
→ 3-4 bullet points of value propositions with emojis
→ CTA line in target language (e.g., "Beğen, abone ol ve bildirimleri aç 🔔")
→ 3-5 related hashtags inline
→ "Chapters:" section with timestamps for each scene if applicable
- tiktokCaption: Under 150 chars, trending hook format + 3-5 hashtags, CTA: "Kaydet 📌" (in target lang)
- instagramCaption: Under 300 chars, emotional hook + value + CTA: "Yorum yap 💬" (in target lang) + 10-15 hashtags at end
- twitterText: Under 280 chars, hot take format + 2 hashtags, CTA: "RT et" (in target lang)
═══════════════════════════════════
OUTPUT FORMAT — STRICT JSON ONLY
@@ -508,10 +554,12 @@ Return ONLY valid JSON. No markdown. No backticks. No explanation.
"hashtags": ["string"] — 5-8 hashtags WITHOUT #
},
"seo": {
"title": "string — SEO-optimized title, primary keyword first, under 60 chars",
"title": "string — BEST SEO-optimized title with emoji, primary keyword first, under 60 chars",
"description": "string — meta description, 150-200 chars, includes secondary keywords",
"keywords": ["string"] — 8-12 LSI keywords,
"keywords": ["string"] — 12-15 LSI keywords (short-tail + long-tail mix),
"hashtags": ["string"] — same as metadata.hashtags,
"trendingHashtags": ["string"] — 3-5 hashtags estimated to be currently trending for this topic,
"estimatedSearchVolume": "string — brief summary of keyword search volume estimates, e.g. 'Primary keyword X has HIGH volume (~500K/mo)'",
"schemaMarkup": {
"@type": "VideoObject",
"name": "string",
@@ -519,6 +567,8 @@ Return ONLY valid JSON. No markdown. No backticks. No explanation.
"duration": "string — ISO 8601 format PT##S"
}
},
"seoTitleAlternatives": ["string"] — exactly 5 alternative SEO titles with emojis, ranked by estimated CTR (best first),
"seoScore": number — 0-100 SEO strength score based on: keyword placement (30), curiosity (25), specificity (20), emotion (15), length (10),
"scenes": [
{
"order": 1,
@@ -542,11 +592,11 @@ Return ONLY valid JSON. No markdown. No backticks. No explanation.
"ambientSoundPrompts": ["string"] — 2-3 project-level ambient sound descriptions for AudioGen,
"voiceStyle": "string — TTS characteristics for ElevenLabs",
"socialContent": {
"youtubeTitle": "string — under 60 chars",
"youtubeDescription": "string — 500+ chars with CTA",
"tiktokCaption": "string — under 150 chars",
"instagramCaption": "string — under 300 chars",
"twitterText": "string — under 280 chars"
"youtubeTitle": "string — BEST title from seoTitleAlternatives, under 60 chars, with emoji",
"youtubeDescription": "string — 500+ chars with CTA, bullet points, chapters",
"tiktokCaption": "string — under 150 chars with CTA",
"instagramCaption": "string — under 300 chars with CTA and 10-15 hashtags",
"twitterText": "string — under 280 chars with CTA"
}
}`;
@@ -620,6 +670,99 @@ export class VideoAiService {
}
}
/**
* Mevcut bir proje için 5 yeni SEO-optimized başlık üretir.
* Her başlık farklı bir hook stratejisi kullanır ve emoji içerir.
*/
async generateAlternativeTitles(
topic: string,
currentTitle: string,
language: string,
keywords: string[],
): Promise<{ titles: string[]; seoScore: number }> {
this.logger.log(`SEO başlık üretimi başladı — Konu: "${topic}", Dil: ${language}`);
const titlePrompt = `Generate exactly 5 alternative SEO-optimized video titles for the following topic.
TOPIC: "${topic}"
CURRENT TITLE: "${currentTitle}"
LANGUAGE: ${language} (generate titles in THIS language)
EXISTING KEYWORDS: ${keywords.join(', ')}
RULES:
1. Each title MUST use a DIFFERENT hook strategy:
- Curiosity Gap: Creates an information gap the viewer must fill
- Data-Driven: Uses specific numbers, statistics, or data points
- How-To/Listicle: Offers practical value or a numbered list
- Emotional: Triggers strong emotional response (surprise, fear, excitement)
- Contrarian: Challenges conventional wisdom or common beliefs
2. Each title MUST:
- Be under 60 characters
- Start with the primary keyword in the first 3 words
- Include 1-2 strategic emojis (🔥⚡🤯😱💀🚀💡🎯✅❌)
- Be significantly DIFFERENT from the current title
- Be in ${language} language
3. Rank titles by estimated CTR (best title FIRST)
4. Calculate seoScore (0-100) for the best title:
- Keyword in first 3 words: 30pts
- Curiosity factor: 25pts
- Specificity (numbers/names): 20pts
- Emotional trigger: 15pts
- Length (40-60 chars): 10pts
Return ONLY valid JSON:
{
"titles": ["best title", "second best", "third", "fourth", "fifth"],
"seoScore": number
}`;
try {
const response = await this.genAI.models.generateContent({
model: this.modelName,
contents: titlePrompt,
config: {
temperature: 0.9,
topP: 0.95,
maxOutputTokens: 2048,
responseMimeType: 'application/json',
},
});
const rawText = response.text ?? '';
if (!rawText.trim()) {
throw new InternalServerErrorException('Gemini API boş yanıt döndü.');
}
let parsed: { titles: string[]; seoScore: number };
try {
parsed = JSON.parse(rawText);
} catch {
const repaired = jsonrepair(rawText);
parsed = JSON.parse(repaired);
}
// Validasyon
if (!Array.isArray(parsed.titles) || parsed.titles.length === 0) {
throw new Error('titles dizisi boş veya eksik');
}
// En fazla 5 başlık, her biri 60 karakter
parsed.titles = parsed.titles.slice(0, 5).map((t: string) => t.substring(0, 60));
parsed.seoScore = Math.min(100, Math.max(0, parsed.seoScore || 75));
this.logger.log(`${parsed.titles.length} SEO başlığı üretildi — Skor: ${parsed.seoScore}`);
return parsed;
} catch (error) {
this.logger.error(`SEO başlık üretim hatası: ${error instanceof Error ? error.message : 'Bilinmeyen'}`);
throw new InternalServerErrorException(
`SEO başlık üretimi başarısız: ${error instanceof Error ? error.message : 'API hatası'}`,
);
}
}
/**
* Uzun metinlerden (kitap, uzun makale vb.) potansiyel video konuları çıkarır.
* Gemini 1.5 Flash kullanarak 3-4 çarpıcı YouTube video başlığı önerir.
@@ -686,6 +829,9 @@ REQUIREMENTS:
`- Visual prompts: ALWAYS in English (for AI image/video generation)\n` +
`- Video style: ${input.videoStyle} — STRICTLY follow the Visual DNA Map for this style\n` +
`- Aspect ratio: ${input.aspectRatio || 'PORTRAIT_9_16'}${aspectRatioGuide}\n` +
`- SCENE MATH: Each scene must represent 4-6 seconds of video. Total scenes = Target duration / 5.\n` +
`- NARRATION PACING: Strict limit of 2 words per second. For a ${input.targetDurationSeconds}s video, write exactly ~${input.targetDurationSeconds * 2} words total across all scenes.\n` +
`- CONTINUITY: Consecutive scenes MUST logically flow into each other. Use matching camera angles, consistent lighting, and coherent subject transitions so the video clips stitch together perfectly.\n` +
`- Make it viral-worthy, visually stunning, and intellectually captivating\n` +
`- The first 2 seconds must hook the viewer immediately\n` +
`- Write narration that sounds HUMAN — avoid AI writing patterns\n` +
@@ -696,11 +842,12 @@ REQUIREMENTS:
// 5-Layer Architecture hatırlatması
prompt += `\n═══ VISUAL PROMPT REQUIREMENTS (CRITICAL) ═══\n`;
prompt += `Each visualPrompt MUST contain ALL 5 layers:\n`;
prompt += `Each visualPrompt MUST contain ALL 5 layers AND explicit "Camera:" and "Lighting:" labels:\n`;
prompt += `1. SUBJECT: Extreme specificity — materials, textures, spatial relationships, lived-in details\n`;
prompt += `2. MOOD/REFERENCE: Film/art/photography references that define the visual universe\n`;
prompt += `3. LIGHTING: Source + Direction + Quality (e.g. "golden hour from camera-right at 15°, warm amber 3200K")\n`;
prompt += `4. COMPOSITION: Camera position + distance + movement + framing rules\n`;
prompt += `3. LIGHTING: Explicitly label as "Lighting: [Source + Direction + Quality]" (e.g. "Lighting: golden hour from camera-right at 15°, warm amber 3200K")\n`;
prompt += `4. COMPOSITION & CAMERA: Explicitly label as "Camera & Lens: [Lens type + Angle + Distance + MOVEMENT]" (e.g. "Camera & Lens: 50mm prime lens, low angle, medium shot, sliding in from left to right, orbiting the subject")\n`;
prompt += ` -> CRITICAL: Ensure camera movements are logically consistent and flow smoothly between consecutive scenes.\n`;
prompt += `5. FINISHING: DOF, film stock, color grade, texture, post-processing\n`;
prompt += `Each visualPrompt MUST end with "Avoid: [list of things to avoid]"\n`;
prompt += `Scene 1 establishes the visual world — all subsequent scenes maintain continuity.\n`;
@@ -1282,37 +1429,32 @@ REQUIREMENTS:
try {
let cleanText = rawText.trim();
// Pass 1: Markdown code fence temizliği (başta, sonda, ortada)
// Pass 1: Markdown code fence temizliği
cleanText = cleanText.replace(/^```(?:json)?\s*/i, '');
cleanText = cleanText.replace(/\s*```\s*$/i, '');
// Ortada kalan fence'leri de temizle
cleanText = cleanText.replace(/```(?:json)?\s*/gi, '');
// Pass 2: BOM ve kontrol karakterleri temizliği
cleanText = cleanText.replace(/^\uFEFF/, '');
// JSON-dışı kontrol karakterleri kaldır (tab, newline, CR hariç)
cleanText = cleanText.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
// Pass 2: JSON bloğunu izole et
const startIndex = cleanText.indexOf('{');
const endIndex = cleanText.lastIndexOf('}');
if (startIndex >= 0 && endIndex > startIndex) {
cleanText = cleanText.substring(startIndex, endIndex + 1);
}
// Pass 3: Trailing comma — JSON'da yasak ama Gemini bazen koyuyor
cleanText = cleanText.replace(/,\s*([}\]])/g, '$1');
cleanText = cleanText.trim();
// İlk deneme: Direkt parse
// Pass 3: jsonrepair ile onar ve parse et
try {
parsed = JSON.parse(cleanText);
} catch {
// Pass 4: Kesilmiş JSON kurtarma — son geçerli } veya ] bul
const repaired = jsonrepair(cleanText);
parsed = JSON.parse(repaired);
} catch (repairError) {
// Eğer jsonrepair doğrudan başaramazsa, en son geçerli kapamaya kadar kesip tekrar deneyelim
const lastBrace = cleanText.lastIndexOf('}');
const lastBracket = cleanText.lastIndexOf(']');
const cutPoint = Math.max(lastBrace, lastBracket);
if (cutPoint > 0) {
const truncated = cleanText.substring(0, cutPoint + 1);
this.logger.warn(`JSON kesilmiş olabilir — son ${cleanText.length - cutPoint - 1} karakter atıldı, kurtarma deneniyor...`);
parsed = JSON.parse(truncated);
if (lastBrace > 0) {
const truncated = cleanText.substring(0, lastBrace + 1);
this.logger.warn(`jsonrepair başarısız, JSON kesilmiş olabilir. Kurtarma deneniyor...`);
const repairedTruncated = jsonrepair(truncated);
parsed = JSON.parse(repairedTruncated);
} else {
throw new Error('Kurtarılabilir JSON bulunamadı');
throw repairError;
}
}
} catch (parseError) {