mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Logging subsystem is in its own class now. Separate progress indicators for segments.
This commit is contained in:
parent
1d337cf5bf
commit
2b412694fb
@ -18,6 +18,7 @@ public sealed class CommandLine
|
|||||||
public bool PlainText { get; private init; }
|
public bool PlainText { get; private init; }
|
||||||
public bool EstimateOnly { get; private init; }
|
public bool EstimateOnly { get; private init; }
|
||||||
public bool ForceFixed { get; private init; }
|
public bool ForceFixed { get; private init; }
|
||||||
|
public bool SingleThreaded { get; private init; }
|
||||||
|
|
||||||
public bool IsValid => !string.IsNullOrEmpty(InputFile) && !string.IsNullOrEmpty(OutputFolder);
|
public bool IsValid => !string.IsNullOrEmpty(InputFile) && !string.IsNullOrEmpty(OutputFolder);
|
||||||
|
|
||||||
@ -79,6 +80,10 @@ public sealed class CommandLine
|
|||||||
{
|
{
|
||||||
Debug = true;
|
Debug = true;
|
||||||
}
|
}
|
||||||
|
else if (arg == "--single-thread")
|
||||||
|
{
|
||||||
|
SingleThreaded = true;
|
||||||
|
}
|
||||||
else if (arg.StartsWith("--duration="))
|
else if (arg.StartsWith("--duration="))
|
||||||
{
|
{
|
||||||
var dur = arg.Substring("--duration=".Length);
|
var dur = arg.Substring("--duration=".Length);
|
||||||
@ -198,6 +203,9 @@ Options:
|
|||||||
|
|
||||||
--text Display log in plain text.
|
--text Display log in plain text.
|
||||||
|
|
||||||
|
--single-thread Run in single-threaded mode (no parallel ffmpeg processes).
|
||||||
|
Useful for debugging or if system is resource-constrained.
|
||||||
|
|
||||||
--debug Show debug overlay during face tracking.
|
--debug Show debug overlay during face tracking.
|
||||||
|
|
||||||
Passthrough:
|
Passthrough:
|
||||||
|
|||||||
10
ISegmentProcessor.cs
Normal file
10
ISegmentProcessor.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public interface ISegmentProcessor
|
||||||
|
{
|
||||||
|
Task ProcessSegment( string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters);
|
||||||
|
}
|
||||||
68
Logger.cs
Normal file
68
Logger.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public static class Logger
|
||||||
|
{
|
||||||
|
static int _logLines = 0;
|
||||||
|
static readonly object _consoleLock = new();
|
||||||
|
|
||||||
|
public static bool PlainText { get; set; }
|
||||||
|
|
||||||
|
public static void Log(string prefix, ConsoleColor color, string msg)
|
||||||
|
{
|
||||||
|
lock (_consoleLock)
|
||||||
|
{
|
||||||
|
if (PlainText)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{prefix} {msg}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = color;
|
||||||
|
Console.WriteLine($"{prefix} {msg}");
|
||||||
|
Console.ResetColor();
|
||||||
|
_logLines++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogInfo(string msg) => Log("[INFO]", ConsoleColor.Cyan, msg);
|
||||||
|
public static void LogSuccess(string msg) => Log("[ OK ]", ConsoleColor.Green, msg);
|
||||||
|
public static void LogWarn(string msg) => Log("[WARN]", ConsoleColor.Yellow, msg);
|
||||||
|
public static void LogError(string msg) => Log("[ERR ]", ConsoleColor.Red, msg);
|
||||||
|
|
||||||
|
public static void DrawProgress(int progressLevel, double progress, TimeSpan eta, double speed)
|
||||||
|
{
|
||||||
|
if (PlainText || progressLevel < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_consoleLock)
|
||||||
|
{
|
||||||
|
var width = Math.Max(20, Console.WindowWidth - 20);
|
||||||
|
var filled = (int)(progress * width);
|
||||||
|
if (filled < 0) filled = 0;
|
||||||
|
if (filled > width) filled = width;
|
||||||
|
|
||||||
|
var barLine = _logLines + 1 + progressLevel*2;
|
||||||
|
var infoLine = _logLines + 2 + progressLevel*2;
|
||||||
|
|
||||||
|
// Progress bar with 24-bit color (green)
|
||||||
|
Console.SetCursorPosition(0, barLine);
|
||||||
|
Console.Write("\u001b[38;2;0;255;0m[");
|
||||||
|
Console.Write(new string('#', filled));
|
||||||
|
Console.Write(new string('-', width - filled));
|
||||||
|
Console.Write("]\u001b[0m");
|
||||||
|
|
||||||
|
// Info line: percentage, ETA, speed
|
||||||
|
Console.SetCursorPosition(0, infoLine);
|
||||||
|
var etaStr = eta.TotalSeconds < 0 || double.IsInfinity(eta.TotalSeconds)
|
||||||
|
? "ETA: --:--"
|
||||||
|
: $"ETA: {eta:mm\\:ss}";
|
||||||
|
var speedStr = double.IsNaN(speed) || double.IsInfinity(speed)
|
||||||
|
? "Speed: -.-x"
|
||||||
|
: $"Speed: {speed:F2}x";
|
||||||
|
|
||||||
|
var info = $"{progress * 100:0.0}% {etaStr} {speedStr} ";
|
||||||
|
Console.Write("\u001b[38;2;180;180;180m" + info.PadRight(Console.WindowWidth - 1) + "\u001b[0m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,20 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
public class LoggingBase(
|
public abstract class LoggingBase(int progressLine)
|
||||||
Action<string/*level*/, ConsoleColor /*color*/, string /*message*/> log,
|
|
||||||
Action<double /*percent*/, TimeSpan /*duration*/, double /*fps*/> drawProgress
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
protected Action<string/*level*/, ConsoleColor /*color*/, string /*message*/> Log = log;
|
protected void Log(string level, ConsoleColor color, string message)
|
||||||
protected Action<double /*percent*/, TimeSpan /*duration*/, double /*fps*/> DrawProgress = drawProgress;
|
=> Logger.Log(level, color, message);
|
||||||
|
|
||||||
protected void LogInfo(string msg) => Log("[INFO]", ConsoleColor.Cyan, msg);
|
protected void LogInfo(string message)
|
||||||
protected void LogSuccess(string msg) => Log("[ OK ]", ConsoleColor.Green, msg);
|
=> Logger.LogInfo(message);
|
||||||
protected void LogWarn(string msg) => Log("[WARN]", ConsoleColor.Yellow, msg);
|
|
||||||
protected void LogError(string msg) => Log("[ERR ]", ConsoleColor.Red, msg);
|
|
||||||
|
|
||||||
|
protected void LogWarn(string message)
|
||||||
|
=> Logger.LogWarn(message);
|
||||||
|
|
||||||
|
protected void LogError(string message)
|
||||||
|
=> Logger.LogError(message);
|
||||||
|
|
||||||
|
protected void DrawProgress(double percent, TimeSpan eta, double fps)
|
||||||
|
=> Logger.DrawProgress(progressLine, percent, eta, fps);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"Debug": {
|
"Debug": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"commandLineArgs": "\"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\20260426_212004.mp4\" \"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\Shorts\" --crop --detect=body --debug --text"
|
"commandLineArgs": "\"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\20260426_212004.mp4\" \"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\Shorts\" --crop --detect=body --debug --single-thread --text"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
107
SimpleSplitter.cs
Normal file
107
SimpleSplitter.cs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public class SimpleSplitter(int segmentNo) : LoggingBase(segmentNo), ISegmentProcessor
|
||||||
|
{
|
||||||
|
public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, string[] passthrough)
|
||||||
|
{
|
||||||
|
RunFFmpegSegment(inputFile, outputFile, start, length, passthrough);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunFFmpegSegment(
|
||||||
|
string inputFile,
|
||||||
|
string outputFile,
|
||||||
|
double start,
|
||||||
|
double length,
|
||||||
|
string[] passthrough)
|
||||||
|
{
|
||||||
|
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||||
|
|
||||||
|
var args =
|
||||||
|
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
||||||
|
$"-i \"{inputFile}\" " +
|
||||||
|
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
||||||
|
$"-c copy {pass} \"{outputFile}\" -y";
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffmpeg",
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg.");
|
||||||
|
|
||||||
|
ShowFFMpegProgress(length, proc);
|
||||||
|
|
||||||
|
proc.WaitForExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowFFMpegProgress(double length, Process proc)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
string? line;
|
||||||
|
while ((line = proc.StandardError.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
// Look for "time=00:00:03.52"
|
||||||
|
var idx = line.IndexOf("time=", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (idx < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var timeStr = ExtractTimestamp(line, idx + 5);
|
||||||
|
if (timeStr == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!TryParseFfmpegTime(timeStr, out var current))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var progress = current.TotalSeconds / length;
|
||||||
|
if (progress < 0) progress = 0;
|
||||||
|
if (progress > 1) progress = 1;
|
||||||
|
|
||||||
|
var elapsed = sw.Elapsed;
|
||||||
|
var speed = current.TotalSeconds > 0
|
||||||
|
? current.TotalSeconds / elapsed.TotalSeconds
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
var remaining = length - current.TotalSeconds;
|
||||||
|
var etaSeconds = speed > 0 ? remaining / speed : remaining;
|
||||||
|
var eta = TimeSpan.FromSeconds(etaSeconds);
|
||||||
|
|
||||||
|
DrawProgress(progress, eta, speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractTimestamp(string line, int startIndex)
|
||||||
|
{
|
||||||
|
// FFmpeg formats: HH:MM:SS.xx
|
||||||
|
// We read until whitespace
|
||||||
|
int end = startIndex;
|
||||||
|
while (end < line.Length && !char.IsWhiteSpace(line[end]))
|
||||||
|
end++;
|
||||||
|
|
||||||
|
if (end <= startIndex)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return line[startIndex..end];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseFfmpegTime(string s, out TimeSpan ts)
|
||||||
|
{
|
||||||
|
// FFmpeg uses "00:00:03.52"
|
||||||
|
return TimeSpan.TryParseExact(
|
||||||
|
s,
|
||||||
|
@"hh\:mm\:ss\.ff",
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -7,49 +7,65 @@ using OpenCvSharp;
|
|||||||
|
|
||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
public class TrackingSplitter(
|
public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||||
Action<string/*level*/, ConsoleColor /*color*/, string /*message*/> log,
|
|
||||||
Action<double /*percent*/, TimeSpan /*duration*/, double /*fps*/> drawProgress
|
|
||||||
) : LoggingBase(log, drawProgress)
|
|
||||||
{
|
{
|
||||||
public async Task TrackAndExtract(
|
private readonly int _segmentNo;
|
||||||
string srcFileName,
|
private readonly int _cropWidth;
|
||||||
string destFileName,
|
private readonly int _cropHeight;
|
||||||
IObjectDetector detector,
|
private readonly bool _debugOverlay;
|
||||||
TimeSpan skip,
|
private readonly bool _plainText;
|
||||||
TimeSpan duration,
|
|
||||||
int cropWidth,
|
private readonly IObjectDetector _detector;
|
||||||
int cropHeight,
|
|
||||||
string[] passthrough,
|
public TrackingSplitter(int segmentNo, int cropWidth, int cropHeight, bool debugOverlay, bool plainText, IObjectDetector detector)
|
||||||
bool debugOverlay)
|
: base(segmentNo)
|
||||||
{
|
{
|
||||||
using var capture = new VideoCapture(srcFileName);
|
_segmentNo = segmentNo;
|
||||||
|
_cropWidth = cropWidth;
|
||||||
|
_cropHeight = cropHeight;
|
||||||
|
_debugOverlay = debugOverlay;
|
||||||
|
_plainText = plainText;
|
||||||
|
_detector = detector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_detector is IDisposable d)
|
||||||
|
d.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters)
|
||||||
|
{
|
||||||
|
using var capture = new VideoCapture(inputFile);
|
||||||
if (!capture.IsOpened())
|
if (!capture.IsOpened())
|
||||||
throw new Exception("Cannot open video");
|
throw new Exception("Cannot open video");
|
||||||
|
|
||||||
capture.Set(VideoCaptureProperties.PosMsec, skip.TotalMilliseconds);
|
var skip = TimeSpan.FromSeconds(start);
|
||||||
|
var duration = TimeSpan.FromSeconds(length);
|
||||||
|
|
||||||
|
capture.Set(VideoCaptureProperties.PosMsec, start);
|
||||||
|
|
||||||
var videoWidth = (int)capture.Get(VideoCaptureProperties.FrameWidth);
|
var videoWidth = (int)capture.Get(VideoCaptureProperties.FrameWidth);
|
||||||
var videoHeight = (int)capture.Get(VideoCaptureProperties.FrameHeight);
|
var videoHeight = (int)capture.Get(VideoCaptureProperties.FrameHeight);
|
||||||
var fps = capture.Get(VideoCaptureProperties.Fps);
|
var fps = capture.Get(VideoCaptureProperties.Fps);
|
||||||
var totalFrames = (int)(duration.TotalSeconds * fps);
|
var totalFrames = (int)(length * fps);
|
||||||
|
|
||||||
var originalCropWidth = cropWidth;
|
var originalCropWidth = _cropWidth;
|
||||||
var originalCropHeight = cropHeight;
|
var originalCropHeight = _cropHeight;
|
||||||
|
|
||||||
Console.WriteLine($"[TrackingSplitter] skip={skip}, duration={duration}, fps={fps}, totalFrames={totalFrames}");
|
Console.WriteLine($"[TrackingSplitter] skip={skip}, duration={duration}, fps={fps}, totalFrames={totalFrames}");
|
||||||
|
|
||||||
var encWidth = debugOverlay ? videoWidth : originalCropWidth;
|
var encWidth = _debugOverlay ? videoWidth : originalCropWidth;
|
||||||
var encHeight = debugOverlay ? videoHeight : originalCropHeight;
|
var encHeight = _debugOverlay ? videoHeight : originalCropHeight;
|
||||||
|
|
||||||
var ffmpeg = StartFfmpegNvenc(
|
var ffmpeg = StartFfmpegNvenc(
|
||||||
srcFileName,
|
inputFile,
|
||||||
destFileName,
|
outputFile,
|
||||||
encWidth,
|
encWidth,
|
||||||
encHeight,
|
encHeight,
|
||||||
fps,
|
fps,
|
||||||
skip,
|
skip,
|
||||||
passthrough);
|
ffmpegPassthroughParameters);
|
||||||
|
|
||||||
using var stdin = ffmpeg.StandardInput.BaseStream;
|
using var stdin = ffmpeg.StandardInput.BaseStream;
|
||||||
|
|
||||||
@ -63,12 +79,12 @@ public class TrackingSplitter(
|
|||||||
// initial reset is now done inside CameraController
|
// initial reset is now done inside CameraController
|
||||||
|
|
||||||
var camera = new CameraController(
|
var camera = new CameraController(
|
||||||
videoWidth,
|
videoWidth,
|
||||||
videoHeight,
|
videoHeight,
|
||||||
originalCropWidth,
|
originalCropWidth,
|
||||||
originalCropHeight,
|
originalCropHeight,
|
||||||
kalman
|
kalman
|
||||||
);
|
);
|
||||||
|
|
||||||
var startTime = DateTime.UtcNow;
|
var startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -80,7 +96,7 @@ public class TrackingSplitter(
|
|||||||
Rect? objectBox = null;
|
Rect? objectBox = null;
|
||||||
Point2f? objectCenter = null;
|
Point2f? objectCenter = null;
|
||||||
|
|
||||||
var objects = detector.DetectAll(frame, videoWidth, videoHeight);
|
var objects = _detector.DetectAll(frame, videoWidth, videoHeight);
|
||||||
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
||||||
|
|
||||||
camera.Update(primary);
|
camera.Update(primary);
|
||||||
@ -94,7 +110,7 @@ public class TrackingSplitter(
|
|||||||
var lostFrames = camera.LostFrames;
|
var lostFrames = camera.LostFrames;
|
||||||
var roi = camera.Roi;
|
var roi = camera.Roi;
|
||||||
|
|
||||||
if (debugOverlay)
|
if (_debugOverlay)
|
||||||
{
|
{
|
||||||
if (objectBox.HasValue)
|
if (objectBox.HasValue)
|
||||||
{
|
{
|
||||||
@ -117,7 +133,7 @@ public class TrackingSplitter(
|
|||||||
DrawText(frame, $"Camera: {cameraCenter.X:F1},{cameraCenter.Y:F1}", 20, 160, Scalar.White);
|
DrawText(frame, $"Camera: {cameraCenter.X:F1},{cameraCenter.Y:F1}", 20, 160, Scalar.White);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugOverlay)
|
if (_debugOverlay)
|
||||||
{
|
{
|
||||||
frame.CopyTo(outputBgr);
|
frame.CopyTo(outputBgr);
|
||||||
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
|
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
|
||||||
@ -134,7 +150,7 @@ public class TrackingSplitter(
|
|||||||
|
|
||||||
var elapsed = DateTime.UtcNow - startTime;
|
var elapsed = DateTime.UtcNow - startTime;
|
||||||
var progress = (double)i / totalFrames;
|
var progress = (double)i / totalFrames;
|
||||||
var speed = i > 0 ? i / elapsed.TotalSeconds : 0.0;
|
var speed = i > 0 ? (i / elapsed.TotalSeconds)/fps : 0.0;
|
||||||
var remainingFrames = totalFrames - i;
|
var remainingFrames = totalFrames - i;
|
||||||
var etaSeconds = speed > 0 ? remainingFrames / speed : 0;
|
var etaSeconds = speed > 0 ? remainingFrames / speed : 0;
|
||||||
var eta = TimeSpan.FromSeconds(etaSeconds);
|
var eta = TimeSpan.FromSeconds(etaSeconds);
|
||||||
@ -147,7 +163,9 @@ public class TrackingSplitter(
|
|||||||
|
|
||||||
await ffmpeg.WaitForExitAsync();
|
await ffmpeg.WaitForExitAsync();
|
||||||
if (ffmpeg.ExitCode != 0)
|
if (ffmpeg.ExitCode != 0)
|
||||||
throw new Exception("FFmpeg NVENC encoding failed");
|
LogError($"Segment {_segmentNo} FFmpeg encoding failed");
|
||||||
|
else
|
||||||
|
LogInfo($"Segment {_segmentNo} processing completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Rect box, Point2f center)? SelectTrackedObject(
|
private (Rect box, Point2f center)? SelectTrackedObject(
|
||||||
@ -243,7 +261,10 @@ public class TrackingSplitter(
|
|||||||
{
|
{
|
||||||
string? line;
|
string? line;
|
||||||
while ((line = process.StandardError.ReadLine()) != null)
|
while ((line = process.StandardError.ReadLine()) != null)
|
||||||
Console.WriteLine($"[ffmpeg] {line}");
|
{
|
||||||
|
if (_plainText)
|
||||||
|
Console.WriteLine($"[ffmpeg] {line}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
});
|
});
|
||||||
@ -256,4 +277,5 @@ public class TrackingSplitter(
|
|||||||
Cv2.PutText(img, text, new Point(x, y),
|
Cv2.PutText(img, text, new Point(x, y),
|
||||||
HersheyFonts.HersheySimplex, 0.6, color, 2);
|
HersheyFonts.HersheySimplex, 0.6, color, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,7 @@ public sealed class UltraFaceDetector: LoggingBase, IDisposable, IObjectDetector
|
|||||||
{
|
{
|
||||||
private readonly UltraFace _ultraFace;
|
private readonly UltraFace _ultraFace;
|
||||||
|
|
||||||
public UltraFaceDetector(
|
public UltraFaceDetector() : base(-1)
|
||||||
Action<string/*level*/, ConsoleColor /*color*/, string /*message*/> log,
|
|
||||||
Action<double /*percent*/, TimeSpan /*duration*/, double /*fps*/> drawProgress
|
|
||||||
) : base(log, drawProgress)
|
|
||||||
{
|
{
|
||||||
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
var param = new UltraFaceParameter
|
var param = new UltraFaceParameter
|
||||||
|
|||||||
@ -51,18 +51,15 @@ public sealed class YoloOnnxObjectDetector : LoggingBase, IObjectDetector, IDisp
|
|||||||
|
|
||||||
public Detection(float x, float y, float w, float h, float score)
|
public Detection(float x, float y, float w, float h, float score)
|
||||||
{
|
{
|
||||||
X = x;
|
X = x;
|
||||||
Y = y;
|
Y = y;
|
||||||
Width = w;
|
Width = w;
|
||||||
Height = h;
|
Height = h;
|
||||||
Score = score;
|
Score = score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public YoloOnnxObjectDetector(
|
public YoloOnnxObjectDetector() : base(-1)
|
||||||
Action<string, ConsoleColor, string> log,
|
|
||||||
Action<double, TimeSpan, double> drawProgress
|
|
||||||
) : base(log, drawProgress)
|
|
||||||
{
|
{
|
||||||
var options = new SessionOptions();
|
var options = new SessionOptions();
|
||||||
options.AppendExecutionProvider_DML();
|
options.AppendExecutionProvider_DML();
|
||||||
@ -75,8 +72,8 @@ public sealed class YoloOnnxObjectDetector : LoggingBase, IObjectDetector, IDisp
|
|||||||
_inputName = _session.InputMetadata.Keys.First();
|
_inputName = _session.InputMetadata.Keys.First();
|
||||||
_outputName = _session.OutputMetadata.Keys.First();
|
_outputName = _session.OutputMetadata.Keys.First();
|
||||||
|
|
||||||
foreach (var kv in _session.OutputMetadata)
|
//foreach (var kv in _session.OutputMetadata)
|
||||||
LogInfo($"[YoloOnnx] {kv.Key}: {string.Join(",", kv.Value.Dimensions)} {kv.Value.ElementType}");
|
// LogInfo($"[YoloOnnx] {kv.Key}: {string.Join(",", kv.Value.Dimensions)} {kv.Value.ElementType}");
|
||||||
|
|
||||||
// Preallocate tensor buffer (fixed size for lifetime)
|
// Preallocate tensor buffer (fixed size for lifetime)
|
||||||
_inputBuffer = new float[1 * 3 * _inputHeight * _inputWidth];
|
_inputBuffer = new float[1 * 3 * _inputHeight * _inputWidth];
|
||||||
|
|||||||
351
splitter.cs
351
splitter.cs
@ -5,12 +5,7 @@ using splitter;
|
|||||||
|
|
||||||
static class Program
|
static class Program
|
||||||
{
|
{
|
||||||
static int _logLines = 0;
|
static async Task Main(string[] args)
|
||||||
static bool _plainText = false;
|
|
||||||
static readonly object _consoleLock = new();
|
|
||||||
static bool _progressRunning = true;
|
|
||||||
|
|
||||||
static void Main(string[] args)
|
|
||||||
{
|
{
|
||||||
var cmd = new CommandLine(args);
|
var cmd = new CommandLine(args);
|
||||||
|
|
||||||
@ -24,7 +19,7 @@ static class Program
|
|||||||
var debug = cmd.Debug;
|
var debug = cmd.Debug;
|
||||||
string? detect = cmd.Detect;
|
string? detect = cmd.Detect;
|
||||||
double? overrideTargetDuration = cmd.OverrideTargetDuration;
|
double? overrideTargetDuration = cmd.OverrideTargetDuration;
|
||||||
_plainText = cmd.PlainText;
|
Logger.PlainText = cmd.PlainText;
|
||||||
|
|
||||||
if (!File.Exists(inputFile))
|
if (!File.Exists(inputFile))
|
||||||
{
|
{
|
||||||
@ -81,90 +76,46 @@ static class Program
|
|||||||
LogInfo($"Segments: {segments}");
|
LogInfo($"Segments: {segments}");
|
||||||
LogInfo($"Equal segment length: {segmentLength:F3}s");
|
LogInfo($"Equal segment length: {segmentLength:F3}s");
|
||||||
|
|
||||||
|
Func<int, ISegmentProcessor> processorFactory;
|
||||||
if (crop != null)
|
if (crop != null)
|
||||||
{
|
{
|
||||||
LogInfo("Starting multi-threaded face tracking crop and splitting...");
|
processorFactory = i =>
|
||||||
RunMultiThreadedCrop(inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough, crop.Value.width, crop.Value.height, debug, detect);
|
{
|
||||||
|
IObjectDetector detector = detect switch
|
||||||
|
{
|
||||||
|
"face" => new UltraFaceDetector(),
|
||||||
|
"body" => new YoloOnnxObjectDetector(),
|
||||||
|
_ => throw new InvalidOperationException($"Unknown detector: {detect}")
|
||||||
|
};
|
||||||
|
return new TrackingSplitter(i, crop.Value.width, crop.Value.height, debug, cmd.PlainText, detector);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LogInfo("Starting multi-threaded ffmpeg splitting...");
|
processorFactory = i => new SimpleSplitter(i);
|
||||||
RunMultiThreadedSplit(inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough);
|
}
|
||||||
|
if (cmd.SingleThreaded)
|
||||||
|
{
|
||||||
|
LogInfo("Starting single-threaded splitting...");
|
||||||
|
await RunSingleThreaded(processorFactory, inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogInfo("Starting multi-threaded splitting...");
|
||||||
|
await RunMultiThreaded(processorFactory, inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogSuccess("Done.");
|
LogInfo("Done.");
|
||||||
_progressRunning = false;
|
|
||||||
// Move cursor below progress area
|
|
||||||
lock (_consoleLock)
|
|
||||||
{
|
|
||||||
Console.SetCursorPosition(0, _logLines + 4);
|
|
||||||
Console.WriteLine();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void LogInfo(string message)
|
||||||
|
=> Logger.LogInfo(message);
|
||||||
|
|
||||||
// -----------------------------
|
private static void LogWarn(string message)
|
||||||
// Logging + Progress UI
|
=> Logger.LogWarn(message);
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
static void Log(string prefix, ConsoleColor color, string msg)
|
private static void LogError(string message)
|
||||||
{
|
=> Logger.LogError(message);
|
||||||
lock (_consoleLock)
|
|
||||||
{
|
|
||||||
if (_plainText)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"{prefix} {msg}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = color;
|
|
||||||
Console.WriteLine($"{prefix} {msg}");
|
|
||||||
Console.ResetColor();
|
|
||||||
_logLines++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void LogInfo(string msg) => Log("[INFO]", ConsoleColor.Cyan, msg);
|
|
||||||
static void LogSuccess(string msg) => Log("[ OK ]", ConsoleColor.Green, msg);
|
|
||||||
static void LogWarn(string msg) => Log("[WARN]", ConsoleColor.Yellow, msg);
|
|
||||||
static void LogError(string msg) => Log("[ERR ]", ConsoleColor.Red, msg);
|
|
||||||
|
|
||||||
static void DrawProgress(double progress, TimeSpan eta, double speed)
|
|
||||||
{
|
|
||||||
if ( _plainText )
|
|
||||||
return;
|
|
||||||
|
|
||||||
lock (_consoleLock)
|
|
||||||
{
|
|
||||||
var width = Math.Max(20, Console.WindowWidth - 20);
|
|
||||||
var filled = (int)(progress * width);
|
|
||||||
if (filled < 0) filled = 0;
|
|
||||||
if (filled > width) filled = width;
|
|
||||||
|
|
||||||
var barLine = _logLines + 1;
|
|
||||||
var infoLine = _logLines + 2;
|
|
||||||
|
|
||||||
// Progress bar with 24-bit color (green)
|
|
||||||
Console.SetCursorPosition(0, barLine);
|
|
||||||
Console.Write("\u001b[38;2;0;255;0m[");
|
|
||||||
Console.Write(new string('#', filled));
|
|
||||||
Console.Write(new string('-', width - filled));
|
|
||||||
Console.Write("]\u001b[0m");
|
|
||||||
|
|
||||||
// Info line: percentage, ETA, speed
|
|
||||||
Console.SetCursorPosition(0, infoLine);
|
|
||||||
var etaStr = eta.TotalSeconds < 0 || double.IsInfinity(eta.TotalSeconds)
|
|
||||||
? "ETA: --:--"
|
|
||||||
: $"ETA: {eta:mm\\:ss}";
|
|
||||||
var speedStr = double.IsNaN(speed) || double.IsInfinity(speed)
|
|
||||||
? "Speed: -.-x"
|
|
||||||
: $"Speed: {speed:F2}x";
|
|
||||||
|
|
||||||
var info = $"{progress * 100:0.0}% {etaStr} {speedStr} ";
|
|
||||||
Console.Write("\u001b[38;2;180;180;180m" + info.PadRight(Console.WindowWidth - 1) + "\u001b[0m");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// ffprobe
|
// ffprobe
|
||||||
@ -196,7 +147,8 @@ static class Program
|
|||||||
// Multi-threaded splitting
|
// Multi-threaded splitting
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
static void RunMultiThreadedSplit(
|
static async Task RunMultiThreaded(
|
||||||
|
Func<int, ISegmentProcessor> processorFactory,
|
||||||
string inputFile,
|
string inputFile,
|
||||||
string outputFolder,
|
string outputFolder,
|
||||||
string mask,
|
string mask,
|
||||||
@ -216,52 +168,32 @@ static class Program
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var completed = 0;
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
// Progress thread
|
|
||||||
var progressThread = new Thread(() =>
|
|
||||||
{
|
|
||||||
while (_progressRunning)
|
|
||||||
{
|
|
||||||
var progress = segments == 0 ? 0 : (double)completed / segments;
|
|
||||||
var processedSeconds = completed * segmentLength;
|
|
||||||
var speed = sw.Elapsed.TotalSeconds > 0
|
|
||||||
? processedSeconds / sw.Elapsed.TotalSeconds
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
var remainingSeconds = (totalDuration - processedSeconds) / Math.Max(speed, 0.0001);
|
|
||||||
if (remainingSeconds < 0) remainingSeconds = 0;
|
|
||||||
var eta = TimeSpan.FromSeconds(remainingSeconds);
|
|
||||||
|
|
||||||
DrawProgress(progress, eta, speed);
|
|
||||||
Thread.Sleep(200);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
{
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
progressThread.Start();
|
|
||||||
|
|
||||||
var maxDegree = Math.Max(1, Environment.ProcessorCount / 2);
|
var maxDegree = Math.Max(1, Environment.ProcessorCount / 2);
|
||||||
|
using var sem = new SemaphoreSlim(maxDegree);
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
Parallel.ForEach(
|
foreach (var job in jobs)
|
||||||
jobs,
|
{
|
||||||
new ParallelOptions { MaxDegreeOfParallelism = maxDegree },
|
await sem.WaitAsync();
|
||||||
job =>
|
|
||||||
|
tasks.Add(Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var outputFile = BuildOutputFileName(outputFolder, mask, job.Index);
|
try
|
||||||
RunFFmpegSegment(inputFile, outputFile, job.Start, job.Length, passthrough);
|
{
|
||||||
Interlocked.Increment(ref completed);
|
await ProcessSegment(processorFactory, inputFile, outputFolder, mask, passthrough, job.Index, job.Start, job.Length);
|
||||||
});
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
sem.Release();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
sw.Stop();
|
await Task.WhenAll(tasks);
|
||||||
_progressRunning = false;
|
|
||||||
progressThread.Join();
|
|
||||||
DrawProgress(1.0, TimeSpan.Zero, totalDuration / Math.Max(sw.Elapsed.TotalSeconds, 0.0001));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void RunSingleThreadedSplit(
|
static async Task RunSingleThreaded(
|
||||||
|
Func<int, ISegmentProcessor> processorFactory,
|
||||||
string inputFile,
|
string inputFile,
|
||||||
string outputFolder,
|
string outputFolder,
|
||||||
string mask,
|
string mask,
|
||||||
@ -271,157 +203,38 @@ static class Program
|
|||||||
string[] passthrough)
|
string[] passthrough)
|
||||||
{
|
{
|
||||||
var jobs = Enumerable.Range(0, segments)
|
var jobs = Enumerable.Range(0, segments)
|
||||||
.Select(i => new
|
.Select(i => new
|
||||||
{
|
|
||||||
Index = i,
|
|
||||||
Start = i * segmentLength,
|
|
||||||
Length = (i == segments - 1)
|
|
||||||
? Math.Max(0.1, totalDuration - i * segmentLength)
|
|
||||||
: segmentLength
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var completed = 0;
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
// Progress thread
|
|
||||||
var progressThread = new Thread(() =>
|
|
||||||
{
|
|
||||||
while (_progressRunning)
|
|
||||||
{
|
{
|
||||||
var progress = segments == 0 ? 0 : (double)completed / segments;
|
Index = i,
|
||||||
var processedSeconds = completed * segmentLength;
|
Start = i * segmentLength,
|
||||||
var speed = sw.Elapsed.TotalSeconds > 0
|
Length = (i == segments - 1)
|
||||||
? processedSeconds / sw.Elapsed.TotalSeconds
|
? Math.Max(0.1, totalDuration - i * segmentLength)
|
||||||
: 0;
|
: segmentLength
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var remainingSeconds = (totalDuration - processedSeconds) / Math.Max(speed, 0.0001);
|
|
||||||
if (remainingSeconds < 0) remainingSeconds = 0;
|
|
||||||
var eta = TimeSpan.FromSeconds(remainingSeconds);
|
|
||||||
|
|
||||||
DrawProgress(progress, eta, speed);
|
|
||||||
Thread.Sleep(200);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
{
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
progressThread.Start();
|
|
||||||
|
|
||||||
// --- SINGLE THREADED LOOP ---
|
|
||||||
foreach (var job in jobs)
|
foreach (var job in jobs)
|
||||||
{
|
{
|
||||||
var outputFile = BuildOutputFileName(outputFolder, mask, job.Index);
|
await ProcessSegment(processorFactory, inputFile, outputFolder, mask, passthrough, job.Index, job.Start, job.Length);
|
||||||
RunFFmpegSegment(inputFile, outputFile, job.Start, job.Length, passthrough);
|
|
||||||
completed++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
|
||||||
_progressRunning = false;
|
|
||||||
progressThread.Join();
|
|
||||||
DrawProgress(1.0, TimeSpan.Zero, totalDuration / Math.Max(sw.Elapsed.TotalSeconds, 0.0001));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------
|
private static async Task ProcessSegment(Func<int, ISegmentProcessor> processorFactory, string inputFile, string outputFolder, string mask, string[] passthrough, int index, double start, double length)
|
||||||
// Multi-threaded cropping
|
|
||||||
// -----------------------------
|
|
||||||
private static void RunMultiThreadedCrop(
|
|
||||||
string inputFile,
|
|
||||||
string outputFolder,
|
|
||||||
string outputMask,
|
|
||||||
double duration,
|
|
||||||
int segments,
|
|
||||||
double segmentLength,
|
|
||||||
string[] passthrough,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
bool showDebugOverlay,
|
|
||||||
string? detect)
|
|
||||||
{
|
{
|
||||||
var tracker = new TrackingSplitter(Log, DrawProgress);
|
var outputFile = BuildOutputFileName(outputFolder, mask, index);
|
||||||
|
var processor = processorFactory(index);
|
||||||
var jobs = Enumerable.Range(0, segments)
|
try
|
||||||
.Select(i => new
|
|
||||||
{
|
{
|
||||||
Index = i,
|
await processor.ProcessSegment(inputFile, outputFile, start, length, passthrough);
|
||||||
Start = i * segmentLength,
|
}
|
||||||
Length = (i == segments - 1)
|
finally
|
||||||
? Math.Max(0.1, duration - i * segmentLength)
|
|
||||||
: segmentLength
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var completed = 0;
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
_progressRunning = true;
|
|
||||||
|
|
||||||
// --- PROGRESS THREAD ---
|
|
||||||
var progressThread = new Thread(() =>
|
|
||||||
{
|
{
|
||||||
while (_progressRunning)
|
if (processor is IDisposable disposable)
|
||||||
{
|
disposable.Dispose();
|
||||||
var progress = segments == 0 ? 0 : (double)completed / segments;
|
}
|
||||||
var processedSeconds = completed * segmentLength;
|
|
||||||
|
|
||||||
var speed = sw.Elapsed.TotalSeconds > 0
|
|
||||||
? processedSeconds / sw.Elapsed.TotalSeconds
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
var remainingSeconds = (duration - processedSeconds) / Math.Max(speed, 0.0001);
|
|
||||||
if (remainingSeconds < 0) remainingSeconds = 0;
|
|
||||||
|
|
||||||
var eta = TimeSpan.FromSeconds(remainingSeconds);
|
|
||||||
|
|
||||||
DrawProgress(progress, eta, speed);
|
|
||||||
Thread.Sleep(200);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
{
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
progressThread.Start();
|
|
||||||
|
|
||||||
// --- PARALLEL EXECUTION ---
|
|
||||||
var maxDegree = Math.Max(1, Environment.ProcessorCount / 2);
|
|
||||||
|
|
||||||
Parallel.ForEach(
|
|
||||||
jobs,
|
|
||||||
new ParallelOptions { MaxDegreeOfParallelism = maxDegree },
|
|
||||||
async job =>
|
|
||||||
{
|
|
||||||
var outputFile = BuildOutputFileName(outputFolder, outputMask, job.Index);
|
|
||||||
using IDisposable detector = detect switch
|
|
||||||
{
|
|
||||||
"face" => new UltraFaceDetector(Log, DrawProgress),
|
|
||||||
"body" => new YoloOnnxObjectDetector(Log, DrawProgress),
|
|
||||||
_ => throw new InvalidOperationException($"Unknown detector: {detect}")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run the face-tracking cropper
|
|
||||||
await tracker.TrackAndExtract(
|
|
||||||
inputFile,
|
|
||||||
outputFile,
|
|
||||||
(IObjectDetector)detector,
|
|
||||||
TimeSpan.FromSeconds(job.Start),
|
|
||||||
TimeSpan.FromSeconds(job.Length),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
passthrough,
|
|
||||||
showDebugOverlay);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref completed);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- CLEANUP ---
|
|
||||||
sw.Stop();
|
|
||||||
_progressRunning = false;
|
|
||||||
progressThread.Join();
|
|
||||||
|
|
||||||
var finalSpeed = duration / Math.Max(sw.Elapsed.TotalSeconds, 0.0001);
|
|
||||||
DrawProgress(1.0, TimeSpan.Zero, finalSpeed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static string BuildOutputFileName(string folder, string mask, int index)
|
static string BuildOutputFileName(string folder, string mask, int index)
|
||||||
{
|
{
|
||||||
string fileName;
|
string fileName;
|
||||||
@ -445,24 +258,4 @@ static class Program
|
|||||||
return Path.Combine(folder, fileName);
|
return Path.Combine(folder, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void RunFFmpegSegment(string inputFile, string outputFile, double start, double length, string[] passthrough)
|
|
||||||
{
|
|
||||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
|
||||||
|
|
||||||
var args =
|
|
||||||
$"-ss {start.ToString(CultureInfo.InvariantCulture)} -i \"{inputFile}\" -t {length.ToString(CultureInfo.InvariantCulture)} -c copy {pass} \"{outputFile}\" -y";
|
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "ffmpeg",
|
|
||||||
Arguments = args,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg.");
|
|
||||||
proc.StandardError.ReadToEnd(); // swallow output
|
|
||||||
proc.WaitForExit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user