mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Compare commits
No commits in common. "9760fbc2e68bd3a4c7dd434456397d082c41e458" and "f2493c1709f63c0ac12c05cd41f8bf6e0015e797" have entirely different histories.
9760fbc2e6
...
f2493c1709
10
.github/workflows/publish.yml
vendored
10
.github/workflows/publish.yml
vendored
@ -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 }}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
fixed‑length segments using multi‑threaded FFmpeg execution. It supports batch input, flexible
|
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
|
duration formats, rotation, smart face/body‑aware cropping, ETA and speed reporting, with nice GUI
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"/>
|
||||||
|
|||||||
@ -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 |
@ -140,41 +140,24 @@ All option names are preserved exactly, and descriptions are consolidated for cl
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Parameter | Description |
|
| Option | Description |
|
||||||
|----------|-------------|
|
|--------|-------------|
|
||||||
| --out=<folder> | Output folder for segments. Default: same folder as input video + "Splitter". |
|
| **--out=<folder>** | Output folder for generated segments. Default: `<input folder>/Splitter`. |
|
||||||
| --file=<path> | 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=<pattern> | 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=<value> | 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=<degrees> | 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[=<w:h>] | 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=<name> | 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=<0-1> | Report detections only if upper bound starts below this threshold (0.0–1.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=<hex> | 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=<x:y> | Gravitate tracking toward normalized point (0.0–1.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:<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
|
## FFmpeg Passthrough
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user