diff --git a/Backend/App/MessageService.cs b/Backend/App/MessageService.cs index 75404e6..099fc7f 100644 --- a/Backend/App/MessageService.cs +++ b/Backend/App/MessageService.cs @@ -94,6 +94,10 @@ public static async Task HandleMessage(string message) root.TryGetProperty("Parameters", out JsonElement aiClipParameterElement); _ = Task.Run(() => HandleCreateAiClip(aiClipParameterElement)); break; + case "CreateLowlight": + root.TryGetProperty("Parameters", out JsonElement lowlightParameterElement); + _ = Task.Run(() => HandleCreateLowlight(lowlightParameterElement)); + break; case "CompressVideo": root.TryGetProperty("Parameters", out JsonElement compressParameterElement); _ = Task.Run(() => HandleCompressVideo(compressParameterElement)); @@ -342,6 +346,13 @@ private static async Task HandleCreateAiClip(JsonElement message) await AiService.CreateHighlight(fileNameElement.GetString()!); } + private static async Task HandleCreateLowlight(JsonElement message) + { + Log.Information($"{message}"); + message.TryGetProperty("FileName", out JsonElement fileNameElement); + await AiService.CreateLowlight(fileNameElement.GetString()!); + } + private static async Task HandleCompressVideo(JsonElement message) { Log.Information($"CompressVideo: {message}"); diff --git a/Backend/Core/Models/Bookmark.cs b/Backend/Core/Models/Bookmark.cs index 38d25f4..97d1ebb 100644 --- a/Backend/Core/Models/Bookmark.cs +++ b/Backend/Core/Models/Bookmark.cs @@ -22,7 +22,7 @@ public enum BookmarkType [IncludeInHighlight] Kill, [IncludeInHighlight] Goal, Assist, - Death + [IncludeInLowlight] Death } /// @@ -31,6 +31,12 @@ public enum BookmarkType [AttributeUsage(AttributeTargets.Field)] public class IncludeInHighlightAttribute : Attribute { } + /// + /// Marks a BookmarkType as one that should be included in auto-generated lowlights. + /// + [AttributeUsage(AttributeTargets.Field)] + public class IncludeInLowlightAttribute : Attribute { } + public static class BookmarkTypeExtensions { /// @@ -39,6 +45,13 @@ public static class BookmarkTypeExtensions public static bool IncludeInHighlight(this BookmarkType type) => typeof(BookmarkType).GetField(type.ToString())! .GetCustomAttributes(typeof(IncludeInHighlightAttribute), false).Length > 0; + + /// + /// Returns true if this bookmark type should be included in auto-generated lowlights. + /// + public static bool IncludeInLowlight(this BookmarkType type) => + typeof(BookmarkType).GetField(type.ToString())! + .GetCustomAttributes(typeof(IncludeInLowlightAttribute), false).Length > 0; } [JsonConverter(typeof(JsonStringEnumConverter))] diff --git a/Backend/Core/Models/Settings.cs b/Backend/Core/Models/Settings.cs index 250474b..1871329 100644 --- a/Backend/Core/Models/Settings.cs +++ b/Backend/Core/Models/Settings.cs @@ -17,6 +17,7 @@ internal class Settings "Replay Buffer", "Clips", "Highlights", + "Lowlights", "Settings" }; @@ -50,6 +51,10 @@ internal class Settings private bool _autoGenerateHighlights = true; private double _highlightPaddingBefore = 4; private double _highlightPaddingAfter = 4; + private bool _enableLowlights = false; + private bool _autoGenerateLowlights = true; + private double _lowlightPaddingBefore = 4; + private double _lowlightPaddingAfter = 4; private bool _runOnStartup = false; private bool _receiveBetaUpdates = false; private bool _airplaneMode = false; @@ -378,6 +383,19 @@ public bool EnableAi } } + [JsonPropertyName("enableLowlights")] + public bool EnableLowlights + { + get => _enableLowlights; + set + { + if (_enableLowlights != value) + { + _enableLowlights = value; + } + } + } + [JsonPropertyName("autoGenerateHighlights")] public bool AutoGenerateHighlights { @@ -417,6 +435,45 @@ public double HighlightPaddingAfter } } + [JsonPropertyName("autoGenerateLowlights")] + public bool AutoGenerateLowlights + { + get => _autoGenerateLowlights; + set + { + if (_autoGenerateLowlights != value) + { + _autoGenerateLowlights = value; + } + } + } + + [JsonPropertyName("lowlightPaddingBefore")] + public double LowlightPaddingBefore + { + get => _lowlightPaddingBefore; + set + { + if (_lowlightPaddingBefore != value) + { + _lowlightPaddingBefore = value; + } + } + } + + [JsonPropertyName("lowlightPaddingAfter")] + public double LowlightPaddingAfter + { + get => _lowlightPaddingAfter; + set + { + if (_lowlightPaddingAfter != value) + { + _lowlightPaddingAfter = value; + } + } + } + [JsonPropertyName("gameIntegrations")] public GameIntegrations GameIntegrations { @@ -1060,7 +1117,8 @@ public enum ContentType Session, Buffer, Clip, - Highlight + Highlight, + Lowlight } public ContentType Type { get; set; } = ContentType.Session; diff --git a/Backend/Media/ContentService.cs b/Backend/Media/ContentService.cs index 03feeb8..038f590 100644 --- a/Backend/Media/ContentService.cs +++ b/Backend/Media/ContentService.cs @@ -408,7 +408,7 @@ public static async Task DeleteContent(string filePath, Content.ContentType type { // Only delete if the folder is empty and is a game subfolder (not the root video type folder) string contentRoot = Settings.Instance.ContentFolder; - string[] rootFolders = { FolderNames.Sessions, FolderNames.Buffers, FolderNames.Clips, FolderNames.Highlights }; + string[] rootFolders = { FolderNames.Sessions, FolderNames.Buffers, FolderNames.Clips, FolderNames.Highlights, FolderNames.Lowlights }; bool isGameSubfolder = rootFolders.Any(rf => videoDirectory.StartsWith(Path.Combine(contentRoot, rf), StringComparison.OrdinalIgnoreCase) && !videoDirectory.Equals(Path.Combine(contentRoot, rf), StringComparison.OrdinalIgnoreCase)); diff --git a/Backend/Media/HighlightService.cs b/Backend/Media/HighlightService.cs index 9cb541a..e03837e 100644 --- a/Backend/Media/HighlightService.cs +++ b/Backend/Media/HighlightService.cs @@ -21,8 +21,6 @@ public static async Task CreateHighlightFromBookmarks(string fileName, Action x.FileName == fileName); if (content == null) { diff --git a/Backend/Media/LowlightService.cs b/Backend/Media/LowlightService.cs new file mode 100644 index 0000000..0d1190a --- /dev/null +++ b/Backend/Media/LowlightService.cs @@ -0,0 +1,303 @@ +using System.Globalization; +using Segra.Backend.App; +using Segra.Backend.Core.Models; +using Segra.Backend.Services; +using Segra.Backend.Shared; +using Segra.Backend.Windows.Storage; +using Serilog; + +namespace Segra.Backend.Media +{ + /// + /// Service for creating lowlight videos from bookmarks using fast stream copy. + /// + public static class LowlightService + { + /// + /// Creates a lowlight video from all lowlight-worthy bookmarks (Death, etc.). + /// Uses stream copy for fast extraction without re-encoding. + /// + public static async Task CreateLowlightFromBookmarks(string fileName, Action? progressCallback = null) + { + try + { + Content? content = AppState.Instance.Content.FirstOrDefault(x => x.FileName == fileName); + if (content == null) + { + Log.Warning($"No content found matching fileName: {fileName}"); + return; + } + + // Get all lowlight-worthy bookmarks + List lowlightBookmarks = content.Bookmarks + .Where(b => b.Type.IncludeInLowlight()) + .OrderBy(b => b.Time) + .ToList(); + + if (lowlightBookmarks.Count == 0) + { + Log.Information($"No lowlight bookmarks found for: {fileName}"); + progressCallback?.Invoke(-1, "No lowlight moments found in this session"); + return; + } + + Log.Information($"Found {lowlightBookmarks.Count} bookmarks to include in lowlight"); + progressCallback?.Invoke(5, $"Found {lowlightBookmarks.Count} moments"); + + double paddingBefore = Settings.Instance.LowlightPaddingBefore; + double paddingAfter = Settings.Instance.LowlightPaddingAfter; + var segments = lowlightBookmarks.Select(b => new TimeSegment + { + StartTime = Math.Max(0, b.Time.TotalSeconds - paddingBefore), + EndTime = b.Time.TotalSeconds + paddingAfter + }).ToList(); + + // Merge overlapping segments + var mergedSegments = MergeOverlappingSegments(segments); + Log.Information($"Merged {segments.Count} segments into {mergedSegments.Count} clips"); + + // Create the lowlight + string videoFolder = Settings.Instance.ContentFolder; + // Input files are organized by game + string inputGameFolder = StorageService.SanitizeGameNameForFolder(content.Game ?? "Unknown"); + string inputFolderName = FolderNames.GetVideoFolderName(content.Type); + string inputFilePath = PathUtils.Combine(videoFolder, inputFolderName, inputGameFolder, $"{content.FileName}.mp4"); + + if (!File.Exists(inputFilePath)) + { + Log.Error($"Input video file not found: {inputFilePath}"); + progressCallback?.Invoke(-1, "Source video not found"); + return; + } + + // Output lowlights are organized by game + string outputGameFolder = StorageService.SanitizeGameNameForFolder(content.Game ?? "Unknown"); + string outputFolder = PathUtils.Combine(videoFolder, FolderNames.Lowlights, outputGameFolder); + Directory.CreateDirectory(outputFolder); + + string outputFileName = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.mp4"; + string outputFilePath = PathUtils.Combine(outputFolder, outputFileName); + + progressCallback?.Invoke(10, "Extracting clips..."); + + // Extract and concatenate segments using stream copy + bool success = await ExtractAndConcatenateSegments( + inputFilePath, + outputFilePath, + mergedSegments, + (progress, message) => progressCallback?.Invoke(10 + (int)(progress * 80), message) + ); + + if (!success || !File.Exists(outputFilePath)) + { + Log.Error("Failed to create lowlight video"); + progressCallback?.Invoke(-1, "Failed to create lowlight"); + return; + } + + // Ensure the output is fully flushed (matters for network drives) before reading it back. + await GeneralUtils.EnsureFileReady(outputFilePath); + + progressCallback?.Invoke(92, "Creating metadata..."); + + // Create metadata, thumbnail, and waveform. + // Lowlights use stream-copy extract+concat, so they preserve the source's audio tracks. + await ContentService.CreateMetadataFile(outputFilePath, Content.ContentType.Lowlight, content.Game!, null, content.Title, igdbId: content.IgdbId, audioTrackNames: content.AudioTrackNames); + + progressCallback?.Invoke(95, "Creating thumbnail..."); + await ContentService.CreateThumbnail(outputFilePath, Content.ContentType.Lowlight); + + progressCallback?.Invoke(98, "Creating waveform..."); + await ContentService.CreateWaveformFile(outputFilePath, Content.ContentType.Lowlight); + + // Reload content + await SettingsService.LoadContentFromFolderIntoState(); + + progressCallback?.Invoke(100, "Done"); + Log.Information($"Lowlight created successfully: {outputFilePath}"); + } + catch (Exception ex) + { + Log.Error(ex, $"Error creating lowlight for {fileName}"); + progressCallback?.Invoke(-1, $"Error: {ex.Message}"); + } + } + + /// + /// Extracts multiple segments from a video and concatenates them using stream copy. + /// This is a fast operation as it doesn't re-encode the video. + /// + /// Path to the source video file + /// Path for the output video file + /// List of time segments to extract + /// Optional callback for progress updates (0.0 to 1.0) + /// True if successful, false otherwise + public static async Task ExtractAndConcatenateSegments( + string inputFilePath, + string outputFilePath, + List segments, + Action? progressCallback = null) + { + if (!FFmpegService.FFmpegExists()) + { + Log.Error($"FFmpeg executable not found"); + return false; + } + + if (segments.Count == 0) + { + Log.Warning("No segments provided for extraction"); + return false; + } + + List tempFiles = new List(); + string? concatFilePath = null; + + try + { + double totalDuration = segments.Sum(s => s.EndTime - s.StartTime); + double processedDuration = 0; + + // Extract each segment to a temp file using stream copy + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + string tempFile = PathUtils.Combine(Path.GetTempPath(), $"lowlight_segment_{Guid.NewGuid()}.mp4"); + double segmentDuration = segment.EndTime - segment.StartTime; + + progressCallback?.Invoke(processedDuration / totalDuration, $"Extracting clip {i + 1} of {segments.Count}"); + + var arguments = new[] + { + "-y", + "-ss", segment.StartTime.ToString(CultureInfo.InvariantCulture), + "-t", segmentDuration.ToString(CultureInfo.InvariantCulture), + "-i", inputFilePath, + "-c", "copy", + "-avoid_negative_ts", "make_zero", + tempFile + }; + + await FFmpegService.RunSimple(arguments); + + if (!File.Exists(tempFile)) + { + Log.Error($"Failed to extract segment {i + 1}"); + continue; + } + + tempFiles.Add(tempFile); + processedDuration += segmentDuration; + } + + if (tempFiles.Count == 0) + { + Log.Error("No segments were successfully extracted"); + return false; + } + + progressCallback?.Invoke(0.9, "Combining clips..."); + + // If only one segment, just move it to output + if (tempFiles.Count == 1) + { + File.Move(tempFiles[0], outputFilePath, overwrite: true); + tempFiles.Clear(); + return true; + } + + concatFilePath = PathUtils.Combine(Path.GetTempPath(), $"lowlight_concat_{Guid.NewGuid()}.txt"); + var concatLines = tempFiles.Select(FFmpegService.BuildConcatListLine); + await File.WriteAllLinesAsync(concatFilePath, concatLines); + + // Concatenate all segments using stream copy + var concatArguments = new[] + { + "-y", + "-f", "concat", + "-safe", "0", + "-i", concatFilePath, + "-c", "copy", + "-movflags", "+faststart", + outputFilePath + }; + await FFmpegService.RunSimple(concatArguments); + + progressCallback?.Invoke(1.0, "Done"); + return File.Exists(outputFilePath); + } + catch (FFmpegException ffEx) + { + Log.Error(ffEx, "Error extracting and concatenating segments"); + _ = MessageService.ShowModal( + "Lowlight creation failed", + FFmpegErrors.DescribeForUser(ffEx.ExitCode), + "error"); + return false; + } + catch (Exception ex) + { + Log.Error(ex, "Error extracting and concatenating segments"); + return false; + } + finally + { + // Cleanup temp files + foreach (var tempFile in tempFiles) + { + try { File.Delete(tempFile); } + catch { /* ignore cleanup errors */ } + } + + if (!string.IsNullOrEmpty(concatFilePath)) + { + try { File.Delete(concatFilePath); } + catch { /* ignore cleanup errors */ } + } + } + } + + /// + /// Merges overlapping time segments into continuous segments. + /// + private static List MergeOverlappingSegments(List segments) + { + if (segments.Count == 0) return new List(); + + var sorted = segments.OrderBy(s => s.StartTime).ToList(); + var merged = new List(); + + var current = new TimeSegment + { + StartTime = sorted[0].StartTime, + EndTime = sorted[0].EndTime + }; + + for (int i = 1; i < sorted.Count; i++) + { + var next = sorted[i]; + + // Check if segments overlap or are adjacent + if (current.EndTime >= next.StartTime) + { + // Extend current segment + current.EndTime = Math.Max(current.EndTime, next.EndTime); + } + else + { + // No overlap, save current and start new + merged.Add(current); + current = new TimeSegment + { + StartTime = next.StartTime, + EndTime = next.EndTime + }; + } + } + + merged.Add(current); + return merged; + } + } + + } diff --git a/Backend/Recorder/OBSService.cs b/Backend/Recorder/OBSService.cs index 481a2d3..7c72bc9 100644 --- a/Backend/Recorder/OBSService.cs +++ b/Backend/Recorder/OBSService.cs @@ -1554,6 +1554,13 @@ public static async Task StopRecording() string fileName = Path.GetFileNameWithoutExtension(filePath); _ = AiService.CreateHighlight(fileName); } + + // If the recording is not a replay buffer recording, AI is enabled, user is authenticated, and auto generate lowlights is enabled -> analyze the video! + if (Settings.Instance.EnableLowlights && Settings.Instance.AutoGenerateLowlights && !isReplayBufferMode && bookmarks.Any(b => b.Type.IncludeInLowlight())) + { + string fileName = Path.GetFileNameWithoutExtension(filePath); + _ = AiService.CreateLowlight(fileName); + } } finally { diff --git a/Backend/Services/AiService.cs b/Backend/Services/AiService.cs index 4eda648..6f8f09a 100644 --- a/Backend/Services/AiService.cs +++ b/Backend/Services/AiService.cs @@ -27,16 +27,16 @@ public static async Task CreateHighlight(string fileName) if (momentCount == 0) { Log.Information($"No highlight bookmarks found for: {fileName}"); - await SendProgress(highlightId, -1, "error", "No highlight moments found in this session", content); + await SendProgress(highlightId, -1, "error", "No highlight moments found in this session", content, "AiProgress"); return; } - await SendProgress(highlightId, 0, "processing", $"Found {momentCount} moments", content); + await SendProgress(highlightId, 0, "processing", $"Found {momentCount} moments", content, "AiProgress"); await HighlightService.CreateHighlightFromBookmarks(fileName, async (progress, message) => { string status = progress < 0 ? "error" : progress >= 100 ? "done" : "processing"; - await SendProgress(highlightId, progress, status, message, content); + await SendProgress(highlightId, progress, status, message, content, "AiProgress"); }); } catch (Exception ex) @@ -44,12 +44,54 @@ await HighlightService.CreateHighlightFromBookmarks(fileName, async (progress, m Log.Error(ex, $"Error creating highlight for {fileName}"); if (content != null) { - await SendProgress(highlightId, -1, "error", $"Error: {ex.Message}", content); + await SendProgress(highlightId, -1, "error", $"Error: {ex.Message}", content, "AiProgress"); } } } - private static async Task SendProgress(string id, int progress, string status, string message, Content content) + public static async Task CreateLowlight(string fileName) + { + string lowlightId = Guid.NewGuid().ToString(); + Content? content = null; + + try + { + Log.Information($"Starting lowlight creation for: {fileName}"); + + content = AppState.Instance.Content.FirstOrDefault(x => x.FileName == fileName); + if (content == null) + { + Log.Warning($"No content found matching fileName: {fileName}"); + return; + } + + int momentCount = content.Bookmarks.Count(b => b.Type.IncludeInLowlight()); + if (momentCount == 0) + { + Log.Information($"No lowlight bookmarks found for: {fileName}"); + await SendProgress(lowlightId, -1, "error", "No lowlight moments found in this session", content, "LowlightAiProgress"); + return; + } + + await SendProgress(lowlightId, 0, "processing", $"Found {momentCount} moments", content, "LowlightAiProgress"); + + await LowlightService.CreateLowlightFromBookmarks(fileName, async (progress, message) => + { + string status = progress < 0 ? "error" : progress >= 100 ? "done" : "processing"; + await SendProgress(lowlightId, progress, status, message, content, "LowlightAiProgress"); + }); + } + catch (Exception ex) + { + Log.Error(ex, $"Error creating lowlight for {fileName}"); + if (content != null) + { + await SendProgress(lowlightId, -1, "error", $"Error: {ex.Message}", content, "LowlightAiProgress"); + } + } + } + + private static async Task SendProgress(string id, int progress, string status, string message, Content content, string messageType) { var progressMessage = new HighlightProgressMessage { @@ -57,10 +99,11 @@ private static async Task SendProgress(string id, int progress, string status, s Progress = progress, Status = status, Message = message, - Content = content + Content = content, + MessageType = messageType }; - await MessageService.SendFrontendMessage("AiProgress", progressMessage); + await MessageService.SendFrontendMessage(messageType, progressMessage); } } @@ -71,5 +114,6 @@ public class HighlightProgressMessage public required string Status { get; set; } public required string Message { get; set; } public required Content Content { get; set; } + public required string MessageType { get; set; } } } diff --git a/Backend/Services/RecoveryService.cs b/Backend/Services/RecoveryService.cs index 5ff832c..fe6d75a 100644 --- a/Backend/Services/RecoveryService.cs +++ b/Backend/Services/RecoveryService.cs @@ -185,6 +185,7 @@ public static async Task CheckForOrphanedFilesAsync() Content.ContentType.Session => "Session Recording", Content.ContentType.Clip => "Clip", Content.ContentType.Highlight => "Highlight", + Content.ContentType.Lowlight => "Lowlight", Content.ContentType.Buffer => "Replay Buffer", _ => orphanedFile.Type.ToString() }; @@ -299,6 +300,7 @@ private static List FindOrphanedVideoFiles() Content.ContentType.Session, Content.ContentType.Clip, Content.ContentType.Highlight, + Content.ContentType.Lowlight, Content.ContentType.Buffer }; diff --git a/Backend/Services/SettingsService.cs b/Backend/Services/SettingsService.cs index 8f6f723..e5fded2 100644 --- a/Backend/Services/SettingsService.cs +++ b/Backend/Services/SettingsService.cs @@ -710,6 +710,38 @@ private static async Task UpdateSettingsInstance(Settings updatedSettings) hasChanges = true; } + // Update EnableLowlights + if (settings.EnableLowlights != updatedSettings.EnableLowlights) + { + Log.Information($"EnableLowlights changed from '{settings.EnableLowlights}' to '{updatedSettings.EnableLowlights}'"); + settings.EnableLowlights = updatedSettings.EnableLowlights; + hasChanges = true; + } + + // Update AutoGenerateLowlights + if (settings.AutoGenerateLowlights != updatedSettings.AutoGenerateLowlights) + { + Log.Information($"AutoGenerateLowlights changed from '{settings.AutoGenerateLowlights}' to '{updatedSettings.AutoGenerateLowlights}'"); + settings.AutoGenerateLowlights = updatedSettings.AutoGenerateLowlights; + hasChanges = true; + } + + // Update LowlightPaddingBefore + if (settings.LowlightPaddingBefore != updatedSettings.LowlightPaddingBefore) + { + Log.Information($"LowlightPaddingBefore changed from '{settings.LowlightPaddingBefore}' to '{updatedSettings.LowlightPaddingBefore}'"); + settings.LowlightPaddingBefore = updatedSettings.LowlightPaddingBefore; + hasChanges = true; + } + + // Update LowlightPaddingAfter + if (settings.LowlightPaddingAfter != updatedSettings.LowlightPaddingAfter) + { + Log.Information($"LowlightPaddingAfter changed from '{settings.LowlightPaddingAfter}' to '{updatedSettings.LowlightPaddingAfter}'"); + settings.LowlightPaddingAfter = updatedSettings.LowlightPaddingAfter; + hasChanges = true; + } + // Update ReceiveBetaUpdates if (settings.ReceiveBetaUpdates != updatedSettings.ReceiveBetaUpdates) { diff --git a/Backend/Shared/FolderNames.cs b/Backend/Shared/FolderNames.cs index 7c06cdc..9180afb 100644 --- a/Backend/Shared/FolderNames.cs +++ b/Backend/Shared/FolderNames.cs @@ -13,6 +13,7 @@ public static class FolderNames public const string Buffers = "Replay Buffers"; public const string Clips = "Clips"; public const string Highlights = "Highlights"; + public const string Lowlights = "Lowlights"; // Legacy folder names (for migration purposes) public const string LegacySessions = "sessions"; @@ -47,6 +48,7 @@ public static string GetVideoFolderName(Content.ContentType type) Content.ContentType.Buffer => Buffers, Content.ContentType.Clip => Clips, Content.ContentType.Highlight => Highlights, + Content.ContentType.Lowlight => Lowlights, _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown content type") }; } @@ -126,6 +128,8 @@ public static string GetWaveformsFolderPath(Content.ContentType type) return Content.ContentType.Clip; if (normalizedPath.Contains($"/{Highlights.ToLower()}/")) return Content.ContentType.Highlight; + if (normalizedPath.Contains($"/{Lowlights.ToLower()}/")) + return Content.ContentType.Lowlight; // Check legacy folder names for backwards compatibility if (normalizedPath.Contains($"/{LegacySessions}/")) diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 5ab1a0e..202c70b 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -5,6 +5,7 @@ import Sessions from './Pages/sessions'; import Clips from './Pages/clips'; import ReplayBuffer from './Pages/replay-buffer'; import Highlights from './Pages/highlights'; +import Lowlights from './Pages/lowlights'; import { SettingsProvider } from './Context/SettingsContext'; import { AppStateProvider } from './Context/AppStateContext'; import Video from './Pages/video'; @@ -23,6 +24,7 @@ import { ContentMigrationProvider } from './Context/ContentMigrationContext'; import { WebSocketProvider } from './Context/WebSocketContext'; import { ClippingProvider } from './Context/ClippingContext'; import { AiHighlightsProvider } from './Context/AiHighlightsContext'; +import { AiLowlightsProvider } from './Context/AiLowlightsContext'; import { CompressionProvider } from './Context/CompressionContext'; import { UpdateProvider } from './Context/UpdateContext'; import { ObsDownloadProvider } from './Context/ObsDownloadContext'; @@ -116,6 +118,8 @@ function App() { return ; case 'Highlights': return ; + case 'Lowlights': + return ; case 'Settings': return ; default: @@ -153,13 +157,15 @@ export default function AppWrapper() { - - - - - - - + + + + + + + + + diff --git a/Frontend/src/Components/AiContentCard.tsx b/Frontend/src/Components/AiContentCard.tsx index fa0242a..d424e99 100644 --- a/Frontend/src/Components/AiContentCard.tsx +++ b/Frontend/src/Components/AiContentCard.tsx @@ -162,7 +162,9 @@ const AiContentCard: React.FC = ({ progress }) => { {/* Centered Content */}
-

Creating Highlight

+

+ Creating {progress.messageType === 'LowlightAiProgress' ? 'Lowlight' : 'Highlight'} +

{ + const parameters: any = { + FileName: content!.fileName, + }; + + sendMessageToBackend('CreateLowlight', parameters); + }; + const handleDelete = () => { const parameters: any = { FileName: content!.fileName, @@ -397,7 +410,7 @@ export default function ContentCard({ tabIndex={0} className="dropdown-content menu bg-base-300 border border-base-400 rounded-box z-999 w-52 p-2" > - {!airplaneMode && (type === 'Clip' || type === 'Highlight') && ( + {!airplaneMode && (type === 'Clip' || type === 'Highlight' || type === 'Lowlight') && (
  • )} - {(type === 'Clip' || type === 'Highlight' || type === 'Buffer') && ( + {(type === 'Clip' || type === 'Highlight' || type === 'Buffer' || type === 'Lowlight') && (
  • )} + {type === 'Session' && enableLowlights && ( +
  • + {(() => { + const hasLowlightBookmarks = content?.bookmarks?.some((b) => + includeInLowlight(b.type), + ); + const isProcessing = Object.values(lowlightAiProgress).some( + (progress) => + progress.content.fileName === content?.fileName && + progress.status === 'processing', + ); + const isDisabled = !hasLowlightBookmarks || isProcessing; + + return ( + + ); + })()} +
  • + )}
  • - {(type === 'Clip' || type === 'Highlight') && + {(type === 'Clip' || type === 'Highlight' || type === 'Lowlight') && !content?.fileName?.endsWith('_compressed') && (
  • - {(video.type === 'Clip' || video.type === 'Highlight') && ( + {(video.type === 'Clip' || video.type === 'Highlight' || video.type === 'Lowlight') && ( <> {!settings.airplaneMode && (
    + ) : id === 'Lowlights' ? ( + ) : (