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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user