Compare commits
No commits in common. "480f501993b9f5dabb55fb10e70cdf1923c17aae" and "8b75f1f62d2c5c6b1ea127ac9fe8f7946d8e57de" have entirely different histories.
480f501993
...
8b75f1f62d
@ -1,7 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,33 +1,73 @@
|
||||
# Por Que Arroz e Não Batata: Uma Análise Histórica e Científica (OTHER)
|
||||
# Debug Groq Response
|
||||
|
||||
Source: Por que ARROZ e nÒo BATATA? | Cortes do Manual do Mundo
|
||||
URL: https://youtu.be/-EaO2S4Lyvk?si=tJrVNLZCRT2unmD1
|
||||
|
||||
## Raw JSON
|
||||
```json
|
||||
{
|
||||
"category": "OTHER",
|
||||
"documentTitle": "Por Que Arroz e Não Batata: Uma Análise Histórica e Científica",
|
||||
"sections": [
|
||||
{
|
||||
"title": "Introdução",
|
||||
"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": "Passo 1: Introdução",
|
||||
"content": "Bem-vindo ao tutorial de The Legend of Zelda: Breath of the Wild. Neste vídeo, vamos explorar 9 dicas para iniciantes para ajudá-los a superar as ameaças iniciais do jogo. [SCREENSHOT: 00:00:20]"
|
||||
},
|
||||
{
|
||||
"title": "A Descoberta da Batata",
|
||||
"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": "Passo 2: Planeje suas batalhas",
|
||||
"content": "Não é sempre a melhor ideia correr direto para a batalha. Em vez disso, tire um momento para formular um plano de ação antes de se aproximar do inimigo. Por exemplo, se você encontrar um acampamento de inimigos, pense sobre como você pode eliminá-los com o mínimo de problemas. [SCREENSHOT: 00:01:20]"
|
||||
},
|
||||
{
|
||||
"title": "A Função da Bolinha no Spray de Tinta",
|
||||
"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": "Passo 3: Gerencie seus equipamentos",
|
||||
"content": "Os equipamentos e armas do jogo têm uma mecânica de durabilidade, o que significa que eles se desgastam com o tempo e eventualmente quebram. Certifique-se de usar seus equipamentos mais poderosos apenas contra inimigos mais difíceis e chefs. [SCREENSHOT: 00:02:30]"
|
||||
},
|
||||
{
|
||||
"title": "Água com Gás e Água Oxigenada",
|
||||
"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": "Passo 4: Escolha suas batalhas",
|
||||
"content": "Não há vergonha em fugir de uma batalha se não há um objetivo em mente. Seus recursos de armas e equipamentos são limitados, então é sempre melhor evitar encontros desnecessários. [SCREENSHOT: 00:03:20]"
|
||||
},
|
||||
{
|
||||
"title": "Conclusão e Novos Projetos",
|
||||
"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]"
|
||||
"title": "Passo 5: Complete os santuários",
|
||||
"content": "Os santuários oferecem uma ótima distração dos períodos prolongados de exploração e completá-los pode recompensar você com espíritos orbitais, que podem ser usados para comprar upgrades úteis. Além disso, completar um santuário permite que você desbloqueie sua localização como um ponto de viagem rápida. [SCREENSHOT: 00:04:30]"
|
||||
},
|
||||
{
|
||||
"title": "Passo 6: Evite os guardiões",
|
||||
"title": "Os guardiões são inimigos mortais que você deve evitar a todo custo, pelo menos no início do jogo. Se você precisar lutar contra um guardião, certifique-se de estar montado a cavalo e equipado com uma arsenal de bombas e flechas elementais. [SCREENSHOT: 00:05:40]"
|
||||
},
|
||||
{
|
||||
"title": "Passo 7: Cozinhe para sobreviver",
|
||||
"content": "A culinária é uma parte essencial da sua sobrevivência e pode ajudá-lo de muitas maneiras. Para cozinhar, basta escolher alguns ingredientes do seu inventário e jogá-los em uma fogueira com uma panela. Dependendo dos ingredientes que você escolher, você pode criar um prato ou elixir que oferte um efeito de status diferente. [SCREENSHOT: 00:06:50]"
|
||||
},
|
||||
{
|
||||
"title": "Passo 8: Use armas pesadas contra inimigos com escudos",
|
||||
"content": "Se um inimigo estiver usando um escudo, você pode usar uma arma pesada como um machado ou um martelo para derrubar o escudo e torná-lo vulnerável ao ataque. [SCREENSHOT: 00:07:40]"
|
||||
},
|
||||
{
|
||||
"title": "Passo 9: Ataque inimigos desprevenidos",
|
||||
"content": "Se você se aproximar de um inimigo por trás, um prompt aparecerá, permitindo que você dê um golpe devastador enquanto o inimigo está desprevenido. Essa manobra é muito útil e pode matar inimigos em um único golpe, independentemente do seu nível de saúde e defesa. [SCREENSHOT: 00:08:30]"
|
||||
},
|
||||
{
|
||||
"title": "Passo 10: Evite riscos desnecessários",
|
||||
"content": "Não use nada de metal durante uma tempestade, pois isso pode ser perigoso. Além disso, certifique-se de seguir as dicas anteriores para minimizar os riscos e maximizar as recompensas. [SCREENSHOT: 00:09:20]"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Sections
|
||||
### Passo 1: Introdução
|
||||
Bem-vindo ao tutorial de The Legend of Zelda: Breath of the Wild. Neste vídeo, vamos explorar 9 dicas para iniciantes para ajudá-los a superar as ameaças iniciais do jogo.
|
||||
**Timestamp:** 00:00:20
|
||||
|
||||
### Passo 2: Planeje suas batalhas
|
||||
Não é sempre a melhor ideia correr direto para a batalha. Em vez disso, tire um momento para formular um plano de ação antes de se aproximar do inimigo. Por exemplo, se você encontrar um acampamento de inimigos, pense sobre como você pode eliminá-los com o mínimo de problemas.
|
||||
**Timestamp:** 00:01:20
|
||||
|
||||
### Passo 3: Gerencie seus equipamentos
|
||||
Os equipamentos e armas do jogo têm uma mecânica de durabilidade, o que significa que eles se desgastam com o tempo e eventualmente quebram. Certifique-se de usar seus equipamentos mais poderosos apenas contra inimigos mais difíceis e chefs.
|
||||
**Timestamp:** 00:02:30
|
||||
|
||||
### Passo 4: Escolha suas batalhas
|
||||
Não há vergonha em fugir de uma batalha se não há um objetivo em mente. Seus recursos de armas e equipamentos são limitados, então é sempre melhor evitar encontros desnecessários.
|
||||
**Timestamp:** 00:03:20
|
||||
|
||||
### Passo 5: Complete os santuários
|
||||
Os santuários oferecem uma ótima distração dos períodos prolongados de exploração e completá-los pode recompensar você com espíritos orbitais, que podem ser usados para comprar upgrades úteis. Além disso, completar um santuário permite que você desbloqueie sua localização como um ponto de viagem rápida.
|
||||
**Timestamp:** 00:04:30
|
||||
@ -92,21 +92,6 @@ app.UseCors("AllowAll");
|
||||
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
|
||||
|
||||
// Video info endpoint (lightweight preview)
|
||||
app.MapGet("/api/video-info", async (string url, AnalysisService service) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await service.GetVideoInfoAsync(url);
|
||||
return Results.Ok(info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
})
|
||||
.WithName("GetVideoInfo");
|
||||
|
||||
// Main analysis endpoint
|
||||
app.MapPost("/api/analyze", async (AnalysisRequest request, AnalysisService service) =>
|
||||
{
|
||||
|
||||
@ -8,9 +8,6 @@ using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using PuppeteerSharp;
|
||||
using VideoStudy.Shared;
|
||||
using SkiaSharp;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography; // Added for MD5 hash
|
||||
|
||||
namespace VideoStudy.API.Services;
|
||||
|
||||
@ -73,46 +70,6 @@ public class AnalysisService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<VideoInfo> GetVideoInfoAsync(string url)
|
||||
{
|
||||
var ytDlpPath = GetYtDlpPath();
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ytDlpPath,
|
||||
Arguments = $"--print title --print channel --print duration --print thumbnail \"{url}\"",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var proc = Process.Start(startInfo) ?? throw new Exception("Failed to start yt-dlp");
|
||||
var output = await proc.StandardOutput.ReadToEndAsync();
|
||||
var error = await proc.StandardError.ReadToEndAsync();
|
||||
await proc.WaitForExitAsync();
|
||||
|
||||
if (proc.ExitCode != 0)
|
||||
throw new Exception($"yt-dlp error: {error}");
|
||||
|
||||
var lines = output.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var title = lines.Length > 0 ? lines[0].Trim() : "Unknown";
|
||||
var channel = lines.Length > 1 ? lines[1].Trim() : "Unknown";
|
||||
var durationStr = lines.Length > 2 ? lines[2].Trim() : "0";
|
||||
var thumbnail = lines.Length > 3 ? lines[3].Trim() : "";
|
||||
|
||||
double.TryParse(durationStr, System.Globalization.NumberStyles.Any,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var durationSeconds);
|
||||
|
||||
return new VideoInfo
|
||||
{
|
||||
Title = title,
|
||||
Author = channel,
|
||||
Duration = TimeSpan.FromSeconds(durationSeconds),
|
||||
Url = url,
|
||||
ThumbnailUrl = thumbnail
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AnalysisResponse> AnalyzeVideoAsync(AnalysisRequest request)
|
||||
{
|
||||
_debugSteps.Clear();
|
||||
@ -122,75 +79,51 @@ public class AnalysisService
|
||||
|
||||
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
|
||||
{
|
||||
// --- Step 1: Transcription ---
|
||||
AddLog("🌐 Obtendo transcrição via yt-dlp...");
|
||||
var (transcript, originalTitle) = await GetTranscriptViaYtDlpAsync(request.VideoUrl, request.Language, tempDir);
|
||||
var transcript = await GetTranscriptViaYtDlpAsync(request.VideoUrl, request.Language, tempDir);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transcript))
|
||||
throw new Exception("Não foi possível obter a transcrição do vídeo.");
|
||||
|
||||
AddLog($"✅ Transcrição obtida: '{originalTitle}' ({transcript.Length} chars).");
|
||||
AddLog($"✅ Transcrição obtida ({transcript.Length} caracteres).");
|
||||
|
||||
// --- Step 2: Intelligence ---
|
||||
AddLog("🧠 Enviando transcrição para o Groq (LLM)...");
|
||||
var (tutorialSections, rawJson, category, docTitle) = await GenerateTutorialContentAsync(transcript, originalTitle, request.Language, request.OutputLanguage, videoInfo.Duration);
|
||||
var (tutorialSections, rawJson) = await GenerateTutorialContentAsync(transcript, request.Language);
|
||||
rawLlmResponse = rawJson;
|
||||
|
||||
// Save debug MD
|
||||
// Save debug MD in project root
|
||||
var debugFile = Path.Combine(Directory.GetCurrentDirectory(), "DEBUG_LAST_RESPONSE.md");
|
||||
var debugContent = $"# {docTitle} ({category})\n\nSource: {originalTitle}\n\n## Raw JSON\n```json\n{rawJson}\n```\n";
|
||||
var debugContent = $"# Debug Groq Response\n\nURL: {request.VideoUrl}\n\n## Raw JSON\n```json\n{rawJson}\n```\n\n## Sections\n" +
|
||||
string.Join("\n\n", tutorialSections.Select(s => $"### {s.Title}\n{s.Content}\n**Timestamp:** {s.ImageTimestamp}"));
|
||||
await File.WriteAllTextAsync(debugFile, debugContent);
|
||||
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 ---
|
||||
var sectionsWithImages = tutorialSections.Where(s => !string.IsNullOrEmpty(s.ImageTimestamp)).ToList();
|
||||
if (sectionsWithImages.Any())
|
||||
{
|
||||
AddLog($"📸 Capturando {sectionsWithImages.Count} prints usando Puppeteer (Direct Bypass)...");
|
||||
// Pass videoInfo.Duration to the screenshot method
|
||||
await CaptureScreenshotsWithPuppeteerAsync(request.VideoUrl, tutorialSections, tempDir, videoInfo.Duration);
|
||||
await CaptureScreenshotsWithPuppeteerAsync(request.VideoUrl, tutorialSections, tempDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddLog("⚠️ Nenhuma tag [SCREENSHOT] foi gerada pela IA, ou todas foram invalidadas.");
|
||||
AddLog("⚠️ Nenhuma tag [SCREENSHOT] foi gerada pela IA.");
|
||||
}
|
||||
|
||||
// --- Step 4: PDF Generation ---
|
||||
AddLog("📄 Gerando PDF final com QuestPDF...");
|
||||
var pdfBytes = GeneratePdf(docTitle, request.VideoUrl, tutorialSections, category);
|
||||
var pdfBytes = GeneratePdf(request.VideoUrl, tutorialSections);
|
||||
|
||||
AddLog("🎉 Processamento concluído com sucesso!");
|
||||
|
||||
return new AnalysisResponse
|
||||
{
|
||||
Status = "success",
|
||||
VideoTitle = originalTitle,
|
||||
DocumentTitle = docTitle,
|
||||
Category = category,
|
||||
VideoTitle = request.VideoUrl,
|
||||
Transcript = transcript,
|
||||
TutorialSections = tutorialSections,
|
||||
PdfData = pdfBytes,
|
||||
@ -219,30 +152,10 @@ public class AnalysisService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string transcript, string title)> GetTranscriptViaYtDlpAsync(string url, string language, string outputDir)
|
||||
private async Task<string> GetTranscriptViaYtDlpAsync(string url, string language, string outputDir)
|
||||
{
|
||||
var ytDlpPath = GetYtDlpPath();
|
||||
// Use a safe output template to avoid filesystem issues, but we want the title.
|
||||
// Better: Fetch title separately or read metadata json.
|
||||
// Let's stick to filename trick but ensure safe chars.
|
||||
// Actually, just fetch title with --print title
|
||||
|
||||
// 1. Fetch Title
|
||||
var titleStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ytDlpPath,
|
||||
Arguments = $"--print title \"{url}\"",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var pTitle = Process.Start(titleStartInfo);
|
||||
var title = (await pTitle!.StandardOutput.ReadToEndAsync()).Trim();
|
||||
await pTitle.WaitForExitAsync();
|
||||
if (string.IsNullOrEmpty(title)) title = "Video Analysis";
|
||||
|
||||
// 2. Fetch Subs
|
||||
var arguments = $"--skip-download --write-sub --write-auto-sub --sub-lang {language},en --sub-format vtt --output \"%(id)s\" \"{url}\"";
|
||||
var arguments = $"--skip-download --write-sub --write-auto-sub --sub-lang {language},en --sub-format vtt --output \"%(title)s\" \"{url}\"";
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
@ -259,9 +172,9 @@ public class AnalysisService
|
||||
await proc.WaitForExitAsync();
|
||||
|
||||
var vttFile = Directory.GetFiles(outputDir, "*.vtt").FirstOrDefault();
|
||||
if (vttFile == null) return (string.Empty, title);
|
||||
if (vttFile == null) return string.Empty;
|
||||
|
||||
return (ParseVttToText(await File.ReadAllTextAsync(vttFile)), title);
|
||||
return ParseVttToText(await File.ReadAllTextAsync(vttFile));
|
||||
}
|
||||
|
||||
private string ParseVttToText(string vttContent)
|
||||
@ -280,75 +193,35 @@ public class AnalysisService
|
||||
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, TimeSpan videoDuration)
|
||||
private async Task<(List<TutorialSection> sections, string rawJson)> GenerateTutorialContentAsync(string transcript, string language)
|
||||
{
|
||||
var langMap = new Dictionary<string, string>
|
||||
{
|
||||
{"en", "English"}, {"pt", "Portuguese (Brazilian)"}, {"es", "Spanish"},
|
||||
{"fr", "French"}, {"de", "German"}, {"it", "Italian"},
|
||||
{"ja", "Japanese"}, {"ko", "Korean"}, {"zh", "Chinese"}
|
||||
};
|
||||
var outputLang = string.IsNullOrWhiteSpace(outputLanguage) ? inputLanguage : outputLanguage;
|
||||
var outputLangName = langMap.GetValueOrDefault(outputLang, outputLang);
|
||||
|
||||
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 = $@"
|
||||
Você é um Editor Chefe e Analista de Conteúdo Sênior.
|
||||
Receberá:
|
||||
A) TÍTULO ORIGINAL: {originalTitle}
|
||||
B) TRANSCRIÇÃO: {transcript[..Math.Min(transcript.Length, 20000)]}
|
||||
C) DURAÇÃO TOTAL DO VÍDEO: {formattedVideoDuration} ({totalSecondsVideoDuration} segundos)
|
||||
|
||||
SUA MISSÃO:
|
||||
1. **Classificar** o vídeo em: 'TUTORIAL', 'MEETING', 'LECTURE' ou 'OTHER'.
|
||||
2. **Criar um Título Profissional**:
|
||||
- Use o TÍTULO ORIGINAL como base.
|
||||
- Remova clickbaits, emojis e CAPS LOCK excessivo.
|
||||
- O título deve parecer o de um documento técnico ou ata oficial.
|
||||
3. **Estruturar o Conteúdo**:
|
||||
- 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.
|
||||
- **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}.**
|
||||
|
||||
SAÍDA JSON OBRIGATÓRIA:
|
||||
Converta a transcrição abaixo em um Tutorial Passo a Passo em {language}.
|
||||
REGRAS:
|
||||
1. Divida em passos lógicos.
|
||||
2. Identifique onde um print da tela é necessário e insira [SCREENSHOT: HH:MM:SS].
|
||||
3. Retorne APENAS JSON.
|
||||
JSON:
|
||||
{{
|
||||
""category"": ""TUTORIAL | MEETING | LECTURE | OTHER"",
|
||||
""documentTitle"": ""Título Profissional Gerado"",
|
||||
""sections"": [
|
||||
{{
|
||||
""title"": ""Título da Seção"",
|
||||
""content"": ""Texto explicativo detalhado... [SCREENSHOT: 00:05:30]""
|
||||
}}
|
||||
{{ ""title"": ""Passo 1"", ""content"": ""Texto... [SCREENSHOT: 00:01:20]"" }}
|
||||
]
|
||||
}}";
|
||||
}}
|
||||
Transcrição: {transcript[..Math.Min(transcript.Length, 15000)]}";
|
||||
|
||||
var result = await chatService.GetChatMessageContentAsync(prompt);
|
||||
var json = result.Content?.Trim() ?? "{}";
|
||||
// Extract JSON from LLM response — handles text before/after the JSON block
|
||||
var jsonMatch = Regex.Match(json, @"\{[\s\S]*\}", RegexOptions.Singleline);
|
||||
if (jsonMatch.Success)
|
||||
json = jsonMatch.Value;
|
||||
if (json.StartsWith("```")) {
|
||||
var idx = json.IndexOf('\n');
|
||||
if (idx > 0) json = json[(idx+1)..];
|
||||
if (json.EndsWith("```")) json = json[..^3];
|
||||
}
|
||||
|
||||
var sections = new List<TutorialSection>();
|
||||
string category = "OTHER";
|
||||
string docTitle = originalTitle;
|
||||
|
||||
try {
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("category", out var catEl)) category = catEl.GetString() ?? "OTHER";
|
||||
if (root.TryGetProperty("documentTitle", out var titleEl)) docTitle = titleEl.GetString() ?? originalTitle;
|
||||
|
||||
foreach (var el in root.GetProperty("sections").EnumerateArray()) {
|
||||
foreach (var el in doc.RootElement.GetProperty("sections").EnumerateArray()) {
|
||||
var content = el.GetProperty("content").GetString() ?? "";
|
||||
var ts = ExtractTimestamp(content);
|
||||
sections.Add(new TutorialSection {
|
||||
@ -358,7 +231,7 @@ SAÍDA JSON OBRIGATÓRIA:
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
return (sections, json, category, docTitle);
|
||||
return (sections, json);
|
||||
}
|
||||
|
||||
private string? ExtractTimestamp(string text)
|
||||
@ -387,18 +260,14 @@ SAÍDA JSON OBRIGATÓRIA:
|
||||
return url?.Trim();
|
||||
}
|
||||
|
||||
private async Task CaptureScreenshotsWithPuppeteerAsync(string videoUrl, List<TutorialSection> sections, string outputDir, TimeSpan videoDuration)
|
||||
private async Task CaptureScreenshotsWithPuppeteerAsync(string videoUrl, List<TutorialSection> sections, string outputDir)
|
||||
{
|
||||
var sectionsWithImages = sections.Where(s => !string.IsNullOrEmpty(s.ImageTimestamp)).ToList();
|
||||
if (!sectionsWithImages.Any()) return;
|
||||
|
||||
AddLog("🔍 Obtendo link direto do vídeo (Bypass YouTube Player)...");
|
||||
var rawVideoUrl = await GetRawVideoStreamUrl(videoUrl);
|
||||
if (string.IsNullOrEmpty(rawVideoUrl))
|
||||
{
|
||||
AddLog("❌ Falha ao obter link direto. As capturas de tela serão ignoradas.");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(rawVideoUrl)) { AddLog("❌ Falha ao obter link direto."); return; }
|
||||
|
||||
try
|
||||
{
|
||||
@ -406,223 +275,57 @@ SAÍDA JSON OBRIGATÓRIA:
|
||||
using var page = await browser.NewPageAsync();
|
||||
await page.SetViewportAsync(new ViewPortOptions { Width = 1280, Height = 720 });
|
||||
|
||||
var htmlContent = $@"
|
||||
<html>
|
||||
<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");
|
||||
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>";
|
||||
await page.SetContentAsync(html);
|
||||
await page.WaitForSelectorAsync("video");
|
||||
|
||||
// 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++)
|
||||
foreach (var section in sectionsWithImages)
|
||||
{
|
||||
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))
|
||||
{
|
||||
AddLog($"📸 Processando captura para {section.ImageTimestamp} ('{section.Title}')");
|
||||
var targetSeconds = (int)ts.TotalSeconds;
|
||||
var sec = (int)ts.TotalSeconds;
|
||||
AddLog($"🌐 Renderizando frame: {section.ImageTimestamp}...");
|
||||
|
||||
var candidates = new List<(byte[] ImageData, double Score, int Time, string Hash, int Size)>();
|
||||
await page.EvaluateFunctionAsync(@"(s) => {
|
||||
return new Promise(r => {
|
||||
const v = document.getElementById('v');
|
||||
v.currentTime = s;
|
||||
v.addEventListener('seeked', r, {once:true});
|
||||
});
|
||||
}", sec);
|
||||
|
||||
// "Best of 3" capture window: T-1, T, T+1 seconds
|
||||
var timeOffsets = new[] { -1, 0, 1 };
|
||||
await Task.Delay(500);
|
||||
var path = Path.Combine(outputDir, $"snap_{sec}.jpg");
|
||||
await page.ScreenshotAsync(path, new ScreenshotOptions { Type = ScreenshotType.Jpeg, Quality = 90 });
|
||||
if (File.Exists(path)) section.ImageData = await File.ReadAllBytesAsync(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { AddLog($"❌ Erro Puppeteer: {ex.Message}"); }
|
||||
}
|
||||
|
||||
foreach (var offset in timeOffsets)
|
||||
private byte[] GeneratePdf(string videoUrl, List<TutorialSection> sections)
|
||||
{
|
||||
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 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)
|
||||
{
|
||||
var categoryColor = category switch
|
||||
{
|
||||
"TUTORIAL" => Colors.Green.Medium,
|
||||
"MEETING" => Colors.Orange.Medium,
|
||||
"LECTURE" => Colors.Purple.Medium,
|
||||
_ => Colors.Blue.Medium
|
||||
};
|
||||
|
||||
var document = Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Segoe UI").Fallback(f => f.FontFamily("Microsoft YaHei")));
|
||||
|
||||
page.Header().Column(c =>
|
||||
{
|
||||
c.Item().Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text(docTitle).SemiBold().FontSize(20).FontColor(Colors.Black);
|
||||
row.ConstantItem(100).AlignRight().Text(category).Bold().FontSize(10).FontColor(categoryColor);
|
||||
});
|
||||
c.Item().PaddingTop(5).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
|
||||
});
|
||||
|
||||
page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial"));
|
||||
page.Header().Text("Video Tutorial").SemiBold().FontSize(24).FontColor(Colors.Blue.Medium);
|
||||
page.Content().PaddingVertical(1, Unit.Centimetre).Column(column =>
|
||||
{
|
||||
column.Item().Text($"Fonte: {videoUrl}").Italic().FontSize(9).FontColor(Colors.Grey.Medium);
|
||||
column.Item().Text($"Fonte: {videoUrl}").Italic().FontSize(10).FontColor(Colors.Grey.Medium);
|
||||
column.Item().PaddingBottom(20);
|
||||
|
||||
foreach (var section in sections)
|
||||
{
|
||||
column.Item().Text(section.Title).Bold().FontSize(14).FontColor(categoryColor);
|
||||
column.Item().Text(section.Title).Bold().FontSize(16);
|
||||
column.Item().Text(text => { text.Span(section.Content); });
|
||||
|
||||
if (section.ImageData != null)
|
||||
{
|
||||
column.Item().PaddingVertical(10).Image(section.ImageData).FitWidth();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(section.ImageTimestamp))
|
||||
{
|
||||
// Placeholder for missing image (Graceful Degradation)
|
||||
column.Item().PaddingVertical(10)
|
||||
.Background(Colors.Grey.Lighten3)
|
||||
.Height(100)
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text($"[Imagem Indisponível: {section.ImageTimestamp}]")
|
||||
.FontSize(10)
|
||||
.FontColor(Colors.Grey.Darken2);
|
||||
}
|
||||
|
||||
column.Item().PaddingBottom(15);
|
||||
column.Item().PaddingVertical(15).Image(section.ImageData).FitWidth();
|
||||
column.Item().PaddingBottom(20);
|
||||
}
|
||||
});
|
||||
|
||||
page.Footer().AlignCenter().Text(x => { x.Span("Gerado por VideoStudy.app - "); x.CurrentPageNumber(); });
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
<?xml version = "1.0" encoding = "UTF-8" ?>
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="VideoStudy.Native.App">
|
||||
<Application.Resources>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@ -1,14 +0,0 @@
|
||||
namespace VideoStudy.Native;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState? activationState)
|
||||
{
|
||||
return new Window(new MainPage());
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui"
|
||||
xmlns:ui="clr-namespace:VideoStudy.UI;assembly=VideoStudy.UI"
|
||||
x:Class="VideoStudy.Native.MainPage"
|
||||
BackgroundColor="White">
|
||||
|
||||
<blazor:BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
|
||||
<blazor:BlazorWebView.RootComponents>
|
||||
<blazor:RootComponent Selector="#app" ComponentType="{x:Type ui:Routes}" />
|
||||
</blazor:BlazorWebView.RootComponents>
|
||||
</blazor:BlazorWebView>
|
||||
|
||||
</ContentPage>
|
||||
@ -1,9 +0,0 @@
|
||||
namespace VideoStudy.Native;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components.WebView.Maui;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using VideoStudy.Native.Services;
|
||||
using VideoStudy.Shared;
|
||||
using VideoStudy.UI;
|
||||
|
||||
namespace VideoStudy.Native;
|
||||
|
||||
public static class MauiProgram
|
||||
{
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>();
|
||||
|
||||
builder.Services.AddMauiBlazorWebView();
|
||||
|
||||
#if DEBUG
|
||||
builder.Services.AddBlazorWebViewDeveloperTools();
|
||||
builder.Logging.AddDebug();
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||
#endif
|
||||
|
||||
// Configurar HttpClient para API Local (timeout longo para análise de vídeo)
|
||||
builder.Services.AddScoped(sp => new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost:5000"),
|
||||
Timeout = TimeSpan.FromMinutes(10)
|
||||
});
|
||||
|
||||
// PDF Saver (FileSavePicker do Windows)
|
||||
builder.Services.AddSingleton<IPdfSaver, WindowsPdfSaver>();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
<maui:MauiWinUIApplication
|
||||
x:Class="VideoStudy.Native.WinUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:maui="using:Microsoft.Maui"
|
||||
xmlns:local="using:VideoStudy.Native.WinUI">
|
||||
</maui:MauiWinUIApplication>
|
||||
@ -1,13 +0,0 @@
|
||||
using Microsoft.Maui;
|
||||
using Microsoft.Maui.Hosting;
|
||||
|
||||
namespace VideoStudy.Native.WinUI;
|
||||
|
||||
public partial class App : MauiWinUIApplication
|
||||
{
|
||||
public App()
|
||||
{
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="com.companyname.videostudy"
|
||||
Publisher="CN=Ricardo"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>VideoStudy</DisplayName>
|
||||
<PublisherDisplayName>Ricardo</PublisherDisplayName>
|
||||
<Logo>Resources\AppIcon\appicon.svg</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="VideoStudy"
|
||||
Description="VideoStudy"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Resources\AppIcon\appicon.svg"
|
||||
Square44x44Logo="Resources\AppIcon\appicon.svg">
|
||||
<uap:DefaultTile Wide310x150Logo="Resources\AppIcon\appicon.svg" />
|
||||
<uap:SplashScreen Image="Resources\Splash\splash.svg" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<Capability Name="internetClient" />
|
||||
</Capabilities>
|
||||
|
||||
</Package>
|
||||
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="64" cy="64" r="60" fill="#512BD4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 200 B |
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="40" fill="white">VS</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 280 B |
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="168" height="208" viewBox="0 0 168 208" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="168" height="208" fill="#512BD4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 200 B |
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" fill="#512BD4"/>
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="20" fill="white">VideoStudy</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 338 B |
@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
|
||||
<Color x:Key="Primary">#512BD4</Color>
|
||||
<Color x:Key="Secondary">#DFD8F7</Color>
|
||||
<Color x:Key="Tertiary">#2B0B98</Color>
|
||||
<Color x:Key="White">#FFFFFF</Color>
|
||||
<Color x:Key="Black">#000000</Color>
|
||||
<Color x:Key="Gray100">#E1E1E1</Color>
|
||||
<Color x:Key="Gray200">#C8C8C8</Color>
|
||||
<Color x:Key="Gray300">#ACACAC</Color>
|
||||
<Color x:Key="Gray400">#919191</Color>
|
||||
<Color x:Key="Gray500">#6E6E6E</Color>
|
||||
<Color x:Key="Gray600">#404040</Color>
|
||||
<Color x:Key="Gray900">#212121</Color>
|
||||
<Color x:Key="Gray950">#141414</Color>
|
||||
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
|
||||
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
|
||||
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
|
||||
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
|
||||
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
|
||||
</ResourceDictionary>
|
||||
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
|
||||
<Style TargetType="ActivityIndicator">
|
||||
<Setter Property="Color" Value="{StaticResource Primary}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="TextColor" Value="{StaticResource White}" />
|
||||
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Padding" Value="14,10" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label">
|
||||
<Setter Property="TextColor" Value="{StaticResource Black}" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Page">
|
||||
<Setter Property="BackgroundColor" Value="{StaticResource White}" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@ -1,38 +0,0 @@
|
||||
using VideoStudy.Shared;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace VideoStudy.Native.Services;
|
||||
|
||||
public class WindowsPdfSaver : IPdfSaver
|
||||
{
|
||||
public async Task<string?> SavePdfAsync(byte[] pdfData, string suggestedFileName)
|
||||
{
|
||||
var savePicker = new FileSavePicker
|
||||
{
|
||||
SuggestedStartLocation = PickerLocationId.Downloads,
|
||||
SuggestedFileName = suggestedFileName
|
||||
};
|
||||
savePicker.FileTypeChoices.Add("PDF", [".pdf"]);
|
||||
|
||||
// Get the window handle for the picker
|
||||
var window = Application.Current?.Windows.FirstOrDefault();
|
||||
var mauiWindow = window?.Handler?.PlatformView;
|
||||
if (mauiWindow is Microsoft.UI.Xaml.Window winuiWindow)
|
||||
{
|
||||
var hwnd = WindowNative.GetWindowHandle(winuiWindow);
|
||||
InitializeWithWindow.Initialize(savePicker, hwnd);
|
||||
}
|
||||
|
||||
var file = await savePicker.PickSaveFileAsync();
|
||||
if (file == null) return null;
|
||||
|
||||
await FileIO.WriteBytesAsync(file, pdfData);
|
||||
|
||||
// Open the PDF after saving
|
||||
await Windows.System.Launcher.LaunchFileAsync(file);
|
||||
|
||||
return file.Path;
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<ApplicationTitle>VideoStudy</ApplicationTitle>
|
||||
<ApplicationId>com.companyname.videostudy</ApplicationId>
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
||||
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
|
||||
<MauiImage Include="Resources\Images\*" />
|
||||
<MauiFont Include="Resources\Fonts\*" />
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.31" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.31" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.31" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\VideoStudy.UI\VideoStudy.UI.csproj" />
|
||||
<ProjectReference Include="..\VideoStudy.Shared\VideoStudy.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>VideoStudy.Native</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="_content/VideoStudy.UI/app.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<div style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<div style="margin-top:10px;">Carregando VideoStudy...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webview.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -6,8 +6,7 @@ namespace VideoStudy.Shared;
|
||||
public class AnalysisRequest
|
||||
{
|
||||
public string VideoUrl { get; set; } = string.Empty;
|
||||
public string Language { get; set; } = "en"; // Input language (subtitles)
|
||||
public string? OutputLanguage { get; set; } // Output language (tutorial). null = same as Language
|
||||
public string Language { get; set; } = "en";
|
||||
public string Mode { get; set; } = "fast"; // fast or advanced
|
||||
public string? TranscriptText { get; set; } // Optional: if client already transcribed it
|
||||
public double DurationSeconds { get; set; }
|
||||
@ -19,8 +18,6 @@ public class AnalysisRequest
|
||||
public class AnalysisResponse
|
||||
{
|
||||
public string VideoTitle { get; set; } = string.Empty;
|
||||
public string DocumentTitle { get; set; } = string.Empty; // New
|
||||
public string Category { get; set; } = "OTHER"; // New
|
||||
public string Transcript { get; set; } = string.Empty;
|
||||
public List<KeyMoment> KeyMoments { get; set; } = [];
|
||||
public string Analysis { get; set; } = string.Empty;
|
||||
@ -98,8 +95,3 @@ public class DownloadProgress
|
||||
public double PercentComplete { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public interface IPdfSaver
|
||||
{
|
||||
Task<string?> SavePdfAsync(byte[] pdfData, string suggestedFileName);
|
||||
}
|
||||
@ -1,10 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
|
||||
<!-- Bootstrap 5.3.2 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="VideoStudy.Desktop.styles.css" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
|
||||
<!-- Bootstrap 5.3.2 JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,36 +0,0 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace VideoStudy.UI;
|
||||
|
||||
// This class provides an example of how JavaScript functionality can be wrapped
|
||||
// in a .NET class for easy consumption. The associated JavaScript module is
|
||||
// loaded on demand when first needed.
|
||||
//
|
||||
// This class can be registered as scoped DI service and then injected into Blazor
|
||||
// components for use.
|
||||
|
||||
public class ExampleJsInterop : IAsyncDisposable
|
||||
{
|
||||
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
|
||||
|
||||
public ExampleJsInterop(IJSRuntime jsRuntime)
|
||||
{
|
||||
moduleTask = new (() => jsRuntime.InvokeAsync<IJSObjectReference>(
|
||||
"import", "./_content/VideoStudy.UI/exampleJsInterop.js").AsTask());
|
||||
}
|
||||
|
||||
public async ValueTask<string> Prompt(string message)
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
return await module.InvokeAsync<string>("showPrompt", message);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (moduleTask.IsValueCreated)
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
await module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<nav class="navbar navbar-dark bg-dark sticky-top mb-4">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">📺 VideoStudy</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main role="main" class="px-4">
|
||||
@Body
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
<div class="alert alert-danger m-3" role="alert">
|
||||
<h4 class="alert-heading">⚠️ Unhandled error</h4>
|
||||
<p>An unhandled error has occurred. Please reload the page.</p>
|
||||
<hr>
|
||||
<button class="btn btn-primary reload">Reload</button>
|
||||
<button class="btn btn-secondary dismiss">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#blazor-error-ui[style*="display"] {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.reload, .dismiss {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@ -1,18 +0,0 @@
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id;
|
||||
}
|
||||
@ -1,327 +0,0 @@
|
||||
@page "/"
|
||||
@inject HttpClient Http
|
||||
@inject IPdfSaver PdfSaver
|
||||
@using VideoStudy.Shared
|
||||
@using System.Net.Http.Json
|
||||
@using System.Text.Json
|
||||
|
||||
<PageTitle>VideoStudy - Video Analysis</PageTitle>
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Header -->
|
||||
<header class="text-center mb-5">
|
||||
<h1 class="display-4 fw-bold" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
|
||||
VideoStudy (Native)
|
||||
</h1>
|
||||
<p class="lead text-muted">Transforme videos em apostilas inteligentes com IA</p>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<ul class="nav nav-pills nav-fill mb-4 shadow-sm p-1 bg-light rounded-pill">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link rounded-pill py-3 fw-bold @(activeTab == "youtube" ? "active bg-primary text-white" : "text-muted")"
|
||||
@onclick="@(() => activeTab = "youtube")" disabled="@(isProcessing || isFetchingInfo)">
|
||||
<span style="font-size:1.1rem;">YouTube Video</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link rounded-pill py-3 fw-bold text-muted" disabled>
|
||||
Local Video (Coming Soon)
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="card shadow-soft border-0 rounded-4 overflow-hidden mb-4 p-4 bg-white">
|
||||
@if (activeTab == "youtube")
|
||||
{
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold text-muted small">YouTube URL</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="https://www.youtube.com/watch?v=..."
|
||||
@bind="videoUrl" disabled="@(isProcessing || isFetchingInfo)" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Video Info Preview -->
|
||||
@if (videoInfo != null)
|
||||
{
|
||||
<div class="border rounded-3 p-3 mb-4 d-flex align-items-start gap-3" style="background: #f0f4ff;">
|
||||
@if (!string.IsNullOrEmpty(videoInfo.ThumbnailUrl))
|
||||
{
|
||||
<img src="@videoInfo.ThumbnailUrl" alt="Thumbnail"
|
||||
style="width: 180px; border-radius: 8px; flex-shrink: 0;" />
|
||||
}
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="fw-bold mb-1">@videoInfo.Title</h6>
|
||||
<div class="text-muted small">
|
||||
<span class="me-3">@videoInfo.Author</span>
|
||||
<span>@videoInfo.Duration.ToString(@"hh\:mm\:ss")</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="ClearVideoInfo"
|
||||
disabled="@(isProcessing)" title="Limpar">X</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Language Selectors -->
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-muted small">Idioma do Video (Legendas)</label>
|
||||
<select class="form-select" @bind="selectedLanguage" disabled="@(isProcessing || isFetchingInfo)">
|
||||
<option value="en">English</option>
|
||||
<option value="pt">Portuguese (BR)</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="ko">Korean</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-muted small">Idioma de Saida (Tutorial)</label>
|
||||
<select class="form-select" @bind="selectedOutputLanguage" disabled="@(isProcessing || isFetchingInfo)">
|
||||
<option value="pt">Portuguese (BR)</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="mt-4">
|
||||
@if (videoInfo == null)
|
||||
{
|
||||
<!-- Step 1: Fetch video info -->
|
||||
<button class="btn btn-lg btn-primary w-100 py-3 fw-bold shadow-sm"
|
||||
@onclick="FetchVideoInfo"
|
||||
disabled="@(isProcessing || isFetchingInfo || string.IsNullOrWhiteSpace(videoUrl))">
|
||||
@if (isFetchingInfo)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
<span>Verificando video...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Generate Tutorial PDF</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Step 2: Confirm and generate -->
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-lg btn-success flex-grow-1 py-3 fw-bold shadow-sm"
|
||||
@onclick="StartAnalysis"
|
||||
disabled="@isProcessing">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
<span>Gerando PDF...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Confirmar e Gerar PDF</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
@if (isProcessing || currentStep > 0)
|
||||
{
|
||||
<div class="progress mb-3" style="height: 25px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: @(progress)%">
|
||||
@statusMessage
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Logs (Collapsible) -->
|
||||
@if (logs.Count > 0)
|
||||
{
|
||||
<div class="card shadow-sm mb-4 border-0 rounded-4 overflow-hidden">
|
||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center" style="cursor: pointer;" @onclick="ToggleLogs">
|
||||
<small class="fw-bold">Execution Logs</small>
|
||||
<small>@logs.Count items @(showLogs ? "v" : ">")</small>
|
||||
</div>
|
||||
@if (showLogs)
|
||||
{
|
||||
<div class="card-body bg-light" style="max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
|
||||
@foreach (var log in logs)
|
||||
{
|
||||
<div class="text-dark border-bottom py-1">
|
||||
<span class="text-muted">[@log.Timestamp:HH:mm:ss]</span> @log.Message
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.nav-pills .nav-link.active {
|
||||
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
.shadow-soft {
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private string activeTab = "youtube";
|
||||
private bool isProcessing = false;
|
||||
private bool isFetchingInfo = false;
|
||||
private int progress = 0;
|
||||
private int currentStep = 0;
|
||||
private string statusMessage = "Ready";
|
||||
private bool showLogs = false;
|
||||
private List<LogEntry> logs = new();
|
||||
|
||||
private string videoUrl = string.Empty;
|
||||
private string selectedLanguage = "en";
|
||||
private string selectedOutputLanguage = "pt";
|
||||
private VideoInfo? videoInfo;
|
||||
|
||||
private class LogEntry
|
||||
{
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private void ToggleLogs() => showLogs = !showLogs;
|
||||
|
||||
private void AddLog(string message)
|
||||
{
|
||||
logs.Insert(0, new LogEntry { Message = message });
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ClearVideoInfo()
|
||||
{
|
||||
videoInfo = null;
|
||||
}
|
||||
|
||||
private async Task FetchVideoInfo()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(videoUrl)) return;
|
||||
isFetchingInfo = true;
|
||||
showLogs = true;
|
||||
|
||||
try
|
||||
{
|
||||
AddLog("Buscando informacoes do video...");
|
||||
var response = await Http.GetAsync($"api/video-info?url={Uri.EscapeDataString(videoUrl)}");
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
throw new Exception($"API Error: {error}");
|
||||
}
|
||||
|
||||
videoInfo = await response.Content.ReadFromJsonAsync<VideoInfo>(
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (videoInfo != null)
|
||||
AddLog($"Video encontrado: {videoInfo.Title} ({videoInfo.Duration:hh\\:mm\\:ss})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"Erro: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isFetchingInfo = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartAnalysis()
|
||||
{
|
||||
isProcessing = true;
|
||||
progress = 0;
|
||||
currentStep = 0;
|
||||
logs.Clear();
|
||||
showLogs = true;
|
||||
|
||||
try
|
||||
{
|
||||
currentStep = 1;
|
||||
statusMessage = "Calling API...";
|
||||
AddLog("Sending request to API...");
|
||||
|
||||
var request = new AnalysisRequest
|
||||
{
|
||||
VideoUrl = videoUrl,
|
||||
Language = selectedLanguage,
|
||||
OutputLanguage = selectedOutputLanguage,
|
||||
Mode = "native"
|
||||
};
|
||||
|
||||
// Long timeout via CancellationToken (HttpClient.Timeout can't be changed after first request)
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
|
||||
|
||||
var response = await Http.PostAsJsonAsync("api/analyze", request, cts.Token);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
throw new Exception($"API Error ({response.StatusCode}): {error}");
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<AnalysisResponse>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (result == null) throw new Exception("Failed to deserialize response");
|
||||
|
||||
if (result.DebugSteps != null)
|
||||
{
|
||||
foreach (var step in result.DebugSteps) AddLog($"[API] {step}");
|
||||
}
|
||||
|
||||
if (result.Status == "error") throw new Exception(result.ErrorMessage);
|
||||
|
||||
AddLog("Analysis Complete!");
|
||||
progress = 100;
|
||||
statusMessage = "Done!";
|
||||
|
||||
if (result.PdfData != null && result.PdfData.Length > 0)
|
||||
{
|
||||
AddLog("Saving PDF...");
|
||||
var fileName = $"VideoStudy_{result.VideoTitle ?? "Tutorial"}_{DateTime.Now:yyyyMMdd_HHmmss}.pdf";
|
||||
// Remove invalid filename chars
|
||||
fileName = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars()));
|
||||
var savedPath = await PdfSaver.SavePdfAsync(result.PdfData, fileName);
|
||||
if (savedPath != null)
|
||||
AddLog($"PDF saved: {savedPath}");
|
||||
else
|
||||
AddLog("PDF save cancelled by user");
|
||||
}
|
||||
else
|
||||
{
|
||||
AddLog("No PDF data returned from API");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"Error: {ex.Message}");
|
||||
statusMessage = "Error";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isProcessing = false;
|
||||
videoInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow-lg">
|
||||
<div class="modal-header bg-gradient text-white">
|
||||
<h5 class="modal-title">📄 PDF Preview</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="OnCancel"></button>
|
||||
</div>
|
||||
<div class="modal-body bg-light p-4 text-center">
|
||||
<div class="pdf-icon mb-3">
|
||||
<span style="font-size: 4rem;">📄</span>
|
||||
</div>
|
||||
<h5>Your Study Notes are ready!</h5>
|
||||
<p class="text-muted">@System.IO.Path.GetFileName(PdfPath)</p>
|
||||
|
||||
<div class="alert alert-info text-start mt-3">
|
||||
<strong>Saved to:</strong> <br/>
|
||||
<small>@PdfPath</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="OnCancel">Close</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="OpenPdf">📂 Open PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string PdfPath { get; set; } = string.Empty;
|
||||
[Parameter] public EventCallback OnCancel { get; set; }
|
||||
|
||||
private void OpenPdf()
|
||||
{
|
||||
// Platform specific open
|
||||
try
|
||||
{
|
||||
var p = new System.Diagnostics.Process();
|
||||
p.StartInfo = new System.Diagnostics.ProcessStartInfo(PdfPath) { UseShellExecute = true };
|
||||
p.Start();
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
<div class="progress-container mb-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="fw-bold text-primary">@Status</span>
|
||||
<span class="text-muted">@Percent%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-gradient"
|
||||
role="progressbar"
|
||||
style="width: @Percent%"></div>
|
||||
</div>
|
||||
|
||||
<div class="steps d-flex justify-content-between mt-3 px-2">
|
||||
@foreach (var step in Steps)
|
||||
{
|
||||
<div class="step text-center @(step.IsActive ? "active" : "") @(step.IsCompleted ? "completed" : "")">
|
||||
<div class="step-circle mb-1">
|
||||
@if (step.IsCompleted) { <span>✓</span> } else { <span>@step.Number</span> }
|
||||
</div>
|
||||
<small class="step-label d-none d-sm-block">@step.Label</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Status { get; set; } = "Ready";
|
||||
[Parameter] public int Percent { get; set; } = 0;
|
||||
|
||||
// Simple step model
|
||||
public class Step
|
||||
{
|
||||
public int Number { get; set; }
|
||||
public string Label { get; set; } = "";
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
||||
|
||||
// We can pass current step index from parent
|
||||
[Parameter] public int CurrentStepIndex { get; set; } = 0;
|
||||
|
||||
private List<Step> Steps = new()
|
||||
{
|
||||
new Step { Number = 1, Label = "Download" },
|
||||
new Step { Number = 2, Label = "Transcrição" },
|
||||
new Step { Number = 3, Label = "Análise IA" },
|
||||
new Step { Number = 4, Label = "PDF" }
|
||||
};
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
for (int i = 0; i < Steps.Count; i++)
|
||||
{
|
||||
Steps[i].IsCompleted = i < CurrentStepIndex;
|
||||
Steps[i].IsActive = i == CurrentStepIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
<Router AppAssembly="typeof(Routes).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@ -1,31 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.22" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\VideoStudy.Shared\VideoStudy.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,12 +0,0 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using VideoStudy.UI
|
||||
@using VideoStudy.UI.Layout
|
||||
@using VideoStudy.UI.Pages
|
||||
@using VideoStudy.Shared
|
||||
@ -1,99 +0,0 @@
|
||||
:root {
|
||||
/* Colors from vcart.me.novo analysis */
|
||||
--primary: #007bff;
|
||||
--secondary: #0056b3;
|
||||
--accent-purple: #764ba2;
|
||||
--accent-blue: #667eea;
|
||||
--success: #28a745;
|
||||
--text-color: #212529;
|
||||
--bg-light: #f8f9fa;
|
||||
--bg-white: #ffffff;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-light);
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Gradients */
|
||||
.bg-gradient {
|
||||
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%) !important;
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.processor-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
/* Accordion Custom */
|
||||
.accordion-button:not(.collapsed) {
|
||||
background-color: rgba(102, 126, 234, 0.1);
|
||||
color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.accordion-button:focus {
|
||||
box-shadow: none;
|
||||
border-color: rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
/* Steps */
|
||||
.step-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: #e9ecef;
|
||||
color: #6c757d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step.active .step-circle {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.2);
|
||||
}
|
||||
|
||||
.step.completed .step-circle {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Mode Selection */
|
||||
.btn-outline-purple {
|
||||
border: 2px solid var(--accent-purple);
|
||||
color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.btn-outline-purple:hover, .btn-outline-purple.active {
|
||||
background: var(--accent-purple);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.shadow-soft {
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.08) !important;
|
||||
}
|
||||
BIN
VideoStudy.pdf
BIN
VideoStudy.pdf
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.2.11415.280
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Shared", "VideoStudy.Shared\VideoStudy.Shared.csproj", "{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}"
|
||||
EndProject
|
||||
@ -13,75 +13,34 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Desktop", "Video
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Desktop.Client", "VideoStudy.Desktop\VideoStudy.Desktop.Client\VideoStudy.Desktop.Client.csproj", "{59672094-7BE6-4CB2-8401-59D59D8AF07A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.UI", "VideoStudy.UI\VideoStudy.UI.csproj", "{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Native", "VideoStudy.Native\VideoStudy.Native.csproj", "{0387DAAC-724C-43D1-9C68-D8383F05E909}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|x64.Build.0 = Debug|x64
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|x64.ActiveCfg = Release|x64
|
||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|x64.Build.0 = Release|x64
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|x64.Build.0 = Debug|x64
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|x64.ActiveCfg = Release|x64
|
||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|x64.Build.0 = Release|x64
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|x64.ActiveCfg = Release|x64
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|x64.Build.0 = Release|x64
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|x64.ActiveCfg = Release|x64
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|x64.Build.0 = Release|x64
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|x64.Build.0 = Debug|x64
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|x64.ActiveCfg = Release|x64
|
||||
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|x64.Build.0 = Release|x64
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|x64.Build.0 = Debug|x64
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|x64.ActiveCfg = Release|x64
|
||||
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC} = {A245341F-91C7-4FC3-9EB8-FC6455180427}
|
||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A} = {A245341F-91C7-4FC3-9EB8-FC6455180427}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {68B75065-924C-42D1-8B6C-A4B5678C2A85}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Loading…
Reference in New Issue
Block a user