fix: ajustes de tempo dentro do video (estava tentando obter imagens depois do fim do video)
This commit is contained in:
parent
c5a3b14449
commit
480f501993
@ -1,4 +1,4 @@
|
|||||||
# A Origem da Alimentação no Brasil e a Importância da Química na Vida Cotidiana (OTHER)
|
# Por Que Arroz e Não Batata: Uma Análise Histórica e Científica (OTHER)
|
||||||
|
|
||||||
Source: Por que ARROZ e nÒo BATATA? | Cortes do Manual do Mundo
|
Source: Por que ARROZ e nÒo BATATA? | Cortes do Manual do Mundo
|
||||||
|
|
||||||
@ -6,27 +6,27 @@ Source: Por que ARROZ e nÒo BATATA? | Cortes do Manual do Mundo
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"category": "OTHER",
|
"category": "OTHER",
|
||||||
"documentTitle": "A Origem da Alimentação no Brasil e a Importância da Química na Vida Cotidiana",
|
"documentTitle": "Por Que Arroz e Não Batata: Uma Análise Histórica e Científica",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"title": "Introdução à Alimentação no Brasil",
|
"title": "Introdução",
|
||||||
"content": "A base alimentar brasileira é composta pelo arroz, mas por que não pela batata, se fomos colonizados por europeus que comem batata? Essa é uma pergunta interessante que leva a uma história complexa. Os indígenas no Brasil já cultivavam alguns tipos de arroz, assim como as populações africanas. O arroz é um alimento básico da dieta de 2,5 bilhões de pessoas, aproximadamente 1/3 do mundo inteiro. [SCREENSHOT: 00:01:30]"
|
"content": "A nossa base alimentar é composta por vários alimentos, mas o arroz é um dos principais. Isso nos leva a questionar por que o arroz é mais comum do que a batata, considerando que fomos colonizados por europeus, que têm a batata como alimento básico. A resposta é complexa e envolve a história da culinária e da colonização. Os indígenas no Brasil já cultivavam alguns tipos de arroz, e na África, o arroz também era cultivado. Isso significa que as populações que se juntaram para formar o Brasil já consumiam arroz. De acordo com a Embrapa, o arroz é um alimento básico da dieta de 2,5 bilhões de pessoas, cerca de 1/3 do mundo inteiro. [SCREENSHOT: 00:00:30]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "A Descoberta da Batata pelos Europeus",
|
"title": "A Descoberta da Batata",
|
||||||
"content": "Os europeus descobriram a batata quando chegaram à América, mais especificamente nos Andes. A batata é originária da América, e foi a população local que ensinou os europeus a comê-la. Além da batata, outros alimentos importantes, como o milho e o cacau, também são originários da América. [SCREENSHOT: 00:03:45]"
|
"content": "Quando os europeus chegaram ao Brasil, eles não comiam batata. Isso porque a batata é originária da América, especificamente dos Andes. Foi a população da América que ensinou os europeus a comer batata. Além da batata, outras plantas importantes, como o milho e o cacau, também são originárias da América. Isso mostra que a culinária europeia antes da descoberta do continente americano devia ser bem diferente. [SCREENSHOT: 00:01:45]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "A Importância da Química na Vida Cotidiana",
|
"title": "A Função da Bolinha no Spray de Tinta",
|
||||||
"content": "A química está presente em todos os aspectos da vida, desde a culinária até a indústria. Um exemplo interessante é o funcionamento de um spray de tinta. A bolinha dentro do spray serve para misturar a tinta com o propelente, um gás inflamável, garantindo que a tinta saia uniformemente. [SCREENSHOT: 00:06:15]"
|
"content": "Muitas pessoas já se perguntaram sobre a função da bolinha que fica dentro do spray de tinta. Essa bolinha é importante para misturar a tinta com o propelente, que é um gás inflamável, como o propano ou butano. A bolinha ajuda a manter a mistura uniforme, permitindo que a tinta seja aplicada de forma correta. [SCREENSHOT: 00:02:50]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "A Diferença entre Água com Gás e Água Oxigenada",
|
"title": "Água com Gás e Água Oxigenada",
|
||||||
"content": "A água com gás contém bolhinhas de gás carbônico, enquanto a água oxigenada é uma substância química diferente, com uma molécula de H2O2. A água oxigenada não é simplesmente água com oxigênio dissolvido, mas sim uma substância com propriedades únicas. [SCREENSHOT: 00:09:30]"
|
"content": "Muitas pessoas confundem água com gás e água oxigenada. No entanto, elas são diferentes. A água com gás contém gás carbônico dissolvido, que forma bolhas quando a garrafa é aberta. Já a água oxigenada é uma substância química diferente, com a fórmula H2O2, que contém um átomo de oxigênio a mais do que a água comum. [SCREENSHOT: 00:03:40]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Conclusão e Novos Projetos",
|
"title": "Conclusão e Novos Projetos",
|
||||||
"content": "A química é uma ciência fascinante que pode ser aprendida de maneira divertida. A equipe do Manual do Mundo está trabalhando em novos projetos, incluindo a revisão do Grande Livro de Biologia. Esses livros são uma ótima forma de aprender sobre ciência de maneira interativa e divertida. [SCREENSHOT: 00:12:00]"
|
"content": "Além de explorar a história e a ciência por trás de nossos alimentos e produtos, também estamos trabalhando em novos projetos, como a revisão do grande livro de biologia, que faz parte da coleção 'O Grande Livro'. Esses livros são uma ótima maneira de aprender de forma divertida e interativa. [SCREENSHOT: 00:04:20]"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,9 @@ using QuestPDF.Helpers;
|
|||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using PuppeteerSharp;
|
using PuppeteerSharp;
|
||||||
using VideoStudy.Shared;
|
using VideoStudy.Shared;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography; // Added for MD5 hash
|
||||||
|
|
||||||
namespace VideoStudy.API.Services;
|
namespace VideoStudy.API.Services;
|
||||||
|
|
||||||
@ -119,6 +122,11 @@ public class AnalysisService
|
|||||||
|
|
||||||
string rawLlmResponse = "";
|
string rawLlmResponse = "";
|
||||||
|
|
||||||
|
// Get video info early to use duration for timestamp validation
|
||||||
|
AddLog("ℹ️ Obtendo informações do vídeo para validação de timestamps...");
|
||||||
|
var videoInfo = await GetVideoInfoAsync(request.VideoUrl);
|
||||||
|
AddLog($"✅ Duração do vídeo (via yt-dlp): {videoInfo.Duration:hh\\:mm\\:ss}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// --- Step 1: Transcription ---
|
// --- Step 1: Transcription ---
|
||||||
@ -132,7 +140,7 @@ public class AnalysisService
|
|||||||
|
|
||||||
// --- Step 2: Intelligence ---
|
// --- Step 2: Intelligence ---
|
||||||
AddLog("🧠 Enviando transcrição para o Groq (LLM)...");
|
AddLog("🧠 Enviando transcrição para o Groq (LLM)...");
|
||||||
var (tutorialSections, rawJson, category, docTitle) = await GenerateTutorialContentAsync(transcript, originalTitle, request.Language, request.OutputLanguage);
|
var (tutorialSections, rawJson, category, docTitle) = await GenerateTutorialContentAsync(transcript, originalTitle, request.Language, request.OutputLanguage, videoInfo.Duration);
|
||||||
rawLlmResponse = rawJson;
|
rawLlmResponse = rawJson;
|
||||||
|
|
||||||
// Save debug MD
|
// Save debug MD
|
||||||
@ -141,16 +149,34 @@ public class AnalysisService
|
|||||||
await File.WriteAllTextAsync(debugFile, debugContent);
|
await File.WriteAllTextAsync(debugFile, debugContent);
|
||||||
AddLog($"📝 Arquivo de debug gerado: {debugFile}");
|
AddLog($"📝 Arquivo de debug gerado: {debugFile}");
|
||||||
|
|
||||||
|
// --- Validate and adjust Image Timestamps ---
|
||||||
|
AddLog("⏳ Validando timestamps de imagem gerados pela IA...");
|
||||||
|
foreach (var section in tutorialSections)
|
||||||
|
{
|
||||||
|
if (TimeSpan.TryParse(section.ImageTimestamp, out var imageTs))
|
||||||
|
{
|
||||||
|
// Allow for a small buffer (e.g., 5 seconds) just in case, but prefer strict bounds.
|
||||||
|
// If timestamp is more than 5 seconds past video end, consider it invalid.
|
||||||
|
if (imageTs.TotalSeconds > videoInfo.Duration.TotalSeconds + 5)
|
||||||
|
{
|
||||||
|
AddLog($" ⚠️ Timestamp de imagem ({section.ImageTimestamp}) para '{section.Title}' excede a duração do vídeo ({videoInfo.Duration:hh\\:mm\\:ss}). Ajustando para null para evitar erros de captura.");
|
||||||
|
section.ImageTimestamp = null; // Set to null to skip screenshot for this section
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddLog("✅ Validação de timestamps concluída.");
|
||||||
|
|
||||||
// --- Step 3: Image Capture ---
|
// --- Step 3: Image Capture ---
|
||||||
var sectionsWithImages = tutorialSections.Where(s => !string.IsNullOrEmpty(s.ImageTimestamp)).ToList();
|
var sectionsWithImages = tutorialSections.Where(s => !string.IsNullOrEmpty(s.ImageTimestamp)).ToList();
|
||||||
if (sectionsWithImages.Any())
|
if (sectionsWithImages.Any())
|
||||||
{
|
{
|
||||||
AddLog($"📸 Capturando {sectionsWithImages.Count} prints usando Puppeteer (Direct Bypass)...");
|
AddLog($"📸 Capturando {sectionsWithImages.Count} prints usando Puppeteer (Direct Bypass)...");
|
||||||
await CaptureScreenshotsWithPuppeteerAsync(request.VideoUrl, tutorialSections, tempDir);
|
// Pass videoInfo.Duration to the screenshot method
|
||||||
|
await CaptureScreenshotsWithPuppeteerAsync(request.VideoUrl, tutorialSections, tempDir, videoInfo.Duration);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddLog("⚠️ Nenhuma tag [SCREENSHOT] foi gerada pela IA.");
|
AddLog("⚠️ Nenhuma tag [SCREENSHOT] foi gerada pela IA, ou todas foram invalidadas.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Step 4: PDF Generation ---
|
// --- Step 4: PDF Generation ---
|
||||||
@ -254,7 +280,7 @@ public class AnalysisService
|
|||||||
return string.Join(" ", textLines);
|
return string.Join(" ", textLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(List<TutorialSection> sections, string rawJson, string category, string docTitle)> GenerateTutorialContentAsync(string transcript, string originalTitle, string inputLanguage, string? outputLanguage)
|
private async Task<(List<TutorialSection> sections, string rawJson, string category, string docTitle)> GenerateTutorialContentAsync(string transcript, string originalTitle, string inputLanguage, string? outputLanguage, TimeSpan videoDuration)
|
||||||
{
|
{
|
||||||
var langMap = new Dictionary<string, string>
|
var langMap = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
@ -266,11 +292,18 @@ public class AnalysisService
|
|||||||
var outputLangName = langMap.GetValueOrDefault(outputLang, outputLang);
|
var outputLangName = langMap.GetValueOrDefault(outputLang, outputLang);
|
||||||
|
|
||||||
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
||||||
|
|
||||||
|
// Pre-format video duration strings to avoid potential issues in interpolated string
|
||||||
|
string formattedVideoDuration = videoDuration.ToString("hh\\:mm\\:ss", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
string totalSecondsVideoDuration = videoDuration.TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
|
||||||
var prompt = $@"
|
var prompt = $@"
|
||||||
Você é um Editor Chefe e Analista de Conteúdo Sênior.
|
Você é um Editor Chefe e Analista de Conteúdo Sênior.
|
||||||
Receberá:
|
Receberá:
|
||||||
A) TÍTULO ORIGINAL: {originalTitle}
|
A) TÍTULO ORIGINAL: {originalTitle}
|
||||||
B) TRANSCRIÇÃO: {transcript[..Math.Min(transcript.Length, 20000)]}
|
B) TRANSCRIÇÃO: {transcript[..Math.Min(transcript.Length, 20000)]}
|
||||||
|
C) DURAÇÃO TOTAL DO VÍDEO: {formattedVideoDuration} ({totalSecondsVideoDuration} segundos)
|
||||||
|
|
||||||
SUA MISSÃO:
|
SUA MISSÃO:
|
||||||
1. **Classificar** o vídeo em: 'TUTORIAL', 'MEETING', 'LECTURE' ou 'OTHER'.
|
1. **Classificar** o vídeo em: 'TUTORIAL', 'MEETING', 'LECTURE' ou 'OTHER'.
|
||||||
@ -281,6 +314,7 @@ SUA MISSÃO:
|
|||||||
3. **Estruturar o Conteúdo**:
|
3. **Estruturar o Conteúdo**:
|
||||||
- Converta o conteúdo em um texto educativo e denso.
|
- Converta o conteúdo em um texto educativo e denso.
|
||||||
- Identifique momentos visuais críticos e insira `[SCREENSHOT: HH:MM:SS]` no final do parágrafo correspondente.
|
- Identifique momentos visuais críticos e insira `[SCREENSHOT: HH:MM:SS]` no final do parágrafo correspondente.
|
||||||
|
- **IMPORTANTE:** Os timestamps `HH:MM:SS` para os `[SCREENSHOT]` **NÃO DEVEM, EM HIPÓTESE ALGUMA, EXCEDER A DURAÇÃO TOTAL DO VÍDEO** ({formattedVideoDuration}). Se um momento visual crítico ocorrer perto do final do vídeo, use um timestamp que esteja dentro da duração total.
|
||||||
|
|
||||||
**IMPORTANTE: Todo o texto de saída (documentTitle, títulos das seções e conteúdo) DEVE ser escrito em {outputLangName}.**
|
**IMPORTANTE: Todo o texto de saída (documentTitle, títulos das seções e conteúdo) DEVE ser escrito em {outputLangName}.**
|
||||||
|
|
||||||
@ -353,14 +387,18 @@ SAÍDA JSON OBRIGATÓRIA:
|
|||||||
return url?.Trim();
|
return url?.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CaptureScreenshotsWithPuppeteerAsync(string videoUrl, List<TutorialSection> sections, string outputDir)
|
private async Task CaptureScreenshotsWithPuppeteerAsync(string videoUrl, List<TutorialSection> sections, string outputDir, TimeSpan videoDuration)
|
||||||
{
|
{
|
||||||
var sectionsWithImages = sections.Where(s => !string.IsNullOrEmpty(s.ImageTimestamp)).ToList();
|
var sectionsWithImages = sections.Where(s => !string.IsNullOrEmpty(s.ImageTimestamp)).ToList();
|
||||||
if (!sectionsWithImages.Any()) return;
|
if (!sectionsWithImages.Any()) return;
|
||||||
|
|
||||||
AddLog("🔍 Obtendo link direto do vídeo (Bypass YouTube Player)...");
|
AddLog("🔍 Obtendo link direto do vídeo (Bypass YouTube Player)...");
|
||||||
var rawVideoUrl = await GetRawVideoStreamUrl(videoUrl);
|
var rawVideoUrl = await GetRawVideoStreamUrl(videoUrl);
|
||||||
if (string.IsNullOrEmpty(rawVideoUrl)) { AddLog("❌ Falha ao obter link direto."); return; }
|
if (string.IsNullOrEmpty(rawVideoUrl))
|
||||||
|
{
|
||||||
|
AddLog("❌ Falha ao obter link direto. As capturas de tela serão ignoradas.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -368,33 +406,163 @@ SAÍDA JSON OBRIGATÓRIA:
|
|||||||
using var page = await browser.NewPageAsync();
|
using var page = await browser.NewPageAsync();
|
||||||
await page.SetViewportAsync(new ViewPortOptions { Width = 1280, Height = 720 });
|
await page.SetViewportAsync(new ViewPortOptions { Width = 1280, Height = 720 });
|
||||||
|
|
||||||
var html = $@"<html><body style='margin:0;background:black;overflow:hidden;'><video id='v' width='1280' height='720' muted><source src='{rawVideoUrl}' type='video/mp4'></video></body></html>";
|
var htmlContent = $@"
|
||||||
await page.SetContentAsync(html);
|
<html>
|
||||||
await page.WaitForSelectorAsync("video");
|
<body style='margin:0; background:black; overflow:hidden;'>
|
||||||
|
<video id='raw-player' width='1280' height='720' muted preload='auto'>
|
||||||
|
<source src='{rawVideoUrl}' type='video/mp4'>
|
||||||
|
</video>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
await page.SetContentAsync(htmlContent);
|
||||||
|
await page.WaitForSelectorAsync("#raw-player");
|
||||||
|
|
||||||
foreach (var section in sectionsWithImages)
|
// Use the passed videoDuration instead of evaluating it from the page
|
||||||
|
AddLog($"🎥 Duração total do vídeo (passada): {videoDuration:hh\\:mm\\:ss}");
|
||||||
|
|
||||||
|
// Loop with index for unique identifiers
|
||||||
|
for (int i = 0; i < sectionsWithImages.Count; i++)
|
||||||
{
|
{
|
||||||
|
var section = sectionsWithImages[i];
|
||||||
|
// ImageTimestamp might be null due to earlier validation
|
||||||
|
if (string.IsNullOrEmpty(section.ImageTimestamp))
|
||||||
|
{
|
||||||
|
AddLog($"📸 Pulando captura para seção '{section.Title}' pois o timestamp foi invalidado.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (TimeSpan.TryParse(section.ImageTimestamp, out var ts))
|
if (TimeSpan.TryParse(section.ImageTimestamp, out var ts))
|
||||||
{
|
{
|
||||||
var sec = (int)ts.TotalSeconds;
|
AddLog($"📸 Processando captura para {section.ImageTimestamp} ('{section.Title}')");
|
||||||
AddLog($"🌐 Renderizando frame: {section.ImageTimestamp}...");
|
var targetSeconds = (int)ts.TotalSeconds;
|
||||||
|
|
||||||
await page.EvaluateFunctionAsync(@"(s) => {
|
var candidates = new List<(byte[] ImageData, double Score, int Time, string Hash, int Size)>();
|
||||||
return new Promise(r => {
|
|
||||||
const v = document.getElementById('v');
|
|
||||||
v.currentTime = s;
|
|
||||||
v.addEventListener('seeked', r, {once:true});
|
|
||||||
});
|
|
||||||
}", sec);
|
|
||||||
|
|
||||||
await Task.Delay(500);
|
// "Best of 3" capture window: T-1, T, T+1 seconds
|
||||||
var path = Path.Combine(outputDir, $"snap_{sec}.jpg");
|
var timeOffsets = new[] { -1, 0, 1 };
|
||||||
await page.ScreenshotAsync(path, new ScreenshotOptions { Type = ScreenshotType.Jpeg, Quality = 90 });
|
|
||||||
if (File.Exists(path)) section.ImageData = await File.ReadAllBytesAsync(path);
|
foreach (var offset in timeOffsets)
|
||||||
|
{
|
||||||
|
var captureTime = Math.Max(0, targetSeconds + offset);
|
||||||
|
|
||||||
|
// Ensure captureTime does not exceed video duration
|
||||||
|
if (captureTime > videoDuration.TotalSeconds + 5) // Add a small buffer for safety
|
||||||
|
{
|
||||||
|
AddLog($" - ⚠️ Tempo de captura {captureTime}s excede a duração do vídeo ({videoDuration.TotalSeconds:F2}s). Ignorando este candidato.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AddLog($" - Solicitando seek para {captureTime}s (candidato para {section.ImageTimestamp}).");
|
||||||
|
// 1. Seek to the target time
|
||||||
|
await page.EvaluateFunctionAsync(@"(s) => {{
|
||||||
|
const video = document.getElementById('raw-player');
|
||||||
|
video.currentTime = s;
|
||||||
|
}}", captureTime);
|
||||||
|
|
||||||
|
// 2. Intelligent Wait: Wait for the video to have enough data to play
|
||||||
|
await page.WaitForFunctionAsync("() => document.getElementById('raw-player').readyState >= 3", new WaitForFunctionOptions { Timeout = 10000 });
|
||||||
|
|
||||||
|
var actualCurrentTime = await page.EvaluateFunctionAsync<double>("() => document.getElementById('raw-player').currentTime");
|
||||||
|
AddLog($" -> Buscado para {captureTime}s. CurrentTime real pós-seek: {actualCurrentTime:F2}s.");
|
||||||
|
|
||||||
|
// 3. Capture screenshot into memory
|
||||||
|
var screenshotData = await page.ScreenshotDataAsync(new ScreenshotOptions { Type = ScreenshotType.Jpeg, Quality = 90 });
|
||||||
|
|
||||||
|
// Calculate MD5 hash
|
||||||
|
string hash;
|
||||||
|
using (var md5 = MD5.Create())
|
||||||
|
{
|
||||||
|
var hashBytes = md5.ComputeHash(screenshotData);
|
||||||
|
hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
AddLog($" -> Imagem Candidata ({captureTime}s): {screenshotData.Length} bytes, Hash MD5: {hash}");
|
||||||
|
|
||||||
|
// 4. Score the image
|
||||||
|
var score = CalculateImageClarityScore(screenshotData);
|
||||||
|
AddLog($" - Candidato {captureTime}s: Score de claridade = {score:F2}");
|
||||||
|
|
||||||
|
if (score > 0)
|
||||||
|
{
|
||||||
|
candidates.Add((screenshotData, score, captureTime, hash, screenshotData.Length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WaitTaskTimeoutException)
|
||||||
|
{
|
||||||
|
AddLog($" - ⚠️ Timeout esperando pelo frame em {captureTime}s. Ignorando.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddLog($" - ❌ Erro ao capturar frame em {captureTime}s: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.Any())
|
||||||
|
{
|
||||||
|
// Select the best image based on the highest score
|
||||||
|
var bestCandidate = candidates.OrderByDescending(c => c.Score).First();
|
||||||
|
section.ImageData = bestCandidate.ImageData;
|
||||||
|
AddLog($" => ✅ Selecionado frame de {bestCandidate.Time}s (Score: {bestCandidate.Score:F2}, Hash: {bestCandidate.Hash}) para o timestamp {section.ImageTimestamp}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddLog($" => ❌ Falha ao capturar um frame válido para {section.ImageTimestamp}. O PDF usará um placeholder.");
|
||||||
|
section.ImageData = null; // Ensure it's null if all attempts fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) { AddLog($"❌ Erro Puppeteer: {ex.Message}"); }
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddLog($"❌ Erro irrecuperável no Puppeteer: {ex.Message}. As capturas de tela restantes serão abortadas.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalculateImageClarityScore(byte[] imageBytes)
|
||||||
|
{
|
||||||
|
if (imageBytes == null || imageBytes.Length == 0)
|
||||||
|
return -1.0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use SKBitmap.Decode for robust image format handling
|
||||||
|
using var image = SKBitmap.Decode(imageBytes);
|
||||||
|
if (image == null || image.Width == 0 || image.Height == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("SkiaSharp failed to decode image or image is empty.");
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var brightnessValues = new List<float>(image.Width * image.Height);
|
||||||
|
|
||||||
|
// Using GetPixel is simpler than handling Pixels array for this use case
|
||||||
|
for (int y = 0; y < image.Height; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < image.Width; x++)
|
||||||
|
{
|
||||||
|
var p = image.GetPixel(x, y);
|
||||||
|
// Luma calculation (standard formula for perceived brightness)
|
||||||
|
var brightness = (p.Red * 0.299f) + (p.Green * 0.587f) + (p.Blue * 0.114f);
|
||||||
|
brightnessValues.Add(brightness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!brightnessValues.Any()) return 0.0;
|
||||||
|
|
||||||
|
var avg = brightnessValues.Average();
|
||||||
|
var sumOfSquares = brightnessValues.Sum(b => Math.Pow(b - avg, 2));
|
||||||
|
var stdDev = Math.Sqrt(sumOfSquares / brightnessValues.Count);
|
||||||
|
|
||||||
|
// A very low standard deviation indicates a uniform image (likely black, white, or single color)
|
||||||
|
return stdDev;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error calculating image clarity score.");
|
||||||
|
// If SkiaSharp throws an exception, it's a bad image.
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] GeneratePdf(string docTitle, string videoUrl, List<TutorialSection> sections, string category)
|
private byte[] GeneratePdf(string docTitle, string videoUrl, List<TutorialSection> sections, string category)
|
||||||
|
|||||||
BIN
VideoStudy.pdf
Normal file
BIN
VideoStudy.pdf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user