mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Compare commits
19 Commits
1fd938416e
...
93de483bc6
| Author | SHA1 | Date | |
|---|---|---|---|
| 93de483bc6 | |||
| 4cf8951851 | |||
| 5df207b27a | |||
| 2ba3a0cd3f | |||
| 48c8b2a5f7 | |||
| 18965515de | |||
| e31414b0bf | |||
| d46dda1271 | |||
| 80a346583d | |||
| a4850dcb90 | |||
| b4f0fe0a80 | |||
| 9a27a083ab | |||
| c2d24a36b9 | |||
| 4a0d5e65ff | |||
| 1982e88006 | |||
| 285df85cd6 | |||
| 9b155473d8 | |||
| ac58534438 | |||
| 5955fb2d29 |
48
.github/workflows/publish.yml
vendored
48
.github/workflows/publish.yml
vendored
@ -2,9 +2,11 @@ name: Build and Publish
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
tags:
|
||||||
pull_request:
|
- 'v*'
|
||||||
branches: [ "main" ]
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -20,22 +22,34 @@ jobs:
|
|||||||
dotnet-version: 10.0.x
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
- name: Restore
|
- name: Restore
|
||||||
run: dotnet restore
|
run: dotnet restore -r win-x64
|
||||||
|
|
||||||
- name: Build Release
|
- name: 'Get Version'
|
||||||
env:
|
id: version
|
||||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
uses: battila7/get-version-action@v2
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
|
||||||
run: dotnet build -c Release --no-restore
|
|
||||||
|
|
||||||
- name: Publish Release
|
- name: Publish Release
|
||||||
env:
|
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 }}
|
||||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
|
||||||
run: dotnet publish -c Release -r win-x64 --self-contained true --no-build
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Create ZIP
|
||||||
uses: actions/upload-artifact@v4
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publish = "splitter-cli/bin/Release/net10.0/win-x64/publish"
|
||||||
|
$version = "${{ steps.version.outputs.version-without-v }}"
|
||||||
|
$zip = "splitter-win-x64-$version.zip"
|
||||||
|
|
||||||
|
if (Test-Path $zip) { Remove-Item $zip }
|
||||||
|
Compress-Archive -Path "$publish/*" -DestinationPath $zip
|
||||||
|
|
||||||
|
Write-Host "Created $zip"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
name: splitter-win-x64
|
tag_name: ${{ github.ref_name }}
|
||||||
path: bin/Release/net10.0/win-x64/publish/
|
name: "Release ${{ github.ref_name }}"
|
||||||
|
files: splitter-win-x64-${{ steps.version.outputs.version-without-v }}.zip
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
28
README.md
28
README.md
@ -45,7 +45,6 @@ model.export(format="onnx", opset=12, half=False) # FP32 ONNX
|
|||||||
6. Applies rotation, crop, and tracking if enabled
|
6. Applies rotation, crop, and tracking if enabled
|
||||||
7. Displays progress, ETA, and speed
|
7. Displays progress, ETA, and speed
|
||||||
|
|
||||||
---
|
|
||||||
## Face Tracking vs Body Tracking
|
## Face Tracking vs Body Tracking
|
||||||
|
|
||||||
Face tracking and body tracking serve different purposes, and Splitter supports both because each
|
Face tracking and body tracking serve different purposes, and Splitter supports both because each
|
||||||
@ -104,7 +103,24 @@ This prevents the crop from drifting off‑screen and ensures that the output re
|
|||||||
when tracking fails. All positions are clamped to valid bounds, guaranteeing that the crop window
|
when tracking fails. All positions are clamped to valid bounds, guaranteeing that the crop window
|
||||||
never leaves the video frame.
|
never leaves the video frame.
|
||||||
|
|
||||||
---
|
### Automatic rotation detection
|
||||||
|
|
||||||
|
The rotation‑estimation method is based on analyzing the distribution of gradient orientations within
|
||||||
|
a video frame. After converting the frame to grayscale, the algorithm computes horizontal and vertical
|
||||||
|
image gradients using Sobel operators and derives per‑pixel gradient magnitudes and orientations.
|
||||||
|
These orientations are folded into the range [0, 180) and accumulated into a fixed‑size,
|
||||||
|
magnitude‑weighted histogram. The histogram represents the structural edge distribution of the frame,
|
||||||
|
independent of brightness fluctuations or local lighting artifacts. By comparing the total gradient
|
||||||
|
energy concentrated near 0 degrees (vertical edges) with the energy near 90 degrees (horizontal edges),
|
||||||
|
the method determines whether the frame is more consistent with an upright or sideways orientation.
|
||||||
|
|
||||||
|
This approach is designed for environments where brightness‑based cues are unreliable, such as
|
||||||
|
live concerts with strobe lights, LED walls, haze, and crowd movement. It relies solely on geometric
|
||||||
|
edge structure, which remains stable even under extreme lighting variation. The implementation is
|
||||||
|
optimized for high‑throughput video processing: all intermediate Mats, buffers, and histograms are
|
||||||
|
preallocated, and pixel data is accessed directly through pointers to avoid per‑frame memory
|
||||||
|
allocation. The method is intentionally biased toward the upright orientation, returning a sideways
|
||||||
|
classification only when the horizontal‑edge energy significantly exceeds the vertical‑edge energy.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -114,7 +130,6 @@ splitter [<input.mp4> ...] [options] [--] <ffmpeg passthrough>
|
|||||||
|
|
||||||
Inputs may be provided directly, via `--file=...`, or using file masks such as `videos/*.mp4`.
|
Inputs may be provided directly, via `--file=...`, or using file masks such as `videos/*.mp4`.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
@ -133,6 +148,7 @@ All option names are preserved exactly, and descriptions are consolidated for cl
|
|||||||
| **--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. |
|
| **--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. |
|
| **--force** | Use the duration exactly as provided. Last segment may be shorter. |
|
||||||
| **--rotate=<degrees>** | Rotate video by 90, 180, or 270 degrees. Useful for correcting orientation metadata. |
|
| **--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. |
|
| **--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. |
|
| **--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). |
|
| **--detect=<name>** | Object detector for tracking. Values: `face` (UltraFace), `body` (YoloOnnx, default), `none` (center crop). |
|
||||||
@ -142,8 +158,6 @@ All option names are preserved exactly, and descriptions are consolidated for cl
|
|||||||
| **--debug** | Show debug overlay during tracking. No cropping performed, but crop region shown. |
|
| **--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. |
|
| **-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. |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FFmpeg Passthrough
|
## FFmpeg Passthrough
|
||||||
|
|
||||||
Anything after `--` is passed directly to FFmpeg.
|
Anything after `--` is passed directly to FFmpeg.
|
||||||
@ -153,16 +167,12 @@ Example:
|
|||||||
splitter video.mp4 --force --duration=45 -- -an -sn
|
splitter video.mp4 --force --duration=45 -- -an -sn
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Input and Output Behavior
|
## Input and Output Behavior
|
||||||
|
|
||||||
- `input.mp4` may be a file mask (`videos/*.mp4`)
|
- `input.mp4` may be a file mask (`videos/*.mp4`)
|
||||||
- Output filenames follow the `--mask` pattern
|
- Output filenames follow the `--mask` pattern
|
||||||
- Output folder defaults to `<input folder>/Splitter` unless overridden
|
- Output folder defaults to `<input folder>/Splitter` unless overridden
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Split into equal 60‑second segments:
|
Split into equal 60‑second segments:
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
namespace splitter;
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
public static class BuildInfo
|
public static class BuildInfo
|
||||||
{
|
{
|
||||||
public static string Version { get; } = ThisAssembly.Version;
|
private static readonly Assembly Assembly = typeof(BuildInfo).Assembly;
|
||||||
public static string BuildNumber { get; } = ThisAssembly.BuildNumber;
|
|
||||||
public static string Commit { get; } = ThisAssembly.Commit;
|
public static string Version => Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown";
|
||||||
|
public static string FileVersion => Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "unknown";
|
||||||
|
public static string AssemblyVersion => Assembly.GetName().Version?.ToString() ?? "unknown";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
// Auto-generated. Do not edit.
|
|
||||||
namespace splitter;
|
|
||||||
|
|
||||||
internal static class ThisAssembly
|
|
||||||
{
|
|
||||||
public const string Version = "@VERSION@";
|
|
||||||
public const string BuildNumber = "@BUILDNUMBER@";
|
|
||||||
public const string Commit = "@COMMIT@";
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using OpenCvSharp;
|
||||||
using OpenCvSharp;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
using System;
|
using System.Globalization;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using OpenCvSharp;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
@ -22,6 +18,7 @@ public class SingleJob
|
|||||||
public bool ForceFixed { get; set; }
|
public bool ForceFixed { get; set; }
|
||||||
public bool SingleThreaded { get; set; }
|
public bool SingleThreaded { get; set; }
|
||||||
public int? Rotate { get; set; }
|
public int? Rotate { get; set; }
|
||||||
|
public bool RotateAuto { get; set; }
|
||||||
public Dictionary<string, string> Parameters { get; set; } = [];
|
public Dictionary<string, string> Parameters { get; set; } = [];
|
||||||
|
|
||||||
public void Override<T>(ref T member, string name)
|
public void Override<T>(ref T member, string name)
|
||||||
@ -143,6 +140,10 @@ public sealed class CommandLine
|
|||||||
{
|
{
|
||||||
Master.SingleThreaded = true;
|
Master.SingleThreaded = true;
|
||||||
}
|
}
|
||||||
|
else if (arg == "--rotate-auto")
|
||||||
|
{
|
||||||
|
Master.RotateAuto = true;
|
||||||
|
}
|
||||||
else if (arg.StartsWith("--gravitate="))
|
else if (arg.StartsWith("--gravitate="))
|
||||||
{
|
{
|
||||||
var val = arg.Substring("--gravitate=".Length);
|
var val = arg.Substring("--gravitate=".Length);
|
||||||
@ -197,6 +198,7 @@ public sealed class CommandLine
|
|||||||
ForceFixed = Master.ForceFixed,
|
ForceFixed = Master.ForceFixed,
|
||||||
SingleThreaded = Master.SingleThreaded,
|
SingleThreaded = Master.SingleThreaded,
|
||||||
Rotate = Master.Rotate,
|
Rotate = Master.Rotate,
|
||||||
|
RotateAuto = Master.RotateAuto,
|
||||||
Parameters = new Dictionary<string, string>(Master.Parameters)
|
Parameters = new Dictionary<string, string>(Master.Parameters)
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
@ -328,7 +330,7 @@ public sealed class CommandLine
|
|||||||
|
|
||||||
public static void PrintVersion()
|
public static void PrintVersion()
|
||||||
{
|
{
|
||||||
Console.WriteLine($"...---=== splitter version {BuildInfo.Version} (commit {BuildInfo.Commit}, build {BuildInfo.BuildNumber}) ===---...");
|
Console.WriteLine($"...---=== splitter version {BuildInfo.Version} (file version: {BuildInfo.FileVersion}, build {BuildInfo.AssemblyVersion}) ===---...");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PrintHelp()
|
public static void PrintHelp()
|
||||||
@ -371,6 +373,9 @@ Options:
|
|||||||
--rotate=<degrees> Rotate video by specified degrees (90, 180, 270).
|
--rotate=<degrees> Rotate video by specified degrees (90, 180, 270).
|
||||||
Useful for videos with incorrect orientation metadata.
|
Useful for videos with incorrect orientation metadata.
|
||||||
|
|
||||||
|
--rotate-auto Auto-detect rotation and rotate accordingly.
|
||||||
|
Uses edge orientation statistics to determine if video is rotated.
|
||||||
|
|
||||||
--estimate Print calculated segment information and exit.
|
--estimate Print calculated segment information and exit.
|
||||||
No splitting is performed.
|
No splitting is performed.
|
||||||
|
|
||||||
@ -393,7 +398,7 @@ Options:
|
|||||||
--debug Show debug overlay during face tracking.
|
--debug Show debug overlay during face tracking.
|
||||||
|
|
||||||
-p:<name>=<value> Set a custom parameter for the object detector.
|
-p:<name>=<value> Set a custom parameter for the object detector.
|
||||||
Example: -p:confidence=0.5
|
Example: -p:EmaFactor=0.65
|
||||||
|
|
||||||
Tracking splitter defaults:
|
Tracking splitter defaults:
|
||||||
DropoutToleranceFrames = 20;
|
DropoutToleranceFrames = 20;
|
||||||
@ -401,6 +406,12 @@ Options:
|
|||||||
CameraEasing = 0.03;
|
CameraEasing = 0.03;
|
||||||
LostFreezeFrames = 60;
|
LostFreezeFrames = 60;
|
||||||
|
|
||||||
|
Rotation detector defaults:
|
||||||
|
RotationDetectorSampleCount = 5;
|
||||||
|
RotationDetectorSampleLength = 0.15;
|
||||||
|
RotationDetectorFrameWidth = 320;
|
||||||
|
RotationDetectorFrameHeight = 180;
|
||||||
|
|
||||||
Passthrough:
|
Passthrough:
|
||||||
Anything after -- is passed directly to ffmpeg.
|
Anything after -- is passed directly to ffmpeg.
|
||||||
|
|
||||||
|
|||||||
102
splitter-cli/FrameRotationDetector.cs
Normal file
102
splitter-cli/FrameRotationDetector.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
using OpenCvSharp;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public sealed class FrameRotationDetector
|
||||||
|
{
|
||||||
|
private readonly Mat _gray;
|
||||||
|
private readonly Mat _gx;
|
||||||
|
private readonly Mat _gy;
|
||||||
|
private readonly Mat _mag;
|
||||||
|
private readonly Mat _angle;
|
||||||
|
|
||||||
|
private readonly float[] _hist;
|
||||||
|
|
||||||
|
private readonly int _w;
|
||||||
|
private readonly int _h;
|
||||||
|
private readonly int _bins;
|
||||||
|
|
||||||
|
public FrameRotationDetector(int width = 320, int height = 180, int bins = 36)
|
||||||
|
{
|
||||||
|
_w = width;
|
||||||
|
_h = height;
|
||||||
|
_bins = bins;
|
||||||
|
|
||||||
|
_gray = new Mat(height, width, MatType.CV_8UC1);
|
||||||
|
_gx = new Mat(height, width, MatType.CV_32F);
|
||||||
|
_gy = new Mat(height, width, MatType.CV_32F);
|
||||||
|
_mag = new Mat(height, width, MatType.CV_32F);
|
||||||
|
_angle = new Mat(height, width, MatType.CV_32F);
|
||||||
|
|
||||||
|
_hist = new float[bins]; // allocated once
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetRotation(Mat frame)
|
||||||
|
{
|
||||||
|
// 1. Grayscale
|
||||||
|
Cv2.CvtColor(frame, _gray, ColorConversionCodes.BGR2GRAY);
|
||||||
|
|
||||||
|
// 2. Sobel
|
||||||
|
Cv2.Sobel(_gray, _gx, MatType.CV_32F, 1, 0, 3);
|
||||||
|
Cv2.Sobel(_gray, _gy, MatType.CV_32F, 0, 1, 3);
|
||||||
|
|
||||||
|
// 3. Magnitude + angle
|
||||||
|
Cv2.CartToPolar(_gx, _gy, _mag, _angle, angleInDegrees: true);
|
||||||
|
|
||||||
|
// 4. Clear histogram
|
||||||
|
for (int i = 0; i < _bins; i++)
|
||||||
|
_hist[i] = 0;
|
||||||
|
|
||||||
|
float binSize = 180f / _bins;
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
float* anglePtr = (float*)_angle.Data;
|
||||||
|
float* magPtr = (float*)_mag.Data;
|
||||||
|
|
||||||
|
int total = _w * _h;
|
||||||
|
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
{
|
||||||
|
float m = magPtr[i];
|
||||||
|
if (m < 5f) continue; // ignore weak gradients
|
||||||
|
|
||||||
|
float a = anglePtr[i];
|
||||||
|
if (a < 0) a += 360f;
|
||||||
|
a = a % 180f;
|
||||||
|
|
||||||
|
int bin = (int)(a / binSize);
|
||||||
|
if (bin < 0) bin = 0;
|
||||||
|
if (bin >= _bins) bin = _bins - 1;
|
||||||
|
|
||||||
|
_hist[bin] += m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Energy around 0° vs 90°
|
||||||
|
float e0 = 0, e90 = 0;
|
||||||
|
int window = 3;
|
||||||
|
|
||||||
|
int bin0 = 0;
|
||||||
|
int bin90 = _bins / 2;
|
||||||
|
|
||||||
|
for (int i = -window; i <= window; i++)
|
||||||
|
{
|
||||||
|
e0 += _hist[Wrap(bin0 + i)];
|
||||||
|
e90 += _hist[Wrap(bin90 + i)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Decide upright vs sideways
|
||||||
|
if (e90 > e0 * 1.6f)
|
||||||
|
return 90; // sideways
|
||||||
|
|
||||||
|
return 0; // upright (concert default)
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Wrap(int b)
|
||||||
|
{
|
||||||
|
if (b < 0) return b + _bins;
|
||||||
|
if (b >= _bins) return b - _bins;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,5 +4,5 @@ namespace splitter;
|
|||||||
|
|
||||||
public interface IObjectDetector : IDisposable
|
public interface IObjectDetector : IDisposable
|
||||||
{
|
{
|
||||||
List<(Rect box, Point2f center)> DetectAll(Mat frameCont, int width, int height);
|
List<(Rect box, Point2f center)> DetectAll(Mat frameCont);
|
||||||
}
|
}
|
||||||
@ -1,8 +1,4 @@
|
|||||||
using System;
|
namespace splitter;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace splitter;
|
|
||||||
|
|
||||||
public interface ISegmentProcessor
|
public interface ISegmentProcessor
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
namespace splitter;
|
||||||
namespace splitter;
|
|
||||||
|
|
||||||
public abstract class LoggingBase(ILogger _logger, int _progressLine)
|
public abstract class LoggingBase(ILogger _logger, int _progressLine)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
@ -11,12 +8,31 @@ public record VideoInfo(
|
|||||||
int Width,
|
int Width,
|
||||||
int Height,
|
int Height,
|
||||||
double Fps,
|
double Fps,
|
||||||
double Bitrate
|
double Bitrate,
|
||||||
|
int Rotation = 0
|
||||||
);
|
);
|
||||||
|
|
||||||
public static class ProbeVideo
|
public static class ProbeVideo
|
||||||
{
|
{
|
||||||
public static VideoInfo Probe(string inputFile)
|
public static async Task<VideoInfo> Probe(SingleJob job)
|
||||||
|
{
|
||||||
|
var info = ProbeSize(job.InputFile);
|
||||||
|
if ( job.RotateAuto)
|
||||||
|
{
|
||||||
|
var rotation = await ProbeRotation(job, info.Duration);
|
||||||
|
info = info with { Rotation = rotation };
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> ProbeRotation(SingleJob job, double duration)
|
||||||
|
{
|
||||||
|
var rotation = await new VideoRotationSampler(job).DetectRotationAsync(job.InputFile, duration);
|
||||||
|
return rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VideoInfo ProbeSize(string inputFile)
|
||||||
{
|
{
|
||||||
var args =
|
var args =
|
||||||
"-v error " +
|
"-v error " +
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"Debug": {
|
"Debug": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"commandLineArgs": "\"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\20260426_212004.mp4\" --crop --detect=body --debug --single-thread --text"
|
"commandLineArgs": "\"C:\\Users\\uncls\\Pictures\\2026\\2026 - Laura Cox\\Video\\MAH00041.MP4\" --rotate-auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,5 @@
|
|||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
|
||||||
using FFmpeg.AutoGen;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
using System;
|
using System.Text;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using Spectre.Console.Rendering;
|
using Spectre.Console.Rendering;
|
||||||
|
|
||||||
@ -178,7 +173,7 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
|
|
||||||
var layout = new Layout("root")
|
var layout = new Layout("root")
|
||||||
.SplitRows(
|
.SplitRows(
|
||||||
new Layout("progress") { Size = Math.Max(3, numberOfProcessesSnapshot + 2) },
|
new Layout("progress") { Size = Math.Max(3, numberOfProcessesSnapshot + 4) },
|
||||||
new Layout("log")
|
new Layout("log")
|
||||||
//new Layout("buttons") { Size = 3 }
|
//new Layout("buttons") { Size = 3 }
|
||||||
);
|
);
|
||||||
@ -233,8 +228,8 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
foreach (var log in slice)
|
foreach (var log in slice)
|
||||||
{
|
{
|
||||||
var time = log.Timestamp.ToString("HH:mm:ss");
|
var time = log.Timestamp.ToString("HH:mm:ss");
|
||||||
var timeColor = "deepskyblue1"; // dark-ish blue
|
var timeColor = "deepskyblue1";
|
||||||
var prefixColor = "lightpink1"; // light magenta
|
var prefixColor = "lightpink1";
|
||||||
var msgColor = MapConsoleColor(log.Color);
|
var msgColor = MapConsoleColor(log.Color);
|
||||||
|
|
||||||
var line =
|
var line =
|
||||||
@ -245,38 +240,10 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
rows.Add(new Markup(line));
|
rows.Add(new Markup(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
IRenderable content =
|
if (rows.Count == 0)
|
||||||
rows.Count == 0
|
return new Markup("[grey]No log messages yet.[/]");
|
||||||
? new Markup("[grey]No log messages yet.[/]")
|
|
||||||
: new Rows(rows);
|
|
||||||
|
|
||||||
var panel = new Panel(content)
|
return new Rows(rows);
|
||||||
{
|
|
||||||
Header = new PanelHeader("Log", Justify.Left),
|
|
||||||
Border = BoxBorder.Rounded,
|
|
||||||
Expand = true
|
|
||||||
};
|
|
||||||
|
|
||||||
return panel;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static IRenderable BuildButtonsPanel()
|
|
||||||
{
|
|
||||||
// Visual [ Cancel ] button; key handling is in RunInputLoopAsync
|
|
||||||
var text = new Markup("[bold white on red] Cancel [/]");
|
|
||||||
var grid = new Grid();
|
|
||||||
grid.AddColumn(new GridColumn().Centered());
|
|
||||||
grid.AddRow(text);
|
|
||||||
|
|
||||||
var panel = new Panel(grid)
|
|
||||||
{
|
|
||||||
Border = BoxBorder.Rounded,
|
|
||||||
Header = new PanelHeader("Actions", Justify.Left),
|
|
||||||
Expand = true
|
|
||||||
};
|
|
||||||
|
|
||||||
return panel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
@ -287,23 +254,23 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
|
|||||||
private static string MapConsoleColor(ConsoleColor color) =>
|
private static string MapConsoleColor(ConsoleColor color) =>
|
||||||
color switch
|
color switch
|
||||||
{
|
{
|
||||||
ConsoleColor.Black => "black",
|
ConsoleColor.Black => "black",
|
||||||
ConsoleColor.DarkBlue => "navy",
|
ConsoleColor.DarkBlue => "navy",
|
||||||
ConsoleColor.DarkGreen => "green",
|
ConsoleColor.DarkGreen => "green",
|
||||||
ConsoleColor.DarkCyan => "teal",
|
ConsoleColor.DarkCyan => "teal",
|
||||||
ConsoleColor.DarkRed => "maroon",
|
ConsoleColor.DarkRed => "maroon",
|
||||||
ConsoleColor.DarkMagenta => "purple",
|
ConsoleColor.DarkMagenta => "purple",
|
||||||
ConsoleColor.DarkYellow => "olive",
|
ConsoleColor.DarkYellow => "olive",
|
||||||
ConsoleColor.Gray => "silver",
|
ConsoleColor.Gray => "silver",
|
||||||
ConsoleColor.DarkGray => "grey",
|
ConsoleColor.DarkGray => "grey",
|
||||||
ConsoleColor.Blue => "blue",
|
ConsoleColor.Blue => "blue",
|
||||||
ConsoleColor.Green => "lime",
|
ConsoleColor.Green => "lime",
|
||||||
ConsoleColor.Cyan => "aqua",
|
ConsoleColor.Cyan => "aqua",
|
||||||
ConsoleColor.Red => "red",
|
ConsoleColor.Red => "red",
|
||||||
ConsoleColor.Magenta => "fuchsia",
|
ConsoleColor.Magenta => "fuchsia",
|
||||||
ConsoleColor.Yellow => "yellow",
|
ConsoleColor.Yellow => "yellow",
|
||||||
ConsoleColor.White => "white",
|
ConsoleColor.White => "white",
|
||||||
_ => "white"
|
_ => "white"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using OpenCvSharp;
|
using OpenCvSharp;
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
@ -112,7 +109,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
// input frame → Mat
|
// input frame → Mat
|
||||||
Marshal.Copy(inBuffer, 0, frameMat.Data, inBytes);
|
Marshal.Copy(inBuffer, 0, frameMat.Data, inBytes);
|
||||||
|
|
||||||
var objects = _detector.DetectAll(frameMat, videoWidth, videoHeight);
|
var objects = _detector.DetectAll(frameMat);
|
||||||
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
||||||
|
|
||||||
camera.Update(primary);
|
camera.Update(primary);
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using NcnnDotNet.Layers;
|
|
||||||
using OpenCvSharp;
|
using OpenCvSharp;
|
||||||
using UltraFaceDotNet;
|
using UltraFaceDotNet;
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ public sealed class UltraFaceDetector: LoggingBase, IDisposable, IObjectDetector
|
|||||||
_ultraFace = UltraFace.Create(param);
|
_ultraFace = UltraFace.Create(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<(Rect box, Point2f center)> DetectAll(Mat frameCont, int width, int height)
|
public List<(Rect box, Point2f center)> DetectAll(Mat frameCont)
|
||||||
{
|
{
|
||||||
// Convert to byte[] for UltraFace
|
// Convert to byte[] for UltraFace
|
||||||
var bytesFull = frameCont.Rows * frameCont.Cols * frameCont.ElemSize();
|
var bytesFull = frameCont.Rows * frameCont.Cols * frameCont.ElemSize();
|
||||||
@ -44,8 +43,8 @@ public sealed class UltraFaceDetector: LoggingBase, IDisposable, IObjectDetector
|
|||||||
using var mat = NcnnDotNet.Mat.FromPixels(
|
using var mat = NcnnDotNet.Mat.FromPixels(
|
||||||
(IntPtr)p,
|
(IntPtr)p,
|
||||||
NcnnDotNet.PixelType.Bgr, // BGR24 input
|
NcnnDotNet.PixelType.Bgr, // BGR24 input
|
||||||
width,
|
frameCont.Width,
|
||||||
height);
|
frameCont.Height);
|
||||||
|
|
||||||
var faces = _ultraFace.Detect(mat);
|
var faces = _ultraFace.Detect(mat);
|
||||||
if (faces == null)
|
if (faces == null)
|
||||||
|
|||||||
169
splitter-cli/VideoRotationSampler.cs
Normal file
169
splitter-cli/VideoRotationSampler.cs
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
using OpenCvSharp;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public sealed class VideoRotationSampler
|
||||||
|
{
|
||||||
|
private readonly FrameRotationDetector _detector = new FrameRotationDetector();
|
||||||
|
|
||||||
|
public static int RotationDetectorSampleCount = 20;
|
||||||
|
public static double RotationDetectorSampleLength = 0.15; // seconds to decode per probe
|
||||||
|
public static int RotationDetectorFrameWidth = 320;
|
||||||
|
public static int RotationDetectorFrameHeight = 180;
|
||||||
|
|
||||||
|
// --- Zero-allocation buffers ---
|
||||||
|
private readonly byte[] _buffer;
|
||||||
|
private readonly Mat _frameMat;
|
||||||
|
|
||||||
|
public VideoRotationSampler(SingleJob _master)
|
||||||
|
{
|
||||||
|
if (_master.Parameters.TryGetValue("RotationDetectorSampleCount", out var s))
|
||||||
|
RotationDetectorSampleCount = int.Parse(s);
|
||||||
|
if (_master.Parameters.TryGetValue("RotationDetectorSampleLength", out s))
|
||||||
|
RotationDetectorSampleLength = double.Parse(s);
|
||||||
|
if (_master.Parameters.TryGetValue("RotationDetectorFrameWidth", out s))
|
||||||
|
RotationDetectorFrameWidth = int.Parse(s);
|
||||||
|
if (_master.Parameters.TryGetValue("RotationDetectorFrameHeight", out s))
|
||||||
|
RotationDetectorFrameHeight = int.Parse(s);
|
||||||
|
|
||||||
|
int w = RotationDetectorFrameWidth;
|
||||||
|
int h = RotationDetectorFrameHeight;
|
||||||
|
|
||||||
|
_buffer = new byte[w * h * 3]; // raw BGR24 buffer
|
||||||
|
_frameMat = new Mat(h, w, MatType.CV_8UC3); // wraps buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> DetectRotationAsync(
|
||||||
|
string inputFile,
|
||||||
|
double videoLengthSeconds)
|
||||||
|
{
|
||||||
|
if (videoLengthSeconds <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var rotations = new List<int>();
|
||||||
|
|
||||||
|
for (int i = 0; i < RotationDetectorSampleCount; i++)
|
||||||
|
{
|
||||||
|
double t = videoLengthSeconds * (i + 1) / (RotationDetectorSampleCount + 1);
|
||||||
|
|
||||||
|
var frame = await DecodeSingleFrameAsync(
|
||||||
|
inputFile,
|
||||||
|
t,
|
||||||
|
RotationDetectorSampleLength,
|
||||||
|
RotationDetectorFrameWidth,
|
||||||
|
RotationDetectorFrameHeight);
|
||||||
|
|
||||||
|
if (frame != null && !frame.Empty())
|
||||||
|
{
|
||||||
|
int rot = _detector.GetRotation(frame);
|
||||||
|
rotations.Add(rot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotations.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return Majority(rotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Majority(List<int> values)
|
||||||
|
{
|
||||||
|
var counts = new Dictionary<int, int>();
|
||||||
|
foreach (var v in values)
|
||||||
|
{
|
||||||
|
if (!counts.ContainsKey(v)) counts[v] = 0;
|
||||||
|
counts[v]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int best = 0;
|
||||||
|
int bestCount = 0;
|
||||||
|
|
||||||
|
foreach (var kv in counts)
|
||||||
|
{
|
||||||
|
if (kv.Value > bestCount)
|
||||||
|
{
|
||||||
|
best = kv.Key;
|
||||||
|
bestCount = kv.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Mat?> DecodeSingleFrameAsync(
|
||||||
|
string inputFile,
|
||||||
|
double start,
|
||||||
|
double length,
|
||||||
|
int width,
|
||||||
|
int height)
|
||||||
|
{
|
||||||
|
var p = StartFfmpegDecode(inputFile, start, length, rotate: null, plainText: false);
|
||||||
|
|
||||||
|
int needed = _buffer.Length;
|
||||||
|
int read = 0;
|
||||||
|
|
||||||
|
using var stdout = p.StandardOutput.BaseStream;
|
||||||
|
|
||||||
|
while (read < needed)
|
||||||
|
{
|
||||||
|
int r = await stdout.ReadAsync(_buffer, read, needed - read);
|
||||||
|
if (r == 0)
|
||||||
|
return null;
|
||||||
|
read += r;
|
||||||
|
}
|
||||||
|
|
||||||
|
try { p.Kill(); } catch { }
|
||||||
|
|
||||||
|
// Copy buffer → Mat (no new Mat)
|
||||||
|
System.Runtime.InteropServices.Marshal.Copy(_buffer, 0, _frameMat.Data, _buffer.Length);
|
||||||
|
|
||||||
|
return _frameMat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process StartFfmpegDecode(
|
||||||
|
string inputFile,
|
||||||
|
double start,
|
||||||
|
double length,
|
||||||
|
int? rotate,
|
||||||
|
bool plainText)
|
||||||
|
{
|
||||||
|
var ss = start.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
var t = length.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
// FFmpeg does the resize + format conversion
|
||||||
|
var args =
|
||||||
|
$"-ss {ss} -t {t} -i \"{inputFile}\" " +
|
||||||
|
"-an -sn " +
|
||||||
|
$"-vf scale={RotationDetectorFrameWidth}:{RotationDetectorFrameHeight},format=bgr24 " +
|
||||||
|
"-f rawvideo -";
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffmpeg",
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var p = new Process { StartInfo = psi };
|
||||||
|
p.Start();
|
||||||
|
|
||||||
|
// Optional stderr logging
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
while ((line = p.StandardError.ReadLine()) != null)
|
||||||
|
if (plainText)
|
||||||
|
Console.WriteLine($"[ffmpeg-decode] {line}");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,4 @@
|
|||||||
using System;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
|
||||||
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 OpenCvSharp;
|
||||||
@ -83,7 +79,7 @@ public sealed class YoloOnnxObjectDetector : LoggingBase, IObjectDetector, IDisp
|
|||||||
_inputs.Add(NamedOnnxValue.CreateFromTensor(_inputName, _inputTensor));
|
_inputs.Add(NamedOnnxValue.CreateFromTensor(_inputName, _inputTensor));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<(Rect box, Point2f center)> DetectAll(Mat frameCont, int width, int height)
|
public List<(Rect box, Point2f center)> DetectAll(Mat frameCont)
|
||||||
{
|
{
|
||||||
if (frameCont.Empty())
|
if (frameCont.Empty())
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using splitter;
|
using splitter;
|
||||||
|
|
||||||
@ -78,7 +76,7 @@ static partial class Program
|
|||||||
if (!Directory.Exists(job.OutputFolder))
|
if (!Directory.Exists(job.OutputFolder))
|
||||||
Directory.CreateDirectory(job.OutputFolder);
|
Directory.CreateDirectory(job.OutputFolder);
|
||||||
|
|
||||||
var info = ProbeVideo.Probe(job.InputFile);
|
var info = await ProbeVideo.Probe(job);
|
||||||
if (info.Duration <= 0)
|
if (info.Duration <= 0)
|
||||||
{
|
{
|
||||||
LogError($"{baseName}: Could not read duration.");
|
LogError($"{baseName}: Could not read duration.");
|
||||||
|
|||||||
@ -27,40 +27,18 @@
|
|||||||
<EnableAVX2>true</EnableAVX2>
|
<EnableAVX2>true</EnableAVX2>
|
||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
<SelfContained>true</SelfContained>
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<PublishTrimmed>false</PublishTrimmed>
|
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
<PublishReadyToRun>false</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.0.0</Version>
|
<Version>1.0.0</Version>
|
||||||
<SourceRevisionId>$(GITHUB_SHA)</SourceRevisionId>
|
<InformationalVersion>$(Version).$(BuildNumber)+$(SourceRevisionId)</InformationalVersion>
|
||||||
|
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||||
|
<FileVersion>$(Version).$(BuildNumber)</FileVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<Target Name="RemoveUnwantedFiles" AfterTargets="Publish" Condition="'$(Configuration)' == 'Release'">
|
||||||
<BuildNumber>$(GITHUB_RUN_NUMBER)</BuildNumber>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<Target Name="GenerateBuildInfo" BeforeTargets="BeforeCompile">
|
|
||||||
<ReadLinesFromFile File="BuildInfo.template">
|
|
||||||
<Output TaskParameter="Lines" ItemName="BuildInfoLines" />
|
|
||||||
</ReadLinesFromFile>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProcessedBuildInfoLines Include="@(BuildInfoLines->Replace('@VERSION@', '$(Version)')->Replace('@BUILDNUMBER@', '$(BuildNumber)')->Replace('@COMMIT@', '$(SourceRevisionId)'))" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<WriteLinesToFile
|
|
||||||
File="BuildInfo.g.cs"
|
|
||||||
Overwrite="true"
|
|
||||||
Lines="@(ProcessedBuildInfoLines)" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Target Name="RemoveUnwantedFiles" AfterTargets="Publish" Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FilesToDelete Include="$(PublishDir)**\*.pdb" />
|
<FilesToDelete Include="$(PublishDir)**\*.pdb" />
|
||||||
<FilesToDelete Include="$(PublishDir)**\*.lib" />
|
<FilesToDelete Include="$(PublishDir)**\*.lib" />
|
||||||
@ -71,6 +49,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Update="ThisAssembly.g.cs" />
|
||||||
|
|
||||||
<Content Include="models/*.*">
|
<Content Include="models/*.*">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user