- Remove projetos mortos: VideoStudy.Native (MAUI), Controllers/LicenseController - Remove serviços não usados: FFmpegService, HardwareIdService, LicenseManager, PdfGeneratorService, ScreenshotService, TranscriptionService do UI - Remove dependências pesadas do UI: Whisper.net, YoutubeExplode, QuestPDF - Remove PuppeteerSharp e SkiaSharp do API (150MB Chromium não é mais necessário) - Screenshots agora usam FFmpeg diretamente (mais simples, mais confiável) - YouTubeService reescrito para chamar /api/video-info em vez de YoutubeExplode - Adiciona campo UserContext em AnalysisRequest (contexto livre do usuário) - UI traduzida para português; aba de arquivo local removida (nunca funcionou) - YouTubeProcessor simplificado: sem modo Advanced/Whisper - GetYtDlpPath busca yt-dlp.exe subindo até 7 níveis de diretório - Cookies do yt-dlp configuráveis via YtDlp:CookiesBrowser no appsettings - Chave Groq agora lida de env var GROQ_API_KEY (appsettings.json sem segredos) - VideoStudy.Linux (Photino) adicionado à solução como host multiplataforma - yt-dlp atualizado de 2025.01.26 para 2026.03.17 (fix do nsig extraction) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
219 lines
8.0 KiB
Plaintext
219 lines
8.0 KiB
Plaintext
@page "/"
|
|
@inject HttpClient Http
|
|
@inject YouTubeService YouTubeService
|
|
@inject PersistenceService PersistenceService
|
|
@inject NavigationManager NavigationManager
|
|
@using VideoStudy.Shared
|
|
@using System.Net.Http.Json
|
|
@using System.Text.Json
|
|
@using System.Threading
|
|
|
|
<PageTitle>VideoStudy</PageTitle>
|
|
|
|
<div class="container-fluid py-4">
|
|
<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
|
|
</h1>
|
|
<p class="lead text-muted">Transforme vídeos em guias de estudo com IA</p>
|
|
</header>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8 mx-auto">
|
|
|
|
<div class="card shadow-sm border-0 rounded-4 overflow-hidden mb-4 p-4 bg-white">
|
|
<YouTubeProcessor
|
|
OnVideoUrlChanged="HandleVideoUrlChanged"
|
|
OnStart="StartAnalysis"
|
|
IsProcessing="@isProcessing"
|
|
VideoInfo="@currentVideoInfo" />
|
|
|
|
<!-- Contexto do usuário -->
|
|
<div class="mt-3">
|
|
<label class="form-label fw-bold text-muted small">
|
|
Contexto <span class="fw-normal">(opcional)</span>
|
|
</label>
|
|
<textarea class="form-control" rows="2"
|
|
placeholder="Ex: tutorial técnico de Python para iniciantes / aula escolar sobre fotossíntese / passo a passo para montar um móvel"
|
|
@bind="userContext" disabled="@isProcessing"></textarea>
|
|
</div>
|
|
|
|
<div class="row g-3 mt-1">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-bold text-muted small">Idioma de saída</label>
|
|
<select class="form-select" @bind="selectedLanguage" disabled="@isProcessing">
|
|
<option value="pt">Português (BR)</option>
|
|
<option value="en">English</option>
|
|
<option value="es">Español</option>
|
|
<option value="fr">Français</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (isProcessing || currentStep > 0)
|
|
{
|
|
<ProgressIndicator Status="@statusMessage" Percent="@progress" CurrentStepIndex="@currentStep" />
|
|
}
|
|
|
|
@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">Log de execução</small>
|
|
<small>@logs.Count eventos @(showLogs ? "▼" : "▶")</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.ToString("HH:mm:ss")]</span> @log.Message
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrEmpty(generatedPdfPath))
|
|
{
|
|
<PdfPreview PdfPath="@generatedPdfPath" OnCancel="@(() => generatedPdfPath = null)" />
|
|
}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private bool isProcessing = false;
|
|
private int progress = 0;
|
|
private int currentStep = 0;
|
|
private string statusMessage = "Pronto";
|
|
private bool showLogs = false;
|
|
private List<LogEntry> logs = new();
|
|
|
|
private string videoUrl = string.Empty;
|
|
private string selectedLanguage = "pt";
|
|
private string? userContext;
|
|
private VideoInfo? currentVideoInfo;
|
|
private string? generatedPdfPath;
|
|
|
|
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, int? eventProgress = null)
|
|
{
|
|
logs.Insert(0, new LogEntry { Message = message });
|
|
if (logs.Count > 100) logs.RemoveAt(logs.Count - 1);
|
|
if (eventProgress.HasValue) progress = eventProgress.Value;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task HandleVideoUrlChanged(string url)
|
|
{
|
|
videoUrl = url;
|
|
try
|
|
{
|
|
AddLog($"Buscando informações: {url}");
|
|
currentVideoInfo = await YouTubeService.GetVideoInfoAsync(url);
|
|
if (currentVideoInfo != null) AddLog($"Encontrado: {currentVideoInfo.Title}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AddLog($"Erro ao buscar vídeo: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task StartAnalysis()
|
|
{
|
|
isProcessing = true;
|
|
progress = 0;
|
|
currentStep = 0;
|
|
generatedPdfPath = null;
|
|
logs.Clear();
|
|
showLogs = true;
|
|
|
|
CancellationTokenSource? cts = null;
|
|
|
|
try
|
|
{
|
|
cts = new CancellationTokenSource();
|
|
var token = cts.Token;
|
|
|
|
currentStep = 1;
|
|
statusMessage = "Iniciando análise...";
|
|
AddLog("Enviando para o servidor...", 5);
|
|
|
|
var request = new AnalysisRequest
|
|
{
|
|
VideoUrl = videoUrl,
|
|
Mode = "fast",
|
|
Language = "en",
|
|
OutputLanguage = selectedLanguage,
|
|
DurationSeconds = currentVideoInfo?.Duration.TotalSeconds ?? 0,
|
|
UserContext = string.IsNullOrWhiteSpace(userContext) ? null : userContext.Trim()
|
|
};
|
|
|
|
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "api/analyze")
|
|
{
|
|
Content = JsonContent.Create(request)
|
|
};
|
|
|
|
using var response = await Http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, token);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var stream = response.Content.ReadFromJsonAsAsyncEnumerable<AnalysisEvent>(
|
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, token);
|
|
|
|
await foreach (var ev in stream.WithCancellation(token))
|
|
{
|
|
if (ev == null) continue;
|
|
if (ev.IsError) throw new Exception(ev.Message);
|
|
|
|
AddLog(ev.Message, ev.ProgressPercentage);
|
|
statusMessage = ev.Message;
|
|
StateHasChanged();
|
|
|
|
if (ev.Result != null)
|
|
{
|
|
AddLog("Análise concluída!", 100);
|
|
|
|
if (ev.Result.PdfData != null && ev.Result.PdfData.Length > 0)
|
|
{
|
|
AddLog("Salvando PDF na biblioteca...", 100);
|
|
var session = await PersistenceService.SaveSessionAsync(
|
|
ev.Result.PdfData,
|
|
ev.Result.DocumentTitle,
|
|
videoUrl);
|
|
|
|
generatedPdfPath = session.FilePath;
|
|
AddLog($"PDF salvo em: {session.FilePath}", 100);
|
|
|
|
await Task.Delay(2000);
|
|
NavigationManager.NavigateTo("library");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AddLog($"Erro: {ex.Message}");
|
|
statusMessage = "Erro na análise";
|
|
}
|
|
finally
|
|
{
|
|
isProcessing = false;
|
|
cts?.Dispose();
|
|
}
|
|
}
|
|
}
|