feat(render+templates): Remotion engine, 16 branded templates (incl. 3D), seconds pricing, coming-soon
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 20s

Render engine
- Add Remotion (code-based) as a 2nd render engine alongside After Effects.
  node-agent dispatches on Job.Engine; RunRemotion maps bindings -> --props,
  renders native then ffmpeg-scales to the quality tier (aspect-preserving).
- content.projects.render_engine + render_remotion_comp (migration 32);
  render-svc claim resolves engine and routes (skips .aep for Remotion).
- Admin TemplatesAdmin gains an engine picker + Remotion composition id field.

Template pack (services/remotion)
- 16 branded, Persian (Vazirmatn), color- and text-editable templates, each in
  3 aspects (16:9 / 1:1 / 9:16): LogoMotion, Opener, InstaPromo, YouTubeIntro,
  Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown,
  GlitterReveal (editable logo image), NowruzGreeting (animated characters),
  and 4 cinematic 3D templates via @remotion/three (Hero3D, Nowruz3D,
  Birthday3D, Promo3D) with reflections + bloom/DOF/vignette.
- scripts/seed_remotion_templates.py seeds containers/projects/scenes/colors.

Pricing
- Rewrite /pricing to the seconds-based model (charge = length x resolution),
  data-driven from /v1/plans, Toman, broker checkout.

Coming-soon
- Persian experimental-build overlay on all pages (launch date + countdown).

Fixes
- middleware matcher bypasses all static asset paths; catalog mapping passes
  cover image + preview video so real thumbnails render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-21 15:52:52 +03:30
parent b9b91397b0
commit 4f04f6bf75
137 changed files with 8942 additions and 135 deletions
@@ -176,6 +176,8 @@ public class TemplateService(ContentDbContext db)
ProjectDurationSec = req.ProjectDurationSec, MinDurationSec = req.MinDurationSec,
MaxDurationSec = req.MaxDurationSec, FreeFps = req.FreeFps, ChooseMode = chooseMode,
Resolution = resolution, VipFactor = req.VipFactor, RenderAepComp = req.RenderAepComp,
RenderEngine = string.IsNullOrWhiteSpace(req.RenderEngine) ? "AfterEffects" : req.RenderEngine,
RenderRemotionComp = req.RenderRemotionComp,
IsPublished = req.IsPublished, Sort = req.Sort
};
@@ -225,6 +227,7 @@ public class TemplateService(ContentDbContext db)
ProjectDurationSec = src.ProjectDurationSec, MinDurationSec = src.MinDurationSec,
MaxDurationSec = src.MaxDurationSec, FreeFps = src.FreeFps, ChooseMode = src.ChooseMode,
Resolution = resolution, VipFactor = src.VipFactor, RenderAepComp = src.RenderAepComp,
RenderEngine = src.RenderEngine, RenderRemotionComp = src.RenderRemotionComp,
SharedLayerImage = src.SharedLayerImage, SharedColorsSvg = src.SharedColorsSvg,
SharedColorPresetsSvg = src.SharedColorPresetsSvg,
IsPublished = false, Sort = src.Sort,
@@ -359,6 +362,8 @@ public class TemplateService(ContentDbContext db)
project.MinDurationSec = req.MinDurationSec; project.MaxDurationSec = req.MaxDurationSec;
project.FreeFps = req.FreeFps; project.ChooseMode = chooseMode; project.Resolution = resolution;
project.VipFactor = req.VipFactor; project.RenderAepComp = req.RenderAepComp;
if (!string.IsNullOrWhiteSpace(req.RenderEngine)) project.RenderEngine = req.RenderEngine;
if (req.RenderRemotionComp != null) project.RenderRemotionComp = req.RenderRemotionComp;
project.SharedLayerImage = req.SharedLayerImage; project.SharedColorsSvg = req.SharedColorsSvg;
project.SharedColorPresetsSvg = req.SharedColorPresetsSvg;
project.IsPublished = req.IsPublished; project.Sort = req.Sort;
@@ -402,6 +407,8 @@ public class TemplateService(ContentDbContext db)
if (req.Image != null) project.Image = req.Image;
if (req.FullDemo != null) project.FullDemo = req.FullDemo;
if (req.SharedColorsSvg != null) project.SharedColorsSvg = req.SharedColorsSvg;
if (!string.IsNullOrWhiteSpace(req.RenderEngine)) project.RenderEngine = req.RenderEngine;
if (req.RenderRemotionComp != null) project.RenderRemotionComp = req.RenderRemotionComp;
project.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
@@ -472,7 +479,8 @@ public class TemplateService(ContentDbContext db)
p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec,
p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(),
p.IsPublished, p.Sort,
p.AepFileUrl, p.AepFileSizeBytes, p.RenderAepComp
p.AepFileUrl, p.AepFileSizeBytes, p.RenderAepComp,
p.RenderEngine, p.RenderRemotionComp
);
/// <summary>Browse/search all projects (template items) across containers.</summary>
@@ -489,7 +497,8 @@ public class TemplateService(ContentDbContext db)
.Skip((page - 1) * pageSize).Take(pageSize)
.Select(p => new ProjectListItemResponse(
p.Id, p.ContainerId, p.Container.Name, p.Container.Slug, p.Name, p.Image,
p.Aspect, p.Resolution.ToString(), p.AepFileUrl, p.RenderAepComp, p.IsPublished, p.Sort))
p.Aspect, p.Resolution.ToString(), p.AepFileUrl, p.RenderAepComp, p.IsPublished, p.Sort,
p.RenderEngine, p.RenderRemotionComp))
.ToListAsync();
return new PagedResponse<ProjectListItemResponse>(items,
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize)));
@@ -529,6 +538,8 @@ public class TemplateService(ContentDbContext db)
if (req.AepFileMd5 != null) project.AepFileMd5 = req.AepFileMd5;
if (req.AepFileSizeBytes.HasValue) project.AepFileSizeBytes = req.AepFileSizeBytes;
if (!string.IsNullOrWhiteSpace(req.RenderAepComp)) project.RenderAepComp = req.RenderAepComp;
if (!string.IsNullOrWhiteSpace(req.RenderEngine)) project.RenderEngine = req.RenderEngine;
if (req.RenderRemotionComp != null) project.RenderRemotionComp = req.RenderRemotionComp;
if (req.Folder != null) project.Folder = req.Folder;
project.AepUploadedAt = DateTime.UtcNow;
project.UpdatedAt = DateTime.UtcNow;
@@ -541,6 +552,7 @@ public class TemplateService(ContentDbContext db)
p.OriginalWidth, p.OriginalHeight, p.Aspect,
p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec,
p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(), p.VipFactor, p.RenderAepComp,
p.RenderEngine, p.RenderRemotionComp,
p.SharedLayerImage, p.IsPublished, p.Sort,
p.Scenes.Select(MapScene).ToList(),
p.SharedColors.Select(sc => new SharedColorResponse(sc.Id, sc.ElementKey, sc.Title, sc.Icon, sc.AttrValue.ToString(), sc.DefaultColor, sc.Sort)).ToList(),
@@ -95,6 +95,11 @@ public class Project
public decimal VipFactor { get; set; } = 1.0m;
public string RenderAepComp { get; set; } = "flatrender";
/// <summary>Render engine for this template: "AfterEffects" (default) or "Remotion".</summary>
public string RenderEngine { get; set; } = "AfterEffects";
/// <summary>For Remotion templates, the composition id to render (e.g. "KineticQuote").</summary>
public string? RenderRemotionComp { get; set; }
public string? SharedLayerImage { get; set; }
public string? SharedColorsSvg { get; set; }
public string? SharedColorPresetsSvg { get; set; }
@@ -307,6 +307,8 @@ public class ContentDbContext(DbContextOptions<ContentDbContext> options) : DbCo
e.Property(x => x.Resolution).HasColumnName("resolution");
e.Property(x => x.VipFactor).HasColumnName("vip_factor");
e.Property(x => x.RenderAepComp).HasColumnName("render_aep_comp");
e.Property(x => x.RenderEngine).HasColumnName("render_engine");
e.Property(x => x.RenderRemotionComp).HasColumnName("render_remotion_comp");
e.Property(x => x.SharedLayerImage).HasColumnName("shared_layer_image");
e.Property(x => x.SharedColorsSvg).HasColumnName("shared_colors_svg");
e.Property(x => x.SharedColorPresetsSvg).HasColumnName("shared_color_presets_svg");
@@ -213,7 +213,9 @@ public record CreateProjectRequest(
decimal VipFactor,
string RenderAepComp,
bool IsPublished,
int Sort
int Sort,
string? RenderEngine = null,
string? RenderRemotionComp = null
);
public record UpdateProjectRequest(
@@ -239,7 +241,9 @@ public record UpdateProjectRequest(
string? SharedColorsSvg,
string? SharedColorPresetsSvg,
bool IsPublished,
int Sort
int Sort,
string? RenderEngine = null,
string? RenderRemotionComp = null
);
// Partial update — only non-null fields are applied, so editing an aspect/resolution
@@ -260,7 +264,9 @@ public record SetAepRequest(
string? AepFileMd5,
long? AepFileSizeBytes,
string? RenderAepComp,
string? Folder
string? Folder,
string? RenderEngine = null,
string? RenderRemotionComp = null
);
public record PatchProjectRequest(
@@ -279,7 +285,9 @@ public record PatchProjectRequest(
int? Sort,
string? Image,
string? FullDemo,
string? SharedColorsSvg
string? SharedColorsSvg,
string? RenderEngine = null,
string? RenderRemotionComp = null
);
// ── CMS ──────────────────────────────────────────────────────────────────────
@@ -131,7 +131,9 @@ public record ProjectResponse(
int Sort,
string? AepFileUrl,
long? AepFileSizeBytes,
string RenderAepComp
string RenderAepComp,
string RenderEngine,
string? RenderRemotionComp
);
public record ProjectListItemResponse(
@@ -146,7 +148,9 @@ public record ProjectListItemResponse(
string? AepFileUrl,
string RenderAepComp,
bool IsPublished,
int Sort
int Sort,
string RenderEngine,
string? RenderRemotionComp
);
public record ProjectAssetResponse(Guid Id, Guid ProjectId, string Name, string Kind, string Url, long? SizeBytes, int Sort);
@@ -171,6 +175,8 @@ public record ProjectDetailResponse(
string Resolution,
decimal VipFactor,
string RenderAepComp,
string RenderEngine,
string? RenderRemotionComp,
string? SharedLayerImage,
bool IsPublished,
int Sort,