feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
87
src/Paca.Core/VideoIntroOutroTrimmer.cs
Normal file
87
src/Paca.Core/VideoIntroOutroTrimmer.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Paca.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Detected interval of silence in an audio/video stream.
|
||||
/// </summary>
|
||||
public sealed record SilenceSpan(double StartSec, double DurationSec, double EndSec);
|
||||
|
||||
/// <summary>
|
||||
/// Effective playback and export window skipping unwanted intro and outro sections.
|
||||
/// </summary>
|
||||
public sealed record TrimWindow(double StartSec, double EndSec, double DurationSec);
|
||||
|
||||
/// <summary>
|
||||
/// Intelligent video trimmer parsing silencedetect logs and producing lossless cuts for intros/outros.
|
||||
/// </summary>
|
||||
public static class VideoIntroOutroTrimmer
|
||||
{
|
||||
private static readonly Regex SilenceStartRegex = new(@"silence_start:\s*([0-9.]+)", RegexOptions.Compiled);
|
||||
private static readonly Regex SilenceEndRegex = new(@"silence_end:\s*([0-9.]+)\s*\|\s*silence_duration:\s*([0-9.]+)", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parses ffmpeg silencedetect stderr output lines into structured silence spans.
|
||||
/// </summary>
|
||||
public static List<SilenceSpan> ParseSilenceSpans(string ffmpegOutput)
|
||||
{
|
||||
var spans = new List<SilenceSpan>();
|
||||
if (string.IsNullOrWhiteSpace(ffmpegOutput))
|
||||
return spans;
|
||||
|
||||
var startMatches = SilenceStartRegex.Matches(ffmpegOutput);
|
||||
var endMatches = SilenceEndRegex.Matches(ffmpegOutput);
|
||||
|
||||
var count = Math.Min(startMatches.Count, endMatches.Count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (double.TryParse(startMatches[i].Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var start) &&
|
||||
double.TryParse(endMatches[i].Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var end) &&
|
||||
double.TryParse(endMatches[i].Groups[2].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var dur))
|
||||
{
|
||||
spans.Add(new SilenceSpan(start, dur, end));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates safe playback trim window, guaranteeing at least 1 second duration.
|
||||
/// </summary>
|
||||
public static TrimWindow CalculateTrimWindow(double totalDurationSec, double skipIntroSec, double skipOutroSec)
|
||||
{
|
||||
if (totalDurationSec <= 0)
|
||||
return new TrimWindow(0, 1.0, 1.0);
|
||||
|
||||
var start = Math.Max(0, skipIntroSec);
|
||||
var end = Math.Max(0, totalDurationSec - skipOutroSec);
|
||||
|
||||
if (start >= end || (end - start) < 1.0)
|
||||
{
|
||||
start = 0;
|
||||
end = Math.Max(1.0, totalDurationSec);
|
||||
}
|
||||
|
||||
var duration = end - start;
|
||||
return new TrimWindow(start, end, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds ffmpeg CLI argument string for ultra-fast lossless video trimming.
|
||||
/// </summary>
|
||||
public static string BuildLosslessTrimCommandLine(
|
||||
string inputPath,
|
||||
string outputPath,
|
||||
double startSec,
|
||||
double durationSec)
|
||||
{
|
||||
var s = startSec.ToString("F2", CultureInfo.InvariantCulture);
|
||||
var d = durationSec.ToString("F2", CultureInfo.InvariantCulture);
|
||||
|
||||
return $"-ss {s} -i \"{inputPath}\" -t {d} -c copy \"{outputPath}\" -y";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue