Compare commits

..

No commits in common. "9760fbc2e68bd3a4c7dd434456397d082c41e458" and "f2493c1709f63c0ac12c05cd41f8bf6e0015e797" have entirely different histories.

12 changed files with 43 additions and 114 deletions

View File

@ -22,19 +22,19 @@ jobs:
dotnet-version: 10.0.x dotnet-version: 10.0.x
- name: Restore - name: Restore
run: dotnet restore Splitter-UI/Splitter-UI.csproj -r win-x64 run: dotnet restore -r win-x64
- name: Get Version - name: 'Get Version'
id: version id: version
uses: battila7/get-version-action@v2 uses: battila7/get-version-action@v2
- name: Publish Release - name: Publish Release
run: dotnet publish Splitter-UI/Splitter-UI.csproj -c Release -r win-x64 /p:Version=${{ steps.version.outputs.version-without-v }} /p:BuildNumber=${{ github.run_number }} /p:SourceRevisionId=${{ github.sha }} run: dotnet publish splitter-cli/splitter.csproj -c Release -r win-x64 /p:Version=${{ steps.version.outputs.version-without-v }} /p:BuildNumber=${{ github.run_number }} /p:SourceRevisionId=${{ github.sha }}
- name: Create ZIP - name: Create ZIP
shell: pwsh shell: pwsh
run: | run: |
$publish = "Splitter-UI/bin/Release/net10.0/win-x64/publish" $publish = "splitter-cli/bin/Release/net10.0/win-x64/publish"
$version = "${{ steps.version.outputs.version-without-v }}" $version = "${{ steps.version.outputs.version-without-v }}"
$zip = "splitter-win-x64-$version.zip" $zip = "splitter-win-x64-$version.zip"
@ -51,3 +51,5 @@ jobs:
files: splitter-win-x64-${{ steps.version.outputs.version-without-v }}.zip files: splitter-win-x64-${{ steps.version.outputs.version-without-v }}.zip
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,7 +1,5 @@
# Splitter # Splitter
This application was built to help me with maintaining my YouTube channel - [UnclShura](https://www.youtube.com/@UnclShura).
Splitter is a high-performance command line tool for cutting one or more video files into equal or Splitter is a high-performance command line tool for cutting one or more video files into equal or
fixedlength segments using multithreaded FFmpeg execution. It supports batch input, flexible fixedlength segments using multithreaded FFmpeg execution. It supports batch input, flexible
duration formats, rotation, smart face/bodyaware cropping, ETA and speed reporting, with nice GUI duration formats, rotation, smart face/bodyaware cropping, ETA and speed reporting, with nice GUI

View File

@ -122,19 +122,6 @@ public partial class JobViewModel : ObservableObject
} }
} }
public float IdentityThreshold
{
get => Job.IdentityThreshold;
set
{
if (Math.Abs(Job.IdentityThreshold - value) < 0.001)
return;
Job.IdentityThreshold = value;
OnPropertyChanged();
Task.Run(CreatePreview);
}
}
public string? Mask public string? Mask
{ {
get => Job.Mask; get => Job.Mask;

View File

@ -54,7 +54,7 @@ public partial class MainViewModel : ViewModelBase
_cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Cancel();
} }
public Task Start() => Task.Run(async () => public async Task Start()
{ {
_cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource = new CancellationTokenSource();
try try
@ -89,6 +89,6 @@ public partial class MainViewModel : ViewModelBase
_cancellationTokenSource?.Dispose(); _cancellationTokenSource?.Dispose();
} }
}); }
} }

View File

@ -1,5 +1,4 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@ -27,7 +26,7 @@ public partial class ProgressViewModel : ObservableObject
_mainModel = mainModel; _mainModel = mainModel;
} }
public void ClearProgress(string name, int progressLine) => Dispatch(() => public void ClearProgress(string name, int progressLine)
{ {
lock (_lock) lock (_lock)
{ {
@ -37,9 +36,8 @@ public partial class ProgressViewModel : ObservableObject
NumberOfProcesses -= 1; NumberOfProcesses -= 1;
Processes[progressLine] = new ProgressInfo("", progressLine, 0, TimeSpan.Zero, 0); Processes[progressLine] = new ProgressInfo("", progressLine, 0, TimeSpan.Zero, 0);
} }
}); }
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) => Dispatch(() =>
{ {
lock (_lock) lock (_lock)
{ {
@ -55,18 +53,6 @@ public partial class ProgressViewModel : ObservableObject
NumberOfProcesses += 1; NumberOfProcesses += 1;
Processes[progressLine] = new ProgressInfo(name, progressLine, progress, eta, speed); Processes[progressLine] = new ProgressInfo(name, progressLine, progress, eta, speed);
} }
});
private void Dispatch(Action action)
{
if (Dispatcher.UIThread.CheckAccess())
{
action();
}
else
{
Dispatcher.UIThread.Post(() => action());
}
} }
} }

View File

@ -12,7 +12,7 @@ x:DataType="vm:InspectorPaneViewModel">
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Spacing="8" Spacing="8"
Margin="0,0,10,0"> Margin="0,10,0,0">
<Button Content="Apply to Selected" <Button Content="Apply to Selected"
Command="{Binding ApplyOverridesCommand}"/> Command="{Binding ApplyOverridesCommand}"/>
@ -103,7 +103,7 @@ x:DataType="vm:InspectorPaneViewModel">
<!-- ScoreThreshold --> <!-- ScoreThreshold -->
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Score threshold" Width="120"/> <TextBlock Text="Score Threshold" Width="120"/>
<StackPanel Orientation="Vertical" Spacing="4" Width="260"> <StackPanel Orientation="Vertical" Spacing="4" Width="260">
<Slider Minimum="0" <Slider Minimum="0"
@ -120,25 +120,6 @@ x:DataType="vm:InspectorPaneViewModel">
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<!-- ScoreThreshold -->
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Identity matching threshold" Width="120"/>
<StackPanel Orientation="Vertical" Spacing="4" Width="260">
<Slider Minimum="0"
Maximum="1"
SmallChange="0.01"
LargeChange="0.1"
TickFrequency="0.05"
IsSnapToTickEnabled="False"
Value="{Binding Selected.IdentityThreshold, Mode=TwoWay}"/>
<TextBlock Text="{Binding Selected.IdentityThreshold, StringFormat='0.00'}"
FontSize="10"
HorizontalAlignment="Right"/>
</StackPanel>
</StackPanel>
<!-- DetectAbove --> <!-- DetectAbove -->
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Detect Above" Width="120"/> <TextBlock Text="Detect Above" Width="120"/>

View File

@ -47,10 +47,6 @@
</Grid> </Grid>
<Grid ColumnDefinitions="*"
IsVisible="{Binding TransformMode}">
<views:ProgressView DataContext="{Binding Progress}"/>
</Grid>
</DockPanel> </DockPanel>
</Window> </Window>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

@ -140,41 +140,24 @@ All option names are preserved exactly, and descriptions are consolidated for cl
## Options ## Options
| Parameter | Description | | Option | Description |
|----------|-------------| |--------|-------------|
| --out=&lt;folder&gt; | Output folder for segments. Default: same folder as input video + "Splitter". | | **--out=<folder>** | Output folder for generated segments. Default: `<input folder>/Splitter`. |
| --file=&lt;path&gt; | Input names or file masks (e.g. "videos/*.mp4"). If not specified, the first non-option argument is used as input. | | **--file=<path>** | Input file list or file mask. If omitted, the first non-option argument is used as input. Examples: `--file=videos/*.mp4`, `--file=file_list.txt`. |
| --mask=&lt;pattern&gt; | Output filename pattern. Default: [NAME]_seg[NN].[EXT]. Supports [NAME], [N], [NN], [NNN], [NNNN], [EXT] placeholders. | | **--mask=<pattern>** | Custom output filename pattern. Default: `[NAME]_seg[NN].[EXT]`. Supports `[NAME]`, `[N]`, `[NN]`, `[NNN]`, `[NNNN]`, `[EXT]`. Example: `--mask="[NAME]_[NNNN].mp4"`. |
| --duration=&lt;value&gt; | Override target segment duration. Formats: Ns, NmMs, N. Examples: 90s, 2m30s, 45. Default (without --force): max 58s, equalized segment lengths. | | **--duration=<value>** | Override target segment duration. Formats: `Ns`, `NmMs`, `N`. Examples: `--duration=90s`, `--duration=2m30s`, `--duration=45`. Without `--force`: max 58 seconds, equalized across segments. |
| --force | Use fixed segment duration exactly as given. Last segment may be shorter. Default OFF. | | **--force** | Use the duration exactly as provided. Last segment may be shorter. |
| --enhance | Enable video enhancement. Output resolution x4 using RealBasicVSR_x4 model. | | **--enhance** | Enable video enhancement. Increases output resolution x4 using RealBasicVSR_x4 model. |
| --rotate=&lt;degrees&gt; | Rotate video by 90, 180, or 270 degrees. | | **--rotate=<degrees>** | Rotate video by 90, 180, or 270 degrees. Useful for correcting orientation metadata. |
| --rotate-auto | Auto-detect rotation using edge orientation statistics. | | **--rotate-auto** | Use automatic rotation detection. |
| --estimate | Print calculated segment information and exit. No splitting performed. | | **--estimate** | Print calculated segment information and exit. No splitting is performed. |
| --crop[=&lt;w:h&gt;] | Crop video to width w and height h with face tracking. Default: 607x1080. | | **--crop[=<w:h>]** | Crop video to a target width and height with face/body tracking. Default: 607x1080. Ideal for Shorts, TikTok, Reels. |
| --detect=&lt;name&gt; | Object detector: face (UltraFace), body (YoloOnnx, default), none. | | **--detect=<name>** | Object detector for tracking. Values: `face` (UltraFace), `body` (YoloOnnx, default), `none` (center crop). |
| --detect-above=&lt;0-1&gt; | Report detections only if upper bound starts below this threshold (0.01.0 mapped to 0..Height). | | **--gravitate=<x:y>** | Bias the crop window toward a normalized point in the frame. Example: `--gravitate=0.2:0.5`. |
| --detect-id=&lt;hex&gt; | Hexadecimal ID of face/person to track across segments. Obtained via --debug overlay. | | **--text** | Use plain-text logging instead of the rich terminal UI. |
| --gravitate=&lt;x:y&gt; | Gravitate tracking toward normalized point (0.01.0). Example: 0.2:0.5. | | **--single-thread** | Disable parallel FFmpeg execution. Useful for debugging or low-resource systems. |
| --text | Display log in plain text. | | **--debug** | Show debug overlay during tracking. No cropping performed, but crop region shown. |
| --single-thread | Run in single-threaded mode. Useful for debugging or constrained systems. | | **-p:<name>=<value>** | Set custom parameters for the object detector. Example: `-p:confidence=0.5`. Defaults: DropoutToleranceFrames=20, EmaFactor=0.65, CameraEasing=0.03, LostFreezeFrames=60. |
| --debug | Show debug overlay during face tracking. |
| -p:&lt;name&gt;=&lt;value&gt; | Set custom detector parameter. Example: -p:EmaFactor=0.65. |
Tracking splitter defaults:
DropoutToleranceFrames = 20;
EmaFactor = 0.65;
CameraEasing = 0.03;
LostFreezeFrames = 60;
Rotation detector defaults:
RotationDetectorSampleCount = 5;
RotationDetectorSampleLength = 0.15;
RotationDetectorFrameWidth = 320;
RotationDetectorFrameHeight = 180;
## FFmpeg Passthrough ## FFmpeg Passthrough

View File

@ -49,10 +49,6 @@ public class SingleJob
/// </summary> /// </summary>
public float ScoreThreshold { get; set; } = 0.25f; public float ScoreThreshold { get; set; } = 0.25f;
/// <summary> /// <summary>
/// Identity matching confidence threshold. This is a value between 0.0 and 1.0 that sets the minimum confidence
/// </summary>
public float IdentityThreshold { get; set; } = 0.25f;
/// <summary>
/// Face or human detectors should only report detections if their upper bound starts below this threshold. /// Face or human detectors should only report detections if their upper bound starts below this threshold.
/// This is a value between 0.0 and 1.0 mapped to 0..Height. /// This is a value between 0.0 and 1.0 mapped to 0..Height.
/// </summary> /// </summary>
@ -148,7 +144,6 @@ public class SingleJob
target.Debug = Debug; target.Debug = Debug;
target.Detect = Detect; target.Detect = Detect;
target.ScoreThreshold = ScoreThreshold; target.ScoreThreshold = ScoreThreshold;
target.IdentityThreshold = IdentityThreshold;
target.DetectAbove = DetectAbove; target.DetectAbove = DetectAbove;
target.DetectId = DetectId; target.DetectId = DetectId;
target.OverrideTargetDuration = OverrideTargetDuration; target.OverrideTargetDuration = OverrideTargetDuration;

View File

@ -9,16 +9,17 @@ public sealed class IdentityCache
private sealed class Identity private sealed class Identity
{ {
public ulong Id; public ulong Id;
public float[] Embedding = null!; // EMA public float[] Embedding; // EMA
public int Samples; public int Samples;
} }
private readonly List<Identity> _ids = new(); private readonly List<Identity> _ids = new();
private ulong _nextId = 1; private ulong _nextId = 1;
private const float _emaAlpha = 0.2f; private const float Threshold = 0.35f; // good for OSNet
private const float EmaAlpha = 0.2f;
public ulong ResolveId(float[] embedding, float threshold) public ulong ResolveId(float[] embedding)
{ {
if (_ids.Count == 0) if (_ids.Count == 0)
return CreateNew(embedding); return CreateNew(embedding);
@ -36,7 +37,7 @@ public sealed class IdentityCache
} }
} }
if (bestDist <= threshold) if (bestDist <= Threshold)
{ {
UpdateEma(_ids[bestIndex].Embedding, embedding); UpdateEma(_ids[bestIndex].Embedding, embedding);
_ids[bestIndex].Samples++; _ids[bestIndex].Samples++;
@ -72,6 +73,6 @@ public sealed class IdentityCache
private static void UpdateEma(float[] ema, float[] v) private static void UpdateEma(float[] ema, float[] v)
{ {
for (int i = 0; i < ema.Length; i++) for (int i = 0; i < ema.Length; i++)
ema[i] = ema[i] * (1 - _emaAlpha) + v[i] * _emaAlpha; ema[i] = ema[i] * (1 - EmaAlpha) + v[i] * EmaAlpha;
} }
} }

View File

@ -26,7 +26,7 @@ public class ObjectTracker(IObjectDetector _detector, IEmbeddingExtractor _embed
rect.Height = Math.Clamp(rect.Height, 1, frameMat.Height - rect.Y); rect.Height = Math.Clamp(rect.Height, 1, frameMat.Height - rect.Y);
var embedding = _embeddingExtractor.Extract(frameMat, rect).ToArray(); // make a copy of the embedding array var embedding = _embeddingExtractor.Extract(frameMat, rect).ToArray(); // make a copy of the embedding array
p.Id = _identityCache.ResolveId(embedding, job.Job.IdentityThreshold); p.Id = _identityCache.ResolveId(embedding);
objects[i] = p; objects[i] = p;
} }