mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
Compare commits
2 Commits
f2493c1709
...
9760fbc2e6
| Author | SHA1 | Date | |
|---|---|---|---|
| 9760fbc2e6 | |||
| e5a9a04265 |
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
@ -3,7 +3,7 @@ name: Build and Publish
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -22,19 +22,19 @@ jobs:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore -r win-x64
|
||||
run: dotnet restore Splitter-UI/Splitter-UI.csproj -r win-x64
|
||||
|
||||
- name: 'Get Version'
|
||||
- name: Get Version
|
||||
id: version
|
||||
uses: battila7/get-version-action@v2
|
||||
|
||||
- name: Publish Release
|
||||
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 }}
|
||||
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 }}
|
||||
|
||||
- name: Create ZIP
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publish = "splitter-cli/bin/Release/net10.0/win-x64/publish"
|
||||
$publish = "Splitter-UI/bin/Release/net10.0/win-x64/publish"
|
||||
$version = "${{ steps.version.outputs.version-without-v }}"
|
||||
$zip = "splitter-win-x64-$version.zip"
|
||||
|
||||
@ -51,5 +51,3 @@ jobs:
|
||||
files: splitter-win-x64-${{ steps.version.outputs.version-without-v }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# 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
|
||||
fixed‑length segments using multi‑threaded FFmpeg execution. It supports batch input, flexible
|
||||
duration formats, rotation, smart face/body‑aware cropping, ETA and speed reporting, with nice GUI
|
||||
|
||||
@ -122,6 +122,19 @@ 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
|
||||
{
|
||||
get => Job.Mask;
|
||||
|
||||
@ -54,14 +54,14 @@ public partial class MainViewModel : ViewModelBase
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
public Task Start() => Task.Run(async () =>
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
try
|
||||
{
|
||||
StatusBar.StatusText = "Processing…";
|
||||
StatusBar.Percent = 0;
|
||||
TransformMode = true;
|
||||
StatusBar.Percent = 0;
|
||||
TransformMode = true;
|
||||
|
||||
var files = FileList.Files.ToList();
|
||||
var jobs = new List<SingleTask>();
|
||||
@ -84,11 +84,11 @@ public partial class MainViewModel : ViewModelBase
|
||||
finally
|
||||
{
|
||||
StatusBar.StatusText = "Ready…";
|
||||
StatusBar.Percent = 0;
|
||||
TransformMode = false;
|
||||
StatusBar.Percent = 0;
|
||||
TransformMode = false;
|
||||
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
@ -26,7 +27,7 @@ public partial class ProgressViewModel : ObservableObject
|
||||
_mainModel = mainModel;
|
||||
}
|
||||
|
||||
public void ClearProgress(string name, int progressLine)
|
||||
public void ClearProgress(string name, int progressLine) => Dispatch(() =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
@ -36,8 +37,9 @@ public partial class ProgressViewModel : ObservableObject
|
||||
NumberOfProcesses -= 1;
|
||||
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)
|
||||
{
|
||||
@ -53,6 +55,18 @@ public partial class ProgressViewModel : ObservableObject
|
||||
NumberOfProcesses += 1;
|
||||
Processes[progressLine] = new ProgressInfo(name, progressLine, progress, eta, speed);
|
||||
}
|
||||
});
|
||||
|
||||
private void Dispatch(Action action)
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => action());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8"
|
||||
Margin="0,10,0,0">
|
||||
Margin="0,0,10,0">
|
||||
|
||||
<Button Content="Apply to Selected"
|
||||
Command="{Binding ApplyOverridesCommand}"/>
|
||||
@ -103,7 +103,7 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
|
||||
<!-- ScoreThreshold -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Score Threshold" Width="120"/>
|
||||
<TextBlock Text="Score threshold" Width="120"/>
|
||||
|
||||
<StackPanel Orientation="Vertical" Spacing="4" Width="260">
|
||||
<Slider Minimum="0"
|
||||
@ -119,6 +119,25 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
HorizontalAlignment="Right"/>
|
||||
</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 -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
|
||||
@ -47,6 +47,10 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*"
|
||||
IsVisible="{Binding TransformMode}">
|
||||
<views:ProgressView DataContext="{Binding Progress}"/>
|
||||
</Grid>
|
||||
|
||||
</DockPanel>
|
||||
</Window>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 367 KiB After Width: | Height: | Size: 572 KiB |
@ -140,24 +140,41 @@ All option names are preserved exactly, and descriptions are consolidated for cl
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| **--out=<folder>** | Output folder for generated segments. Default: `<input folder>/Splitter`. |
|
||||
| **--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=<pattern>** | Custom output filename pattern. Default: `[NAME]_seg[NN].[EXT]`. Supports `[NAME]`, `[N]`, `[NN]`, `[NNN]`, `[NNNN]`, `[EXT]`. Example: `--mask="[NAME]_[NNNN].mp4"`. |
|
||||
| **--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 the duration exactly as provided. Last segment may be shorter. |
|
||||
| **--enhance** | Enable video enhancement. Increases output resolution x4 using RealBasicVSR_x4 model. |
|
||||
| **--rotate=<degrees>** | Rotate video by 90, 180, or 270 degrees. Useful for correcting orientation metadata. |
|
||||
| **--rotate-auto** | Use automatic rotation detection. |
|
||||
| **--estimate** | Print calculated segment information and exit. No splitting is performed. |
|
||||
| **--crop[=<w:h>]** | Crop video to a target width and height with face/body tracking. Default: 607x1080. Ideal for Shorts, TikTok, Reels. |
|
||||
| **--detect=<name>** | Object detector for tracking. Values: `face` (UltraFace), `body` (YoloOnnx, default), `none` (center crop). |
|
||||
| **--gravitate=<x:y>** | Bias the crop window toward a normalized point in the frame. Example: `--gravitate=0.2:0.5`. |
|
||||
| **--text** | Use plain-text logging instead of the rich terminal UI. |
|
||||
| **--single-thread** | Disable parallel FFmpeg execution. Useful for debugging or low-resource systems. |
|
||||
| **--debug** | Show debug overlay during tracking. No cropping performed, but crop region shown. |
|
||||
| **-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. |
|
||||
| Parameter | Description |
|
||||
|----------|-------------|
|
||||
| --out=<folder> | Output folder for segments. Default: same folder as input video + "Splitter". |
|
||||
| --file=<path> | Input names or file masks (e.g. "videos/*.mp4"). If not specified, the first non-option argument is used as input. |
|
||||
| --mask=<pattern> | Output filename pattern. Default: [NAME]_seg[NN].[EXT]. Supports [NAME], [N], [NN], [NNN], [NNNN], [EXT] placeholders. |
|
||||
| --duration=<value> | Override target segment duration. Formats: Ns, NmMs, N. Examples: 90s, 2m30s, 45. Default (without --force): max 58s, equalized segment lengths. |
|
||||
| --force | Use fixed segment duration exactly as given. Last segment may be shorter. Default OFF. |
|
||||
| --enhance | Enable video enhancement. Output resolution x4 using RealBasicVSR_x4 model. |
|
||||
| --rotate=<degrees> | Rotate video by 90, 180, or 270 degrees. |
|
||||
| --rotate-auto | Auto-detect rotation using edge orientation statistics. |
|
||||
| --estimate | Print calculated segment information and exit. No splitting performed. |
|
||||
| --crop[=<w:h>] | Crop video to width w and height h with face tracking. Default: 607x1080. |
|
||||
| --detect=<name> | Object detector: face (UltraFace), body (YoloOnnx, default), none. |
|
||||
| --detect-above=<0-1> | Report detections only if upper bound starts below this threshold (0.0–1.0 mapped to 0..Height). |
|
||||
| --detect-id=<hex> | Hexadecimal ID of face/person to track across segments. Obtained via --debug overlay. |
|
||||
| --gravitate=<x:y> | Gravitate tracking toward normalized point (0.0–1.0). Example: 0.2:0.5. |
|
||||
| --text | Display log in plain text. |
|
||||
| --single-thread | Run in single-threaded mode. Useful for debugging or constrained systems. |
|
||||
| --debug | Show debug overlay during face tracking. |
|
||||
| -p:<name>=<value> | 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
|
||||
|
||||
|
||||
@ -49,6 +49,10 @@ public class SingleJob
|
||||
/// </summary>
|
||||
public float ScoreThreshold { get; set; } = 0.25f;
|
||||
/// <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.
|
||||
/// This is a value between 0.0 and 1.0 mapped to 0..Height.
|
||||
/// </summary>
|
||||
@ -144,6 +148,7 @@ public class SingleJob
|
||||
target.Debug = Debug;
|
||||
target.Detect = Detect;
|
||||
target.ScoreThreshold = ScoreThreshold;
|
||||
target.IdentityThreshold = IdentityThreshold;
|
||||
target.DetectAbove = DetectAbove;
|
||||
target.DetectId = DetectId;
|
||||
target.OverrideTargetDuration = OverrideTargetDuration;
|
||||
|
||||
@ -9,17 +9,16 @@ public sealed class IdentityCache
|
||||
private sealed class Identity
|
||||
{
|
||||
public ulong Id;
|
||||
public float[] Embedding; // EMA
|
||||
public float[] Embedding = null!; // EMA
|
||||
public int Samples;
|
||||
}
|
||||
|
||||
private readonly List<Identity> _ids = new();
|
||||
private ulong _nextId = 1;
|
||||
|
||||
private const float Threshold = 0.35f; // good for OSNet
|
||||
private const float EmaAlpha = 0.2f;
|
||||
private const float _emaAlpha = 0.2f;
|
||||
|
||||
public ulong ResolveId(float[] embedding)
|
||||
public ulong ResolveId(float[] embedding, float threshold)
|
||||
{
|
||||
if (_ids.Count == 0)
|
||||
return CreateNew(embedding);
|
||||
@ -37,7 +36,7 @@ public sealed class IdentityCache
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDist <= Threshold)
|
||||
if (bestDist <= threshold)
|
||||
{
|
||||
UpdateEma(_ids[bestIndex].Embedding, embedding);
|
||||
_ids[bestIndex].Samples++;
|
||||
@ -73,6 +72,6 @@ public sealed class IdentityCache
|
||||
private static void UpdateEma(float[] ema, float[] v)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ public class ObjectTracker(IObjectDetector _detector, IEmbeddingExtractor _embed
|
||||
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
|
||||
p.Id = _identityCache.ResolveId(embedding);
|
||||
p.Id = _identityCache.ResolveId(embedding, job.Job.IdentityThreshold);
|
||||
|
||||
objects[i] = p;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user