Add Shorts wizard and v4 video-style pipeline

Turn the Shorts tab into a step-by-step wizard and port
amigadb-short-fx-v4.sh: Video background style, auto/manual crop,
source fade-in, and blur/opacity/margin settings.
This commit is contained in:
2026-08-15 17:59:18 +02:00
parent 58e11ba4e3
commit 8a734d58d9
10 changed files with 1911 additions and 145 deletions
+92
View File
@@ -0,0 +1,92 @@
using AmiReel.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Models;
[TestClass]
public class ShortWizardTests
{
[TestMethod]
public void StepsFor_BrandStyle_IncludesEffectSettingsPage()
{
// Act
IReadOnlyList<ShortWizardStep> steps = ShortWizard.StepsFor(ShortStyle.Brand);
// Assert
CollectionAssert.AreEqual(
new[]
{
ShortWizardStep.Clip,
ShortWizardStep.Effect,
ShortWizardStep.StyleOptions,
ShortWizardStep.Titles,
ShortWizardStep.Encode,
},
steps.ToArray());
}
[TestMethod]
public void StepsFor_VideoStyle_IncludesEffectSettingsPage()
{
// Act
IReadOnlyList<ShortWizardStep> steps = ShortWizard.StepsFor(ShortStyle.Video);
// Assert
Assert.IsTrue(steps.Contains(ShortWizardStep.StyleOptions));
Assert.AreEqual(5, steps.Count);
}
[TestMethod]
public void StepsFor_CropStyle_SkipsEffectSettingsPage()
{
// Act
IReadOnlyList<ShortWizardStep> steps = ShortWizard.StepsFor(ShortStyle.Crop);
// Assert
CollectionAssert.AreEqual(
new[]
{
ShortWizardStep.Clip,
ShortWizardStep.Effect,
ShortWizardStep.Titles,
ShortWizardStep.Encode,
},
steps.ToArray());
}
[TestMethod]
public void NeedsStyleOptions_OnlyBrandAndVideo_ReturnsTrue()
{
// Assert
Assert.IsTrue(ShortWizard.NeedsStyleOptions(ShortStyle.Brand));
Assert.IsTrue(ShortWizard.NeedsStyleOptions(ShortStyle.Video));
Assert.IsFalse(ShortWizard.NeedsStyleOptions(ShortStyle.Pixel));
Assert.IsFalse(ShortWizard.NeedsStyleOptions(ShortStyle.Spectrum));
}
[TestMethod]
public void Description_ForEveryStyle_IsNonEmpty()
{
foreach (ShortStyle style in Enum.GetValues<ShortStyle>())
{
// Act
string description = ShortWizard.Description(style);
// Assert
Assert.IsFalse(string.IsNullOrWhiteSpace(description), $"{style} is missing a description.");
}
}
[TestMethod]
public void Title_ForEveryStep_IsNonEmpty()
{
foreach (ShortWizardStep step in Enum.GetValues<ShortWizardStep>())
{
// Act
string title = ShortWizard.Title(step);
// Assert
Assert.IsFalse(string.IsNullOrWhiteSpace(title), $"{step} is missing a title.");
}
}
}
@@ -186,6 +186,74 @@ public class ShortsPipelineTests
ShortsPipeline.Validate(settings);
}
[TestMethod]
public void Validate_VideoStyleWithoutBackgroundVideo_ThrowsFileNotFoundException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
ShortSettings settings = ValidSettings(input, font, ShortStyle.Video);
// Act & Assert
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void Validate_VideoStyleWithBackgroundVideo_DoesNotThrow()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
string background = Path.Combine(directory, "bg.mp4");
File.WriteAllText(background, "data");
ShortSettings settings = ValidSettings(input, font, ShortStyle.Video) with { BackgroundVideo = background };
// Act & Assert (no exception)
ShortsPipeline.Validate(settings);
}
[TestMethod]
public void Validate_VideoOpacityAboveOne_ThrowsArgumentException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
string background = Path.Combine(directory, "bg.mp4");
File.WriteAllText(background, "data");
ShortSettings settings = ValidSettings(input, font, ShortStyle.Video) with
{
BackgroundVideo = background,
VideoOpacity = 1.2,
};
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void Validate_SourceMarginTooWide_ThrowsArgumentException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
ShortSettings settings = ValidSettings(input, font) with { SourceMarginX = 540 };
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void GetBaseFilter_ForEveryStyle_ReturnsAFilterEndingInBaseTag()
{
@@ -220,4 +288,105 @@ public class ShortsPipelineTests
StringAssert.Contains(result, "enable='gte(t,26.5)'");
StringAssert.Contains(result, "[vout]");
}
[TestMethod]
public void GetBaseFilter_VideoStyle_UsesBackgroundVideoInput()
{
// Act
string filter = ShortsPipeline.GetBaseFilter(ShortStyle.Video);
// Assert
StringAssert.Contains(filter, "[1:v]");
StringAssert.Contains(filter, "colorchannelmixer=aa=1");
StringAssert.Contains(filter, "[base]");
Assert.IsFalse(filter.Contains("gblur=", StringComparison.Ordinal));
}
[TestMethod]
public void BuildVideoBaseFilter_WithBlur_InsertsGblur()
{
// Act
string filter = ShortsPipeline.BuildVideoBaseFilter(
"crop=iw:ih:0:0", "scale=1000:-2:flags=lanczos", blur: 8, opacity: 0.65);
// Assert
StringAssert.Contains(filter, "gblur=sigma=8");
StringAssert.Contains(filter, "colorchannelmixer=aa=0.65");
StringAssert.Contains(filter, "crop=iw:ih:0:0,scale=1000:-2:flags=lanczos");
}
[TestMethod]
public void ApplySourceFade_WhenFadeIsZero_ReturnsOriginalFilter()
{
// Arrange
const string baseFilter = "[0:v]split=2[bg][fg]";
// Act
string result = ShortsPipeline.ApplySourceFade(baseFilter, 0);
// Assert
Assert.AreEqual(baseFilter, result);
}
[TestMethod]
public void ApplySourceFade_WhenFadeIsPositive_RewritesSourceLabel()
{
// Act
string result = ShortsPipeline.ApplySourceFade("[0:v]split=2[bg][fg];[0:a]anull[a]", 1.5);
// Assert
StringAssert.StartsWith(result, "[0:v]format=rgba,fade=t=in:st=0:d=1.5:alpha=1[srcfade];");
StringAssert.Contains(result, "[srcfade]split=2[bg][fg]");
StringAssert.Contains(result, "[0:a]anull[a]");
Assert.IsFalse(result.Contains("[0:v]split", StringComparison.Ordinal));
}
[TestMethod]
public void SelectMostCommonCropSpec_PicksTheMostFrequentSpec()
{
// Arrange
const string output =
"crop=800:480:100:0\n" +
"crop=780:480:120:0\n" +
"crop=780:480:120:0\n" +
"crop=760:480:140:0";
// Act
string? spec = ShortsPipeline.SelectMostCommonCropSpec(output);
// Assert
Assert.AreEqual("crop=780:480:120:0", spec);
}
[TestMethod]
public void TryBuildAutoHorizontalCrop_AppliesEvenSafetyInset()
{
// Act
bool ok = ShortsPipeline.TryBuildAutoHorizontalCrop(800, 100, out int width, out int x);
// Assert
Assert.IsTrue(ok);
Assert.AreEqual(792, width);
Assert.AreEqual(104, x);
}
[TestMethod]
public void BuildManualCropFilter_UsesEdgeInsets()
{
// Act
string filter = ShortsPipeline.BuildManualCropFilter(120, 80, 10, 20);
// Assert
Assert.AreEqual("crop=iw-120-80:ih-10-20:120:10", filter);
}
[TestMethod]
public void SourceTargetWidth_SubtractsBothMargins()
{
// Act
int width = ShortsPipeline.SourceTargetWidth(40);
// Assert
Assert.AreEqual(1000, width);
}
}