@productmaker/mcp 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/http.js +858 -460
- package/dist/stdio.js +858 -460
- package/package.json +3 -2
package/dist/http.js
CHANGED
|
@@ -153,7 +153,7 @@ var ApiClient = class {
|
|
|
153
153
|
};
|
|
154
154
|
|
|
155
155
|
// src/index.ts
|
|
156
|
-
var MCP_VERSION = "2.
|
|
156
|
+
var MCP_VERSION = "2.3.1";
|
|
157
157
|
|
|
158
158
|
// src/tools/tasks.ts
|
|
159
159
|
import { z as z40 } from "zod";
|
|
@@ -935,7 +935,7 @@ var Product = z4.object({
|
|
|
935
935
|
});
|
|
936
936
|
|
|
937
937
|
// ../../packages/types/src/ingest.ts
|
|
938
|
-
import { z as
|
|
938
|
+
import { z as z14 } from "zod";
|
|
939
939
|
|
|
940
940
|
// ../../packages/types/src/video-tier.ts
|
|
941
941
|
import { z as z5 } from "zod";
|
|
@@ -947,18 +947,204 @@ var VideoTier = z5.preprocess((v) => v === "grok" ? "single_call" : v, z5.enum([
|
|
|
947
947
|
]));
|
|
948
948
|
var DEFAULT_VIDEO_TIER = "single_call";
|
|
949
949
|
|
|
950
|
+
// ../../packages/types/src/angle.ts
|
|
951
|
+
import { z as z6 } from "zod";
|
|
952
|
+
var ImageWinnerSignal = z6.enum([
|
|
953
|
+
"winner",
|
|
954
|
+
"opportunity",
|
|
955
|
+
"saturated_twist"
|
|
956
|
+
]);
|
|
957
|
+
var WinnerVisualMode = z6.enum([
|
|
958
|
+
"auto",
|
|
959
|
+
"inspired",
|
|
960
|
+
"original"
|
|
961
|
+
]);
|
|
962
|
+
var AnchoredIntel = z6.object({
|
|
963
|
+
painPointIndices: z6.array(z6.number().int().nonnegative()).max(10),
|
|
964
|
+
hookIndices: z6.array(z6.number().int().nonnegative()).max(10),
|
|
965
|
+
claimIndices: z6.array(z6.number().int().nonnegative()).max(10),
|
|
966
|
+
sourceAngleType: z6.enum([
|
|
967
|
+
"gap",
|
|
968
|
+
"validated_local",
|
|
969
|
+
"saturated_differentiated",
|
|
970
|
+
"novel"
|
|
971
|
+
]),
|
|
972
|
+
// SNAPSHOT of the resolved intel strings at angle-generation time.
|
|
973
|
+
// Indices reference into Task.competitorIntel which we treat as immutable
|
|
974
|
+
// post-scan, but defensive: a future re-scan path that rewrites
|
|
975
|
+
// Task.competitorIntel would invalidate the indices silently. Persisting
|
|
976
|
+
// the resolved strings ALONGSIDE the indices means downstream consumers
|
|
977
|
+
// (script writer, landing copy) can safely use the strings directly
|
|
978
|
+
// without re-resolving — staleness-immune. The indices remain useful for
|
|
979
|
+
// analytics ("which intel items did angles cite?") and as a sanity-check.
|
|
980
|
+
resolvedPainPoints: z6.array(z6.string()).max(10).optional(),
|
|
981
|
+
resolvedHooks: z6.array(z6.string()).max(10).optional(),
|
|
982
|
+
resolvedClaims: z6.array(z6.string()).max(10).optional(),
|
|
983
|
+
/**
|
|
984
|
+
* Free-form pillar name describing the AXIS of the product this angle
|
|
985
|
+
* attacks (e.g. "ciencia/mecanismo", "tiempo/ritual", "comparación-con-
|
|
986
|
+
* alternativas", "social-proof", "dolor-emocional", "identidad/persona").
|
|
987
|
+
* The LLM picks the label — we do NOT lock an enum because the right
|
|
988
|
+
* pillar taxonomy varies per product (a supplement's "mecanismo" pillar
|
|
989
|
+
* is different from a gadget's "fricción de uso"). Constraint enforced
|
|
990
|
+
* at generation time: the 3 angles emitted for one task MUST use 3
|
|
991
|
+
* DISTINCT pillars (anti-convergence). Lives inside AnchoredIntel because
|
|
992
|
+
* it ships as analytics metadata alongside the anchoredIntel sourceType +
|
|
993
|
+
* indices, and persists as JSON without a column migration. Optional
|
|
994
|
+
* because angles persisted before this change have no pillar.
|
|
995
|
+
*/
|
|
996
|
+
pillar: z6.string().min(1).max(60).optional(),
|
|
997
|
+
// Telemetry: why the angle did NOT import a gapAngle. The legacy single value
|
|
998
|
+
// 'none_relevant' was OVERLOADED (it meant "no gaps existed" AND "gaps existed
|
|
999
|
+
// but none fit" AND was the sanitizer default) which made the gap feature
|
|
1000
|
+
// unmeasurable. Split into distinct, one-meaning-each states:
|
|
1001
|
+
// - persona_mismatch / product_mismatch: the LLM saw gaps and judged them a
|
|
1002
|
+
// wrong-persona / wrong-subcategory fit (trusted from the LLM).
|
|
1003
|
+
// - no_gaps_in_block: there were no gapAngles in the block (or no block).
|
|
1004
|
+
// - gaps_present_not_imported: gaps existed and the angle went novel anyway
|
|
1005
|
+
// (the sanitizer's honest default when the LLM didn't pick persona/product).
|
|
1006
|
+
// - gaps_all_filtered_noise: the reference HAD gaps but every one was dropped
|
|
1007
|
+
// as detection noise (retailer-meta/cross-category) so the LLM saw none.
|
|
1008
|
+
// - n_a: the angle imported a gap (sourceAngleType="gap"), so "why skipped"
|
|
1009
|
+
// is moot.
|
|
1010
|
+
// The deterministic gap-presence states (no_gaps_in_block / gaps_present_not_
|
|
1011
|
+
// imported / gaps_all_filtered_noise) are set by sanitizeAnchoredIntel from
|
|
1012
|
+
// facts it computes (intel.gapAngles.length, intel.gapAnglesFilteredCount), NOT
|
|
1013
|
+
// trusted from the lazy LLM. z.preprocess aliases historical 'none_relevant'
|
|
1014
|
+
// rows → 'no_gaps_in_block' so the JSON column parses with no migration.
|
|
1015
|
+
// Analytics-only: NO runtime path branches on this value.
|
|
1016
|
+
skippedGapReason: z6.preprocess((v) => v === "none_relevant" ? "no_gaps_in_block" : v, z6.enum([
|
|
1017
|
+
"persona_mismatch",
|
|
1018
|
+
"product_mismatch",
|
|
1019
|
+
"no_gaps_in_block",
|
|
1020
|
+
"gaps_present_not_imported",
|
|
1021
|
+
"gaps_all_filtered_noise",
|
|
1022
|
+
"n_a"
|
|
1023
|
+
])).optional(),
|
|
1024
|
+
// Telemetry: captures the prod-observed case where a "novel" angle actually
|
|
1025
|
+
// reflects a GAP's theme/promise without formally anchoring indices (gapAngles
|
|
1026
|
+
// are a separate array, not index-cited, so a real gap import is reclassified
|
|
1027
|
+
// 'novel' by the empty-indices coherence guard). Self-reported by the LLM:
|
|
1028
|
+
// - gap_imported: the angle's theme imports a gap's logic.
|
|
1029
|
+
// - product_derived: derived purely from the product.
|
|
1030
|
+
// - hybrid: a mix.
|
|
1031
|
+
// SOFT analytics-only signal (the LLM may confabulate) — validated against
|
|
1032
|
+
// known prod cases before trusting aggregates; never a control-flow input.
|
|
1033
|
+
// Optional in storage; on the wire it ships as a required enum with a 'none'
|
|
1034
|
+
// sentinel (strict structured output forbids optional properties) that the
|
|
1035
|
+
// sanitizer maps to undefined.
|
|
1036
|
+
themeSourceType: z6.enum([
|
|
1037
|
+
"gap_imported",
|
|
1038
|
+
"product_derived",
|
|
1039
|
+
"hybrid"
|
|
1040
|
+
]).optional()
|
|
1041
|
+
});
|
|
1042
|
+
var Angle = z6.object({
|
|
1043
|
+
angle: z6.string().min(1).max(480),
|
|
1044
|
+
buyerPersona: z6.string().min(1).max(360),
|
|
1045
|
+
/** Character description for video (role + appearance + attitude). Drives voice selection and Frame0Spec. */
|
|
1046
|
+
actor: z6.string().min(1).max(360).optional(),
|
|
1047
|
+
// Binary only: voice catalog has no non-binary voices.
|
|
1048
|
+
// See packages/angles/src/index.ts + getAngles coercion in angle.repository.ts.
|
|
1049
|
+
gender: z6.enum([
|
|
1050
|
+
"female",
|
|
1051
|
+
"male"
|
|
1052
|
+
]).optional(),
|
|
1053
|
+
/** Per-angle user-supplied script/visual notes. Hydrated from the
|
|
1054
|
+
* `Angle.scriptNotes` DB column or the `angleVideoOptions[i].scriptNotes`
|
|
1055
|
+
* passed from the chat. Per-angle overrides global `videoScriptNotes`. */
|
|
1056
|
+
scriptNotes: z6.string().max(2e3).optional(),
|
|
1057
|
+
/** AI-chosen per-angle narrative (set by the batch selector in angles.worker).
|
|
1058
|
+
* Pre-fills the review dropdown; user can override per angle. */
|
|
1059
|
+
narrativeStyle: NarrativeStyle.optional(),
|
|
1060
|
+
/** See AnchoredIntel docs. Optional because angles persisted before this
|
|
1061
|
+
* change have no anchoring captured; new angles ALWAYS emit it (with empty
|
|
1062
|
+
* arrays + 'novel' when intel was unavailable). */
|
|
1063
|
+
anchoredIntel: AnchoredIntel.optional(),
|
|
1064
|
+
/** Server-computed: true when this angle is eligible for the gap_replication
|
|
1065
|
+
* narrative (isReplicationAngle(anchoredIntel, intel) && intel.gapBlueprint
|
|
1066
|
+
* present). Computed server-side (not derived client-side) because the web
|
|
1067
|
+
* only sees this angle's anchoredIntel, not the task's
|
|
1068
|
+
* competitorIntel.gapBlueprint. Gates the review dropdown's gap_replication
|
|
1069
|
+
* option — absent/false = hidden. */
|
|
1070
|
+
gapReplicationEligible: z6.boolean().optional(),
|
|
1071
|
+
/** Server-computed: true when this angle's effective narrative is
|
|
1072
|
+
* gap_replication, which FORCES the multi-shot STANDARD ("Multiescena")
|
|
1073
|
+
* render pipeline regardless of the task's chosen tier (single_call can't
|
|
1074
|
+
* produce the multi-shot replication — see task-results.service.ts and the
|
|
1075
|
+
* render-time tier override). Surfaces "renders in Multiescena" on the
|
|
1076
|
+
* review card so a single_call user isn't surprised by the STANDARD-tier
|
|
1077
|
+
* charge. */
|
|
1078
|
+
rendersMultiShot: z6.boolean().optional(),
|
|
1079
|
+
/** Server-computed product-level origin bucket for the review card's badge
|
|
1080
|
+
* (NOT the internal sourceAngleType taxonomy — see resolveAngleOrigin in
|
|
1081
|
+
* task-results.service.ts): 'gap' = market opportunity competitors aren't
|
|
1082
|
+
* using; 'saturated_twist' = competitors' most-used approach with our own
|
|
1083
|
+
* twist; 'ai_recommended' = AI-authored for this product (default). */
|
|
1084
|
+
angleOrigin: z6.enum([
|
|
1085
|
+
"gap",
|
|
1086
|
+
"saturated_twist",
|
|
1087
|
+
"ai_recommended"
|
|
1088
|
+
]).optional(),
|
|
1089
|
+
/** Server-computed badge TIER for the replication angle (two-tier UX,
|
|
1090
|
+
* v1.32.0 — see resolveReplicationTier). 'winner' = a validated angle
|
|
1091
|
+
* opportunity backed by a proven ad (anchor gap/saturated → golden badge);
|
|
1092
|
+
* 'opportunity' = a product-coherent replicable winner with no validated
|
|
1093
|
+
* angle gap (anchor 'dominant' → opportunity/twist badge, no overclaim).
|
|
1094
|
+
* Present only on the angle that carries gap_replication; absent otherwise. */
|
|
1095
|
+
replicationTier: z6.enum([
|
|
1096
|
+
"winner",
|
|
1097
|
+
"opportunity"
|
|
1098
|
+
]).optional(),
|
|
1099
|
+
/** Server-confirmed availability of a coherent, captured IMAGE winner for
|
|
1100
|
+
* this task. The review UI must not claim "anuncio ganador" from the user's
|
|
1101
|
+
* mode choice alone: fail-open scans may legitimately have no winner. */
|
|
1102
|
+
imageWinnerAvailable: z6.boolean().optional(),
|
|
1103
|
+
/** True when `winnerVisualMode=auto` will actually attach the image reference.
|
|
1104
|
+
* A single-image request requires `imageWinnerSignal="winner"`; a multi-image
|
|
1105
|
+
* portfolio may spend one experimental slot on opportunity/twist. Explicit
|
|
1106
|
+
* `inspired` may also apply it; `original` always suppresses it. */
|
|
1107
|
+
imageWinnerAutoApplied: z6.boolean().optional(),
|
|
1108
|
+
/** Server-authored evidence tier for the image reference. The UI renders
|
|
1109
|
+
* this on the inspired control and the selected strategy badge. */
|
|
1110
|
+
imageWinnerSignal: ImageWinnerSignal.optional()
|
|
1111
|
+
});
|
|
1112
|
+
var AdVideo = z6.object({
|
|
1113
|
+
source: z6.enum([
|
|
1114
|
+
"tiktok_shop",
|
|
1115
|
+
"meta_ads"
|
|
1116
|
+
]),
|
|
1117
|
+
url: z6.string().url(),
|
|
1118
|
+
title: z6.string().optional(),
|
|
1119
|
+
description: z6.string().optional(),
|
|
1120
|
+
metrics: z6.object({
|
|
1121
|
+
impressions: z6.number().int().nonnegative().optional(),
|
|
1122
|
+
views: z6.number().int().nonnegative().optional(),
|
|
1123
|
+
likes: z6.number().int().nonnegative().optional(),
|
|
1124
|
+
ctaClicks: z6.number().int().nonnegative().optional(),
|
|
1125
|
+
diggCount: z6.number().int().nonnegative().optional(),
|
|
1126
|
+
shareCount: z6.number().int().nonnegative().optional(),
|
|
1127
|
+
commentCount: z6.number().int().nonnegative().optional(),
|
|
1128
|
+
playCount: z6.number().int().nonnegative().optional(),
|
|
1129
|
+
collectCount: z6.number().int().nonnegative().optional(),
|
|
1130
|
+
durationSeconds: z6.number().int().nonnegative().optional()
|
|
1131
|
+
}).partial().optional(),
|
|
1132
|
+
country: z6.string().optional(),
|
|
1133
|
+
lastSeen: z6.string().datetime().optional()
|
|
1134
|
+
});
|
|
1135
|
+
|
|
950
1136
|
// ../../packages/types/src/creative.ts
|
|
951
|
-
import { z as
|
|
1137
|
+
import { z as z10 } from "zod";
|
|
952
1138
|
|
|
953
1139
|
// ../../packages/types/src/landing.ts
|
|
954
|
-
import { z as
|
|
1140
|
+
import { z as z9 } from "zod";
|
|
955
1141
|
|
|
956
1142
|
// ../../packages/types/src/landing-strategy.ts
|
|
957
|
-
import { z as
|
|
1143
|
+
import { z as z8 } from "zod";
|
|
958
1144
|
|
|
959
1145
|
// ../../packages/types/src/landing-role.ts
|
|
960
|
-
import { z as
|
|
961
|
-
var LandingRole =
|
|
1146
|
+
import { z as z7 } from "zod";
|
|
1147
|
+
var LandingRole = z7.enum([
|
|
962
1148
|
"hero",
|
|
963
1149
|
"benefits1",
|
|
964
1150
|
"socialproof",
|
|
@@ -970,7 +1156,7 @@ var LandingRole = z6.enum([
|
|
|
970
1156
|
"authority",
|
|
971
1157
|
"logistics"
|
|
972
1158
|
]);
|
|
973
|
-
var LandingRoleLegacy =
|
|
1159
|
+
var LandingRoleLegacy = z7.enum([
|
|
974
1160
|
"hero",
|
|
975
1161
|
"benefits1",
|
|
976
1162
|
"benefits2",
|
|
@@ -996,7 +1182,7 @@ var LANDING_STRATEGIES = [
|
|
|
996
1182
|
"offer_value",
|
|
997
1183
|
"lifestyle_identity"
|
|
998
1184
|
];
|
|
999
|
-
var LandingStrategySchema =
|
|
1185
|
+
var LandingStrategySchema = z8.enum(LANDING_STRATEGIES);
|
|
1000
1186
|
var EffectiveLandingStrategySchema = LandingStrategySchema.exclude([
|
|
1001
1187
|
"auto"
|
|
1002
1188
|
]);
|
|
@@ -1007,8 +1193,8 @@ var SEMANTIC_LANDING_STRATEGIES = [
|
|
|
1007
1193
|
"offer_value",
|
|
1008
1194
|
"lifestyle_identity"
|
|
1009
1195
|
];
|
|
1010
|
-
var SemanticLandingStrategySchema =
|
|
1011
|
-
var LandingVisualStyleFamilySchema =
|
|
1196
|
+
var SemanticLandingStrategySchema = z8.enum(SEMANTIC_LANDING_STRATEGIES);
|
|
1197
|
+
var LandingVisualStyleFamilySchema = z8.enum([
|
|
1012
1198
|
"editorial_dtc",
|
|
1013
1199
|
"ugc_scrapbook",
|
|
1014
1200
|
"technical_diagram",
|
|
@@ -1016,39 +1202,39 @@ var LandingVisualStyleFamilySchema = z7.enum([
|
|
|
1016
1202
|
"warm_organic",
|
|
1017
1203
|
"cinematic_problem_solution"
|
|
1018
1204
|
]);
|
|
1019
|
-
var LandingVisualTypographySchema =
|
|
1205
|
+
var LandingVisualTypographySchema = z8.enum([
|
|
1020
1206
|
"serif_editorial",
|
|
1021
1207
|
"sans_modern",
|
|
1022
1208
|
"display_bold",
|
|
1023
1209
|
"handwritten_warm"
|
|
1024
1210
|
]);
|
|
1025
|
-
var LandingVisualCompositionSchema =
|
|
1211
|
+
var LandingVisualCompositionSchema = z8.enum([
|
|
1026
1212
|
"full_bleed",
|
|
1027
1213
|
"editorial_panels",
|
|
1028
1214
|
"cards_collage",
|
|
1029
1215
|
"diagrammatic",
|
|
1030
1216
|
"commerce_modules"
|
|
1031
1217
|
]);
|
|
1032
|
-
var LandingVisualImageTreatmentSchema =
|
|
1218
|
+
var LandingVisualImageTreatmentSchema = z8.enum([
|
|
1033
1219
|
"studio_editorial",
|
|
1034
1220
|
"phone_ugc",
|
|
1035
1221
|
"documentary_lifestyle",
|
|
1036
1222
|
"technical_macro",
|
|
1037
1223
|
"high_contrast_direct_response"
|
|
1038
1224
|
]);
|
|
1039
|
-
var LandingVisualDensitySchema =
|
|
1225
|
+
var LandingVisualDensitySchema = z8.enum([
|
|
1040
1226
|
"minimal",
|
|
1041
1227
|
"balanced",
|
|
1042
1228
|
"rich"
|
|
1043
1229
|
]);
|
|
1044
|
-
var LandingVisualSignatureSchema =
|
|
1230
|
+
var LandingVisualSignatureSchema = z8.object({
|
|
1045
1231
|
styleFamily: LandingVisualStyleFamilySchema,
|
|
1046
1232
|
typography: LandingVisualTypographySchema,
|
|
1047
1233
|
composition: LandingVisualCompositionSchema,
|
|
1048
1234
|
imageTreatment: LandingVisualImageTreatmentSchema,
|
|
1049
1235
|
density: LandingVisualDensitySchema,
|
|
1050
|
-
paletteIntent:
|
|
1051
|
-
recurringMotif:
|
|
1236
|
+
paletteIntent: z8.string().trim().min(1).max(160),
|
|
1237
|
+
recurringMotif: z8.string().trim().min(1).max(160)
|
|
1052
1238
|
});
|
|
1053
1239
|
var LANDING_SECTION_KEYS = [
|
|
1054
1240
|
"problem_hook",
|
|
@@ -1090,18 +1276,18 @@ var LANDING_SECTION_KEYS = [
|
|
|
1090
1276
|
"lifestyle_fit",
|
|
1091
1277
|
"join_lifestyle"
|
|
1092
1278
|
];
|
|
1093
|
-
var LandingSectionKeySchema =
|
|
1094
|
-
var LandingSectionPlanEntrySchema =
|
|
1279
|
+
var LandingSectionKeySchema = z8.enum(LANDING_SECTION_KEYS);
|
|
1280
|
+
var LandingSectionPlanEntrySchema = z8.object({
|
|
1095
1281
|
/** Stable persistence/regeneration identifier. Never shown to the user. */
|
|
1096
1282
|
role: LandingRole,
|
|
1097
1283
|
/** Strategy-specific section identity shown in UI and used by prompts. */
|
|
1098
1284
|
sectionKey: LandingSectionKeySchema,
|
|
1099
|
-
index:
|
|
1285
|
+
index: z8.number().int().min(1).max(10)
|
|
1100
1286
|
});
|
|
1101
|
-
var LandingSectionPlanSchema =
|
|
1287
|
+
var LandingSectionPlanSchema = z8.array(LandingSectionPlanEntrySchema).superRefine((plan, ctx) => {
|
|
1102
1288
|
if (plan.length !== 7 && plan.length !== 10) {
|
|
1103
1289
|
ctx.addIssue({
|
|
1104
|
-
code:
|
|
1290
|
+
code: z8.ZodIssueCode.custom,
|
|
1105
1291
|
message: "sectionPlan must contain exactly 7 or 10 entries"
|
|
1106
1292
|
});
|
|
1107
1293
|
}
|
|
@@ -1110,13 +1296,13 @@ var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefi
|
|
|
1110
1296
|
const positions = new Set(plan.map((entry) => entry.index));
|
|
1111
1297
|
if (roles.size !== plan.length) {
|
|
1112
1298
|
ctx.addIssue({
|
|
1113
|
-
code:
|
|
1299
|
+
code: z8.ZodIssueCode.custom,
|
|
1114
1300
|
message: "sectionPlan roles must be unique"
|
|
1115
1301
|
});
|
|
1116
1302
|
}
|
|
1117
1303
|
if (keys.size !== plan.length) {
|
|
1118
1304
|
ctx.addIssue({
|
|
1119
|
-
code:
|
|
1305
|
+
code: z8.ZodIssueCode.custom,
|
|
1120
1306
|
message: "sectionPlan sectionKeys must be unique"
|
|
1121
1307
|
});
|
|
1122
1308
|
}
|
|
@@ -1125,48 +1311,50 @@ var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefi
|
|
|
1125
1311
|
}, (_, index) => index + 1);
|
|
1126
1312
|
if (positions.size !== plan.length || !requiredPositions.every((position) => positions.has(position))) {
|
|
1127
1313
|
ctx.addIssue({
|
|
1128
|
-
code:
|
|
1314
|
+
code: z8.ZodIssueCode.custom,
|
|
1129
1315
|
message: "sectionPlan indexes must be contiguous from 1 through plan length"
|
|
1130
1316
|
});
|
|
1131
1317
|
}
|
|
1132
1318
|
});
|
|
1133
1319
|
|
|
1134
1320
|
// ../../packages/types/src/landing.ts
|
|
1135
|
-
var LandingImageReadable =
|
|
1136
|
-
index:
|
|
1321
|
+
var LandingImageReadable = z9.object({
|
|
1322
|
+
index: z9.number().int().min(1).max(20),
|
|
1137
1323
|
/** Child task that owns this image when variants are rendered alongside the root task. */
|
|
1138
|
-
variantTaskId:
|
|
1324
|
+
variantTaskId: z9.string().uuid().optional(),
|
|
1139
1325
|
role: LandingRoleLegacy,
|
|
1140
1326
|
/** Strategy-specific display identity; absent on legacy tasks. */
|
|
1141
1327
|
sectionKey: LandingSectionKeySchema.optional(),
|
|
1142
|
-
url:
|
|
1143
|
-
width:
|
|
1144
|
-
height:
|
|
1145
|
-
notes:
|
|
1328
|
+
url: z9.string().min(1),
|
|
1329
|
+
width: z9.number().int(),
|
|
1330
|
+
height: z9.number().int(),
|
|
1331
|
+
notes: z9.string().optional()
|
|
1146
1332
|
});
|
|
1147
|
-
var LandingImage =
|
|
1148
|
-
index:
|
|
1333
|
+
var LandingImage = z9.object({
|
|
1334
|
+
index: z9.number().int().min(1).max(10),
|
|
1149
1335
|
role: LandingRole,
|
|
1150
1336
|
sectionKey: LandingSectionKeySchema.optional(),
|
|
1151
|
-
url:
|
|
1152
|
-
width:
|
|
1153
|
-
height:
|
|
1154
|
-
notes:
|
|
1337
|
+
url: z9.string().min(1),
|
|
1338
|
+
width: z9.number().int(),
|
|
1339
|
+
height: z9.number().int(),
|
|
1340
|
+
notes: z9.string().optional()
|
|
1155
1341
|
});
|
|
1156
1342
|
|
|
1157
1343
|
// ../../packages/types/src/creative.ts
|
|
1158
|
-
var VideoCreativeAspectRatio =
|
|
1344
|
+
var VideoCreativeAspectRatio = z10.enum([
|
|
1159
1345
|
"16:9",
|
|
1160
1346
|
"9:16",
|
|
1161
1347
|
"Auto"
|
|
1162
1348
|
]);
|
|
1163
|
-
var
|
|
1349
|
+
var PINNED_HEADLINE_MAX_CHARS = 120;
|
|
1350
|
+
var PINNED_SUBHEADLINE_MAX_CHARS = 160;
|
|
1351
|
+
var ImageCreativeStyle = z10.enum([
|
|
1164
1352
|
"studio",
|
|
1165
1353
|
"floating",
|
|
1166
1354
|
"ingredients",
|
|
1167
1355
|
"in_use"
|
|
1168
1356
|
]);
|
|
1169
|
-
var ImageCreativeArchetype =
|
|
1357
|
+
var ImageCreativeArchetype = z10.enum([
|
|
1170
1358
|
"direct_response_product_ad",
|
|
1171
1359
|
"hero_outcome",
|
|
1172
1360
|
"offer_value_prop",
|
|
@@ -1175,215 +1363,228 @@ var ImageCreativeArchetype = z9.enum([
|
|
|
1175
1363
|
"problem_solution",
|
|
1176
1364
|
"lifestyle_identity"
|
|
1177
1365
|
]);
|
|
1178
|
-
var ImageCreativeVisualTreatment =
|
|
1366
|
+
var ImageCreativeVisualTreatment = z10.enum([
|
|
1179
1367
|
"performance_product_ad",
|
|
1180
1368
|
"premium_dtc_carousel"
|
|
1181
1369
|
]);
|
|
1182
|
-
var ImageCreativeAspectRatio =
|
|
1370
|
+
var ImageCreativeAspectRatio = z10.enum([
|
|
1183
1371
|
"1:1",
|
|
1184
1372
|
"4:5",
|
|
1185
1373
|
"9:16",
|
|
1186
1374
|
"16:9"
|
|
1187
1375
|
]);
|
|
1188
|
-
var VideoCreativeVariant =
|
|
1189
|
-
id:
|
|
1190
|
-
assetKey:
|
|
1191
|
-
url:
|
|
1192
|
-
thumbnailUrl:
|
|
1193
|
-
label:
|
|
1194
|
-
format:
|
|
1195
|
-
});
|
|
1196
|
-
var VideoCreativeVideo =
|
|
1197
|
-
provider:
|
|
1198
|
-
model:
|
|
1199
|
-
durationSeconds:
|
|
1200
|
-
variants:
|
|
1201
|
-
clipUrls:
|
|
1202
|
-
adCopy:
|
|
1203
|
-
});
|
|
1204
|
-
var VideoCreativeContext =
|
|
1205
|
-
productTitle:
|
|
1206
|
-
primaryAngle:
|
|
1207
|
-
buyerPersona:
|
|
1208
|
-
brandId:
|
|
1209
|
-
features:
|
|
1210
|
-
scriptNotes:
|
|
1211
|
-
cta:
|
|
1212
|
-
language:
|
|
1376
|
+
var VideoCreativeVariant = z10.object({
|
|
1377
|
+
id: z10.string(),
|
|
1378
|
+
assetKey: z10.string(),
|
|
1379
|
+
url: z10.string().url().optional(),
|
|
1380
|
+
thumbnailUrl: z10.string().url().optional(),
|
|
1381
|
+
label: z10.string().optional(),
|
|
1382
|
+
format: z10.string().optional()
|
|
1383
|
+
});
|
|
1384
|
+
var VideoCreativeVideo = z10.object({
|
|
1385
|
+
provider: z10.string(),
|
|
1386
|
+
model: z10.string(),
|
|
1387
|
+
durationSeconds: z10.number().int().positive().optional(),
|
|
1388
|
+
variants: z10.array(VideoCreativeVariant).default([]),
|
|
1389
|
+
clipUrls: z10.array(z10.string().url()).optional(),
|
|
1390
|
+
adCopy: z10.string().optional()
|
|
1391
|
+
});
|
|
1392
|
+
var VideoCreativeContext = z10.object({
|
|
1393
|
+
productTitle: z10.string(),
|
|
1394
|
+
primaryAngle: z10.string(),
|
|
1395
|
+
buyerPersona: z10.string().optional(),
|
|
1396
|
+
brandId: z10.string().uuid().optional(),
|
|
1397
|
+
features: z10.array(z10.string()).default([]),
|
|
1398
|
+
scriptNotes: z10.string().optional(),
|
|
1399
|
+
cta: z10.string().optional(),
|
|
1400
|
+
language: z10.string(),
|
|
1213
1401
|
aspectRatio: VideoCreativeAspectRatio,
|
|
1214
|
-
durationSeconds:
|
|
1402
|
+
durationSeconds: z10.number().int().positive()
|
|
1215
1403
|
});
|
|
1216
|
-
var VideoCreativeJobResult =
|
|
1404
|
+
var VideoCreativeJobResult = z10.object({
|
|
1217
1405
|
video: VideoCreativeVideo,
|
|
1218
1406
|
context: VideoCreativeContext,
|
|
1219
|
-
createdAt:
|
|
1407
|
+
createdAt: z10.string().datetime().optional()
|
|
1220
1408
|
});
|
|
1221
|
-
var VideoCreativeJobStatusEnum =
|
|
1409
|
+
var VideoCreativeJobStatusEnum = z10.enum([
|
|
1222
1410
|
"pending",
|
|
1223
1411
|
"processing",
|
|
1224
1412
|
"completed",
|
|
1225
1413
|
"error"
|
|
1226
1414
|
]);
|
|
1227
|
-
var VideoCreativeJobStatus =
|
|
1228
|
-
jobId:
|
|
1415
|
+
var VideoCreativeJobStatus = z10.object({
|
|
1416
|
+
jobId: z10.string(),
|
|
1229
1417
|
status: VideoCreativeJobStatusEnum,
|
|
1230
|
-
progress:
|
|
1231
|
-
etaSeconds:
|
|
1232
|
-
message:
|
|
1233
|
-
error:
|
|
1418
|
+
progress: z10.number().min(0).max(100).optional(),
|
|
1419
|
+
etaSeconds: z10.number().int().nonnegative().optional(),
|
|
1420
|
+
message: z10.string().optional(),
|
|
1421
|
+
error: z10.string().optional(),
|
|
1234
1422
|
/**
|
|
1235
1423
|
* Primary angle that the backend will use for generation.
|
|
1236
1424
|
* Can be user-provided (request.primaryAngle) or generated (angles[0]).
|
|
1237
1425
|
*/
|
|
1238
|
-
primaryAngle:
|
|
1239
|
-
primaryAngleSource:
|
|
1426
|
+
primaryAngle: z10.string().optional(),
|
|
1427
|
+
primaryAngleSource: z10.enum([
|
|
1240
1428
|
"user",
|
|
1241
1429
|
"generated"
|
|
1242
1430
|
]).optional(),
|
|
1243
|
-
buyerPersona:
|
|
1431
|
+
buyerPersona: z10.string().optional(),
|
|
1244
1432
|
result: VideoCreativeJobResult.nullable().optional()
|
|
1245
1433
|
});
|
|
1246
|
-
var VideoCreativeJobPayload =
|
|
1247
|
-
jobId:
|
|
1434
|
+
var VideoCreativeJobPayload = z10.object({
|
|
1435
|
+
jobId: z10.string().optional(),
|
|
1248
1436
|
status: VideoCreativeJobStatusEnum.optional(),
|
|
1249
|
-
progress:
|
|
1250
|
-
etaSeconds:
|
|
1251
|
-
message:
|
|
1252
|
-
error:
|
|
1253
|
-
primaryAngle:
|
|
1254
|
-
primaryAngleSource:
|
|
1437
|
+
progress: z10.number().min(0).max(100).optional(),
|
|
1438
|
+
etaSeconds: z10.number().int().nonnegative().optional(),
|
|
1439
|
+
message: z10.string().optional(),
|
|
1440
|
+
error: z10.string().optional(),
|
|
1441
|
+
primaryAngle: z10.string().optional(),
|
|
1442
|
+
primaryAngleSource: z10.enum([
|
|
1255
1443
|
"user",
|
|
1256
1444
|
"generated"
|
|
1257
1445
|
]).optional(),
|
|
1258
|
-
buyerPersona:
|
|
1446
|
+
buyerPersona: z10.string().optional(),
|
|
1259
1447
|
result: VideoCreativeJobResult.nullable().optional()
|
|
1260
1448
|
});
|
|
1261
|
-
var VideoCreativeHistoryEntry =
|
|
1262
|
-
id:
|
|
1263
|
-
createdAt:
|
|
1449
|
+
var VideoCreativeHistoryEntry = z10.object({
|
|
1450
|
+
id: z10.string(),
|
|
1451
|
+
createdAt: z10.string().datetime(),
|
|
1264
1452
|
status: VideoCreativeJobStatusEnum,
|
|
1265
1453
|
result: VideoCreativeJobResult.nullable(),
|
|
1266
|
-
error:
|
|
1454
|
+
error: z10.string().optional()
|
|
1455
|
+
});
|
|
1456
|
+
var AdCreative = z10.object({
|
|
1457
|
+
provider: z10.string(),
|
|
1458
|
+
model: z10.string(),
|
|
1459
|
+
prompt: z10.string().min(1),
|
|
1460
|
+
url: z10.string().min(1),
|
|
1461
|
+
width: z10.number().int().positive(),
|
|
1462
|
+
height: z10.number().int().positive(),
|
|
1463
|
+
error: z10.string().optional(),
|
|
1464
|
+
variants: z10.array(z10.string().url()).optional(),
|
|
1465
|
+
/** One-to-one with `variants`: null for ProductMaker-original images and the
|
|
1466
|
+
* evidence tier for the competitor-reference variant. */
|
|
1467
|
+
variantSignals: z10.array(ImageWinnerSignal.nullable()).optional(),
|
|
1468
|
+
createdAt: z10.string().datetime().optional(),
|
|
1469
|
+
adCopy: z10.string().optional()
|
|
1470
|
+
}).superRefine((creative, ctx) => {
|
|
1471
|
+
if (creative.variantSignals !== void 0 && (creative.variants === void 0 || creative.variantSignals.length !== creative.variants.length)) {
|
|
1472
|
+
ctx.addIssue({
|
|
1473
|
+
code: z10.ZodIssueCode.custom,
|
|
1474
|
+
path: [
|
|
1475
|
+
"variantSignals"
|
|
1476
|
+
],
|
|
1477
|
+
message: "variantSignals must align one-to-one with variants"
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1267
1480
|
});
|
|
1268
|
-
var
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
variants: z9.array(z9.string().url()).optional(),
|
|
1277
|
-
createdAt: z9.string().datetime().optional(),
|
|
1278
|
-
adCopy: z9.string().optional()
|
|
1279
|
-
});
|
|
1280
|
-
var ImageCreativeCopy = z9.object({
|
|
1281
|
-
title: z9.string().optional(),
|
|
1282
|
-
subtitle: z9.string().optional(),
|
|
1283
|
-
headline: z9.string().optional(),
|
|
1284
|
-
bullets: z9.array(z9.string()).optional(),
|
|
1285
|
-
discount: z9.string().optional(),
|
|
1286
|
-
promoSticker: z9.string().optional(),
|
|
1287
|
-
colors: z9.array(z9.string()).optional()
|
|
1481
|
+
var ImageCreativeCopy = z10.object({
|
|
1482
|
+
title: z10.string().optional(),
|
|
1483
|
+
subtitle: z10.string().optional(),
|
|
1484
|
+
headline: z10.string().optional(),
|
|
1485
|
+
bullets: z10.array(z10.string()).optional(),
|
|
1486
|
+
discount: z10.string().optional(),
|
|
1487
|
+
promoSticker: z10.string().optional(),
|
|
1488
|
+
colors: z10.array(z10.string()).optional()
|
|
1288
1489
|
}).partial();
|
|
1289
|
-
var ImageCreativeVariant =
|
|
1290
|
-
id:
|
|
1291
|
-
url:
|
|
1292
|
-
downloadUrl:
|
|
1293
|
-
});
|
|
1294
|
-
var ImageCreativeResult =
|
|
1295
|
-
provider:
|
|
1296
|
-
model:
|
|
1297
|
-
prompt:
|
|
1298
|
-
width:
|
|
1299
|
-
height:
|
|
1300
|
-
variants:
|
|
1490
|
+
var ImageCreativeVariant = z10.object({
|
|
1491
|
+
id: z10.string(),
|
|
1492
|
+
url: z10.string().url(),
|
|
1493
|
+
downloadUrl: z10.string().url().optional()
|
|
1494
|
+
});
|
|
1495
|
+
var ImageCreativeResult = z10.object({
|
|
1496
|
+
provider: z10.string(),
|
|
1497
|
+
model: z10.string(),
|
|
1498
|
+
prompt: z10.string(),
|
|
1499
|
+
width: z10.number().int().positive(),
|
|
1500
|
+
height: z10.number().int().positive(),
|
|
1501
|
+
variants: z10.array(ImageCreativeVariant).min(1),
|
|
1301
1502
|
copy: ImageCreativeCopy.optional(),
|
|
1302
|
-
createdAt:
|
|
1303
|
-
adCopy:
|
|
1304
|
-
});
|
|
1305
|
-
var ImageCreativeEditableRequest =
|
|
1306
|
-
price:
|
|
1307
|
-
offer:
|
|
1308
|
-
compareAtPrice:
|
|
1309
|
-
country:
|
|
1503
|
+
createdAt: z10.string().datetime().optional(),
|
|
1504
|
+
adCopy: z10.string().optional()
|
|
1505
|
+
});
|
|
1506
|
+
var ImageCreativeEditableRequest = z10.object({
|
|
1507
|
+
price: z10.string(),
|
|
1508
|
+
offer: z10.string(),
|
|
1509
|
+
compareAtPrice: z10.string(),
|
|
1510
|
+
country: z10.string()
|
|
1310
1511
|
}).partial();
|
|
1311
|
-
var ImageCreativeJobStatusEnum =
|
|
1512
|
+
var ImageCreativeJobStatusEnum = z10.enum([
|
|
1312
1513
|
"pending",
|
|
1313
1514
|
"processing",
|
|
1314
1515
|
"completed",
|
|
1315
1516
|
"error"
|
|
1316
1517
|
]);
|
|
1317
|
-
var ImageCreativeJobStatus =
|
|
1318
|
-
jobId:
|
|
1518
|
+
var ImageCreativeJobStatus = z10.object({
|
|
1519
|
+
jobId: z10.string().uuid(),
|
|
1319
1520
|
status: ImageCreativeJobStatusEnum,
|
|
1320
|
-
error:
|
|
1521
|
+
error: z10.string().optional(),
|
|
1321
1522
|
result: ImageCreativeResult.nullable().optional(),
|
|
1322
1523
|
request: ImageCreativeEditableRequest.optional()
|
|
1323
1524
|
});
|
|
1324
|
-
var ImageCreativeJobPayload =
|
|
1325
|
-
jobId:
|
|
1525
|
+
var ImageCreativeJobPayload = z10.object({
|
|
1526
|
+
jobId: z10.string().uuid(),
|
|
1326
1527
|
status: ImageCreativeJobStatusEnum
|
|
1327
1528
|
});
|
|
1328
|
-
var ImageCreativeHistoryEntry =
|
|
1329
|
-
id:
|
|
1330
|
-
createdAt:
|
|
1529
|
+
var ImageCreativeHistoryEntry = z10.object({
|
|
1530
|
+
id: z10.string().uuid(),
|
|
1531
|
+
createdAt: z10.string().datetime(),
|
|
1331
1532
|
status: ImageCreativeJobStatusEnum,
|
|
1332
1533
|
result: ImageCreativeResult.nullable(),
|
|
1333
|
-
error:
|
|
1534
|
+
error: z10.string().optional()
|
|
1334
1535
|
});
|
|
1335
|
-
var LandingCreativeJobStatusEnum =
|
|
1536
|
+
var LandingCreativeJobStatusEnum = z10.enum([
|
|
1336
1537
|
"pending",
|
|
1337
1538
|
"processing",
|
|
1338
1539
|
"completed",
|
|
1339
1540
|
"error"
|
|
1340
1541
|
]);
|
|
1341
|
-
var LandingCreativeResult =
|
|
1342
|
-
images:
|
|
1343
|
-
createdAt:
|
|
1542
|
+
var LandingCreativeResult = z10.object({
|
|
1543
|
+
images: z10.array(LandingImageReadable).max(20),
|
|
1544
|
+
createdAt: z10.string().datetime().optional()
|
|
1344
1545
|
});
|
|
1345
|
-
var LandingCreativeJobStatus =
|
|
1346
|
-
jobId:
|
|
1546
|
+
var LandingCreativeJobStatus = z10.object({
|
|
1547
|
+
jobId: z10.string().uuid(),
|
|
1347
1548
|
status: LandingCreativeJobStatusEnum,
|
|
1348
|
-
error:
|
|
1549
|
+
error: z10.string().optional(),
|
|
1349
1550
|
result: LandingCreativeResult.nullable().optional()
|
|
1350
1551
|
});
|
|
1351
|
-
var LandingCreativeJobPayload =
|
|
1352
|
-
jobId:
|
|
1552
|
+
var LandingCreativeJobPayload = z10.object({
|
|
1553
|
+
jobId: z10.string().uuid(),
|
|
1353
1554
|
status: LandingCreativeJobStatusEnum
|
|
1354
1555
|
});
|
|
1355
|
-
var LandingCreativeHistoryEntry =
|
|
1356
|
-
id:
|
|
1357
|
-
createdAt:
|
|
1556
|
+
var LandingCreativeHistoryEntry = z10.object({
|
|
1557
|
+
id: z10.string().uuid(),
|
|
1558
|
+
createdAt: z10.string().datetime(),
|
|
1358
1559
|
status: LandingCreativeJobStatusEnum,
|
|
1359
1560
|
result: LandingCreativeResult.nullable(),
|
|
1360
|
-
error:
|
|
1561
|
+
error: z10.string().optional()
|
|
1361
1562
|
});
|
|
1362
|
-
var CreativeGeneratorJob =
|
|
1363
|
-
taskId:
|
|
1364
|
-
cleanImagesWaitAttempt:
|
|
1563
|
+
var CreativeGeneratorJob = z10.object({
|
|
1564
|
+
taskId: z10.string().uuid(),
|
|
1565
|
+
cleanImagesWaitAttempt: z10.number().int().nonnegative().optional()
|
|
1365
1566
|
});
|
|
1366
1567
|
|
|
1367
1568
|
// ../../packages/types/src/product-label.ts
|
|
1368
|
-
import { z as
|
|
1569
|
+
import { z as z11 } from "zod";
|
|
1369
1570
|
var PRODUCT_LABEL_MAX_CHARS = 80;
|
|
1370
|
-
var ProductLabelSchema =
|
|
1571
|
+
var ProductLabelSchema = z11.string().transform((value) => value.trim()).pipe(z11.string().max(PRODUCT_LABEL_MAX_CHARS)).optional();
|
|
1371
1572
|
|
|
1372
1573
|
// ../../packages/types/src/landing-mode.ts
|
|
1373
|
-
import { z as
|
|
1374
|
-
var LandingModeSchema =
|
|
1574
|
+
import { z as z12 } from "zod";
|
|
1575
|
+
var LandingModeSchema = z12.enum([
|
|
1375
1576
|
"classic",
|
|
1376
1577
|
"code"
|
|
1377
1578
|
]);
|
|
1378
1579
|
|
|
1379
1580
|
// ../../packages/types/src/landing-image-format.ts
|
|
1380
|
-
import { z as
|
|
1581
|
+
import { z as z13 } from "zod";
|
|
1381
1582
|
var LANDING_IMAGE_FORMATS = [
|
|
1382
1583
|
"9:16",
|
|
1383
1584
|
"4:5",
|
|
1384
1585
|
"1:1"
|
|
1385
1586
|
];
|
|
1386
|
-
var LandingImageFormatSchema =
|
|
1587
|
+
var LandingImageFormatSchema = z13.enum(LANDING_IMAGE_FORMATS);
|
|
1387
1588
|
|
|
1388
1589
|
// ../../packages/types/src/ingest.ts
|
|
1389
1590
|
var INGEST_MAX_REFERENCE_IMAGES = 5;
|
|
@@ -1402,9 +1603,9 @@ var INGEST_ALLOWED_IMAGE_MIME_TYPES = [
|
|
|
1402
1603
|
];
|
|
1403
1604
|
var PRODUCT_CONTEXT_MAX_CHARS = 3e3;
|
|
1404
1605
|
var INGEST_ALLOWED_IMAGE_MIME_TYPE_SET = new Set(INGEST_ALLOWED_IMAGE_MIME_TYPES);
|
|
1405
|
-
var IngestOptions =
|
|
1606
|
+
var IngestOptions = z14.object({
|
|
1406
1607
|
/** Optional owner-scoped guided journey context. Never bypasses ingest gates. */
|
|
1407
|
-
dropshippingProjectId:
|
|
1608
|
+
dropshippingProjectId: z14.string().uuid().optional(),
|
|
1408
1609
|
languageCode: LanguageCodeSchema.default("es-CO"),
|
|
1409
1610
|
ugcFormat: VideoFormat.default("single_15s"),
|
|
1410
1611
|
country: CountrySchema,
|
|
@@ -1417,27 +1618,33 @@ var IngestOptions = z13.object({
|
|
|
1417
1618
|
videoTier: VideoTier.default(DEFAULT_VIDEO_TIER),
|
|
1418
1619
|
narrativeStyle: NarrativeStyle.default("review"),
|
|
1419
1620
|
videoPreset: VideoPreset.optional(),
|
|
1420
|
-
multiAngleVideos:
|
|
1421
|
-
videoAngleCount:
|
|
1621
|
+
multiAngleVideos: z14.boolean().default(false),
|
|
1622
|
+
videoAngleCount: z14.number().int().min(1).max(3).optional(),
|
|
1422
1623
|
/** Per-angle narrative override (positional). angleNarratives[i] forces the
|
|
1423
1624
|
* narrative of angle position i, overriding the batch auto-selector; a list
|
|
1424
1625
|
* shorter than the generated angle count leaves the remaining positions
|
|
1425
1626
|
* auto-selected. Empty/omitted → all angles auto-select as before. */
|
|
1426
|
-
angleNarratives:
|
|
1627
|
+
angleNarratives: z14.array(NarrativeStyle).max(3).optional(),
|
|
1427
1628
|
// 480p is the recommended default for every tier (flipped back 2026-07-30 — the
|
|
1428
1629
|
// 720p premium is real but small, and users on the entry plan were defaulting
|
|
1429
1630
|
// into it while burning half a month of credits on one video).
|
|
1430
1631
|
// This Zod default is what callers that omit the field (MCP/API) actually get,
|
|
1431
1632
|
// so it must agree with resolveSingleCallAwareResolution's unset fallback (480p).
|
|
1432
1633
|
videoResolution: VideoResolution.default("480p"),
|
|
1433
|
-
generateAdCopy:
|
|
1434
|
-
generateAiMusic:
|
|
1634
|
+
generateAdCopy: z14.boolean().default(true),
|
|
1635
|
+
generateAiMusic: z14.boolean().optional(),
|
|
1435
1636
|
voiceId: VoiceId.optional(),
|
|
1436
|
-
isCombo:
|
|
1437
|
-
productContext:
|
|
1438
|
-
creativeDirection:
|
|
1637
|
+
isCombo: z14.boolean().default(false),
|
|
1638
|
+
productContext: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PRODUCT_CONTEXT_MAX_CHARS)).optional(),
|
|
1639
|
+
creativeDirection: z14.string().transform((value) => value.trim()).pipe(z14.string().max(500)).optional(),
|
|
1640
|
+
/** Exact copy the user pinned. Rendered VERBATIM on the image creatives this task
|
|
1641
|
+
* produces and on the landing hero — never on video, which steers via scriptNotes.
|
|
1642
|
+
* `.pipe(...max)` REJECTS over-cap instead of truncating: silently shortening a pin
|
|
1643
|
+
* would ship copy the user never wrote, which is the bug the pin exists to prevent. */
|
|
1644
|
+
headline: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PINNED_HEADLINE_MAX_CHARS)).optional(),
|
|
1645
|
+
subheadline: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PINNED_SUBHEADLINE_MAX_CHARS)).optional(),
|
|
1439
1646
|
productLabel: ProductLabelSchema,
|
|
1440
|
-
selectedOutputs:
|
|
1647
|
+
selectedOutputs: z14.array(z14.enum([
|
|
1441
1648
|
"landing",
|
|
1442
1649
|
"video",
|
|
1443
1650
|
"image"
|
|
@@ -1449,26 +1656,26 @@ var IngestOptions = z13.object({
|
|
|
1449
1656
|
landingStrategy: LandingStrategySchema.optional(),
|
|
1450
1657
|
/** Aspect ratio for classic image landing blocks. Ignored by code landing. Absent ⇒ '9:16'. */
|
|
1451
1658
|
landingImageFormat: LandingImageFormatSchema.optional(),
|
|
1452
|
-
imageStyle:
|
|
1453
|
-
creativeArchetype: ImageCreativeArchetype.or(
|
|
1454
|
-
visualTreatment: ImageCreativeVisualTreatment.or(
|
|
1455
|
-
aspectRatio:
|
|
1456
|
-
brandColors:
|
|
1457
|
-
nVariants:
|
|
1458
|
-
imageModel:
|
|
1459
|
-
styleMix:
|
|
1460
|
-
aiDecidesAll:
|
|
1659
|
+
imageStyle: z14.string().optional(),
|
|
1660
|
+
creativeArchetype: ImageCreativeArchetype.or(z14.literal("ai_decides")).optional(),
|
|
1661
|
+
visualTreatment: ImageCreativeVisualTreatment.or(z14.literal("ai_decides")).optional(),
|
|
1662
|
+
aspectRatio: z14.string().optional(),
|
|
1663
|
+
brandColors: z14.string().optional(),
|
|
1664
|
+
nVariants: z14.number().int().min(1).max(6).optional(),
|
|
1665
|
+
imageModel: z14.string().optional(),
|
|
1666
|
+
styleMix: z14.string().optional(),
|
|
1667
|
+
aiDecidesAll: z14.boolean().optional(),
|
|
1461
1668
|
/** Opt out of the competitor-spy analysis. When false the worker skips
|
|
1462
1669
|
* enqueueCompetitorScan entirely, saving ~$0.25-0.35 user-visible per
|
|
1463
1670
|
* scan. Default true preserves the existing flow; users with their own
|
|
1464
1671
|
* competitor intel or in privacy-sensitive niches can disable. */
|
|
1465
|
-
competitorScanEnabled:
|
|
1466
|
-
brandId:
|
|
1672
|
+
competitorScanEnabled: z14.boolean().default(true),
|
|
1673
|
+
brandId: z14.string().uuid().optional()
|
|
1467
1674
|
}).partial().superRefine((data, ctx) => {
|
|
1468
1675
|
const message = validatePriceInversion(data.price, data.compareAtPrice);
|
|
1469
1676
|
if (message) {
|
|
1470
1677
|
ctx.addIssue({
|
|
1471
|
-
code:
|
|
1678
|
+
code: z14.ZodIssueCode.custom,
|
|
1472
1679
|
path: [
|
|
1473
1680
|
"compareAtPrice"
|
|
1474
1681
|
],
|
|
@@ -1479,11 +1686,11 @@ var IngestOptions = z13.object({
|
|
|
1479
1686
|
});
|
|
1480
1687
|
}
|
|
1481
1688
|
});
|
|
1482
|
-
var IngestRequest =
|
|
1483
|
-
url:
|
|
1689
|
+
var IngestRequest = z14.object({
|
|
1690
|
+
url: z14.string().url(),
|
|
1484
1691
|
options: IngestOptions.optional()
|
|
1485
1692
|
});
|
|
1486
|
-
var VideoOptionsOverride =
|
|
1693
|
+
var VideoOptionsOverride = z14.object({
|
|
1487
1694
|
videoModel: VideoModel.optional(),
|
|
1488
1695
|
videoTier: VideoTier.optional(),
|
|
1489
1696
|
ugcFormat: VideoFormat.optional(),
|
|
@@ -1492,38 +1699,38 @@ var VideoOptionsOverride = z13.object({
|
|
|
1492
1699
|
videoResolution: VideoResolution.optional(),
|
|
1493
1700
|
voiceId: VoiceId.optional(),
|
|
1494
1701
|
/** Actor/character description (role + appearance + attitude). Drives voice and video character. */
|
|
1495
|
-
actor:
|
|
1702
|
+
actor: z14.string().max(360).optional()
|
|
1496
1703
|
});
|
|
1497
1704
|
var AngleVideoOptions = VideoOptionsOverride.extend({
|
|
1498
|
-
angleIndex:
|
|
1705
|
+
angleIndex: z14.number().int().nonnegative(),
|
|
1499
1706
|
/** Per-angle angle/hook text override. When provided, replaces the auto-generated angle. */
|
|
1500
|
-
angle:
|
|
1707
|
+
angle: z14.string().max(480).optional(),
|
|
1501
1708
|
/** Per-angle creative direction (camera/action/setting/tone). NOT spoken dialogue — the script is authored/edited at script review. */
|
|
1502
|
-
scriptNotes:
|
|
1709
|
+
scriptNotes: z14.string().max(2e3).optional(),
|
|
1503
1710
|
/** Per-angle buyer persona override (describes the target audience) */
|
|
1504
|
-
buyerPersona:
|
|
1711
|
+
buyerPersona: z14.string().max(360).optional(),
|
|
1505
1712
|
/** Per-angle actor/character description (role + appearance + attitude). Drives voice and video character. */
|
|
1506
|
-
actor:
|
|
1507
|
-
});
|
|
1508
|
-
var RapidScriptBlueprintShotForApproval =
|
|
1509
|
-
id:
|
|
1510
|
-
beatSource:
|
|
1511
|
-
durationSec:
|
|
1512
|
-
role:
|
|
1513
|
-
text:
|
|
1514
|
-
visualIntent:
|
|
1515
|
-
variantKey:
|
|
1516
|
-
targetWords:
|
|
1517
|
-
minWords:
|
|
1518
|
-
maxWords:
|
|
1713
|
+
actor: z14.string().max(360).optional()
|
|
1714
|
+
});
|
|
1715
|
+
var RapidScriptBlueprintShotForApproval = z14.object({
|
|
1716
|
+
id: z14.string().min(1),
|
|
1717
|
+
beatSource: z14.string().min(1),
|
|
1718
|
+
durationSec: z14.number().positive(),
|
|
1719
|
+
role: z14.string().min(1).max(120),
|
|
1720
|
+
text: z14.string().min(1).max(320),
|
|
1721
|
+
visualIntent: z14.string().min(1).max(500).optional(),
|
|
1722
|
+
variantKey: z14.string().min(1).max(120).optional(),
|
|
1723
|
+
targetWords: z14.number().int().positive().optional(),
|
|
1724
|
+
minWords: z14.number().int().positive(),
|
|
1725
|
+
maxWords: z14.number().int().positive()
|
|
1519
1726
|
}).refine((shot) => shot.maxWords >= shot.minWords, {
|
|
1520
1727
|
message: "maxWords must be greater than or equal to minWords",
|
|
1521
1728
|
path: [
|
|
1522
1729
|
"maxWords"
|
|
1523
1730
|
]
|
|
1524
1731
|
});
|
|
1525
|
-
var RapidScriptBlueprintForApproval =
|
|
1526
|
-
shots:
|
|
1732
|
+
var RapidScriptBlueprintForApproval = z14.object({
|
|
1733
|
+
shots: z14.array(RapidScriptBlueprintShotForApproval).min(1).max(10)
|
|
1527
1734
|
}).refine((blueprint) => {
|
|
1528
1735
|
const ids = blueprint.shots.map((shot) => shot.id);
|
|
1529
1736
|
return new Set(ids).size === ids.length;
|
|
@@ -1536,9 +1743,9 @@ var RapidScriptBlueprintForApproval = z13.object({
|
|
|
1536
1743
|
var AngleVideoOptionsWithBlueprint = AngleVideoOptions.extend({
|
|
1537
1744
|
rapidScriptBlueprint: RapidScriptBlueprintForApproval.optional()
|
|
1538
1745
|
});
|
|
1539
|
-
var ProductImageSelection =
|
|
1540
|
-
var IngestResumePayload =
|
|
1541
|
-
selectedAngleIndices:
|
|
1746
|
+
var ProductImageSelection = z14.string().min(1).max(2048).refine((value) => !value.includes("\0"), "Invalid image reference");
|
|
1747
|
+
var IngestResumePayload = z14.object({
|
|
1748
|
+
selectedAngleIndices: z14.array(z14.number().int().nonnegative()).min(1).max(3),
|
|
1542
1749
|
ugcFormat: VideoFormat.default("single_15s"),
|
|
1543
1750
|
videoStyle: VideoStyle.default("ugc"),
|
|
1544
1751
|
videoModel: VideoModel.default("veo"),
|
|
@@ -1547,16 +1754,16 @@ var IngestResumePayload = z13.object({
|
|
|
1547
1754
|
// 480p is the recommended default for every tier (flipped back 2026-07-30) — keep in
|
|
1548
1755
|
// sync with IngestOptions.videoResolution and resolveSingleCallAwareResolution.
|
|
1549
1756
|
videoResolution: VideoResolution.default("480p"),
|
|
1550
|
-
pauseForVideoScriptReview:
|
|
1551
|
-
generateAiMusic:
|
|
1552
|
-
scriptNotes:
|
|
1757
|
+
pauseForVideoScriptReview: z14.boolean().optional(),
|
|
1758
|
+
generateAiMusic: z14.boolean().optional(),
|
|
1759
|
+
scriptNotes: z14.string().max(2e3).optional(),
|
|
1553
1760
|
languageCode: LanguageCodeSchema.optional(),
|
|
1554
1761
|
country: CountrySchema.optional(),
|
|
1555
1762
|
price: PriceSchema.optional(),
|
|
1556
1763
|
offer: OfferSchema.optional(),
|
|
1557
1764
|
compareAtPrice: CompareAtPriceSchema.optional(),
|
|
1558
1765
|
paymentMethods: PaymentMethodsSchema.optional(),
|
|
1559
|
-
angleVideoOptions:
|
|
1766
|
+
angleVideoOptions: z14.array(AngleVideoOptionsWithBlueprint).max(3).refine((opts) => {
|
|
1560
1767
|
if (!opts?.length) return true;
|
|
1561
1768
|
const indices = opts.map((o) => o.angleIndex);
|
|
1562
1769
|
return new Set(indices).size === indices.length;
|
|
@@ -1565,191 +1772,29 @@ var IngestResumePayload = z13.object({
|
|
|
1565
1772
|
}).optional(),
|
|
1566
1773
|
voiceId: VoiceId.optional(),
|
|
1567
1774
|
/** Global actor/character description (role + appearance + attitude). Default for all angles. */
|
|
1568
|
-
actor:
|
|
1775
|
+
actor: z14.string().max(360).optional(),
|
|
1569
1776
|
/** Custom angle override for landing/creative (uses this instead of first angle) */
|
|
1570
|
-
customAngleOverride:
|
|
1777
|
+
customAngleOverride: z14.string().min(1).max(480).optional(),
|
|
1571
1778
|
/** Pain points to emphasize in landings, creatives, and videos */
|
|
1572
|
-
painPoints:
|
|
1779
|
+
painPoints: z14.string().max(1500).optional(),
|
|
1573
1780
|
/** Benefits to highlight in landings, creatives, and videos */
|
|
1574
|
-
benefits:
|
|
1781
|
+
benefits: z14.string().max(1500).optional(),
|
|
1575
1782
|
/** Target gender for landing/creative images */
|
|
1576
|
-
creativeGender:
|
|
1783
|
+
creativeGender: z14.enum([
|
|
1577
1784
|
"female",
|
|
1578
1785
|
"male",
|
|
1579
1786
|
"mixed"
|
|
1580
1787
|
]).optional(),
|
|
1788
|
+
/** Non-blocking static winner preference. Omitted defaults to original. */
|
|
1789
|
+
winnerVisualMode: WinnerVisualMode.optional(),
|
|
1581
1790
|
/** Which product image URLs to use for this generation (ephemeral selection) */
|
|
1582
|
-
selectedImageUrls:
|
|
1791
|
+
selectedImageUrls: z14.array(ProductImageSelection).max(INGEST_MAX_REFERENCE_IMAGES).optional(),
|
|
1583
1792
|
/** User-confirmed intended product target for the selected reference image set */
|
|
1584
1793
|
productTargetConfirmation: ProductTargetConfirmationSchema.optional(),
|
|
1585
1794
|
/** User-confirmed grouping for the selected reference image set */
|
|
1586
1795
|
productReferenceConfirmation: ProductReferenceConfirmationSchema.optional()
|
|
1587
1796
|
});
|
|
1588
1797
|
|
|
1589
|
-
// ../../packages/types/src/angle.ts
|
|
1590
|
-
import { z as z14 } from "zod";
|
|
1591
|
-
var AnchoredIntel = z14.object({
|
|
1592
|
-
painPointIndices: z14.array(z14.number().int().nonnegative()).max(10),
|
|
1593
|
-
hookIndices: z14.array(z14.number().int().nonnegative()).max(10),
|
|
1594
|
-
claimIndices: z14.array(z14.number().int().nonnegative()).max(10),
|
|
1595
|
-
sourceAngleType: z14.enum([
|
|
1596
|
-
"gap",
|
|
1597
|
-
"validated_local",
|
|
1598
|
-
"saturated_differentiated",
|
|
1599
|
-
"novel"
|
|
1600
|
-
]),
|
|
1601
|
-
// SNAPSHOT of the resolved intel strings at angle-generation time.
|
|
1602
|
-
// Indices reference into Task.competitorIntel which we treat as immutable
|
|
1603
|
-
// post-scan, but defensive: a future re-scan path that rewrites
|
|
1604
|
-
// Task.competitorIntel would invalidate the indices silently. Persisting
|
|
1605
|
-
// the resolved strings ALONGSIDE the indices means downstream consumers
|
|
1606
|
-
// (script writer, landing copy) can safely use the strings directly
|
|
1607
|
-
// without re-resolving — staleness-immune. The indices remain useful for
|
|
1608
|
-
// analytics ("which intel items did angles cite?") and as a sanity-check.
|
|
1609
|
-
resolvedPainPoints: z14.array(z14.string()).max(10).optional(),
|
|
1610
|
-
resolvedHooks: z14.array(z14.string()).max(10).optional(),
|
|
1611
|
-
resolvedClaims: z14.array(z14.string()).max(10).optional(),
|
|
1612
|
-
/**
|
|
1613
|
-
* Free-form pillar name describing the AXIS of the product this angle
|
|
1614
|
-
* attacks (e.g. "ciencia/mecanismo", "tiempo/ritual", "comparación-con-
|
|
1615
|
-
* alternativas", "social-proof", "dolor-emocional", "identidad/persona").
|
|
1616
|
-
* The LLM picks the label — we do NOT lock an enum because the right
|
|
1617
|
-
* pillar taxonomy varies per product (a supplement's "mecanismo" pillar
|
|
1618
|
-
* is different from a gadget's "fricción de uso"). Constraint enforced
|
|
1619
|
-
* at generation time: the 3 angles emitted for one task MUST use 3
|
|
1620
|
-
* DISTINCT pillars (anti-convergence). Lives inside AnchoredIntel because
|
|
1621
|
-
* it ships as analytics metadata alongside the anchoredIntel sourceType +
|
|
1622
|
-
* indices, and persists as JSON without a column migration. Optional
|
|
1623
|
-
* because angles persisted before this change have no pillar.
|
|
1624
|
-
*/
|
|
1625
|
-
pillar: z14.string().min(1).max(60).optional(),
|
|
1626
|
-
// Telemetry: why the angle did NOT import a gapAngle. The legacy single value
|
|
1627
|
-
// 'none_relevant' was OVERLOADED (it meant "no gaps existed" AND "gaps existed
|
|
1628
|
-
// but none fit" AND was the sanitizer default) which made the gap feature
|
|
1629
|
-
// unmeasurable. Split into distinct, one-meaning-each states:
|
|
1630
|
-
// - persona_mismatch / product_mismatch: the LLM saw gaps and judged them a
|
|
1631
|
-
// wrong-persona / wrong-subcategory fit (trusted from the LLM).
|
|
1632
|
-
// - no_gaps_in_block: there were no gapAngles in the block (or no block).
|
|
1633
|
-
// - gaps_present_not_imported: gaps existed and the angle went novel anyway
|
|
1634
|
-
// (the sanitizer's honest default when the LLM didn't pick persona/product).
|
|
1635
|
-
// - gaps_all_filtered_noise: the reference HAD gaps but every one was dropped
|
|
1636
|
-
// as detection noise (retailer-meta/cross-category) so the LLM saw none.
|
|
1637
|
-
// - n_a: the angle imported a gap (sourceAngleType="gap"), so "why skipped"
|
|
1638
|
-
// is moot.
|
|
1639
|
-
// The deterministic gap-presence states (no_gaps_in_block / gaps_present_not_
|
|
1640
|
-
// imported / gaps_all_filtered_noise) are set by sanitizeAnchoredIntel from
|
|
1641
|
-
// facts it computes (intel.gapAngles.length, intel.gapAnglesFilteredCount), NOT
|
|
1642
|
-
// trusted from the lazy LLM. z.preprocess aliases historical 'none_relevant'
|
|
1643
|
-
// rows → 'no_gaps_in_block' so the JSON column parses with no migration.
|
|
1644
|
-
// Analytics-only: NO runtime path branches on this value.
|
|
1645
|
-
skippedGapReason: z14.preprocess((v) => v === "none_relevant" ? "no_gaps_in_block" : v, z14.enum([
|
|
1646
|
-
"persona_mismatch",
|
|
1647
|
-
"product_mismatch",
|
|
1648
|
-
"no_gaps_in_block",
|
|
1649
|
-
"gaps_present_not_imported",
|
|
1650
|
-
"gaps_all_filtered_noise",
|
|
1651
|
-
"n_a"
|
|
1652
|
-
])).optional(),
|
|
1653
|
-
// Telemetry: captures the prod-observed case where a "novel" angle actually
|
|
1654
|
-
// reflects a GAP's theme/promise without formally anchoring indices (gapAngles
|
|
1655
|
-
// are a separate array, not index-cited, so a real gap import is reclassified
|
|
1656
|
-
// 'novel' by the empty-indices coherence guard). Self-reported by the LLM:
|
|
1657
|
-
// - gap_imported: the angle's theme imports a gap's logic.
|
|
1658
|
-
// - product_derived: derived purely from the product.
|
|
1659
|
-
// - hybrid: a mix.
|
|
1660
|
-
// SOFT analytics-only signal (the LLM may confabulate) — validated against
|
|
1661
|
-
// known prod cases before trusting aggregates; never a control-flow input.
|
|
1662
|
-
// Optional in storage; on the wire it ships as a required enum with a 'none'
|
|
1663
|
-
// sentinel (strict structured output forbids optional properties) that the
|
|
1664
|
-
// sanitizer maps to undefined.
|
|
1665
|
-
themeSourceType: z14.enum([
|
|
1666
|
-
"gap_imported",
|
|
1667
|
-
"product_derived",
|
|
1668
|
-
"hybrid"
|
|
1669
|
-
]).optional()
|
|
1670
|
-
});
|
|
1671
|
-
var Angle = z14.object({
|
|
1672
|
-
angle: z14.string().min(1).max(480),
|
|
1673
|
-
buyerPersona: z14.string().min(1).max(360),
|
|
1674
|
-
/** Character description for video (role + appearance + attitude). Drives voice selection and Frame0Spec. */
|
|
1675
|
-
actor: z14.string().min(1).max(360).optional(),
|
|
1676
|
-
// Binary only: voice catalog has no non-binary voices.
|
|
1677
|
-
// See packages/angles/src/index.ts + getAngles coercion in angle.repository.ts.
|
|
1678
|
-
gender: z14.enum([
|
|
1679
|
-
"female",
|
|
1680
|
-
"male"
|
|
1681
|
-
]).optional(),
|
|
1682
|
-
/** Per-angle user-supplied script/visual notes. Hydrated from the
|
|
1683
|
-
* `Angle.scriptNotes` DB column or the `angleVideoOptions[i].scriptNotes`
|
|
1684
|
-
* passed from the chat. Per-angle overrides global `videoScriptNotes`. */
|
|
1685
|
-
scriptNotes: z14.string().max(2e3).optional(),
|
|
1686
|
-
/** AI-chosen per-angle narrative (set by the batch selector in angles.worker).
|
|
1687
|
-
* Pre-fills the review dropdown; user can override per angle. */
|
|
1688
|
-
narrativeStyle: NarrativeStyle.optional(),
|
|
1689
|
-
/** See AnchoredIntel docs. Optional because angles persisted before this
|
|
1690
|
-
* change have no anchoring captured; new angles ALWAYS emit it (with empty
|
|
1691
|
-
* arrays + 'novel' when intel was unavailable). */
|
|
1692
|
-
anchoredIntel: AnchoredIntel.optional(),
|
|
1693
|
-
/** Server-computed: true when this angle is eligible for the gap_replication
|
|
1694
|
-
* narrative (isReplicationAngle(anchoredIntel, intel) && intel.gapBlueprint
|
|
1695
|
-
* present). Computed server-side (not derived client-side) because the web
|
|
1696
|
-
* only sees this angle's anchoredIntel, not the task's
|
|
1697
|
-
* competitorIntel.gapBlueprint. Gates the review dropdown's gap_replication
|
|
1698
|
-
* option — absent/false = hidden. */
|
|
1699
|
-
gapReplicationEligible: z14.boolean().optional(),
|
|
1700
|
-
/** Server-computed: true when this angle's effective narrative is
|
|
1701
|
-
* gap_replication, which FORCES the multi-shot STANDARD ("Multiescena")
|
|
1702
|
-
* render pipeline regardless of the task's chosen tier (single_call can't
|
|
1703
|
-
* produce the multi-shot replication — see task-results.service.ts and the
|
|
1704
|
-
* render-time tier override). Surfaces "renders in Multiescena" on the
|
|
1705
|
-
* review card so a single_call user isn't surprised by the STANDARD-tier
|
|
1706
|
-
* charge. */
|
|
1707
|
-
rendersMultiShot: z14.boolean().optional(),
|
|
1708
|
-
/** Server-computed product-level origin bucket for the review card's badge
|
|
1709
|
-
* (NOT the internal sourceAngleType taxonomy — see resolveAngleOrigin in
|
|
1710
|
-
* task-results.service.ts): 'gap' = market opportunity competitors aren't
|
|
1711
|
-
* using; 'saturated_twist' = competitors' most-used approach with our own
|
|
1712
|
-
* twist; 'ai_recommended' = AI-authored for this product (default). */
|
|
1713
|
-
angleOrigin: z14.enum([
|
|
1714
|
-
"gap",
|
|
1715
|
-
"saturated_twist",
|
|
1716
|
-
"ai_recommended"
|
|
1717
|
-
]).optional(),
|
|
1718
|
-
/** Server-computed badge TIER for the replication angle (two-tier UX,
|
|
1719
|
-
* v1.32.0 — see resolveReplicationTier). 'winner' = a validated angle
|
|
1720
|
-
* opportunity backed by a proven ad (anchor gap/saturated → golden badge);
|
|
1721
|
-
* 'opportunity' = a product-coherent replicable winner with no validated
|
|
1722
|
-
* angle gap (anchor 'dominant' → opportunity/twist badge, no overclaim).
|
|
1723
|
-
* Present only on the angle that carries gap_replication; absent otherwise. */
|
|
1724
|
-
replicationTier: z14.enum([
|
|
1725
|
-
"winner",
|
|
1726
|
-
"opportunity"
|
|
1727
|
-
]).optional()
|
|
1728
|
-
});
|
|
1729
|
-
var AdVideo = z14.object({
|
|
1730
|
-
source: z14.enum([
|
|
1731
|
-
"tiktok_shop",
|
|
1732
|
-
"meta_ads"
|
|
1733
|
-
]),
|
|
1734
|
-
url: z14.string().url(),
|
|
1735
|
-
title: z14.string().optional(),
|
|
1736
|
-
description: z14.string().optional(),
|
|
1737
|
-
metrics: z14.object({
|
|
1738
|
-
impressions: z14.number().int().nonnegative().optional(),
|
|
1739
|
-
views: z14.number().int().nonnegative().optional(),
|
|
1740
|
-
likes: z14.number().int().nonnegative().optional(),
|
|
1741
|
-
ctaClicks: z14.number().int().nonnegative().optional(),
|
|
1742
|
-
diggCount: z14.number().int().nonnegative().optional(),
|
|
1743
|
-
shareCount: z14.number().int().nonnegative().optional(),
|
|
1744
|
-
commentCount: z14.number().int().nonnegative().optional(),
|
|
1745
|
-
playCount: z14.number().int().nonnegative().optional(),
|
|
1746
|
-
collectCount: z14.number().int().nonnegative().optional(),
|
|
1747
|
-
durationSeconds: z14.number().int().nonnegative().optional()
|
|
1748
|
-
}).partial().optional(),
|
|
1749
|
-
country: z14.string().optional(),
|
|
1750
|
-
lastSeen: z14.string().datetime().optional()
|
|
1751
|
-
});
|
|
1752
|
-
|
|
1753
1798
|
// ../../packages/types/src/frame0.ts
|
|
1754
1799
|
import { z as z15 } from "zod";
|
|
1755
1800
|
var Frame0SpecVersion = z15.literal("frame0_spec_v1");
|
|
@@ -1928,7 +1973,15 @@ var VideoGenerator = z16.object({
|
|
|
1928
1973
|
angleKey: z16.string().min(1),
|
|
1929
1974
|
angleIndex: z16.number().int().nonnegative(),
|
|
1930
1975
|
angle: z16.string().min(1).max(480).optional(),
|
|
1931
|
-
|
|
1976
|
+
// READ contract: the cap must fit what the generators actually persist, not
|
|
1977
|
+
// what the write surfaces accept. Angle-derived personas are LLM-written and
|
|
1978
|
+
// routinely exceed the 360-char write cap (prod alert 88badc8: 419 chars), and
|
|
1979
|
+
// this schema serializes existing DB rows — a too-tight cap turns a valid row
|
|
1980
|
+
// into a ZodError that 500s the whole `GET /v1/results/:taskId` payload and
|
|
1981
|
+
// hides the entire task. Same reasoning as the landing-images cap in
|
|
1982
|
+
// `result.ts`. Write-side caps (angle.ts, wizard.ts, task-creative-variants.ts)
|
|
1983
|
+
// intentionally stay at 360.
|
|
1984
|
+
buyerPersona: z16.string().min(1).max(1200).optional(),
|
|
1932
1985
|
script: z16.string().min(1),
|
|
1933
1986
|
prompt: z16.string().min(1),
|
|
1934
1987
|
// Empty string allowed: Pro pipeline pauses for script review BEFORE the grid
|
|
@@ -2357,6 +2410,8 @@ var ApproveImageAnglesRequest = z19.object({
|
|
|
2357
2410
|
painPoints: z19.string().max(1500).optional(),
|
|
2358
2411
|
/** Features to include in the image */
|
|
2359
2412
|
features: z19.array(z19.string().min(1).max(240)).max(20).optional(),
|
|
2413
|
+
/** Non-blocking static winner preference. Omitted defaults to original. */
|
|
2414
|
+
winnerVisualMode: WinnerVisualMode.optional(),
|
|
2360
2415
|
/** Which product image URLs to use for this generation (ephemeral selection) */
|
|
2361
2416
|
selectedImageUrls: z19.array(ProductImageSelection).max(5).optional(),
|
|
2362
2417
|
productTargetConfirmation: ProductTargetConfirmationSchema.optional(),
|
|
@@ -2381,12 +2436,13 @@ var ImageSuggestionsResponse = z19.object({
|
|
|
2381
2436
|
buyerPersona: z19.string().optional()
|
|
2382
2437
|
});
|
|
2383
2438
|
var RefineAngleRequest = z19.object({
|
|
2384
|
-
/** Refine mode: '
|
|
2439
|
+
/** Refine mode: 'reference' adapts a competitor image winner's selling structure to the user's product. */
|
|
2385
2440
|
mode: z19.enum([
|
|
2386
2441
|
"all",
|
|
2387
2442
|
"pain_points",
|
|
2388
2443
|
"benefits",
|
|
2389
|
-
"video"
|
|
2444
|
+
"video",
|
|
2445
|
+
"reference"
|
|
2390
2446
|
]),
|
|
2391
2447
|
/** Current angle text (used as context for aligned generation) */
|
|
2392
2448
|
currentAngle: z19.string().max(480).optional(),
|
|
@@ -2479,7 +2535,6 @@ var TASK_CREATIVE_VARIANT_TEXT_LIMITS = {
|
|
|
2479
2535
|
recommendationRationale: 600
|
|
2480
2536
|
};
|
|
2481
2537
|
var TaskCreativeVariantVideoPreset = VideoPreset.exclude([
|
|
2482
|
-
"problem_solution",
|
|
2483
2538
|
"ugc_unboxing_chaos",
|
|
2484
2539
|
"ugc_curiosity_reveal",
|
|
2485
2540
|
"ugc_listicle_3",
|
|
@@ -2836,7 +2891,10 @@ var StatusResponse = z22.object({
|
|
|
2836
2891
|
/** Derived from landingEnabled/videoGeneratorEnabled/adCreativeEnabled flags;
|
|
2837
2892
|
* canonical [landing, video, image] order regardless of input order.
|
|
2838
2893
|
* Always present (server derivation has a "legacy = enabled" fallback). */
|
|
2839
|
-
selectedOutputs: SelectedOutputsSchema
|
|
2894
|
+
selectedOutputs: SelectedOutputsSchema,
|
|
2895
|
+
/** Persisted image portfolio size so a refreshed review reconstructs the
|
|
2896
|
+
* same visual plan even when browser session state is unavailable. */
|
|
2897
|
+
imageVariantCount: z22.number().int().min(1).max(6).optional()
|
|
2840
2898
|
});
|
|
2841
2899
|
var TaskSummary = z22.object({
|
|
2842
2900
|
taskId: z22.string().uuid(),
|
|
@@ -4121,6 +4179,8 @@ var TikTokCreateCampaignResponseSchema = z32.object({
|
|
|
4121
4179
|
// ../../packages/types/src/pricing.ts
|
|
4122
4180
|
var CREDITS_PER_USD_CENT = 10;
|
|
4123
4181
|
var CREDITS_PER_DOLLAR = 100 * CREDITS_PER_USD_CENT;
|
|
4182
|
+
var KALODATA_USD_CENTS_PER_PROVIDER_CREDIT = 9;
|
|
4183
|
+
var KALODATA_PROVIDER_CREDITS_PER_SOURCE_CEILING = 7.5;
|
|
4124
4184
|
var DEFAULT_PRICING = {
|
|
4125
4185
|
// IMPORTANT: despite the field name, these values are in *USD cents* (not USD dollars).
|
|
4126
4186
|
// We store cents (and sometimes fractional cents) everywhere so we can convert to credits:
|
|
@@ -4166,16 +4226,16 @@ var DEFAULT_PRICING = {
|
|
|
4166
4226
|
userPriceCents: 0.8
|
|
4167
4227
|
},
|
|
4168
4228
|
"openrouter:chat-parse": {
|
|
4169
|
-
ourCostCents: 0.
|
|
4170
|
-
userPriceCents: 0.
|
|
4229
|
+
ourCostCents: 0.2,
|
|
4230
|
+
userPriceCents: 0.4
|
|
4171
4231
|
},
|
|
4172
4232
|
"openrouter:audio-tag-injection": {
|
|
4173
|
-
ourCostCents: 0.
|
|
4174
|
-
userPriceCents: 0.
|
|
4233
|
+
ourCostCents: 0.1,
|
|
4234
|
+
userPriceCents: 0.2
|
|
4175
4235
|
},
|
|
4176
4236
|
"openrouter:product-consolidation": {
|
|
4177
|
-
ourCostCents: 0.
|
|
4178
|
-
userPriceCents: 0.
|
|
4237
|
+
ourCostCents: 0.07,
|
|
4238
|
+
userPriceCents: 0.14
|
|
4179
4239
|
},
|
|
4180
4240
|
"openrouter:video-final-simplify-rewrite": {
|
|
4181
4241
|
ourCostCents: 0.4,
|
|
@@ -4194,12 +4254,12 @@ var DEFAULT_PRICING = {
|
|
|
4194
4254
|
userPriceCents: 0.3
|
|
4195
4255
|
},
|
|
4196
4256
|
"openrouter:vision-extract-combo": {
|
|
4197
|
-
ourCostCents: 0.
|
|
4198
|
-
userPriceCents:
|
|
4257
|
+
ourCostCents: 0.6,
|
|
4258
|
+
userPriceCents: 1.2
|
|
4199
4259
|
},
|
|
4200
4260
|
"openrouter:rich-scene-analyzer": {
|
|
4201
|
-
ourCostCents:
|
|
4202
|
-
userPriceCents:
|
|
4261
|
+
ourCostCents: 13.42,
|
|
4262
|
+
userPriceCents: 26.84
|
|
4203
4263
|
},
|
|
4204
4264
|
"openrouter:product-info-extractor": {
|
|
4205
4265
|
ourCostCents: 0.15,
|
|
@@ -4539,6 +4599,10 @@ var DEFAULT_PRICING = {
|
|
|
4539
4599
|
ourCostCents: 2,
|
|
4540
4600
|
userPriceCents: 4
|
|
4541
4601
|
},
|
|
4602
|
+
"kie:image-generation:nano-banana-2-lite": {
|
|
4603
|
+
ourCostCents: 2,
|
|
4604
|
+
userPriceCents: 4
|
|
4605
|
+
},
|
|
4542
4606
|
"kie:image-generation:nano-banana-2": {
|
|
4543
4607
|
ourCostCents: 4,
|
|
4544
4608
|
userPriceCents: 8
|
|
@@ -4575,6 +4639,10 @@ var DEFAULT_PRICING = {
|
|
|
4575
4639
|
ourCostCents: 3,
|
|
4576
4640
|
userPriceCents: 6
|
|
4577
4641
|
},
|
|
4642
|
+
"fal:image-generation:gpt-image-2:1k": {
|
|
4643
|
+
ourCostCents: 1.5,
|
|
4644
|
+
userPriceCents: 6
|
|
4645
|
+
},
|
|
4578
4646
|
// Video reference pricing varies by model:
|
|
4579
4647
|
// - gpt-image-2 @ 2K (primary since 2026-04-24): $0.05 / $0.10
|
|
4580
4648
|
// - nano-banana-2 (fallback for 5xx/timeout): $0.04 / $0.08
|
|
@@ -4771,15 +4839,16 @@ var DEFAULT_PRICING = {
|
|
|
4771
4839
|
ourCostCents: 2.5,
|
|
4772
4840
|
userPriceCents: 5
|
|
4773
4841
|
},
|
|
4774
|
-
// Usage spec extraction (
|
|
4842
|
+
// Usage spec extraction (gemini-3-flash-preview:online). Measured 1.5807¢ — the :online
|
|
4843
|
+
// web-search surcharge is what makes it ~4x the plain-Flash rate it used to be priced at.
|
|
4775
4844
|
"openrouter:product-usage-extract": {
|
|
4776
|
-
ourCostCents:
|
|
4777
|
-
userPriceCents:
|
|
4845
|
+
ourCostCents: 1.58,
|
|
4846
|
+
userPriceCents: 3.16
|
|
4778
4847
|
},
|
|
4779
|
-
// Functional demonstration extraction (
|
|
4848
|
+
// Functional demonstration extraction (gemini-3-flash-preview:online). Measured 1.0643¢.
|
|
4780
4849
|
"openrouter:functional-demo-extract": {
|
|
4781
|
-
ourCostCents:
|
|
4782
|
-
userPriceCents:
|
|
4850
|
+
ourCostCents: 1.06,
|
|
4851
|
+
userPriceCents: 2.12
|
|
4783
4852
|
},
|
|
4784
4853
|
// Fusion of angles and usage into visual context (Gemini 3 Flash Preview)
|
|
4785
4854
|
"openrouter:visual-context-fusion": {
|
|
@@ -4796,6 +4865,38 @@ var DEFAULT_PRICING = {
|
|
|
4796
4865
|
ourCostCents: 2,
|
|
4797
4866
|
userPriceCents: 4
|
|
4798
4867
|
},
|
|
4868
|
+
// Free-form (v2) code landings — estimator inputs + usage.cost fallbacks. Edit-chat
|
|
4869
|
+
// rows are the FLAT per-message price (100/300 credits).
|
|
4870
|
+
"openrouter:landing-art-direction": {
|
|
4871
|
+
ourCostCents: 5,
|
|
4872
|
+
userPriceCents: 10
|
|
4873
|
+
},
|
|
4874
|
+
// v2.1: ONE coherent call authors every section (replaced the per-section
|
|
4875
|
+
// 'landing-section-code' fan-out — kept below for legacy BillingEntry rows).
|
|
4876
|
+
"openrouter:landing-section-authoring": {
|
|
4877
|
+
ourCostCents: 20,
|
|
4878
|
+
userPriceCents: 40
|
|
4879
|
+
},
|
|
4880
|
+
"openrouter:landing-section-code": {
|
|
4881
|
+
ourCostCents: 5,
|
|
4882
|
+
userPriceCents: 10
|
|
4883
|
+
},
|
|
4884
|
+
"openrouter:landing-visual-judge": {
|
|
4885
|
+
ourCostCents: 3,
|
|
4886
|
+
userPriceCents: 6
|
|
4887
|
+
},
|
|
4888
|
+
"system:landing-qa": {
|
|
4889
|
+
ourCostCents: 1,
|
|
4890
|
+
userPriceCents: 0
|
|
4891
|
+
},
|
|
4892
|
+
"openrouter:landing-edit-chat-section": {
|
|
4893
|
+
ourCostCents: 5,
|
|
4894
|
+
userPriceCents: 10
|
|
4895
|
+
},
|
|
4896
|
+
"openrouter:landing-edit-chat-full": {
|
|
4897
|
+
ourCostCents: 15,
|
|
4898
|
+
userPriceCents: 30
|
|
4899
|
+
},
|
|
4799
4900
|
// Product mix helper calls (Gemini 3 Flash Preview)
|
|
4800
4901
|
"openrouter:product-mix-script-generation": {
|
|
4801
4902
|
ourCostCents: 0.4,
|
|
@@ -4826,15 +4927,18 @@ var DEFAULT_PRICING = {
|
|
|
4826
4927
|
ourCostCents: 0.2,
|
|
4827
4928
|
userPriceCents: 0.4
|
|
4828
4929
|
},
|
|
4829
|
-
// Landing image QA (
|
|
4930
|
+
// Landing image QA (gemini-3.6-flash since 2026-08-01, 1-2 images). Anatomy/text/background cutoff.
|
|
4931
|
+
// 1.88¢ = the post-model-swap steady state at prod averages (11,109 prompt / 928 completion
|
|
4932
|
+
// tokens) WITH the prompt-cache breakpoint; without caching it would be ~2.36¢.
|
|
4830
4933
|
"openrouter:landing-image-qa": {
|
|
4831
|
-
ourCostCents:
|
|
4832
|
-
userPriceCents:
|
|
4934
|
+
ourCostCents: 1.88,
|
|
4935
|
+
userPriceCents: 3.76
|
|
4833
4936
|
},
|
|
4834
|
-
// Creative image QA (
|
|
4937
|
+
// Creative image QA (gemini-3-flash-preview — deliberately NOT on 3.6; see ai_models.md).
|
|
4938
|
+
// Measured 0.8196¢ over 30d of prod.
|
|
4835
4939
|
"openrouter:creative-image-qa": {
|
|
4836
|
-
ourCostCents: 0.
|
|
4837
|
-
userPriceCents:
|
|
4940
|
+
ourCostCents: 0.82,
|
|
4941
|
+
userPriceCents: 1.64
|
|
4838
4942
|
},
|
|
4839
4943
|
// Anatomy check (Gemini 3 Flash Preview, 1 image). Standalone anatomy verification.
|
|
4840
4944
|
"openrouter:anatomy-check": {
|
|
@@ -4873,10 +4977,10 @@ var DEFAULT_PRICING = {
|
|
|
4873
4977
|
ourCostCents: 0.15,
|
|
4874
4978
|
userPriceCents: 0.3
|
|
4875
4979
|
},
|
|
4876
|
-
// Policy rewrite helper (
|
|
4980
|
+
// Policy rewrite helper (gemini-3-flash-preview). Measured 0.0416¢ — a short text-only call.
|
|
4877
4981
|
"openrouter:policy-prompt-rewrite": {
|
|
4878
|
-
ourCostCents: 0.
|
|
4879
|
-
userPriceCents: 0.
|
|
4982
|
+
ourCostCents: 0.04,
|
|
4983
|
+
userPriceCents: 0.08
|
|
4880
4984
|
},
|
|
4881
4985
|
// TTS dialogue rewrite helper (OpenRouter model: google/gemini-3-flash-preview)
|
|
4882
4986
|
// Our cost: ~$0.0005 USD per request => 0.05 USD cents; keep 2x markup.
|
|
@@ -4947,14 +5051,25 @@ var DEFAULT_PRICING = {
|
|
|
4947
5051
|
userPriceCents: 0.8
|
|
4948
5052
|
},
|
|
4949
5053
|
// single_call: flattens the approved plan into the one Grok render prompt
|
|
4950
|
-
// (gemini-3.6-flash, medium reasoning, 8k maxTokens).
|
|
4951
|
-
//
|
|
4952
|
-
//
|
|
4953
|
-
//
|
|
4954
|
-
//
|
|
5054
|
+
// (gemini-3.6-flash, medium reasoning, 8k maxTokens).
|
|
5055
|
+
//
|
|
5056
|
+
// RE-DERIVED 2026-08-01 from 860 prod rows (July 2026, still-paper-34632546):
|
|
5057
|
+
// mean 4.573¢ our, p50 4.674¢, p95 6.205¢. The previous 2¢ was inferred by
|
|
5058
|
+
// ANALOGY from `rapid-grok-shot-action` (2.11¢) rather than measured on this
|
|
5059
|
+
// label, and was 2.3x too low — real calls bill the actual usage.cost x2, so
|
|
5060
|
+
// the money was always collected correctly, but every QUOTE understated this
|
|
5061
|
+
// line by that factor. 5¢ sits above the mean and the median with headroom,
|
|
5062
|
+
// below the p95.
|
|
5063
|
+
//
|
|
5064
|
+
// ⚠️ This is a PER-CALL rate and the flatten is NOT called once per render:
|
|
5065
|
+
// measured 4.34 calls per (task, angle) on average, because a structurally
|
|
5066
|
+
// valid prompt that exceeds Grok's 4090-BYTE ceiling is regenerated shorter
|
|
5067
|
+
// (up to MAX_FLATTEN_ATTEMPTS). The estimator multiplies this rate by that cap.
|
|
5068
|
+
// Reducing the CALL COUNT is the real cost lever here, not this rate — see the
|
|
5069
|
+
// note on the flatten line in cost-estimator.service.ts.
|
|
4955
5070
|
"openrouter:single-call-flatten": {
|
|
4956
|
-
ourCostCents:
|
|
4957
|
-
userPriceCents:
|
|
5071
|
+
ourCostCents: 5,
|
|
5072
|
+
userPriceCents: 10
|
|
4958
5073
|
},
|
|
4959
5074
|
"openrouter:hand-detector": {
|
|
4960
5075
|
ourCostCents: 0.15,
|
|
@@ -4973,8 +5088,8 @@ var DEFAULT_PRICING = {
|
|
|
4973
5088
|
userPriceCents: 0.8
|
|
4974
5089
|
},
|
|
4975
5090
|
"openrouter:wearable-product-semantics": {
|
|
4976
|
-
ourCostCents: 0.
|
|
4977
|
-
userPriceCents: 0.
|
|
5091
|
+
ourCostCents: 0.11,
|
|
5092
|
+
userPriceCents: 0.22
|
|
4978
5093
|
},
|
|
4979
5094
|
// Clip Judge — text-only comparison of QA results between original and retry clips
|
|
4980
5095
|
// (google/gemini-3-flash-preview, text-only). Our cost: ~$0.0006 => 0.06 cents; 2x markup.
|
|
@@ -4991,14 +5106,14 @@ var DEFAULT_PRICING = {
|
|
|
4991
5106
|
// Image Judge — text-only comparison of QA results between image candidates
|
|
4992
5107
|
// (google/gemini-3-flash-preview, text-only). Our cost: ~$0.0006 => 0.06 cents; 2x markup.
|
|
4993
5108
|
"openrouter:image-judge-text": {
|
|
4994
|
-
ourCostCents: 0.
|
|
4995
|
-
userPriceCents: 0.
|
|
5109
|
+
ourCostCents: 0.39,
|
|
5110
|
+
userPriceCents: 0.78
|
|
4996
5111
|
},
|
|
4997
5112
|
// Image Judge — vision comparison of two images + product reference (google/gemini-3-flash-preview)
|
|
4998
|
-
//
|
|
5113
|
+
// Measured 0.8863¢ over 30d of prod (the old ~0.15¢ predated the multi-image comparison).
|
|
4999
5114
|
"openrouter:image-judge-vision": {
|
|
5000
|
-
ourCostCents: 0.
|
|
5001
|
-
userPriceCents:
|
|
5115
|
+
ourCostCents: 0.89,
|
|
5116
|
+
userPriceCents: 1.78
|
|
5002
5117
|
},
|
|
5003
5118
|
// Video QA — product integrity scanner (Gemini 3 Flash Preview, 5 frames + reference).
|
|
5004
5119
|
"openrouter:video-product-integrity": {
|
|
@@ -5008,10 +5123,19 @@ var DEFAULT_PRICING = {
|
|
|
5008
5123
|
// Video QA — anatomy. THREE keys, and they do NOT share a unit. Getting this wrong
|
|
5009
5124
|
// over-charges by ~42x, which is exactly what a first pass at the 2026-07-30 change did.
|
|
5010
5125
|
//
|
|
5011
|
-
// (a) The arm-scan's per-FRAME counting sweep.
|
|
5012
|
-
//
|
|
5013
|
-
//
|
|
5014
|
-
//
|
|
5126
|
+
// (a) The arm-scan's per-FRAME counting sweep. RETIRED 2026-07-30 — it has NO producer left.
|
|
5127
|
+
// Its last call site was the whole-video concat gate, removed as redundant with the
|
|
5128
|
+
// per-scene frame judge (b). It was ALSO duration-blind on `double_30s` — the sweep
|
|
5129
|
+
// samples t=0,1,2… capped at 16, so a 30s assembled video was only partly read while all
|
|
5130
|
+
// 16 frames still billed — but NOT on `single_15s` (~15s, covered completely), which is
|
|
5131
|
+
// the dominant format. Redundancy is what carries the removal; the blindness is secondary.
|
|
5132
|
+
// The estimator no longer quotes it either.
|
|
5133
|
+
// The KEY STAYS DEFINED ON PURPOSE. `BillingService.record` treats an unknown key as
|
|
5134
|
+
// "No pricing rule found; skipping" and bills NOTHING — that silent hole already cost us
|
|
5135
|
+
// 10,303 unbilled calls before 2026-07-18. `scanClipArms` is still exported, so if
|
|
5136
|
+
// anything ever calls it again it must land on a rate, not on the skip branch.
|
|
5137
|
+
// Rate stays PER FRAME, sized above the
|
|
5138
|
+
// 30d prod average (10303 rows => 0.132c user/frame).
|
|
5015
5139
|
"openrouter:video-anatomy-arm-scan": {
|
|
5016
5140
|
ourCostCents: 0.1,
|
|
5017
5141
|
userPriceCents: 0.2
|
|
@@ -5077,10 +5201,11 @@ var DEFAULT_PRICING = {
|
|
|
5077
5201
|
ourCostCents: 1,
|
|
5078
5202
|
userPriceCents: 2
|
|
5079
5203
|
},
|
|
5080
|
-
// LLM image ranking (
|
|
5204
|
+
// LLM image ranking (gemini-3-flash-preview, ~10 images multi-modal). Unit = one CANDIDATE
|
|
5205
|
+
// image, not one call, so the per-unit rate is small: measured 0.0924¢/unit.
|
|
5081
5206
|
"openrouter:image-ranking": {
|
|
5082
|
-
ourCostCents: 0.
|
|
5083
|
-
userPriceCents: 0.
|
|
5207
|
+
ourCostCents: 0.09,
|
|
5208
|
+
userPriceCents: 0.18
|
|
5084
5209
|
},
|
|
5085
5210
|
// Visual context pass — frame0 description (Gemini 3 Flash Preview, 1 image)
|
|
5086
5211
|
"openrouter:visual-context-frame0-describe": {
|
|
@@ -5169,10 +5294,50 @@ var DEFAULT_PRICING = {
|
|
|
5169
5294
|
ourCostCents: 1,
|
|
5170
5295
|
userPriceCents: 2
|
|
5171
5296
|
},
|
|
5172
|
-
// Product research
|
|
5173
|
-
//
|
|
5297
|
+
// ─── Product research (winning products) ─────────────────────────────────
|
|
5298
|
+
// BUNDLE, billed as two rows against the same `productResearchRunId`:
|
|
5299
|
+
// revenue → `system:product-research-report` = 200¢ / 2,000 credits, charged
|
|
5300
|
+
// UP FRONT by purchaseProductResearchRun (which also debits
|
|
5301
|
+
// User.creditCents in the same transaction), and reversed by a
|
|
5302
|
+
// `system:product-research-failure-refund` row when the run fails.
|
|
5303
|
+
// cost → `kalodata:product-research-source` (+ `openrouter:*` analysis),
|
|
5304
|
+
// recorded AFTER the provider answers, with creditOperation
|
|
5305
|
+
// 'ledger_only' — the user was already debited.
|
|
5306
|
+
//
|
|
5307
|
+
// ⚠️ `userPriceCents: 0` on the kalodata row is DELIBERATE, not a missing
|
|
5308
|
+
// charge. Revenue cannot live here: the cost row is written after the
|
|
5309
|
+
// provider bills us, while the price is committed at purchase time, and a
|
|
5310
|
+
// run that fails post-source is refunded in full while its kalodata cost
|
|
5311
|
+
// stands. Any per-provider markup report that divides price÷cost will
|
|
5312
|
+
// therefore show kalodata (and the openrouter product-research labels) at
|
|
5313
|
+
// 0.00×. The cost row carries `bundledRevenue*` metadata so the pairing is
|
|
5314
|
+
// discoverable from the row itself.
|
|
5315
|
+
//
|
|
5316
|
+
// ⚠️ Two traps in aggregating this bundle, both measured on the 27 runs to
|
|
5317
|
+
// 2026-08-01 — read before writing a P&L query:
|
|
5318
|
+
// 1. Refunds are stored as a POSITIVE `userPriceCents` on
|
|
5319
|
+
// `system:product-research-failure-refund` (metadata.target 'refund',
|
|
5320
|
+
// not 'product_research'). Grouping on target='product_research' counts
|
|
5321
|
+
// the purchase and drops its reversal; summing userPriceCents across
|
|
5322
|
+
// the run double-counts instead of netting. Retained revenue is
|
|
5323
|
+
// Σ(label='product-research-report') − Σ(label='product-research-failure-refund').
|
|
5324
|
+
// For those 27 runs that is $54 charged − $30 refunded = $24 kept.
|
|
5325
|
+
// 2. A cost row is per PROVIDER CALL, not per run: `getFreshProductResearchSource`
|
|
5326
|
+
// serves repeat queries from the source cache, so a charged run can have
|
|
5327
|
+
// no kalodata row at all (12 of the 27 did). `bundledRevenueCents` is
|
|
5328
|
+
// therefore context for THIS row — never a revenue total to sum.
|
|
5329
|
+
// Total COGS is Σ ourCostCents over every row with a non-null
|
|
5330
|
+
// productResearchRunId, which needs no per-provider grouping at all.
|
|
5331
|
+
//
|
|
5332
|
+
// The source worker overrides ourCostCents with the exact reported usage
|
|
5333
|
+
// (providerCredits × KALODATA_USD_CENTS_PER_PROVIDER_CREDIT). Nothing reads
|
|
5334
|
+
// this entry today — no cost-estimator key and no credit gate resolves it,
|
|
5335
|
+
// since the gate is a flat `creditCents >= 200` in
|
|
5336
|
+
// purchaseProductResearchRun. It is kept as the audited reference rate so
|
|
5337
|
+
// the estimate stops contradicting the ledger the moment something does
|
|
5338
|
+
// quote from it.
|
|
5174
5339
|
"kalodata:product-research-source": {
|
|
5175
|
-
ourCostCents:
|
|
5340
|
+
ourCostCents: KALODATA_PROVIDER_CREDITS_PER_SOURCE_CEILING * KALODATA_USD_CENTS_PER_PROVIDER_CREDIT,
|
|
5176
5341
|
userPriceCents: 0
|
|
5177
5342
|
},
|
|
5178
5343
|
"system:product-research-report": {
|
|
@@ -5280,9 +5445,9 @@ var DEFAULT_PRICING = {
|
|
|
5280
5445
|
// here (0.0653¢ our/frame) is LOWER, so the prod-derived rate stands per the
|
|
5281
5446
|
// quote-at-the-ceiling rule. Do not re-add a second key: TS1117 duplicate.
|
|
5282
5447
|
// Unit = one winner-selection pass over the scraped ad pool (measured 0.3744¢).
|
|
5283
|
-
// ⚠️ NOT once per scan any more: the deep-capture candidate fallback re-runs
|
|
5284
|
-
// selector for each retry,
|
|
5285
|
-
//
|
|
5448
|
+
// ⚠️ NOT once per scan any more: the deep-capture candidate fallback re-runs the
|
|
5449
|
+
// video selector for each retry, and static replication independently selects
|
|
5450
|
+
// one image winner. The estimate quotes GAP_DEEP_CAPTURE_MAX_CANDIDATES + 1.
|
|
5286
5451
|
"openrouter:gap-winner-selection": {
|
|
5287
5452
|
ourCostCents: 0.4,
|
|
5288
5453
|
userPriceCents: 0.8
|
|
@@ -5294,6 +5459,13 @@ var DEFAULT_PRICING = {
|
|
|
5294
5459
|
ourCostCents: 2.5,
|
|
5295
5460
|
userPriceCents: 5
|
|
5296
5461
|
},
|
|
5462
|
+
// Unit = one independent multimodal argument extraction for the selected
|
|
5463
|
+
// static-image winner. Quote at the blueprint ceiling until BillingEntry has
|
|
5464
|
+
// enough production volume to establish a narrower measured rate.
|
|
5465
|
+
"openrouter:competitor-image-winner-argument": {
|
|
5466
|
+
ourCostCents: 2.5,
|
|
5467
|
+
userPriceCents: 5
|
|
5468
|
+
},
|
|
5297
5469
|
// Unit = one multimodal temporal observation of the captured winner MP4.
|
|
5298
5470
|
// Includes the bounded full video plus measured frame samples and high
|
|
5299
5471
|
// reasoning. Unmeasured at launch, so quote at 2x the keyframe-only blueprint
|
|
@@ -5312,6 +5484,19 @@ var DEFAULT_PRICING = {
|
|
|
5312
5484
|
ourCostCents: 0.2,
|
|
5313
5485
|
userPriceCents: 0.4
|
|
5314
5486
|
},
|
|
5487
|
+
// Unit = one product-use SHEET derivation: Flash reads the product photos + the seller's raw
|
|
5488
|
+
// text and writes the three lines the video QA judge needs to rule on product MISUSE. Same
|
|
5489
|
+
// model, same effort and the same image budget as the exposure classifier directly above, so
|
|
5490
|
+
// it is priced at that rate rather than re-measured on n=1. Sized ABOVE the measured campaign
|
|
5491
|
+
// cost of the equivalent call (~2.4¢ USER for the sheet+judge PAIR, of which the sheet is the
|
|
5492
|
+
// smaller half) because this table is only the fallback for when OpenRouter omits usage.cost.
|
|
5493
|
+
// ONCE PER TASK, not per clip and not per render attempt: the sheet is derived at ingest and
|
|
5494
|
+
// every QA call on that task reads the stored copy. Quoting it per scene would over-quote a
|
|
5495
|
+
// 3-scene single by 3x and inflate the credit gate for spend that never happens.
|
|
5496
|
+
"openrouter:product-use-sheet": {
|
|
5497
|
+
ourCostCents: 0.2,
|
|
5498
|
+
userPriceCents: 0.4
|
|
5499
|
+
},
|
|
5315
5500
|
// Unit = one reference-grouping pass over the user's product images (0.1241¢).
|
|
5316
5501
|
"openrouter:product-reference-grouping": {
|
|
5317
5502
|
ourCostCents: 0.15,
|
|
@@ -5359,6 +5544,68 @@ var DEFAULT_PRICING = {
|
|
|
5359
5544
|
ourCostCents: 1,
|
|
5360
5545
|
userPriceCents: 2
|
|
5361
5546
|
},
|
|
5547
|
+
// ── Rapid / single_call pipeline: keys that did NOT exist until 2026-08-01 ──
|
|
5548
|
+
//
|
|
5549
|
+
// These labels bill real money every day but had NO entry here, so whenever
|
|
5550
|
+
// OpenRouter omitted `usage.cost` the fallback resolved to `undefined` and the
|
|
5551
|
+
// call was recorded at ZERO. `rapid-plan` alone is the single most expensive
|
|
5552
|
+
// OpenRouter label we have ($116.60 / 30d). The rates below are MEASURED, not
|
|
5553
|
+
// guessed: SUM(ourCostCents)/SUM(units) over 30 days of prod BillingEntry
|
|
5554
|
+
// (neon still-paper-34632546, queried 2026-08-01), at the standard 2x markup.
|
|
5555
|
+
//
|
|
5556
|
+
// None of these keys is read by cost-estimator.service.ts — the rapid pipeline
|
|
5557
|
+
// quotes them through the calibrated STANDARD_PIPELINE_OVERHEAD_CENTS blob — so
|
|
5558
|
+
// adding them changes the billing FALLBACK only and cannot move any quote.
|
|
5559
|
+
// Unit = one rapid plan authoring call (measured 4.3968¢).
|
|
5560
|
+
"openrouter:rapid-plan": {
|
|
5561
|
+
ourCostCents: 4.4,
|
|
5562
|
+
userPriceCents: 8.8
|
|
5563
|
+
},
|
|
5564
|
+
// Unit = one rapid script blueprint call (measured 4.2748¢).
|
|
5565
|
+
"openrouter:rapid-script-blueprint": {
|
|
5566
|
+
ourCostCents: 4.28,
|
|
5567
|
+
userPriceCents: 8.56
|
|
5568
|
+
},
|
|
5569
|
+
// Unit = one whole-video standard QA pass (measured 2.4104¢).
|
|
5570
|
+
"openrouter:video-qa-standard": {
|
|
5571
|
+
ourCostCents: 2.41,
|
|
5572
|
+
userPriceCents: 4.82
|
|
5573
|
+
},
|
|
5574
|
+
// Unit = one frame0 anchor compose (measured 0.7351¢).
|
|
5575
|
+
"openrouter:frame0-anchor-compose": {
|
|
5576
|
+
ourCostCents: 0.74,
|
|
5577
|
+
userPriceCents: 1.48
|
|
5578
|
+
},
|
|
5579
|
+
// Unit = one voiceover↔visual alignment pass (measured 0.9092¢).
|
|
5580
|
+
"openrouter:rapid-voiceover-visual-alignment": {
|
|
5581
|
+
ourCostCents: 0.91,
|
|
5582
|
+
userPriceCents: 1.82
|
|
5583
|
+
},
|
|
5584
|
+
// Unit = one rapid plan character/setting pass (measured 0.5547¢).
|
|
5585
|
+
"openrouter:rapid-plan-character": {
|
|
5586
|
+
ourCostCents: 0.55,
|
|
5587
|
+
userPriceCents: 1.1
|
|
5588
|
+
},
|
|
5589
|
+
// Unit = one segment naturalization pass (measured 0.5470¢).
|
|
5590
|
+
"openrouter:rapid-naturalize": {
|
|
5591
|
+
ourCostCents: 0.55,
|
|
5592
|
+
userPriceCents: 1.1
|
|
5593
|
+
},
|
|
5594
|
+
// Unit = one rapid script validation pass (measured 0.3704¢).
|
|
5595
|
+
"openrouter:rapid-script-validation": {
|
|
5596
|
+
ourCostCents: 0.37,
|
|
5597
|
+
userPriceCents: 0.74
|
|
5598
|
+
},
|
|
5599
|
+
// Unit = one voiceover shortening pass (measured 0.3081¢).
|
|
5600
|
+
"openrouter:rapid-voiceover-shorten": {
|
|
5601
|
+
ourCostCents: 0.31,
|
|
5602
|
+
userPriceCents: 0.62
|
|
5603
|
+
},
|
|
5604
|
+
// Unit = one POV judge call on the rapid plan (measured 0.0769¢).
|
|
5605
|
+
"openrouter:rapid-plan-pov-judge": {
|
|
5606
|
+
ourCostCents: 0.08,
|
|
5607
|
+
userPriceCents: 0.16
|
|
5608
|
+
},
|
|
5362
5609
|
// Unit = one ffmpeg mix laying the replicated winner's own audio track under our
|
|
5363
5610
|
// voiceover, producing the extra `ad_final_with_winner_music` deliverable
|
|
5364
5611
|
// (gap_replication only, behind GAP_WINNER_MUSIC_ENABLED, default OFF). No AI
|
|
@@ -6539,9 +6786,107 @@ var LandingConfigSchema = z35.object({
|
|
|
6539
6786
|
*/
|
|
6540
6787
|
showVariantPicker: z35.boolean().optional()
|
|
6541
6788
|
});
|
|
6789
|
+
var FreeformSlotTypeSchema = z35.enum([
|
|
6790
|
+
"text",
|
|
6791
|
+
"image",
|
|
6792
|
+
"price",
|
|
6793
|
+
"cta",
|
|
6794
|
+
"list"
|
|
6795
|
+
]);
|
|
6796
|
+
var FreeformSlotSchema = z35.object({
|
|
6797
|
+
type: FreeformSlotTypeSchema,
|
|
6798
|
+
/** Human label shown in the editor ("Título", "Imagen principal"). */
|
|
6799
|
+
label: z35.string().min(1).max(60),
|
|
6800
|
+
/** text/price/cta: display text. image: absolute URL. list: unused (see items). */
|
|
6801
|
+
value: z35.string().max(2e3),
|
|
6802
|
+
/** Only for type 'list'. */
|
|
6803
|
+
items: z35.array(z35.string().max(300)).max(12).optional()
|
|
6804
|
+
});
|
|
6805
|
+
var FreeformSectionSchema = z35.object({
|
|
6806
|
+
id: z35.string().regex(/^[a-z0-9][a-z0-9_-]{0,39}$/),
|
|
6807
|
+
label: z35.string().min(1).max(60),
|
|
6808
|
+
/** Sanitized free markup with {{slot:key}} markers and data-cl-* attributes. */
|
|
6809
|
+
layoutHtml: z35.string().min(1).max(16e3),
|
|
6810
|
+
slots: z35.record(z35.string().regex(/^[a-zA-Z0-9_]{1,40}$/), FreeformSlotSchema),
|
|
6811
|
+
/** Section CSS, selectors prefixed with [data-cl-s="<id>"]. */
|
|
6812
|
+
css: z35.string().max(8e3).optional()
|
|
6813
|
+
});
|
|
6814
|
+
var DesignSystemSchema = z35.object({
|
|
6815
|
+
/** Same 4 roles as v1 preset — they feed the --cl-* CSS vars the buy form uses. */
|
|
6816
|
+
colors: z35.object({
|
|
6817
|
+
primary: HexColorSchema,
|
|
6818
|
+
bg: HexColorSchema,
|
|
6819
|
+
text: HexColorSchema,
|
|
6820
|
+
accent: HexColorSchema
|
|
6821
|
+
}),
|
|
6822
|
+
fonts: z35.object({
|
|
6823
|
+
heading: FontNameSchema,
|
|
6824
|
+
body: FontNameSchema
|
|
6825
|
+
}),
|
|
6826
|
+
radius: z35.string().regex(/^\d{1,2}(px|rem|em)$/),
|
|
6827
|
+
/** Prompt-only vocabulary ("brutalist", "editorial"); not rendered. */
|
|
6828
|
+
styleKeywords: z35.array(z35.string().max(40)).max(8).default([])
|
|
6829
|
+
});
|
|
6830
|
+
var FreeformLandingConfigSchema = z35.object({
|
|
6831
|
+
version: z35.literal(2),
|
|
6832
|
+
themeVersion: z35.literal(2),
|
|
6833
|
+
locale: z35.string().min(2),
|
|
6834
|
+
designSystem: DesignSystemSchema,
|
|
6835
|
+
globalCss: z35.string().max(2e4).default(""),
|
|
6836
|
+
sections: z35.array(FreeformSectionSchema).min(1).max(14),
|
|
6837
|
+
/** Buy form is inserted AFTER this section (assembler adds BUY_FORM_MARKER). */
|
|
6838
|
+
buyFormAfterSectionId: z35.string(),
|
|
6839
|
+
/** Assembled + sanitized final HTML — source of truth for render/publish. */
|
|
6840
|
+
html: z35.string().min(1),
|
|
6841
|
+
product: LandingProductSchema,
|
|
6842
|
+
showVariantPicker: z35.boolean().optional(),
|
|
6843
|
+
/**
|
|
6844
|
+
* Suppress the native Shopify buy form (the `BUY_FORM_MARKER` block). The page
|
|
6845
|
+
* CTAs already carry `data-cl-buy-trigger` → they open the ReleaseIt COD popup,
|
|
6846
|
+
* so on ReleaseIt-only stores the native add-to-cart form is a redundant second
|
|
6847
|
+
* buy path (it rendered as a bare "price + Comprar" block). When true the
|
|
6848
|
+
* assembler omits the marker entirely. Undefined/false = keep the native form
|
|
6849
|
+
* (backward compatible with already-assembled configs).
|
|
6850
|
+
*/
|
|
6851
|
+
hideNativeBuyForm: z35.boolean().optional()
|
|
6852
|
+
});
|
|
6853
|
+
var AnyLandingConfigSchema = z35.union([
|
|
6854
|
+
FreeformLandingConfigSchema,
|
|
6855
|
+
LandingConfigSchema
|
|
6856
|
+
]);
|
|
6542
6857
|
var UpdateLandingConfigResponse = z35.object({
|
|
6543
6858
|
ok: z35.boolean()
|
|
6544
6859
|
});
|
|
6860
|
+
var CreateLandingEditRequest = z35.object({
|
|
6861
|
+
message: z35.string().min(3).max(1e3),
|
|
6862
|
+
sectionId: z35.string().optional()
|
|
6863
|
+
});
|
|
6864
|
+
var LandingEditDto = z35.object({
|
|
6865
|
+
id: z35.string(),
|
|
6866
|
+
scope: z35.enum([
|
|
6867
|
+
"section",
|
|
6868
|
+
"full"
|
|
6869
|
+
]),
|
|
6870
|
+
sectionId: z35.string().nullable(),
|
|
6871
|
+
message: z35.string(),
|
|
6872
|
+
status: z35.enum([
|
|
6873
|
+
"pending",
|
|
6874
|
+
"processing",
|
|
6875
|
+
"applied",
|
|
6876
|
+
"failed",
|
|
6877
|
+
"reverted"
|
|
6878
|
+
]),
|
|
6879
|
+
costCents: z35.number(),
|
|
6880
|
+
error: z35.string().nullable(),
|
|
6881
|
+
createdAt: z35.string()
|
|
6882
|
+
});
|
|
6883
|
+
var CreateLandingEditResponse = z35.object({
|
|
6884
|
+
edit: LandingEditDto,
|
|
6885
|
+
chargedCredits: z35.number()
|
|
6886
|
+
});
|
|
6887
|
+
var ListLandingEditsResponse = z35.object({
|
|
6888
|
+
edits: z35.array(LandingEditDto)
|
|
6889
|
+
});
|
|
6545
6890
|
|
|
6546
6891
|
// ../../packages/types/src/video-edit.ts
|
|
6547
6892
|
import { z as z36 } from "zod";
|
|
@@ -6837,12 +7182,17 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
|
|
|
6837
7182
|
{
|
|
6838
7183
|
name: "create_creative_variant",
|
|
6839
7184
|
confirmation: "credit_consuming",
|
|
6840
|
-
schemaHash: "
|
|
7185
|
+
schemaHash: "6a777fbe7ad17966d16502e8872e15e4db2dcfacdfd2992d24853340d4f22a00"
|
|
6841
7186
|
},
|
|
6842
7187
|
{
|
|
6843
7188
|
name: "create_product_task",
|
|
6844
7189
|
confirmation: "credit_consuming",
|
|
6845
|
-
schemaHash: "
|
|
7190
|
+
schemaHash: "3f03c4886f65a030e03a531c02e0ef879aaf3f5820e90ab3ca04a02350e693e6"
|
|
7191
|
+
},
|
|
7192
|
+
{
|
|
7193
|
+
name: "edit_image_creative",
|
|
7194
|
+
confirmation: "credit_consuming",
|
|
7195
|
+
schemaHash: "ce0abba871436850bd1db0ea4bcdade9ef35420ebd83833e2d5c2dcf70378ec0"
|
|
6846
7196
|
},
|
|
6847
7197
|
{
|
|
6848
7198
|
name: "edit_task_draft",
|
|
@@ -6852,7 +7202,7 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
|
|
|
6852
7202
|
{
|
|
6853
7203
|
name: "estimate_creative_variant",
|
|
6854
7204
|
confirmation: "none",
|
|
6855
|
-
schemaHash: "
|
|
7205
|
+
schemaHash: "d4453364c0706bf88f480cebc977916f06d222d7eebebc9cfa91165929a1a998"
|
|
6856
7206
|
},
|
|
6857
7207
|
{
|
|
6858
7208
|
name: "find_winning_products",
|
|
@@ -6867,17 +7217,17 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
|
|
|
6867
7217
|
{
|
|
6868
7218
|
name: "generate_image_creatives",
|
|
6869
7219
|
confirmation: "credit_consuming",
|
|
6870
|
-
schemaHash: "
|
|
7220
|
+
schemaHash: "bf6a7599dac65c31a43e23badad8fd8e1ab3e8aee8a5a55ac47f9c39268b2fa6"
|
|
6871
7221
|
},
|
|
6872
7222
|
{
|
|
6873
7223
|
name: "generate_landing",
|
|
6874
7224
|
confirmation: "credit_consuming",
|
|
6875
|
-
schemaHash: "
|
|
7225
|
+
schemaHash: "15465f9bda1ec945cc49f4f4a830d56a17a43b0cb5f6063d5594ddc92c830c21"
|
|
6876
7226
|
},
|
|
6877
7227
|
{
|
|
6878
7228
|
name: "generate_video_creative",
|
|
6879
7229
|
confirmation: "credit_consuming",
|
|
6880
|
-
schemaHash: "
|
|
7230
|
+
schemaHash: "c80208df75f2c7cbc459e9ae494a75a0a5f5bc4cf044b3fc9ef9c0e9af2c2452"
|
|
6881
7231
|
},
|
|
6882
7232
|
{
|
|
6883
7233
|
name: "get_meta_campaign_status",
|
|
@@ -7238,6 +7588,7 @@ var VideoPresetEnum = z39.enum([
|
|
|
7238
7588
|
"ugc_dynamic",
|
|
7239
7589
|
"ugc_how_to",
|
|
7240
7590
|
"ugc_unboxing_chaos",
|
|
7591
|
+
"problem_solution",
|
|
7241
7592
|
"product_showcase",
|
|
7242
7593
|
"wearable_showcase",
|
|
7243
7594
|
"cinematic",
|
|
@@ -7249,6 +7600,7 @@ var TaskCreativeVariantVideoPresetEnum = z39.enum([
|
|
|
7249
7600
|
"auto",
|
|
7250
7601
|
"ugc_dynamic",
|
|
7251
7602
|
"ugc_how_to",
|
|
7603
|
+
"problem_solution",
|
|
7252
7604
|
"product_showcase",
|
|
7253
7605
|
"cinematic",
|
|
7254
7606
|
"wild_card",
|
|
@@ -7274,6 +7626,11 @@ var VIDEO_PRESET_RESOLUTION = {
|
|
|
7274
7626
|
narrativeStyle: "ugc_unboxing_chaos",
|
|
7275
7627
|
videoModel: "veo"
|
|
7276
7628
|
},
|
|
7629
|
+
problem_solution: {
|
|
7630
|
+
videoStyle: "ai_dynamic",
|
|
7631
|
+
narrativeStyle: "problem_product_cta",
|
|
7632
|
+
videoModel: "kling"
|
|
7633
|
+
},
|
|
7277
7634
|
pixar_problem: {
|
|
7278
7635
|
videoStyle: "ai_dynamic",
|
|
7279
7636
|
narrativeStyle: "animated_problem_pixar",
|
|
@@ -7321,6 +7678,12 @@ function resolveWireVideoTier(tier, durationSeconds) {
|
|
|
7321
7678
|
return durationSeconds === 30 ? "standard" : resolveVideoTier(tier);
|
|
7322
7679
|
}
|
|
7323
7680
|
__name(resolveWireVideoTier, "resolveWireVideoTier");
|
|
7681
|
+
function assertVideoPresetDurationCompatibility(videoPreset, durationSeconds) {
|
|
7682
|
+
if (videoPreset === "problem_solution" && durationSeconds !== 30) {
|
|
7683
|
+
throw new Error("Problema y Soluci\xF3n solo est\xE1 disponible en videos Multiescena de 30 segundos.");
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
__name(assertVideoPresetDurationCompatibility, "assertVideoPresetDurationCompatibility");
|
|
7324
7687
|
function resolveVideoFormatForDuration(durationSeconds) {
|
|
7325
7688
|
return durationSeconds === 30 ? "double_30s" : "single_15s";
|
|
7326
7689
|
}
|
|
@@ -7356,6 +7719,7 @@ var PublicNarrativeStyleEnum = z39.enum([
|
|
|
7356
7719
|
"review",
|
|
7357
7720
|
"ugc_how_to",
|
|
7358
7721
|
"ugc_unboxing_chaos",
|
|
7722
|
+
"problem_product_cta",
|
|
7359
7723
|
"product_showcase",
|
|
7360
7724
|
"wearable_showcase",
|
|
7361
7725
|
"cinematic",
|
|
@@ -8038,6 +8402,7 @@ function proxifyOutputs(outputs, proxyBaseUrl) {
|
|
|
8038
8402
|
}) ?? url, "rewrite");
|
|
8039
8403
|
return {
|
|
8040
8404
|
webappUrl: outputs.webappUrl,
|
|
8405
|
+
adCopy: outputs.adCopy,
|
|
8041
8406
|
images: outputs.images.map((img, i) => ({
|
|
8042
8407
|
...img,
|
|
8043
8408
|
url: rewrite(img.url, `image-${i}.png`)
|
|
@@ -8105,7 +8470,8 @@ async function fetchTaskOutputs(api, taskId) {
|
|
|
8105
8470
|
landing: {
|
|
8106
8471
|
sections
|
|
8107
8472
|
},
|
|
8108
|
-
images
|
|
8473
|
+
images,
|
|
8474
|
+
adCopy: typeof raw.adCreative?.adCopy === "string" && raw.adCreative.adCopy.length ? raw.adCreative.adCopy : void 0
|
|
8109
8475
|
};
|
|
8110
8476
|
}
|
|
8111
8477
|
__name(fetchTaskOutputs, "fetchTaskOutputs");
|
|
@@ -8120,6 +8486,10 @@ function renderOutputsMarkdown(outputs) {
|
|
|
8120
8486
|
parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
|
|
8121
8487
|
});
|
|
8122
8488
|
}
|
|
8489
|
+
if (outputs.adCopy) {
|
|
8490
|
+
parts.push("\n### Ad copy");
|
|
8491
|
+
parts.push(outputs.adCopy);
|
|
8492
|
+
}
|
|
8123
8493
|
if (outputs.landing.sections.length) {
|
|
8124
8494
|
parts.push(`
|
|
8125
8495
|
### Landing (${outputs.landing.sections.length} secciones)`);
|
|
@@ -8153,7 +8523,7 @@ var CreateProductTaskInput = z40.object({
|
|
|
8153
8523
|
paymentMethods: z40.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
|
|
8154
8524
|
productContext: z40.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
|
|
8155
8525
|
productLabel: z40.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
|
|
8156
|
-
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto).
|
|
8526
|
+
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s y 30s, excepto "problem_solution", que requiere 30s Multiescena. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
|
|
8157
8527
|
durationSeconds: z40.union([
|
|
8158
8528
|
z40.literal(15),
|
|
8159
8529
|
z40.literal(30)
|
|
@@ -8162,7 +8532,7 @@ var CreateProductTaskInput = z40.object({
|
|
|
8162
8532
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
8163
8533
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
|
|
8164
8534
|
multiAngleVideos: z40.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
|
|
8165
|
-
angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_how_to", "ugc_unboxing_chaos", "product_showcase", "wearable_showcase", "cinematic", "wild_card", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
|
|
8535
|
+
angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_how_to", "ugc_unboxing_chaos", "problem_product_cta" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase", "wearable_showcase", "cinematic", "wild_card", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
|
|
8166
8536
|
outputs: z40.array(z40.enum([
|
|
8167
8537
|
"landing",
|
|
8168
8538
|
"video",
|
|
@@ -8171,6 +8541,8 @@ var CreateProductTaskInput = z40.object({
|
|
|
8171
8541
|
competitorScanEnabled: z40.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
|
|
8172
8542
|
brandId: z40.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer los outputs generados."),
|
|
8173
8543
|
creativeDirection: z40.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo) que aplica a los outputs de IMAGEN y LANDING (no al video \u2014 el estilo/ambiente del video se dirige con scriptNotes). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Ej. "amarillo y azul, minimalista". M\xE1x 500 caracteres.'),
|
|
8544
|
+
headline: z40.string().max(120).optional().describe("Opcional. Titular EXACTO a renderizar. Se imprime tal cual: la IA no lo reescribe, no lo acorta y no lo traduce. Se aplica a las IM\xC1GENES y a la portada de la landing de esta task (el video no lo usa; para el video usa scriptNotes). \xDAsalo cuando el copy es de marca y no es negociable. M\xE1x 120 caracteres. Si lo omites, la IA escribe el titular."),
|
|
8545
|
+
subheadline: z40.string().max(160).optional().describe("Opcional. Subt\xEDtulo EXACTO a renderizar, misma regla que headline. Independiente: puedes fijar solo el subt\xEDtulo. M\xE1x 160 caracteres."),
|
|
8174
8546
|
landingStrategy: LandingStrategyEnum.default("classic").describe('Opcional. Estructura de la landing cl\xE1sica cuando landing est\xE1 entre outputs: "classic" (estructura original probada y recomendada; default del primer rollout), "auto" (la IA decide; experimental), "pain_solution", "social_proof" (Landing Viral: UGC, WhatsApp, Instagram y evidencia social), "benefits_clarity", "offer_value" o "lifestyle_identity". Default "classic".'),
|
|
8175
8547
|
landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica cuando landing est\xE1 entre outputs: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".')
|
|
8176
8548
|
}).strict();
|
|
@@ -8337,6 +8709,7 @@ __name(resolveStatusTaskKind, "resolveStatusTaskKind");
|
|
|
8337
8709
|
function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
|
|
8338
8710
|
server.tool("create_product_task", "Crea una tarea de producto desde fotos o desde un proyecto guiado propio. Por defecto genera los 3 outputs (landing + video + image creatives) EN UN MISMO TASK. Devuelve un taskId.\n\nINPUTS REQUERIDOS: images (1-5 fotos del MISMO producto; la primera es la hero) O projectId si el proyecto ya tiene una imagen guardada; adem\xE1s language y country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son inputs de alto impacto en la landing y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto del chat: preg\xFAntale expl\xEDcitamente, incluso si est\xE1 en LATAM.\n\nSi env\xEDas projectId, debe ser el proyecto propio que el usuario acaba de costear; el backend resolver\xE1 la imagen privada y conservar\xE1 todos los controles de cr\xE9ditos/confirmaci\xF3n. Si el usuario adjunta images junto con projectId, esas fotos reemplazan la referencia solo para este task.\n\nANTES de invocar, DILE CLARAMENTE qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task.\n\nUsa SOLO valores declarados en los enums.", CreateProductTaskInput.shape, async (input) => {
|
|
8339
8711
|
try {
|
|
8712
|
+
assertVideoPresetDurationCompatibility(input.videoPreset, input.durationSeconds);
|
|
8340
8713
|
if (!input.images && !input.projectId) {
|
|
8341
8714
|
throw new Error("images es requerido salvo que env\xEDes projectId.");
|
|
8342
8715
|
}
|
|
@@ -8372,6 +8745,8 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
8372
8745
|
"options.competitorScanEnabled": input.competitorScanEnabled,
|
|
8373
8746
|
"options.brandId": input.brandId,
|
|
8374
8747
|
"options.creativeDirection": input.creativeDirection,
|
|
8748
|
+
"options.headline": input.headline,
|
|
8749
|
+
"options.subheadline": input.subheadline,
|
|
8375
8750
|
"options.landingStrategy": input.landingStrategy,
|
|
8376
8751
|
"options.landingImageFormat": input.landingImageFormat,
|
|
8377
8752
|
pauseForAngleReview: input.pauseForAngleReview,
|
|
@@ -8636,7 +9011,9 @@ var CreativeRefs = {
|
|
|
8636
9011
|
competitorScanEnabled: z41.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
|
|
8637
9012
|
brandId: z41.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer el creativo."),
|
|
8638
9013
|
projectId: z41.string().uuid().optional().describe("Opcional. ID de un proyecto guiado de dropshipping activo del usuario. Enlaza esta generaci\xF3n con su progreso y recompensas."),
|
|
8639
|
-
creativeDirection: z41.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Aplica SOLO a im\xE1genes y landing. Ej. im\xE1genes: "amarillo, azul y rojo, minimalista"; landing: "elegante, paleta fr\xEDa". Para video usa scriptNotes (ambiente/escenario). M\xE1x 500 caracteres.')
|
|
9014
|
+
creativeDirection: z41.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Aplica SOLO a im\xE1genes y landing. Ej. im\xE1genes: "amarillo, azul y rojo, minimalista"; landing: "elegante, paleta fr\xEDa". Para video usa scriptNotes (ambiente/escenario). M\xE1x 500 caracteres.'),
|
|
9015
|
+
headline: z41.string().max(120).optional().describe("Opcional. Titular EXACTO a renderizar en la imagen (o en la portada de la landing). Se imprime tal cual: la IA no lo reescribe, no lo acorta y no lo traduce. \xDAsalo cuando el copy es de marca y no es negociable. M\xE1x 120 caracteres. Si lo omites, la IA escribe el titular."),
|
|
9016
|
+
subheadline: z41.string().max(160).optional().describe("Opcional. Subt\xEDtulo EXACTO a renderizar, misma regla que headline. Independiente: puedes fijar solo el subt\xEDtulo. M\xE1x 160 caracteres.")
|
|
8640
9017
|
};
|
|
8641
9018
|
var VideoInput = z41.object({
|
|
8642
9019
|
images: CreativeRefs.images,
|
|
@@ -8653,7 +9030,7 @@ var VideoInput = z41.object({
|
|
|
8653
9030
|
z41.literal(15),
|
|
8654
9031
|
z41.literal(30)
|
|
8655
9032
|
]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
8656
|
-
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto).
|
|
9033
|
+
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s y 30s, excepto "problem_solution", que requiere 30s Multiescena. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla).'),
|
|
8657
9034
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista"; para durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
8658
9035
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
|
|
8659
9036
|
features: z41.array(z41.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
|
|
@@ -8732,7 +9109,14 @@ var ImageInput = z41.object({
|
|
|
8732
9109
|
competitorScanEnabled: CreativeRefs.competitorScanEnabled,
|
|
8733
9110
|
brandId: CreativeRefs.brandId,
|
|
8734
9111
|
projectId: CreativeRefs.projectId,
|
|
8735
|
-
creativeDirection: CreativeRefs.creativeDirection
|
|
9112
|
+
creativeDirection: CreativeRefs.creativeDirection,
|
|
9113
|
+
headline: CreativeRefs.headline,
|
|
9114
|
+
subheadline: CreativeRefs.subheadline
|
|
9115
|
+
});
|
|
9116
|
+
var EditImageCreativeInput = z41.object({
|
|
9117
|
+
taskId: z41.string().uuid().describe("ID de la tarea de imagen ya finalizada."),
|
|
9118
|
+
variantIndex: z41.number().int().min(0).max(10).describe("\xCDndice de la variante a corregir (0 = primera imagen)."),
|
|
9119
|
+
instruction: z41.string().min(1).max(1e3).describe('Qu\xE9 cambiar en esa imagen. Ej. "quita el badge de env\xEDo gratis", "cambia el fondo a blanco".')
|
|
8736
9120
|
});
|
|
8737
9121
|
var LandingInput = z41.object({
|
|
8738
9122
|
images: CreativeRefs.images,
|
|
@@ -8752,7 +9136,9 @@ var LandingInput = z41.object({
|
|
|
8752
9136
|
projectId: CreativeRefs.projectId,
|
|
8753
9137
|
creativeDirection: CreativeRefs.creativeDirection,
|
|
8754
9138
|
landingStrategy: LandingStrategyEnum.default("classic").describe('Opcional. Estructura de la landing cl\xE1sica: "classic" (estructura original probada y recomendada; default del primer rollout), "auto" (la IA decide; experimental), "pain_solution" (problema\u2192soluci\xF3n), "social_proof" (Landing Viral: UGC, WhatsApp, Instagram y evidencia social), "benefits_clarity" (beneficios claros), "offer_value" (promoci\xF3n/valor) o "lifestyle_identity" (estilo de vida). Default "classic".'),
|
|
8755
|
-
landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".')
|
|
9139
|
+
landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".'),
|
|
9140
|
+
headline: CreativeRefs.headline,
|
|
9141
|
+
subheadline: CreativeRefs.subheadline
|
|
8756
9142
|
});
|
|
8757
9143
|
function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
8758
9144
|
server.tool("generate_free_video", 'Genera UN video libre con prompt/guion exacto, opcionalmente con imagen de referencia. Devuelve un taskId y procesa el job de forma as\xEDncrona.\n\nREQUERIDOS: rawPrompt, quality, durationSeconds y videoResolution.\n\nCALIDAD: "economical" = Econ\xF3mico y "high_detail" = Alto detalle. La duraci\xF3n debe pertenecer a la lista admitida por la calidad escogida; si el usuario pide otra, preg\xFAntale una duraci\xF3n v\xE1lida.\n\nSUBT\xCDTULOS: si addSubtitles=true, el rawPrompt completo se usa como texto exacto de subt\xEDtulos y debe tener m\xE1ximo 500 caracteres; no existe una pregunta adicional de transcripci\xF3n.\n\nPara videos normales con narrativa/\xE1ngulo usa generate_video_creative. Para tareas combinadas de producto usa create_product_task.', FreeVideoInput.shape, async (input) => {
|
|
@@ -8787,6 +9173,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
8787
9173
|
});
|
|
8788
9174
|
server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, async (input) => {
|
|
8789
9175
|
try {
|
|
9176
|
+
assertVideoPresetDurationCompatibility(input.videoPreset, input.durationSeconds);
|
|
8790
9177
|
const resolved = resolveVideoPreset(input.videoPreset);
|
|
8791
9178
|
const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
|
|
8792
9179
|
const { videoTier: _publicTier, videoResolution: _vr, projectId, ...rest } = input;
|
|
@@ -8828,6 +9215,17 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
8828
9215
|
throw translateError(e);
|
|
8829
9216
|
}
|
|
8830
9217
|
});
|
|
9218
|
+
server.tool("edit_image_creative", "Corrige UNA imagen ya generada de una tarea de creativos, sin regenerar el set completo.\n\nCUESTA CR\xC9DITOS: cobra una sola imagen (sin brief ni extracci\xF3n), mucho menos que volver a generar todo. ANTES de invocar, PREG\xDANTALE al usuario si quiere pagar esa correcci\xF3n.\n\nSolo funciona con la tarea en estado `done` \u2014 si a\xFAn est\xE1 procesando, espera con get_task_status. Una tarea en error terminal no se puede corregir.\n\nUsa variantIndex para elegir cu\xE1l de las im\xE1genes corregir (0 = la primera) e instruction para describir el cambio en lenguaje natural.", EditImageCreativeInput.shape, async (input) => {
|
|
9219
|
+
try {
|
|
9220
|
+
const r = await api.postJson(`/v1/creatives/images/${encodeURIComponent(input.taskId)}/regenerate`, {
|
|
9221
|
+
comment: input.instruction,
|
|
9222
|
+
variantIndex: input.variantIndex
|
|
9223
|
+
});
|
|
9224
|
+
return asText(r);
|
|
9225
|
+
} catch (e) {
|
|
9226
|
+
throw translateError(e);
|
|
9227
|
+
}
|
|
9228
|
+
});
|
|
8831
9229
|
server.tool("generate_landing", 'Genera una landing page cl\xE1sica basada en im\xE1genes, lista para Shopify. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para esos campos, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults): salesAngle, buyerPersona, isCombo, creativeDirection, landingStrategy.', LandingInput.shape, async (input) => {
|
|
8832
9230
|
try {
|
|
8833
9231
|
const { projectId, ...rest } = input;
|