diff --git a/Splitter-UI/App.axaml b/Splitter-UI/App.axaml
index 11ae681..78cddfc 100644
--- a/Splitter-UI/App.axaml
+++ b/Splitter-UI/App.axaml
@@ -17,6 +17,7 @@
+
\ No newline at end of file
diff --git a/Splitter-UI/Models/ParameterEntry.cs b/Splitter-UI/Models/ParameterEntry.cs
new file mode 100644
index 0000000..d8a7f4e
--- /dev/null
+++ b/Splitter-UI/Models/ParameterEntry.cs
@@ -0,0 +1,15 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Splitter_UI.Models;
+
+public partial class ParameterEntry : ObservableObject
+{
+ public string Key { get; }
+ [ObservableProperty] private string _value;
+
+ public ParameterEntry(string key, string value)
+ {
+ Key = key;
+ Value = value;
+ }
+}
diff --git a/Splitter-UI/Services/FileJobFactory.cs b/Splitter-UI/Services/FileJobFactory.cs
index 921a07c..421fc3d 100644
--- a/Splitter-UI/Services/FileJobFactory.cs
+++ b/Splitter-UI/Services/FileJobFactory.cs
@@ -9,9 +9,9 @@ public sealed class FileJobFactory : IFileJobFactory
_services = services;
}
- public FileJobViewModel Create(SingleJob job)
+ public JobViewModel Create(SingleJob job)
{
// Resolve a fresh VM + fresh services
- return ActivatorUtilities.CreateInstance(_services, job);
+ return ActivatorUtilities.CreateInstance(_services, job);
}
}
diff --git a/Splitter-UI/Services/IFileJobFactory.cs b/Splitter-UI/Services/IFileJobFactory.cs
index 883cdb3..58c4a0b 100644
--- a/Splitter-UI/Services/IFileJobFactory.cs
+++ b/Splitter-UI/Services/IFileJobFactory.cs
@@ -6,5 +6,5 @@ namespace Splitter_UI.Services;
public interface IFileJobFactory
{
- FileJobViewModel Create(SingleJob job);
+ JobViewModel Create(SingleJob job);
}
diff --git a/Splitter-UI/Splitter-UI.csproj b/Splitter-UI/Splitter-UI.csproj
index 60aaf87..56135ad 100644
--- a/Splitter-UI/Splitter-UI.csproj
+++ b/Splitter-UI/Splitter-UI.csproj
@@ -18,6 +18,7 @@
+
diff --git a/Splitter-UI/ViewModels/FileJobViewModel.cs b/Splitter-UI/ViewModels/FileJobViewModel.cs
deleted file mode 100644
index 7b23c7c..0000000
--- a/Splitter-UI/ViewModels/FileJobViewModel.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Avalonia.Media.Imaging;
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace Splitter_UI.ViewModels;
-
-public partial class FileJobViewModel : ObservableObject
-{
- public SingleJob Job { get; }
- public VideoInfo? Probe { get; set; }
- public PreviewData? Preview { get; set; }
- public ProgressInfo? Progress { get; set; }
-
- [ObservableProperty]
- private Bitmap? _thumbnail;
-
- public string FileName { get; set; }
-
-
- [ObservableProperty]
- private string _suggestedAction = "";
-
- private readonly IThumbnailService _thumbnails;
- private readonly IFileProbeService _fileProbe;
-
- public FileJobViewModel(SingleJob job, IThumbnailService thumbnails, IFileProbeService fileProbe)
- {
- Job = job;
- _thumbnails = thumbnails;
- _fileProbe = fileProbe;
-
- FileName = Path.GetFileName(job.InputFile);
-
- _ = Task.Run( LoadThumbnailAsync );
- }
-
- private async Task LoadThumbnailAsync()
- {
- Probe = await _fileProbe.ProbeAsync(Job);
- Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe);
- SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
- }
-}
diff --git a/Splitter-UI/ViewModels/FileListViewModel.cs b/Splitter-UI/ViewModels/FileListViewModel.cs
index c27b3a5..5256e98 100644
--- a/Splitter-UI/ViewModels/FileListViewModel.cs
+++ b/Splitter-UI/ViewModels/FileListViewModel.cs
@@ -7,20 +7,20 @@ namespace Splitter_UI.ViewModels;
public partial class FileListViewModel : ObservableObject
{
private readonly IFileJobFactory _factory;
- public ObservableCollection Files { get; } = [];
- public ObservableCollection SelectedFiles { get; } = [];
+ public ObservableCollection Files { get; } = [];
+ public ObservableCollection SelectedFiles { get; } = [];
[ObservableProperty]
- private FileJobViewModel? _selected;
+ private JobViewModel? _selected;
- public event Action? SelectedFileChanged;
+ public event Action? SelectedFileChanged;
public FileListViewModel(IFileJobFactory factory)
{
_factory = factory;
}
- partial void OnSelectedChanged(FileJobViewModel? value)
+ partial void OnSelectedChanged(JobViewModel? value)
=> SelectedFileChanged?.Invoke(value);
[RelayCommand]
diff --git a/Splitter-UI/ViewModels/InspectorPaneViewModel.cs b/Splitter-UI/ViewModels/InspectorPaneViewModel.cs
index f8270cf..22506a5 100644
--- a/Splitter-UI/ViewModels/InspectorPaneViewModel.cs
+++ b/Splitter-UI/ViewModels/InspectorPaneViewModel.cs
@@ -8,13 +8,18 @@ namespace Splitter_UI.ViewModels;
public partial class InspectorPaneViewModel : ObservableObject
{
[ObservableProperty]
- private FileJobViewModel? _selected;
+ private JobViewModel? _selected;
public List DetectModes =>
[
"face", "body", "none"
];
+ public List RotationAngles =>
+ [
+ 0, 90, 180, 270
+ ];
+
[RelayCommand]
private void ApplyOverrides()
{
@@ -22,4 +27,26 @@ public partial class InspectorPaneViewModel : ObservableObject
return;
}
+
+ public IRelayCommand RotateLeftCommand { get; }
+ public IRelayCommand RotateRightCommand { get; }
+
+ public InspectorPaneViewModel()
+ {
+ RotateLeftCommand = new RelayCommand(() => AdjustRotation(-90));
+ RotateRightCommand = new RelayCommand(() => AdjustRotation(+90));
+ }
+
+ private void AdjustRotation(int delta)
+ {
+ if (Selected?.Job == null)
+ return;
+
+ var r = Selected.Job.Rotate ?? 0;
+ r = (r + delta) % 360;
+ if (r < 0) r += 360;
+
+ Selected.Rotate = r;
+ }
+
}
diff --git a/Splitter-UI/ViewModels/JobViewModel.cs b/Splitter-UI/ViewModels/JobViewModel.cs
new file mode 100644
index 0000000..5b92c3f
--- /dev/null
+++ b/Splitter-UI/ViewModels/JobViewModel.cs
@@ -0,0 +1,156 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using Avalonia.Media.Imaging;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Splitter_UI.ViewModels;
+
+public partial class JobViewModel : ObservableObject
+{
+ public SingleJob Job { get; }
+ public VideoInfo? Probe { get; set; }
+ public PreviewData? Preview { get; set; }
+ public ProgressInfo? Progress { get; set; }
+
+ [ObservableProperty]
+ private Bitmap? _thumbnail;
+
+ [ObservableProperty]
+ private string _suggestedAction = "";
+
+ private readonly IThumbnailService _thumbnails;
+ private readonly IFileProbeService _fileProbe;
+
+ public string FileName => Path.GetFileName(Job.InputFile);
+
+ public ObservableCollection ParametersList { get; }
+ = new();
+
+ public string CropText
+ {
+ get => Job.Crop is { } c ? $"{c.width},{c.height}" : "";
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ Job.Crop = null;
+ }
+ else
+ {
+ var parts = value.Split(',');
+ if (parts.Length == 2 &&
+ int.TryParse(parts[0], out var w) &&
+ int.TryParse(parts[1], out var h))
+ Job.Crop = (w, h);
+ }
+ OnPropertyChanged();
+ }
+ }
+
+ public string GravitateText
+ {
+ get => Job.GravitateTo is { } p ? $"{p.X:F3},{p.Y:F3}" : "";
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ Job.GravitateTo = null;
+ }
+ else
+ {
+ var parts = value.Split(',');
+ if (parts.Length == 2 &&
+ float.TryParse(parts[0], out var x) &&
+ float.TryParse(parts[1], out var y))
+ Job.GravitateTo = new Point2f(x, y);
+ }
+ OnPropertyChanged();
+ }
+ }
+
+ public string PassthroughText
+ {
+ get => string.Join(' ', Job.Passthrough);
+ set
+ {
+ Job.Passthrough = string.IsNullOrWhiteSpace(value)
+ ? Array.Empty()
+ : value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ OnPropertyChanged();
+ }
+ }
+
+ public int? Rotate
+ {
+ get => Job.Rotate;
+ set
+ {
+ Job.Rotate = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public JobViewModel(SingleJob job, IThumbnailService thumbnails, IFileProbeService fileProbe)
+ {
+ Job = job;
+ _thumbnails = thumbnails;
+ _fileProbe = fileProbe;
+
+ ParametersList.Add(new ParameterEntry("DropoutToleranceFrames", ""));
+ ParametersList.Add(new ParameterEntry("EmaFactor", ""));
+ ParametersList.Add(new ParameterEntry("CameraEasing", ""));
+ ParametersList.Add(new ParameterEntry("LostFreezeFrames", ""));
+ ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount", ""));
+ ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", ""));
+ ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth", ""));
+ ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight", ""));
+
+ foreach (var entry in ParametersList)
+ {
+ entry.PropertyChanged += OnParameterChanged;
+ }
+
+ ParametersList.CollectionChanged += OnParametersCollectionChanged;
+
+
+ _ = Task.Run( LoadThumbnailAsync );
+ }
+
+ private async Task LoadThumbnailAsync()
+ {
+ Probe = await _fileProbe.ProbeAsync(Job);
+ Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe);
+ SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
+ }
+
+ private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value))
+ {
+ Job.Parameters[p.Key] = p.Value;
+ }
+ }
+
+ private void OnParametersCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.NewItems != null)
+ {
+ foreach (ParameterEntry p in e.NewItems)
+ {
+ Job.Parameters[p.Key] = p.Value;
+ p.PropertyChanged += OnParameterChanged;
+ }
+ }
+
+ if (e.OldItems != null)
+ {
+ foreach (ParameterEntry p in e.OldItems)
+ {
+ Job.Parameters.Remove(p.Key);
+ p.PropertyChanged -= OnParameterChanged;
+ }
+ }
+ }
+
+}
diff --git a/Splitter-UI/ViewModels/PreviewPaneViewModel.cs b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs
index 464cc5b..e29d1b4 100644
--- a/Splitter-UI/ViewModels/PreviewPaneViewModel.cs
+++ b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs
@@ -5,7 +5,7 @@ namespace Splitter_UI.ViewModels;
public partial class PreviewPaneViewModel : ObservableObject
{
[ObservableProperty]
- private FileJobViewModel? _selected;
+ private JobViewModel? _selected;
public PreviewPaneViewModel()
{
diff --git a/Splitter-UI/Views/FileListView.axaml b/Splitter-UI/Views/FileListView.axaml
index bcd7b89..a43ed8a 100644
--- a/Splitter-UI/Views/FileListView.axaml
+++ b/Splitter-UI/Views/FileListView.axaml
@@ -51,7 +51,7 @@
-
+
+xmlns="https://github.com/avaloniaui"
+xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+x:Class="Splitter_UI.Views.InspectorPane"
+xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
+x:DataType="vm:InspectorPaneViewModel">
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/splitter-cli/CommandLine.cs b/splitter-cli/CommandLine.cs
index cca7519..8e9609c 100644
--- a/splitter-cli/CommandLine.cs
+++ b/splitter-cli/CommandLine.cs
@@ -4,21 +4,93 @@ namespace splitter;
public class SingleJob
{
+ ///
+ /// File path of the input video. This is required for each job and should be
+ /// set to a valid video file path. The splitter will read this file, analyze it,
+ /// and split it into segments based on the specified parameters.
+ /// The output segments will be saved in the OutputFolder with names
+ /// derived from this input file and the Mask pattern if provided.
+ ///
public string InputFile { get; set; } = null!;
+ ///
+ /// Output folder where the split segments will be saved. This should be set
+ /// to a valid directory path.
+ ///
public string OutputFolder { get; set; } = null!;
+ ///
+ /// Crop parameters. Width and height for cropping the video. If set, the
+ /// splitter will crop the video to the specified dimensions while tracking the subject.
+ ///
public (int width, int height)? Crop { get; set; }
+ ///
+ /// The fallback point to gravitate towards when tracking the subject. Coordinates are normalized (0.0 to 1.0).
+ /// By default , the splitter gravitates towards the center of the frame (0.5, 0.5).
+ /// Setting this allows you to bias the tracking towards a specific area of the frame,
+ /// such as left-center (0.2, 0.5) or top-right (0.8, 0.2). This can be useful for
+ /// videos where the subject tends to be off-center or for creative framing choices.
+ ///
public Point2f? GravitateTo { get; set; }
+ ///
+ /// Destination file mask.
+ ///
public string? Mask { get; set; }
+ ///
+ /// Instead of producing the output, just generate debug frames with tracking
+ /// overlay to visually verify that the tracking is working correctly.
+ ///
public bool Debug { get; set; }
+ ///
+ /// Type of detector to use for tracking. Supported values are: face (UltraFace),
+ /// body (YoloOnnx, default), none (no tracking, just a center point).
+ ///
public string? Detect { get; set; }
+ ///
+ /// Set starget segments length explicitly. By default, the splitter calculates segment
+ /// lengths to be equal and not exceed 58 seconds.
+ ///
public double? OverrideTargetDuration { get; set; }
+ ///
+ /// Parameters to pass thru to ffmpeg. These are specified after "--" in the command
+ /// line and are passed directly to the ffmpeg command line for each segment.
+ ///
public string[] Passthrough { get; set; } = [];
+ ///
+ /// Debugging parameter. Instead of text UI putput lines in plain text.
+ /// This is useful when the output is being piped to a file or another program,
+ /// or when the user prefers a simpler log format without progress bars and dynamic updates.
+ ///
public bool PlainText { get; set; }
+ ///
+ /// Debugging parameter. Just show estimated segments length, count, and other info
+ /// without actually performing the splitting.
+ ///
public bool EstimateOnly { get; set; }
+ ///
+ /// Do not adapt segment length. When set, the splitter will use the exact
+ /// segment duration specified by --duration for all segments except possibly
+ /// the last one, which may be shorter.
+ ///
public bool ForceFixed { get; set; }
+ ///
+ /// Use single thread for operations. When set, the splitter will not run
+ /// multiple ffmpeg processes in parallel.
+ ///
public bool SingleThreaded { get; set; }
+ ///
+ /// Rotation angle: 90, 180, or 270 degrees. This is useful for videos that
+ /// have incorrect orientation metadata.
+ ///
public int? Rotate { get; set; }
+ ///
+ /// Autodetect if rotation is needed. Not very reliable but can work for some videos.
+ /// Uses edge orientation statistics to determine if the video is rotated and
+ /// applies the appropriate rotation if needed.
+ ///
public bool RotateAuto { get; set; }
+ ///
+ /// Override internal parameters. This allows you to set custom parameters for the
+ /// object detector or rotation detector.
+ ///
public Dictionary Parameters { get; set; } = [];
public void Override(ref T member, string name)
diff --git a/splitter-cli/splitter.csproj b/splitter-cli/splitter.csproj
index 13c8a91..a99264e 100644
--- a/splitter-cli/splitter.csproj
+++ b/splitter-cli/splitter.csproj
@@ -9,6 +9,7 @@
true
x64
win-x64
+ 0