feat: AI Brand Voice Translator integration and Mesh Content fix

This commit is contained in:
2026-06-03 23:56:58 -05:00
parent ad8622e243
commit 0676e9f0a8
40 changed files with 3186 additions and 1481 deletions
+17 -91
View File
@@ -14,15 +14,19 @@ function resolveBrandValue(
): string {
if (userValue && userValue.trim()) return userValue;
// Resolve from brand asset ID (e.g. a logo badge piece)
if (field.brandAssetId && brandContent) {
const asset = brandContent.find(a => a.id === field.brandAssetId);
if (asset) {
// For images, return the thumbnail/image URL
if (asset.content.imageUrl) return asset.content.imageUrl;
if (asset.thumbnail) return asset.thumbnail;
// For text cards, return the text
if (asset.content.text) return asset.content.text;
// Resolve from brand asset ID (e.g. a logo badge piece or generic brand asset)
if (field.brandAssetId) {
if (designMD.brandAssets) {
const asset = designMD.brandAssets.find(a => a.id === field.brandAssetId);
if (asset) return asset.url;
}
if (brandContent) {
const asset = brandContent.find(a => a.id === field.brandAssetId);
if (asset) {
if (asset.content.imageUrl) return asset.content.imageUrl;
if (asset.thumbnail) return asset.thumbnail;
if (asset.content.text) return asset.content.text;
}
}
}
@@ -32,8 +36,6 @@ function resolveBrandValue(
case 'brand-name': return company?.name || designMD.brandName || 'Tu Marca';
case 'tagline': return company?.tagline || '';
case 'logo': return designMD.logoUrl || '';
case 'intro-video': return designMD.introVideoUrl || '';
case 'outro-video': return designMD.outroVideoUrl || '';
case 'primary-color': return designMD.primaryColor;
case 'secondary-color': return designMD.secondaryColor;
// Social handles
@@ -90,20 +92,6 @@ export function getTemplateDuration(
designMD?: DesignMD,
): number {
return template.scenes.reduce((sum, scene) => {
// If it's a brand segment, read duration from designMD instead of template
if (scene.segmentSource === 'brand' && designMD) {
if (scene.type === 'intro') {
if (!designMD.introVideoUrl) return sum; // Skipped
const frames = designMD.introDurationFrames || (scene.durationSeconds * 30);
return sum + (frames / 30);
}
if (scene.type === 'outro') {
if (!designMD.outroVideoUrl) return sum; // Skipped
const frames = designMD.outroDurationFrames || (scene.durationSeconds * 30);
return sum + (frames / 30);
}
}
// If we know the actual video duration for this scene, use it
if (videoDurations && videoDurations[scene.id]) {
return sum + videoDurations[scene.id];
@@ -140,18 +128,8 @@ export function compileExpressToTimeline(
// Default to template's duration
let sceneDuration = scene.durationSeconds;
// Override if brand segment
if (scene.segmentSource === 'brand' && designMD) {
if (scene.type === 'intro') {
if (!designMD.introVideoUrl) { sceneDuration = 0; }
else { sceneDuration = (designMD.introDurationFrames || (scene.durationSeconds * 30)) / 30; }
}
if (scene.type === 'outro') {
if (!designMD.outroVideoUrl) { sceneDuration = 0; }
else { sceneDuration = (designMD.outroDurationFrames || (scene.durationSeconds * 30)) / 30; }
}
} else if (videoDurations && videoDurations[scene.id]) {
// Override if user uploaded video
// Override if user uploaded video
if (videoDurations && videoDurations[scene.id]) {
sceneDuration = videoDurations[scene.id];
}
@@ -163,50 +141,7 @@ export function compileExpressToTimeline(
if ((scene.type === 'intro' || scene.type === 'outro') && scene.segmentSource) {
const isIntro = scene.type === 'intro';
if (scene.segmentSource === 'brand') {
// Resolve video from DesignMD
const videoUrl = isIntro ? designMD.introVideoUrl : designMD.outroVideoUrl;
if (!videoUrl) {
// Brand has no video for this segment — skip entirely
// Don't advance frameOffset so it doesn't create a gap
continue;
}
// Convert from center-based coords (SegmentVideoFrame) to top-left coords (CompositionElement)
const segW = scene.segmentVideoW ?? 100;
const segH = scene.segmentVideoH ?? 100;
const segX = (scene.segmentVideoX ?? 50) - segW / 2;
const segY = (scene.segmentVideoY ?? 50) - segH / 2;
elements.push({
id: `express-segment-${scene.id}`,
type: 'video',
content: videoUrl,
x: segX,
y: segY,
startFrame: sceneStart,
endFrame: sceneEnd,
width: segW,
height: segH,
// CompositionElement's isFullscreenBrand path reads w/h (not width/height)
w: segW,
h: segH,
objectFit: (isIntro
? (designMD.introVideoFit || 'cover')
: (designMD.outroVideoFit || 'cover')) as 'cover' | 'contain' | 'fill',
containBgColor: isIntro ? designMD.introVideoBgColor : designMD.outroVideoBgColor,
layerId: 'layer-express-bg',
isBrandElement: true,
isLocked: true,
elementName: isIntro ? 'Intro de marca' : 'Outro de marca',
scale: 1,
rotation: 0,
opacity: 100,
transitionIn: scene.segmentTransition
? { type: scene.segmentTransition.type as TransitionType, duration: scene.segmentTransition.duration }
: undefined,
});
} else if (scene.segmentSource === 'form') {
if (scene.segmentSource === 'form') {
// Form-sourced: look up the uploaded video from fieldData
const segmentFieldId = `segment-${scene.id}`;
const videoUrl = fieldData[segmentFieldId] || '';
@@ -380,16 +315,7 @@ export function compileExpressToTimeline(
...(field.type === 'image' || field.type === 'video' ? {
width: position.w,
height: position.h,
objectFit: ((field.nature === 'brand-variable' && field.brandSource === 'intro-video')
? (designMD.introVideoFit || field.style.mediaFit || 'cover')
: (field.nature === 'brand-variable' && field.brandSource === 'outro-video')
? (designMD.outroVideoFit || field.style.mediaFit || 'cover')
: (field.style.mediaFit || 'cover')) as 'cover' | 'contain' | 'fill',
containBgColor: (field.nature === 'brand-variable' && field.brandSource === 'intro-video')
? designMD.introVideoBgColor
: (field.nature === 'brand-variable' && field.brandSource === 'outro-video')
? designMD.outroVideoBgColor
: undefined,
objectFit: (field.style.mediaFit || 'cover') as 'cover' | 'contain' | 'fill',
} : {}),
...(field.type === 'shape' ? {
width: position.w,