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:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View 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";
}
}