mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Compare commits
3 Commits
c6ca4fcbb6
...
af363ebb9a
| Author | SHA1 | Date | |
|---|---|---|---|
| af363ebb9a | |||
| 9cdf611ec8 | |||
| 2dc7b050c8 |
@ -8,7 +8,9 @@ namespace Splitter_UI;
|
|||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
private readonly ServiceProvider _provider;
|
private readonly ServiceProvider _provider = null!;
|
||||||
|
|
||||||
|
public App() { }
|
||||||
|
|
||||||
public App(ServiceProvider provider)
|
public App(ServiceProvider provider)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,12 +7,15 @@ public sealed class ActionToIconConverter : IValueConverter
|
|||||||
{
|
{
|
||||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
return value switch
|
if (value == null)
|
||||||
{
|
return null;
|
||||||
"crop" => "\uf125", // FA7 crop
|
|
||||||
"rotate" => "\uf2f1", // FA7 rotate
|
var p = System.Convert.ToInt32(value);
|
||||||
_ => null
|
|
||||||
};
|
return p == 0
|
||||||
|
? "\uf125" // FA7 crop
|
||||||
|
: "\uf2f1" // FA7 rotate
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
|||||||
15
Splitter-UI/Converters/BoolInvertConverter.cs
Normal file
15
Splitter-UI/Converters/BoolInvertConverter.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace Splitter_UI.Converters;
|
||||||
|
|
||||||
|
public sealed class BoolInvertConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public static readonly BoolInvertConverter Instance = new();
|
||||||
|
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
=> value is bool b ? !b : value;
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
=> value is bool b ? !b : value;
|
||||||
|
}
|
||||||
39
Splitter-UI/Converters/ConsoleColorToBrushConverter.cs
Normal file
39
Splitter-UI/Converters/ConsoleColorToBrushConverter.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Splitter_UI.Converters;
|
||||||
|
|
||||||
|
public sealed class ConsoleColorToBrushConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is ConsoleColor c)
|
||||||
|
return new SolidColorBrush(ToColor(c));
|
||||||
|
|
||||||
|
return Brushes.White;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color ToColor(ConsoleColor c) =>
|
||||||
|
c switch
|
||||||
|
{
|
||||||
|
ConsoleColor.Black => Colors.Black,
|
||||||
|
ConsoleColor.DarkBlue => Colors.DarkBlue,
|
||||||
|
ConsoleColor.DarkGreen => Colors.DarkGreen,
|
||||||
|
ConsoleColor.DarkCyan => Colors.DarkCyan,
|
||||||
|
ConsoleColor.DarkRed => Colors.DarkRed,
|
||||||
|
ConsoleColor.DarkMagenta => Colors.DarkMagenta,
|
||||||
|
ConsoleColor.DarkYellow => Colors.Olive,
|
||||||
|
ConsoleColor.Gray => Colors.Gray,
|
||||||
|
ConsoleColor.DarkGray => Colors.DarkGray,
|
||||||
|
ConsoleColor.Blue => Colors.Blue,
|
||||||
|
ConsoleColor.Green => Colors.Green,
|
||||||
|
ConsoleColor.Cyan => Colors.Cyan,
|
||||||
|
ConsoleColor.Red => Colors.Red,
|
||||||
|
ConsoleColor.Magenta => Colors.Magenta,
|
||||||
|
ConsoleColor.Yellow => Colors.Yellow,
|
||||||
|
ConsoleColor.White => Colors.White,
|
||||||
|
_ => Colors.White
|
||||||
|
};
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
@ -1,6 +1,4 @@
|
|||||||
using Avalonia;
|
namespace Splitter_UI.Models;
|
||||||
|
|
||||||
namespace Splitter_UI.Models;
|
|
||||||
|
|
||||||
public class PreviewData
|
public class PreviewData
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
namespace Splitter_UI.Models;
|
|
||||||
|
|
||||||
public class ProgressInfo
|
|
||||||
{
|
|
||||||
public double Percent { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using splitter.algo;
|
|
||||||
using splitter.tui;
|
|
||||||
|
|
||||||
namespace Splitter_UI;
|
namespace Splitter_UI;
|
||||||
|
|
||||||
@ -25,13 +23,16 @@ internal sealed class Program
|
|||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var logPaveVM = new LogPaneViewModel();
|
||||||
// ViewModels
|
// ViewModels
|
||||||
services.AddTransient<MainViewModel>();
|
services.AddTransient<MainViewModel>();
|
||||||
services.AddTransient<FileListViewModel>();
|
services.AddTransient<FileListViewModel>();
|
||||||
services.AddTransient<PreviewPaneViewModel>();
|
services.AddTransient<PreviewPaneViewModel>();
|
||||||
services.AddTransient<InspectorPaneViewModel>();
|
services.AddTransient<InspectorPaneViewModel>();
|
||||||
services.AddTransient<StatusBarViewModel>();
|
services.AddSingleton<StatusBarViewModel>();
|
||||||
services.AddTransient<LogPaneViewModel>();
|
services.AddSingleton<ProgressViewModel>();
|
||||||
|
services.AddSingleton<LogPaneViewModel>(logPaveVM);
|
||||||
|
services.AddSingleton<ILogService>(logPaveVM);
|
||||||
|
|
||||||
// splitter services
|
// splitter services
|
||||||
services.AddSingleton<UltraFaceDetector>();
|
services.AddSingleton<UltraFaceDetector>();
|
||||||
@ -48,13 +49,12 @@ internal sealed class Program
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
services.AddSingleton<ILogger, GlobalLogger>();
|
services.AddSingleton<ILogger, GlobalLogger>();
|
||||||
|
services.AddSingleton<IJobProcessor, JobProcessor>();
|
||||||
|
|
||||||
// Domain services (your pipeline)
|
// Domain services (your pipeline)
|
||||||
services.AddTransient<IFileProbeService, FileProbeService>();
|
services.AddTransient<IFileProbeService, FileProbeService>();
|
||||||
services.AddTransient<IThumbnailService, ThumbnailService>();
|
services.AddTransient<IThumbnailService, ThumbnailService>();
|
||||||
services.AddSingleton<IAutoDecisionService, AutoDecisionService>();
|
services.AddSingleton<IAutoDecisionService, AutoDecisionService>();
|
||||||
services.AddSingleton<IProcessingService, ProcessingService>();
|
|
||||||
services.AddSingleton<ILogService, LogService>();
|
|
||||||
|
|
||||||
services.AddSingleton<IFileJobFactory, FileJobFactory>();
|
services.AddSingleton<IFileJobFactory, FileJobFactory>();
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
using NcnnDotNet.Layers;
|
namespace Splitter_UI.Services;
|
||||||
using OpenCvSharp;
|
|
||||||
using splitter.tui;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProbeService _fileProbe, ILogger _log) : IAutoDecisionService
|
public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProbeService _fileProbe, ILogger _log) : IAutoDecisionService
|
||||||
{
|
{
|
||||||
@ -15,19 +11,65 @@ public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProb
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
job.GravitateTo = new(0.5f, 0.5f);
|
||||||
|
job.OverrideTargetDuration = 58.0;
|
||||||
|
job.Mask = "[NAME]_seg[NN].[EXT]";
|
||||||
|
job.OutputFolder = Path.Combine(Path.GetDirectoryName(job.InputFile)!, "splitter");
|
||||||
|
|
||||||
job.Probe = await _fileProbe.ProbeAsync(job.InputFile);
|
job.Probe = await _fileProbe.ProbeAsync(job.InputFile);
|
||||||
job.Thumbnail = await _thumbnails.CreateThumbnailAsync(job.InputFile, job.Probe, rotateDegree: job.Rotate);
|
job.Thumbnail = await _thumbnails.CreateThumbnailAsync(job.InputFile, job.Probe, rotateDegree: job.Rotate);
|
||||||
|
|
||||||
|
if (job.Probe.Width > job.Probe.Height)
|
||||||
|
{
|
||||||
|
job.Detect = "body";
|
||||||
|
job.Rotate = 0;
|
||||||
|
|
||||||
|
CalculateCrop(job);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
var sampler = new VideoRotationSampler(null);
|
var sampler = new VideoRotationSampler(null);
|
||||||
job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration);
|
job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration);
|
||||||
job.SuggestedAction = job.Rotate == 0 ? "crop" : "rotate";
|
job.Detect = job.Rotate == 0 ? null : "body";
|
||||||
|
}
|
||||||
|
|
||||||
if (job.SuggestedAction == "crop")
|
_log.LogInfo(job.ToString());
|
||||||
job.Detect = "body";
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_log.LogError($"Error creating thumbnail for {Path.GetFileName(job.InputFile)}: {ex.Message}");
|
_log.LogError($"Error creating thumbnail for {Path.GetFileName(job.InputFile)}: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void CalculateCrop(JobViewModel job)
|
||||||
|
{
|
||||||
|
var targetAR = (float)CommandLine.DefaultW / CommandLine.DefaultH;
|
||||||
|
var pixelAspect = job.Probe!.Sar.X / job.Probe.Sar.Y;
|
||||||
|
|
||||||
|
float srcW = job.Probe.Width * pixelAspect;
|
||||||
|
float srcH = job.Probe.Height;
|
||||||
|
var srcAR = srcW / srcH;
|
||||||
|
|
||||||
|
float cropH = srcH;
|
||||||
|
float cropW = cropH * targetAR;
|
||||||
|
|
||||||
|
if (cropW > srcW)
|
||||||
|
{
|
||||||
|
cropW = srcW;
|
||||||
|
cropH = cropW / targetAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x = (srcW - cropW) * 0.5f;
|
||||||
|
float y = (srcH - cropH) * 0.5f;
|
||||||
|
|
||||||
|
float invPixelAspect = 1f / pixelAspect;
|
||||||
|
|
||||||
|
float cropW_px = cropW * invPixelAspect;
|
||||||
|
float cropH_px = cropH;
|
||||||
|
|
||||||
|
float x_px = x * invPixelAspect;
|
||||||
|
float y_px = y;
|
||||||
|
|
||||||
|
job.CropText = $"{(int)MathF.Round(cropW_px)},{(int)MathF.Round(cropH_px)}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using OpenCvSharp;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
using OpenCvSharp;
|
namespace Splitter_UI.Services;
|
||||||
using splitter.algo;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
internal class DummyDetector : IObjectDetector
|
internal class DummyDetector : IObjectDetector
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using splitter.probe;
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
public sealed class FileProbeService : IFileProbeService
|
public sealed class FileProbeService : IFileProbeService
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,13 +1,24 @@
|
|||||||
using splitter.tui;
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
internal class GlobalLogger(ILogService _logService, StatusBarViewModel _statusBar, ProgressViewModel _progress) : ILogger
|
||||||
|
|
||||||
internal class GlobalLogger(ILogService _logService) : ILogger
|
|
||||||
{
|
{
|
||||||
public void ClearProgress(int progressLevel) { }
|
public void ClearProgress(string name, int progressLine)
|
||||||
public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed) { }
|
{
|
||||||
|
if (progressLine == 0)
|
||||||
|
_statusBar.Percent = 0;
|
||||||
|
else
|
||||||
|
_progress.ClearProgress(name, progressLine-1);
|
||||||
|
}
|
||||||
|
public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed)
|
||||||
|
{
|
||||||
|
if (progressLine == 0)
|
||||||
|
_statusBar.Percent = progress;
|
||||||
|
else
|
||||||
|
_progress.DrawProgress(name, progressLine - 1, progress, eta, speed);
|
||||||
|
}
|
||||||
|
|
||||||
public void Log(string prefix, ConsoleColor color, string msg)
|
public void Log(string prefix, ConsoleColor color, string msg)
|
||||||
{
|
{
|
||||||
_logService.Write($"[{prefix}] {msg}");
|
_logService.Log(prefix, color, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using splitter.probe;
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
public interface IFileProbeService
|
public interface IFileProbeService
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,7 +3,5 @@ namespace Splitter_UI.Services;
|
|||||||
|
|
||||||
public interface ILogService
|
public interface ILogService
|
||||||
{
|
{
|
||||||
event Action<string>? MessageLogged;
|
void Log(string prefix, ConsoleColor color, string msg);
|
||||||
|
|
||||||
void Write(string message);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
public interface IProcessingService
|
|
||||||
{
|
|
||||||
event Action<string, ProgressInfo>? ProgressChanged;
|
|
||||||
|
|
||||||
Task ProcessAsync(IEnumerable<SingleJob> jobs, CancellationToken token);
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using splitter.probe;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
public sealed class LogService : ILogService
|
|
||||||
{
|
|
||||||
public event Action<string>? MessageLogged;
|
|
||||||
|
|
||||||
public void Write(string message)
|
|
||||||
{
|
|
||||||
MessageLogged?.Invoke(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
public sealed class ProcessingService : IProcessingService
|
|
||||||
{
|
|
||||||
public event Action<string, ProgressInfo>? ProgressChanged;
|
|
||||||
|
|
||||||
public async Task ProcessAsync(IEnumerable<SingleJob> jobs, CancellationToken token)
|
|
||||||
{
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
for (int i = 0; i <= 100; i += 20)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var progress = new ProgressInfo { Percent = i };
|
|
||||||
|
|
||||||
// Notify UI
|
|
||||||
ProgressChanged?.Invoke(job.InputFile, progress);
|
|
||||||
|
|
||||||
await Task.Delay(100, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,4 @@
|
|||||||
using OpenCvSharp;
|
namespace Splitter_UI.Services;
|
||||||
using splitter.algo;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
|
||||||
|
|
||||||
public class SingleThreadedDetector<T>(IObjectDetector _detector) : IObjectDetector
|
public class SingleThreadedDetector<T>(IObjectDetector _detector) : IObjectDetector
|
||||||
where T : IObjectDetector
|
where T : IObjectDetector
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using splitter.probe;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
|
|||||||
@ -39,4 +39,21 @@ public partial class FileListViewModel : ObservableObject
|
|||||||
|
|
||||||
Selected = Files.LastOrDefault();
|
Selected = Files.LastOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void DeleteSelected()
|
||||||
|
{
|
||||||
|
if (SelectedFiles.Any())
|
||||||
|
{
|
||||||
|
var toDelete = SelectedFiles.ToList();
|
||||||
|
foreach (var item in toDelete)
|
||||||
|
Files.Remove(item);
|
||||||
|
}
|
||||||
|
else if ( Selected != null)
|
||||||
|
{
|
||||||
|
var sel = Selected;
|
||||||
|
Files.Remove(sel);
|
||||||
|
}
|
||||||
|
|
||||||
|
Selected = Files.LastOrDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,10 +16,12 @@ public partial class InspectorPaneViewModel : ObservableObject
|
|||||||
"face", "body", "none"
|
"face", "body", "none"
|
||||||
];
|
];
|
||||||
|
|
||||||
public List<int> RotationAngles =>
|
[RelayCommand]
|
||||||
[
|
private void TransformAll()
|
||||||
0, 90, 180, 270
|
{
|
||||||
];
|
_ = _main.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ApplyOverrides()
|
private void ApplyOverrides()
|
||||||
@ -48,12 +50,16 @@ public partial class InspectorPaneViewModel : ObservableObject
|
|||||||
public IRelayCommand RotateLeftCommand { get; }
|
public IRelayCommand RotateLeftCommand { get; }
|
||||||
public IRelayCommand RotateRightCommand { get; }
|
public IRelayCommand RotateRightCommand { get; }
|
||||||
|
|
||||||
|
private MainViewModel _main = null!;
|
||||||
|
|
||||||
public InspectorPaneViewModel()
|
public InspectorPaneViewModel()
|
||||||
{
|
{
|
||||||
RotateLeftCommand = new RelayCommand(() => AdjustRotation(-90));
|
RotateLeftCommand = new RelayCommand(() => AdjustRotation(-90));
|
||||||
RotateRightCommand = new RelayCommand(() => AdjustRotation(+90));
|
RotateRightCommand = new RelayCommand(() => AdjustRotation(+90));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetMain(MainViewModel main) => _main = main;
|
||||||
|
|
||||||
private void AdjustRotation(int delta)
|
private void AdjustRotation(int delta)
|
||||||
{
|
{
|
||||||
if ( Selected == null)
|
if ( Selected == null)
|
||||||
|
|||||||
@ -5,9 +5,6 @@ using Avalonia.Media.Imaging;
|
|||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using splitter.algo;
|
|
||||||
using splitter.probe;
|
|
||||||
using splitter.tui;
|
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
@ -15,11 +12,11 @@ public partial class JobViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
private SingleJob Job { get; }
|
private SingleJob Job { get; }
|
||||||
|
|
||||||
|
public SingleJob GetJob() => Job;
|
||||||
|
|
||||||
[ObservableProperty] private VideoInfo? _probe;
|
[ObservableProperty] private VideoInfo? _probe;
|
||||||
[ObservableProperty] private PreviewData? _preview = new(null, [], null, new(0.5f, 0.5f));
|
[ObservableProperty] private PreviewData? _preview = new(null, [], null, new(0.5f, 0.5f));
|
||||||
[ObservableProperty] private ProgressInfo? _progress;
|
|
||||||
[ObservableProperty] private Bitmap? _thumbnail;
|
[ObservableProperty] private Bitmap? _thumbnail;
|
||||||
[ObservableProperty] private string _suggestedAction = "";
|
|
||||||
[ObservableProperty] private double _sliderLiveValue;
|
[ObservableProperty] private double _sliderLiveValue;
|
||||||
[ObservableProperty] private double _positionSeconds;
|
[ObservableProperty] private double _positionSeconds;
|
||||||
|
|
||||||
@ -40,7 +37,7 @@ public partial class JobViewModel : ObservableObject
|
|||||||
? $"{Probe.Width}x{Probe.Height}, {TimeSpan.FromSeconds(Probe.Duration).ToString(@"hh\:mm\:ss")}), FPS: {Probe.Fps:F2}, Bitrate: {Probe.Bitrate/1024/1024:F2} MB/s"
|
? $"{Probe.Width}x{Probe.Height}, {TimeSpan.FromSeconds(Probe.Duration).ToString(@"hh\:mm\:ss")}), FPS: {Probe.Fps:F2}, Bitrate: {Probe.Bitrate/1024/1024:F2} MB/s"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
public override string ToString() => $"{FileName} - {TextDesc}";
|
public override string ToString() => $"{FileName}: {TextDesc}";
|
||||||
|
|
||||||
public ObservableCollection<ParameterEntry> ParametersList { get; }
|
public ObservableCollection<ParameterEntry> ParametersList { get; }
|
||||||
= new();
|
= new();
|
||||||
@ -217,6 +214,13 @@ public partial class JobViewModel : ObservableObject
|
|||||||
entry.PropertyChanged += OnParameterChanged;
|
entry.PropertyChanged += OnParameterChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PropertyChanged += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(Probe))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(DurationSeconds));
|
||||||
|
}
|
||||||
|
};
|
||||||
ParametersList.CollectionChanged += OnParametersCollectionChanged;
|
ParametersList.CollectionChanged += OnParametersCollectionChanged;
|
||||||
|
|
||||||
StepForwardCommand = new RelayCommand(StepForward);
|
StepForwardCommand = new RelayCommand(StepForward);
|
||||||
@ -364,4 +368,5 @@ public partial class JobViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
Task.Run(CreatePreview);
|
Task.Run(CreatePreview);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,35 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using Avalonia.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
public partial class LogPaneViewModel : ObservableObject
|
public sealed record LogEntry(string Prefix, ConsoleColor Color, string Message);
|
||||||
{
|
|
||||||
public ObservableCollection<string> Logs { get; } = [];
|
|
||||||
|
|
||||||
public void Add(string message)
|
public partial class LogPaneViewModel : ObservableObject, ILogService
|
||||||
{
|
{
|
||||||
Logs.Add(message);
|
public ObservableCollection<LogEntry> Logs { get; } = [];
|
||||||
|
|
||||||
|
public void Log(string prefix, ConsoleColor color, string msg)
|
||||||
|
{
|
||||||
|
Add(new LogEntry(prefix.Replace("[", "").Replace("]", ""), color, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(LogEntry entry)
|
||||||
|
{
|
||||||
|
if (Dispatcher.UIThread.CheckAccess())
|
||||||
|
{
|
||||||
|
AddInternal(entry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => AddInternal(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddInternal(LogEntry entry)
|
||||||
|
{
|
||||||
|
Logs.Add(entry);
|
||||||
if (Logs.Count > 5000)
|
if (Logs.Count > 5000)
|
||||||
Logs.RemoveAt(0);
|
Logs.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +1,82 @@
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
public partial class MainViewModel : ViewModelBase
|
public partial class MainViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public FileListViewModel FileList { get; }
|
public FileListViewModel FileList { get; }
|
||||||
public PreviewPaneViewModel Preview { get; } = new PreviewPaneViewModel();
|
public PreviewPaneViewModel Preview { get; }
|
||||||
public InspectorPaneViewModel Inspector { get; } = new InspectorPaneViewModel();
|
public InspectorPaneViewModel Inspector { get; }
|
||||||
public StatusBarViewModel StatusBar { get; } = new StatusBarViewModel();
|
public StatusBarViewModel StatusBar { get; }
|
||||||
public LogPaneViewModel LogPane { get; } = new LogPaneViewModel();
|
public LogPaneViewModel LogPane { get; }
|
||||||
|
public ProgressViewModel Progress { get; }
|
||||||
|
private IJobProcessor _processor = null!;
|
||||||
|
|
||||||
public MainViewModel(IFileJobFactory fileJobFactory, IAutoDecisionService autoDecisionService)
|
[ObservableProperty] private bool _transformMode = false;
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
public MainViewModel(
|
||||||
|
FileListViewModel fileListVM,
|
||||||
|
PreviewPaneViewModel ppVM,
|
||||||
|
InspectorPaneViewModel iVM,
|
||||||
|
LogPaneViewModel lpVM,
|
||||||
|
StatusBarViewModel sbVM,
|
||||||
|
ProgressViewModel pVM,
|
||||||
|
IJobProcessor processor,
|
||||||
|
ILogger logger
|
||||||
|
)
|
||||||
{
|
{
|
||||||
FileList = new FileListViewModel(fileJobFactory, autoDecisionService);
|
FileList = fileListVM;
|
||||||
// Wire selection → preview + inspector
|
Preview = ppVM;
|
||||||
|
Inspector = iVM;
|
||||||
|
LogPane = lpVM;
|
||||||
|
StatusBar = sbVM;
|
||||||
|
Progress = pVM;
|
||||||
|
_processor = processor;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Wire selection -> preview + inspector
|
||||||
FileList.SelectedFileChanged += file =>
|
FileList.SelectedFileChanged += file =>
|
||||||
{
|
{
|
||||||
Preview.Selected = file;
|
Preview.Selected = file;
|
||||||
Inspector.Selected = file;
|
Inspector.Selected = file;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Inspector.SetMain(this);
|
||||||
Inspector.Files = FileList.Files;
|
Inspector.Files = FileList.Files;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
public async Task Start()
|
||||||
private void Start()
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
StatusBar.StatusText = "Processing…";
|
StatusBar.StatusText = "Processing…";
|
||||||
// call IProcessingService here
|
StatusBar.Percent = 0;
|
||||||
|
TransformMode = true;
|
||||||
|
|
||||||
|
var files = FileList.Files.ToList();
|
||||||
|
var jobs = new List<SingleTask>();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var fileJobs = await _processor.GenerateJobs(file.GetJob(), false);
|
||||||
|
jobs.AddRange(fileJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
await _processor.ProcessJobs(jobs, false);
|
||||||
private void Stop()
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusBar.StatusText = "Stopped";
|
// Handle exception
|
||||||
|
StatusBar.StatusText = "Error occurred…";
|
||||||
|
_logger.LogError($"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
StatusBar.StatusText = "Ready…";
|
||||||
|
StatusBar.Percent = 0;
|
||||||
|
TransformMode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using splitter.algo;
|
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
|
|||||||
43
Splitter-UI/ViewModels/ProgressViewModel.cs
Normal file
43
Splitter-UI/ViewModels/ProgressViewModel.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
|
public record ProgressInfo(string Name, int ProgressLine, double Progress, TimeSpan Eta, double Speed);
|
||||||
|
|
||||||
|
public partial class ProgressViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private int _numberOfProcesses = 0;
|
||||||
|
public ObservableCollection<ProgressInfo> Processes { get; } = [];
|
||||||
|
|
||||||
|
private Lock _lock = new();
|
||||||
|
public void ClearProgress(string name, int progressLine)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (progressLine < 0 || progressLine > Processes.Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
NumberOfProcesses -= 1;
|
||||||
|
Processes[progressLine] = new ProgressInfo("", progressLine, 0, TimeSpan.Zero, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (progressLine < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (Processes.Count <= progressLine)
|
||||||
|
{
|
||||||
|
Processes.Add(new ProgressInfo("", Processes.Count, 0, TimeSpan.Zero, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Processes[progressLine].Name == "")
|
||||||
|
NumberOfProcesses += 1;
|
||||||
|
Processes[progressLine] = new ProgressInfo(name, progressLine, progress, eta, speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -10,6 +10,4 @@ public partial class StatusBarViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _percent;
|
private double _percent;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _threadInfo = "Threads: 0/0";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,9 @@
|
|||||||
xmlns:views="clr-namespace:Splitter_UI.Views"
|
xmlns:views="clr-namespace:Splitter_UI.Views"
|
||||||
xmlns:conv="clr-namespace:Splitter_UI.Converters"
|
xmlns:conv="clr-namespace:Splitter_UI.Converters"
|
||||||
x:Class="Splitter_UI.Views.FileListView"
|
x:Class="Splitter_UI.Views.FileListView"
|
||||||
x:DataType="vm:FileListViewModel">
|
x:DataType="vm:FileListViewModel"
|
||||||
|
KeyDown="OnKeyDown"
|
||||||
|
Focusable="True">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<conv:ZeroToBoolConverter x:Key="ZeroToBoolConverter"/>
|
<conv:ZeroToBoolConverter x:Key="ZeroToBoolConverter"/>
|
||||||
@ -67,47 +69,10 @@
|
|||||||
|
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:JobViewModel">
|
<DataTemplate x:DataType="vm:JobViewModel">
|
||||||
<Border x:Name="ItemRoot"
|
<views:JobListItemView/>
|
||||||
Margin="0"
|
|
||||||
Padding="0"
|
|
||||||
CornerRadius="4"
|
|
||||||
Background="#2A2A2A">
|
|
||||||
<StackPanel MinWidth="160" MaxWidth="160">
|
|
||||||
|
|
||||||
<Border Width="160" Height="90" ClipToBounds="True">
|
|
||||||
<Grid>
|
|
||||||
<Image Source="{Binding Thumbnail}"
|
|
||||||
Stretch="UniformToFill"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
|
|
||||||
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
|
||||||
Text="{Binding SuggestedAction, Converter={StaticResource ActionToIconConverter}}"
|
|
||||||
FontSize="12"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Foreground="LimeGreen"/>
|
|
||||||
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
|
||||||
Text="{Binding Rotate, Converter={StaticResource RotationAngleToIconConverter}}"
|
|
||||||
FontSize="12"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Foreground="LimeGreen"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<TextBlock Text="{Binding FileName}"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,6,0,0"
|
|
||||||
FontSize="10"/>
|
|
||||||
|
|
||||||
<ProgressBar MinWidth="160"
|
|
||||||
MaxWidth="160"
|
|
||||||
Height="10"
|
|
||||||
Margin="0,4,0,0"
|
|
||||||
Value="{Binding Progress.Percent}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -21,6 +21,14 @@ public partial class FileListView : UserControl
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnKeyDown(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Delete)
|
||||||
|
{
|
||||||
|
if (DataContext is FileListViewModel vm)
|
||||||
|
vm.DeleteSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
private void OnDragEnter(object? sender, DragEventArgs e)
|
private void OnDragEnter(object? sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
IsDragActive = true;
|
IsDragActive = true;
|
||||||
|
|||||||
@ -147,10 +147,21 @@ x:DataType="vm:InspectorPaneViewModel">
|
|||||||
<TextBox Text="{Binding Selected.PassthroughText}" Width="260"/>
|
<TextBox Text="{Binding Selected.PassthroughText}" Width="260"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Content="Apply to Selected"
|
<StackPanel Orientation="Horizontal"
|
||||||
Command="{Binding ApplyOverridesCommand}"
|
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Margin="0,10,0,0"/>
|
Spacing="8"
|
||||||
|
Margin="0,10,0,0">
|
||||||
|
|
||||||
|
<Button Content="Apply to Selected"
|
||||||
|
Command="{Binding ApplyOverridesCommand}"/>
|
||||||
|
|
||||||
|
<Button Content="Transform all"
|
||||||
|
Background="#AA0000"
|
||||||
|
Foreground="White"
|
||||||
|
Command="{Binding TransformAllCommand}"/>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
50
Splitter-UI/Views/JobListItemView.axaml
Normal file
50
Splitter-UI/Views/JobListItemView.axaml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Splitter_UI.Views.JobListItemView"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||||
|
xmlns:conv="clr-namespace:Splitter_UI.Converters"
|
||||||
|
x:DataType="vm:JobViewModel">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<conv:RotationAngleToIconConverter x:Key="RotationAngleToIconConverter"/>
|
||||||
|
<conv:ActionToIconConverter x:Key="ActionToIconConverter"/>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Border Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
CornerRadius="4"
|
||||||
|
Background="#2A2A2A">
|
||||||
|
|
||||||
|
<StackPanel MinWidth="160" MaxWidth="160">
|
||||||
|
|
||||||
|
<Border Width="160" Height="90" ClipToBounds="True">
|
||||||
|
<Grid>
|
||||||
|
<Image Source="{Binding Thumbnail}"
|
||||||
|
Stretch="UniformToFill"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
||||||
|
Text="{Binding Rotate, Converter={StaticResource ActionToIconConverter}}"
|
||||||
|
FontSize="12"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Foreground="LimeGreen"/>
|
||||||
|
|
||||||
|
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
||||||
|
Text="{Binding Rotate, Converter={StaticResource RotationAngleToIconConverter}}"
|
||||||
|
FontSize="12"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Foreground="LimeGreen"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding FileName}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="0,6,0,0"
|
||||||
|
FontSize="10"/>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
12
Splitter-UI/Views/JobListItemView.axaml.cs
Normal file
12
Splitter-UI/Views/JobListItemView.axaml.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Splitter_UI.Views;
|
||||||
|
|
||||||
|
public partial class JobListItemView : UserControl
|
||||||
|
{
|
||||||
|
public JobListItemView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -2,15 +2,30 @@
|
|||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="Splitter_UI.Views.LogPane"
|
x:Class="Splitter_UI.Views.LogPane"
|
||||||
|
xmlns:conv="clr-namespace:Splitter_UI.Converters"
|
||||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||||
x:DataType="vm:LogPaneViewModel">
|
x:DataType="vm:LogPaneViewModel">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<conv:ConsoleColorToBrushConverter x:Key="ConsoleColorToBrushConverter"/>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Border Background="#111" Padding="8">
|
<Border Background="#111" Padding="8">
|
||||||
<ScrollViewer>
|
<ScrollViewer x:Name="Scroller">
|
||||||
<ItemsControl ItemsSource="{Binding Logs}">
|
<ItemsControl ItemsSource="{Binding Logs}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate x:DataType="vm:LogEntry">
|
||||||
<TextBlock Text="{Binding}" FontFamily="Consolas" FontSize="12"/>
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="[" FontFamily="Consolas" FontSize="12"/>
|
||||||
|
<TextBlock Text="{Binding Prefix}"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{Binding Color, Converter={StaticResource ConsoleColorToBrushConverter}}"/>
|
||||||
|
<TextBlock Text="] " FontFamily="Consolas" FontSize="12"/>
|
||||||
|
<TextBlock Text="{Binding Message}"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
FontSize="12"/>
|
||||||
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
namespace Splitter_UI.Views;
|
namespace Splitter_UI.Views;
|
||||||
|
|
||||||
@ -7,5 +8,24 @@ public partial class LogPane : UserControl
|
|||||||
public LogPane()
|
public LogPane()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
// When DataContext changes, subscribe to collection changes
|
||||||
|
this.DataContextChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (DataContext is LogPaneViewModel vm)
|
||||||
|
{
|
||||||
|
vm.Logs.CollectionChanged += (_, _) => ScrollToEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScrollToEnd()
|
||||||
|
{
|
||||||
|
// Must run after layout pass
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (Scroller != null)
|
||||||
|
Scroller.ScrollToEnd();
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,18 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:views="clr-namespace:Splitter_UI.Views"
|
xmlns:views="clr-namespace:Splitter_UI.Views"
|
||||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||||
|
xmlns:conv="clr-namespace:Splitter_UI.Converters"
|
||||||
x:Class="Splitter_UI.Views.MainWindow"
|
x:Class="Splitter_UI.Views.MainWindow"
|
||||||
x:DataType="vm:MainViewModel"
|
x:DataType="vm:MainViewModel"
|
||||||
|
x:Name="Root"
|
||||||
Width="1400"
|
Width="1400"
|
||||||
Height="950"
|
Height="950"
|
||||||
Title="Splitter UI">
|
Title="Splitter UI">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<conv:BoolInvertConverter x:Key="BoolInvertConverter"/>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
@ -19,8 +25,9 @@
|
|||||||
<views:LogPane DockPanel.Dock="Bottom" Height="150"
|
<views:LogPane DockPanel.Dock="Bottom" Height="150"
|
||||||
DataContext="{Binding LogPane}" />
|
DataContext="{Binding LogPane}" />
|
||||||
|
|
||||||
|
<Grid>
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<Grid ColumnDefinitions="2*,3*,430">
|
<Grid ColumnDefinitions="2*,3*,430" IsVisible="{Binding TransformMode, Converter={StaticResource BoolInvertConverter}}">
|
||||||
|
|
||||||
<!-- File List -->
|
<!-- File List -->
|
||||||
<views:FileListView Grid.Column="0"
|
<views:FileListView Grid.Column="0"
|
||||||
@ -36,6 +43,12 @@
|
|||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Progress view (replaces entire grid) -->
|
||||||
|
<views:ProgressView
|
||||||
|
DataContext="{Binding Progress}"
|
||||||
|
IsVisible="{Binding #Root.DataContext.TransformMode}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Window>
|
</Window>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Views;
|
namespace Splitter_UI.Views;
|
||||||
|
|
||||||
public partial class MainWindow : Avalonia.Controls.Window
|
public partial class MainWindow : Avalonia.Controls.Window
|
||||||
|
|||||||
@ -4,7 +4,6 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using splitter.algo;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Views;
|
namespace Splitter_UI.Views;
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
Preview="{Binding Preview}"
|
Preview="{Binding Preview}"
|
||||||
Sar="{Binding Sar}"
|
Sar="{Binding Sar}"
|
||||||
RotateAngle="{Binding Rotate}"
|
RotateAngle="{Binding Rotate}"
|
||||||
GravitateTo="{Binding GravitateTo}"/>
|
GravitateTo="{Binding GravitateTo, Mode=TwoWay}"/>
|
||||||
|
|
||||||
<Grid Grid.Row="1"
|
<Grid Grid.Row="1"
|
||||||
ColumnDefinitions="Auto,*,Auto"
|
ColumnDefinitions="Auto,*,Auto"
|
||||||
|
|||||||
50
Splitter-UI/Views/ProgressView.axaml
Normal file
50
Splitter-UI/Views/ProgressView.axaml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Splitter_UI.Views.ProgressView"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||||
|
x:DataType="vm:ProgressViewModel">
|
||||||
|
|
||||||
|
<Border Background="#111" Padding="8">
|
||||||
|
<ItemsControl ItemsSource="{Binding Processes}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ProgressInfo">
|
||||||
|
<Grid ColumnDefinitions="2*,3*,Auto,Auto"
|
||||||
|
Margin="0,2">
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{Binding Name}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="12"/>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<ProgressBar Grid.Column="1"
|
||||||
|
Height="12"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="1"
|
||||||
|
Value="{Binding Progress}"
|
||||||
|
Margin="8,0"/>
|
||||||
|
|
||||||
|
<!-- ETA -->
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
Width="70"
|
||||||
|
Text="{Binding Eta, StringFormat={}{0:hh\\:mm\\:ss}}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="12,0"
|
||||||
|
FontSize="12"/>
|
||||||
|
|
||||||
|
<!-- Speed -->
|
||||||
|
<TextBlock Grid.Column="3"
|
||||||
|
Width="70"
|
||||||
|
Text="{Binding Speed, StringFormat={}{0:0.00}}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="12,0"
|
||||||
|
FontSize="12"/>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
12
Splitter-UI/Views/ProgressView.axaml.cs
Normal file
12
Splitter-UI/Views/ProgressView.axaml.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Splitter_UI.Views;
|
||||||
|
|
||||||
|
public partial class ProgressView : UserControl
|
||||||
|
{
|
||||||
|
public ProgressView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -12,13 +12,13 @@
|
|||||||
Text="{Binding StatusText}" />
|
Text="{Binding StatusText}" />
|
||||||
|
|
||||||
<ProgressBar Grid.Column="1"
|
<ProgressBar Grid.Column="1"
|
||||||
Width="200" Height="16"
|
Width="200"
|
||||||
|
Height="16"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="1"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Value="{Binding Percent}" />
|
Value="{Binding Percent}" />
|
||||||
|
|
||||||
<TextBlock Grid.Column="2"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Text="{Binding ThreadInfo}" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using splitter.algo;
|
|
||||||
using splitter.util;
|
using splitter.util;
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|||||||
7
splitter-cli/IJobProcessor.cs
Normal file
7
splitter-cli/IJobProcessor.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public interface IJobProcessor
|
||||||
|
{
|
||||||
|
Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly);
|
||||||
|
Task<bool> ProcessJobs(List<SingleTask> tasks, bool singleThreaded);
|
||||||
|
}
|
||||||
217
splitter-cli/JobProcessor.cs
Normal file
217
splitter-cli/JobProcessor.cs
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcessor
|
||||||
|
{
|
||||||
|
public async Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly)
|
||||||
|
{
|
||||||
|
var baseName = Path.GetFileNameWithoutExtension(job.InputFile);
|
||||||
|
|
||||||
|
if (!File.Exists(job.InputFile))
|
||||||
|
{
|
||||||
|
LogError($"{baseName}: Input file not found.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(job.OutputFolder))
|
||||||
|
Directory.CreateDirectory(job.OutputFolder);
|
||||||
|
|
||||||
|
var info = await ProbeVideo.Probe(job.InputFile, job.RotateAuto);
|
||||||
|
if (info.Duration <= 0)
|
||||||
|
{
|
||||||
|
LogError($"{baseName}: Could not read duration.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = job.OverrideTargetDuration ?? 58.0;
|
||||||
|
|
||||||
|
int segments;
|
||||||
|
double segmentLength;
|
||||||
|
|
||||||
|
if (job.ForceFixed)
|
||||||
|
{
|
||||||
|
// Fixed chunk size, last one may be shorter
|
||||||
|
segments = (int)Math.Ceiling(info.Duration / target);
|
||||||
|
segmentLength = target;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Equalized segments
|
||||||
|
segments = (int)Math.Ceiling(info.Duration / target);
|
||||||
|
segmentLength = info.Duration / segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfo($"{baseName}: Duration {info.Duration:F2}s, {info.Width}x{info.Height} @ {info.Fps:F3}fps {info.Bitrate / 1024:F0}kbps," +
|
||||||
|
$" Target duration: {target:F2}s Segments: {segments} segment length: {segmentLength:F2}s {(job.ForceFixed ? " fixed" : "")}");
|
||||||
|
|
||||||
|
if (estimateOnly)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
Func<int, ISegmentProcessor> processorFactory;
|
||||||
|
if (job.Crop != null)
|
||||||
|
{
|
||||||
|
processorFactory = i =>
|
||||||
|
{
|
||||||
|
IObjectDetector detector = job.Detect switch
|
||||||
|
{
|
||||||
|
"face" => new UltraFaceDetector(_logger),
|
||||||
|
"body" => new YoloOnnxObjectDetector(_logger),
|
||||||
|
_ => throw new InvalidOperationException($"Unknown detector: {job.Detect}")
|
||||||
|
};
|
||||||
|
return new TrackingSplitter(i, detector, job, _logger);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
processorFactory = i => new SimpleSplitter(i, _logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs = Enumerable.Range(0, segments)
|
||||||
|
.Select(i => new SingleTask
|
||||||
|
(
|
||||||
|
Job : job,
|
||||||
|
Info: info,
|
||||||
|
OutputFileName : BuildOutputFileName(job, i),
|
||||||
|
SegmentIndex : i,
|
||||||
|
TotalSegments : segments,
|
||||||
|
SegmentStart : i * segmentLength,
|
||||||
|
SegmentLength : (i == segments - 1)
|
||||||
|
? Math.Max(0.1, info.Duration - i * segmentLength)
|
||||||
|
: segmentLength,
|
||||||
|
ProcessorFactory : processorFactory
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ProcessJobs(List<SingleTask> tasks, bool singleThreaded)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (singleThreaded)
|
||||||
|
{
|
||||||
|
LogInfo("Starting single-threaded splitting...");
|
||||||
|
await RunSingleThreaded(tasks);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogInfo("Starting multi-threaded splitting...");
|
||||||
|
await RunMultiThreaded(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfo("Done.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogProgress(double progress, TimeSpan eta, double speed) => _logger.DrawProgress("Total", 0, progress, eta, speed);
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// ffprobe
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Multi-threaded splitting
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
private async Task RunMultiThreaded(List<SingleTask> jobs)
|
||||||
|
{
|
||||||
|
LogProgress(0.0, TimeSpan.Zero, 0.0);
|
||||||
|
|
||||||
|
var maxDegree = Math.Max(1, Environment.ProcessorCount / 2);
|
||||||
|
|
||||||
|
using var sem = new SemaphoreSlim(maxDegree);
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
// Slot pool: 0..maxDegree-1
|
||||||
|
var freeSlots = new ConcurrentQueue<int>(Enumerable.Range(0, maxDegree));
|
||||||
|
|
||||||
|
var totalSegments = jobs.Count;
|
||||||
|
var processedSegments = 0;
|
||||||
|
var totalDuration = jobs.Sum(j => j.SegmentLength);
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
foreach (var job in jobs)
|
||||||
|
{
|
||||||
|
await sem.WaitAsync();
|
||||||
|
|
||||||
|
tasks.Add(Task.Run(async () =>
|
||||||
|
{
|
||||||
|
int slot = -1;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Acquire a slot ID
|
||||||
|
while (!freeSlots.TryDequeue(out slot))
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
await ProcessSegment(job, slot + 1);
|
||||||
|
|
||||||
|
var processed = Interlocked.Increment(ref processedSegments);
|
||||||
|
var elapsed = sw.Elapsed;
|
||||||
|
var eta = TimeSpan.FromTicks(elapsed.Ticks * (totalSegments - processed) / processed);
|
||||||
|
var speed = (processed * totalDuration) / elapsed.TotalSeconds;
|
||||||
|
LogProgress((double)processed / totalSegments, eta, speed);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Return slot to pool
|
||||||
|
if (slot >= 0)
|
||||||
|
freeSlots.Enqueue(slot);
|
||||||
|
|
||||||
|
sem.Release();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Single-threaded splitting
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
private async Task RunSingleThreaded(List<SingleTask> jobs)
|
||||||
|
{
|
||||||
|
foreach (var job in jobs)
|
||||||
|
{
|
||||||
|
await ProcessSegment(job, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessSegment(SingleTask t, int slot)
|
||||||
|
{
|
||||||
|
var processor = t.ProcessorFactory(slot);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await processor.ProcessSegment(t);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (processor is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildOutputFileName(SingleJob job, int index)
|
||||||
|
{
|
||||||
|
string fileName;
|
||||||
|
|
||||||
|
fileName = Path.GetFileName(job.Mask ?? "[NAME]_seg[NN].[EXT]")
|
||||||
|
.Replace("[NAME]", Path.GetFileNameWithoutExtension(job.InputFile))
|
||||||
|
.Replace("[N]", index.ToString())
|
||||||
|
.Replace("[NN]", index.ToString("00"))
|
||||||
|
.Replace("[NNN]", index.ToString("000"))
|
||||||
|
.Replace("[NNNN]", index.ToString("0000"))
|
||||||
|
.Replace("[EXT]", Path.GetExtension(job.InputFile).TrimStart('.'))
|
||||||
|
;
|
||||||
|
|
||||||
|
return Path.Combine(job.OutputFolder, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using splitter.algo;
|
|
||||||
using splitter.tui;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
@ -59,7 +57,7 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
|
|||||||
|
|
||||||
proc.WaitForExit();
|
proc.WaitForExit();
|
||||||
|
|
||||||
ClearProgress();
|
ClearProgress(name);
|
||||||
|
|
||||||
if (proc.ExitCode != 0)
|
if (proc.ExitCode != 0)
|
||||||
LogError($"Segment {name} FFmpeg encoding failed");
|
LogError($"Segment {name} FFmpeg encoding failed");
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using splitter.algo;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using OpenCvSharp;
|
|
||||||
using splitter.algo;
|
|
||||||
using splitter.tui;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
@ -155,7 +152,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
try { if (!decode.HasExited) decode.Kill(entireProcessTree: true); } catch { }
|
try { if (!decode.HasExited) decode.Kill(entireProcessTree: true); } catch { }
|
||||||
try { if (!decode.HasExited) await decode.WaitForExitAsync(); } catch { }
|
try { if (!decode.HasExited) await decode.WaitForExitAsync(); } catch { }
|
||||||
|
|
||||||
ClearProgress();
|
ClearProgress(name);
|
||||||
|
|
||||||
|
|
||||||
if (encode.ExitCode != 0)
|
if (encode.ExitCode != 0)
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using OpenCvSharp;
|
namespace splitter.algo;
|
||||||
|
|
||||||
namespace splitter.algo;
|
|
||||||
|
|
||||||
public enum TrackState
|
public enum TrackState
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using OpenCvSharp;
|
namespace splitter.algo;
|
||||||
|
|
||||||
namespace splitter.algo;
|
|
||||||
|
|
||||||
public interface IObjectDetector : IDisposable
|
public interface IObjectDetector : IDisposable
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using OpenCvSharp;
|
|
||||||
using splitter.tui;
|
|
||||||
using UltraFaceDotNet;
|
using UltraFaceDotNet;
|
||||||
|
|
||||||
namespace splitter.algo;
|
namespace splitter.algo;
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Microsoft.ML.OnnxRuntime;
|
using Microsoft.ML.OnnxRuntime;
|
||||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||||
using OpenCvSharp;
|
|
||||||
using splitter.tui;
|
|
||||||
|
|
||||||
namespace splitter.algo;
|
namespace splitter.algo;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
using System;
|
using System.Text.Json.Serialization;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace splitter.probe;
|
namespace splitter.probe;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
using System;
|
namespace splitter.probe;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using static splitter.probe.ProbeVideo;
|
|
||||||
|
|
||||||
namespace splitter.probe;
|
|
||||||
|
|
||||||
public sealed class FfprobeResult
|
public sealed class FfprobeResult
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
using System;
|
using System.Text.Json.Serialization;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace splitter.probe;
|
namespace splitter.probe;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using OpenCvSharp;
|
namespace splitter.probe;
|
||||||
|
|
||||||
namespace splitter.probe;
|
|
||||||
|
|
||||||
public sealed class FrameRotationDetector
|
public sealed class FrameRotationDetector
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using splitter.algo;
|
|
||||||
|
|
||||||
namespace splitter.probe;
|
namespace splitter.probe;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
using OpenCvSharp;
|
namespace splitter.probe;
|
||||||
using splitter.algo;
|
|
||||||
|
|
||||||
namespace splitter.probe;
|
|
||||||
|
|
||||||
public record VideoInfo(
|
public record VideoInfo(
|
||||||
double Duration,
|
double Duration,
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using OpenCvSharp;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace splitter.probe;
|
namespace splitter.probe;
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using Spectre.Console;
|
|
||||||
using splitter;
|
using splitter;
|
||||||
using splitter.algo;
|
|
||||||
using splitter.probe;
|
|
||||||
using splitter.tui;
|
|
||||||
|
|
||||||
static partial class Program
|
static partial class Program
|
||||||
{
|
{
|
||||||
@ -36,24 +30,26 @@ static partial class Program
|
|||||||
uiTask = logger.RunAsync(cts.Token);
|
uiTask = logger.RunAsync(cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var processor = new JobProcessor(_logger);
|
||||||
|
|
||||||
if (cmd.Master.EstimateOnly)
|
if (cmd.Master.EstimateOnly)
|
||||||
LogInfo("=== ESTIMATE MODE ===");
|
_logger.LogInfo("=== ESTIMATE MODE ===");
|
||||||
|
|
||||||
var allJobs = new List<SingleTask>();
|
var allJobs = new List<SingleTask>();
|
||||||
foreach ( var job in cmd.Jobs )
|
foreach ( var job in cmd.Jobs )
|
||||||
{
|
{
|
||||||
var jobs = await GenerateJobs(cmd, job);
|
var jobs = await processor.GenerateJobs(job, cmd.Master.EstimateOnly);
|
||||||
allJobs.AddRange(jobs);
|
allJobs.AddRange(jobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( allJobs.Count == 0)
|
if ( allJobs.Count == 0)
|
||||||
{
|
{
|
||||||
if ( !cmd.Master.EstimateOnly)
|
if ( !cmd.Master.EstimateOnly)
|
||||||
LogWarn("No valid jobs to process.");
|
_logger.LogWarn("No valid jobs to process.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = await ProcessJobs(cmd, allJobs);
|
var success = await processor.ProcessJobs(allJobs, cmd.Master.SingleThreaded);
|
||||||
if (uiTask != null)
|
if (uiTask != null)
|
||||||
{
|
{
|
||||||
if ( cts != null )
|
if ( cts != null )
|
||||||
@ -66,217 +62,4 @@ static partial class Program
|
|||||||
return success ? 1 : 0;
|
return success ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<SingleTask>> GenerateJobs(CommandLine cmd, SingleJob job)
|
|
||||||
{
|
|
||||||
var baseName = Path.GetFileNameWithoutExtension(job.InputFile);
|
|
||||||
|
|
||||||
if (!File.Exists(job.InputFile))
|
|
||||||
{
|
|
||||||
LogError($"{baseName}: Input file not found.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(job.OutputFolder))
|
|
||||||
Directory.CreateDirectory(job.OutputFolder);
|
|
||||||
|
|
||||||
var info = await ProbeVideo.Probe(job.InputFile, job.RotateAuto);
|
|
||||||
if (info.Duration <= 0)
|
|
||||||
{
|
|
||||||
LogError($"{baseName}: Could not read duration.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var target = job.OverrideTargetDuration ?? 58.0;
|
|
||||||
|
|
||||||
int segments;
|
|
||||||
double segmentLength;
|
|
||||||
|
|
||||||
if (job.ForceFixed)
|
|
||||||
{
|
|
||||||
// Fixed chunk size, last one may be shorter
|
|
||||||
segments = (int)Math.Ceiling(info.Duration / target);
|
|
||||||
segmentLength = target;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Equalized segments
|
|
||||||
segments = (int)Math.Ceiling(info.Duration / target);
|
|
||||||
segmentLength = info.Duration / segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
LogInfo($"{baseName}: Duration {info.Duration:F2}s, {info.Width}x{info.Height} @ {info.Fps:F3}fps {info.Bitrate/1024:F0}kbps," +
|
|
||||||
$" Target duration: {target:F2}s Segments: {segments} segment length: {segmentLength:F2}s {(job.ForceFixed ? " fixed" : "")}" );
|
|
||||||
|
|
||||||
if (cmd.Master.EstimateOnly)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
Func<int, ISegmentProcessor> processorFactory;
|
|
||||||
if (job.Crop != null)
|
|
||||||
{
|
|
||||||
processorFactory = i =>
|
|
||||||
{
|
|
||||||
IObjectDetector detector = job.Detect switch
|
|
||||||
{
|
|
||||||
"face" => new UltraFaceDetector(_logger),
|
|
||||||
"body" => new YoloOnnxObjectDetector(_logger),
|
|
||||||
_ => throw new InvalidOperationException($"Unknown detector: {job.Detect}")
|
|
||||||
};
|
|
||||||
return new TrackingSplitter(i, detector, job, _logger);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
processorFactory = i => new SimpleSplitter(i, _logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobs = Enumerable.Range(0, segments)
|
|
||||||
.Select(i => new SingleTask
|
|
||||||
(
|
|
||||||
Job : job,
|
|
||||||
Info: info,
|
|
||||||
OutputFileName : BuildOutputFileName(job, i),
|
|
||||||
SegmentIndex : i,
|
|
||||||
TotalSegments : segments,
|
|
||||||
SegmentStart : i * segmentLength,
|
|
||||||
SegmentLength : (i == segments - 1)
|
|
||||||
? Math.Max(0.1, info.Duration - i * segmentLength)
|
|
||||||
: segmentLength,
|
|
||||||
ProcessorFactory : processorFactory
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return jobs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<bool> ProcessJobs(CommandLine cmd, List<SingleTask> tasks)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (cmd.Master.SingleThreaded)
|
|
||||||
{
|
|
||||||
LogInfo("Starting single-threaded splitting...");
|
|
||||||
await RunSingleThreaded(tasks);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LogInfo("Starting multi-threaded splitting...");
|
|
||||||
await RunMultiThreaded(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
LogInfo("Done.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void LogInfo(string message) => _logger.LogInfo(message);
|
|
||||||
private static void LogWarn(string message) => _logger.LogWarn(message);
|
|
||||||
private static void LogError(string message) => _logger.LogError(message);
|
|
||||||
private static void LogProgress(double progress, TimeSpan eta, double speed) => _logger.DrawProgress("Total", 0, progress, eta, speed);
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// ffprobe
|
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// Multi-threaded splitting
|
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
static async Task RunMultiThreaded(List<SingleTask> jobs)
|
|
||||||
{
|
|
||||||
LogProgress(0.0, TimeSpan.Zero, 0.0);
|
|
||||||
|
|
||||||
var maxDegree = Math.Max(1, Environment.ProcessorCount / 2);
|
|
||||||
|
|
||||||
using var sem = new SemaphoreSlim(maxDegree);
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
|
|
||||||
// Slot pool: 0..maxDegree-1
|
|
||||||
var freeSlots = new ConcurrentQueue<int>(Enumerable.Range(0, maxDegree));
|
|
||||||
|
|
||||||
var totalSegments = jobs.Count;
|
|
||||||
var processedSegments = 0;
|
|
||||||
var totalDuration = jobs.Sum(j => j.SegmentLength);
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
await sem.WaitAsync();
|
|
||||||
|
|
||||||
tasks.Add(Task.Run(async () =>
|
|
||||||
{
|
|
||||||
int slot = -1;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Acquire a slot ID
|
|
||||||
while (!freeSlots.TryDequeue(out slot))
|
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
await ProcessSegment(job,slot + 1);
|
|
||||||
|
|
||||||
var processed = Interlocked.Increment(ref processedSegments);
|
|
||||||
var elapsed = sw.Elapsed;
|
|
||||||
var eta = TimeSpan.FromTicks(elapsed.Ticks * (totalSegments - processed) / processed);
|
|
||||||
var speed = (processed * totalDuration) / elapsed.TotalSeconds;
|
|
||||||
LogProgress((double)processed / totalSegments, eta, speed);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// Return slot to pool
|
|
||||||
if (slot >= 0)
|
|
||||||
freeSlots.Enqueue(slot);
|
|
||||||
|
|
||||||
sem.Release();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// Single-threaded splitting
|
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
static async Task RunSingleThreaded(List<SingleTask> jobs)
|
|
||||||
{
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
await ProcessSegment(job, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ProcessSegment(SingleTask t, int slot)
|
|
||||||
{
|
|
||||||
var processor = t.ProcessorFactory(slot);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await processor.ProcessSegment(t);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (processor is IDisposable disposable)
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static string BuildOutputFileName(SingleJob job, int index)
|
|
||||||
{
|
|
||||||
string fileName;
|
|
||||||
|
|
||||||
fileName = Path.GetFileName(job.Mask ?? "[NAME]_seg[NN].[EXT]")
|
|
||||||
.Replace("[NAME]", Path.GetFileNameWithoutExtension(job.InputFile))
|
|
||||||
.Replace("[N]" , index.ToString())
|
|
||||||
.Replace("[NN]" , index.ToString("00"))
|
|
||||||
.Replace("[NNN]" , index.ToString("000"))
|
|
||||||
.Replace("[NNNN]", index.ToString("0000"))
|
|
||||||
.Replace("[EXT]" , Path.GetExtension(job.InputFile).TrimStart('.'))
|
|
||||||
;
|
|
||||||
|
|
||||||
return Path.Combine(job.OutputFolder, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
public interface ILogger
|
public interface ILogger
|
||||||
{
|
{
|
||||||
void ClearProgress(int progressLevel);
|
void ClearProgress(string name, int progressLine);
|
||||||
void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed);
|
void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed);
|
||||||
void Log(string prefix, ConsoleColor color, string msg);
|
void Log(string prefix, ConsoleColor color, string msg);
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
namespace splitter.tui;
|
namespace splitter.tui;
|
||||||
|
|
||||||
public abstract class LoggingBase(ILogger _logger, int _progressLine)
|
public abstract class LoggingBase(ILogger logger, int _progressLine)
|
||||||
{
|
{
|
||||||
|
#pragma warning disable IDE1006 // Naming Styles
|
||||||
|
protected ILogger _logger = logger;
|
||||||
|
#pragma warning restore IDE1006 // Naming Styles
|
||||||
|
|
||||||
protected void Log(string level, ConsoleColor color, string message)
|
protected void Log(string level, ConsoleColor color, string message)
|
||||||
=> _logger.Log(level, color, message);
|
=> _logger.Log(level, color, message);
|
||||||
|
|
||||||
@ -17,6 +21,6 @@ public abstract class LoggingBase(ILogger _logger, int _progressLine)
|
|||||||
protected void DrawProgress(string name, double percent, TimeSpan eta, double fps)
|
protected void DrawProgress(string name, double percent, TimeSpan eta, double fps)
|
||||||
=> _logger.DrawProgress(name, _progressLine, percent, eta, fps);
|
=> _logger.DrawProgress(name, _progressLine, percent, eta, fps);
|
||||||
|
|
||||||
protected void ClearProgress()
|
protected void ClearProgress(string name)
|
||||||
=> _logger.ClearProgress(_progressLine);
|
=> _logger.ClearProgress(name,_progressLine);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,11 +51,11 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
|
|
||||||
// ---- ILogger ----
|
// ---- ILogger ----
|
||||||
|
|
||||||
public void ClearProgress(int progressLevel)
|
public void ClearProgress(string name, int progressLine)
|
||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
_progress[progressLevel] = ProgressEntry.Empty;
|
_progress[progressLine] = ProgressEntry.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,6 @@ public class TextLogger() : ILogger
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed) {}
|
public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed) {}
|
||||||
public void ClearProgress(int progressLevel){}
|
public void ClearProgress(string name, int progressLine) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using splitter;
|
using splitter;
|
||||||
using splitter.algo;
|
|
||||||
using splitter.probe;
|
|
||||||
|
|
||||||
public record SingleTask(
|
public record SingleTask(
|
||||||
SingleJob Job,
|
SingleJob Job,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user