feat(media): wire marketing into the API, add segment results and teaser reset

Brand kits are validated through saveBrandKit and return their warnings;
manual metrics refit the teaser model.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w
This commit is contained in:
Claude 2026-10-03 18:50:45 +00:00
parent f29e6379cb
commit 21edc4add7
No known key found for this signature in database
6 changed files with 60 additions and 20 deletions

View File

@ -383,5 +383,7 @@ export function sampleMediaInputs(
foreign: "not_found",
},
"metrics/funnel": { input: { projectId: ids.projectId }, foreign: "not_found" },
"metrics/segments": { input: { projectId: ids.projectId }, foreign: "not_found" },
"metrics/resetTeaserModel": { input: {}, foreign: "scoped" },
};
}

View File

@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import { implement, ORPCError } from "@orpc/server";
import { UnsupportedMediaOperationError } from "@rakazo/adapter-kit";
import type { MediaServices } from "@rakazo/adapters";
import { recordMetric, resetTeaserModel, saveBrandKit, segmentFunnel } from "@rakazo/adapters";
import type { Actor, BrandKit, MediaCapabilities, MediaSourceKind } from "@rakazo/contracts";
import { appContract, MediaInferenceOperationSchema } from "@rakazo/contracts";
import type { PrismaClient } from "@rakazo/db";
@ -389,7 +390,7 @@ export function createMediaRouter(deps: MediaRouterDeps) {
context.actor.spaceId,
brandKitArtifactIds(input.kit),
);
return context.services.deps.repos.upsertBrandKit(context.actor.spaceId, input);
return saveBrandKit(context.services.deps, context.actor, input);
}),
remove: media.brandKits.remove.handler(async ({ context, input }) => {
await context.services.deps.repos.removeBrandKit(context.actor.spaceId, input.id);
@ -534,14 +535,18 @@ export function createMediaRouter(deps: MediaRouterDeps) {
return context.services.marketing.importMetricsCsv(context.actor, input);
}),
record: media.metrics.record.handler(({ context, input }) =>
context.services.deps.repos.recordMetric(context.actor.spaceId, {
...input,
source: "manual",
}),
recordMetric(context.services.deps, context.actor, input),
),
funnel: media.metrics.funnel.handler(({ context, input }) =>
context.services.deps.repos.funnel(context.actor.spaceId, input.projectId),
),
segments: media.metrics.segments.handler(({ context, input }) =>
segmentFunnel(context.services.deps, context.actor, input.projectId),
),
resetTeaserModel: media.metrics.resetTeaserModel.handler(async ({ context }) => {
await resetTeaserModel(context.services.deps, context.actor);
return { ok: true as const };
}),
},
});
}

View File

@ -76,13 +76,21 @@ export * from "./mcp-emulator.js";
export * from "./mcp-oauth.js";
export * from "./mcp-server-tool.js";
export * from "./mcp-transport.js";
export * from "./media-brand.js";
export * from "./media-context.js";
export * from "./media-emulator.js";
export * from "./media-emulator-clips.js";
export * from "./media-emulator-source.js";
export * from "./media-factory.js";
export * from "./media-ffmpeg-renderer.js";
export * from "./media-folder-source.js";
export * from "./media-folder-source-probe.js";
export * from "./media-marketing.js";
export * from "./media-marketing-copy.js";
export * from "./media-marketing-files.js";
export * from "./media-marketing-plan.js";
export * from "./media-marketing-thumbnails.js";
export * from "./media-metrics.js";
export type {
MotionFrameContext,
MotionTemplate,
@ -93,7 +101,17 @@ export {
MotionTemplateError,
ResvgMotionGraphicsRenderer,
} from "./media-motion-renderer.js";
export * from "./media-publish.js";
export * from "./media-publisher-bundle.js";
export {
createMediaRenderHandler,
createMediaRenderService,
MediaRenderFailure,
pickEncoder,
softwareEncoder,
} from "./media-render.js";
export * from "./media-sidecar.js";
export * from "./media-zip.js";
export * from "./memory-provider-factory.js";
export * from "./messaging-context.js";
export * from "./messaging-delivery.js";

View File

@ -1,4 +1,10 @@
import type { Edl, FunnelRow, MediaMetric, MediaSegment } from "@rakazo/contracts";
import type {
Edl,
FunnelRow,
MediaMetric,
MediaSegment,
SegmentFunnelRow,
} from "@rakazo/contracts";
import { TEASER_FEATURES, TEASER_LEARNING_MIN_SAMPLES } from "@rakazo/contracts";
import type { NewMediaMetric, TeaserModelRecord } from "@rakazo/db";
import { InvalidMediaInputError } from "@rakazo/db";
@ -87,19 +93,6 @@ async function withPlans(
return { samples, segments: await repos.listSegments(spaceId, assetIds) };
}
export interface SegmentFunnelRow {
segmentId: string;
assetId: string;
startMs: number;
endMs: number;
publications: number;
views: number;
clicks: number;
conversions: number;
revenueCents: number;
currency: string;
}
/**
* Results per teaser segment (spec 10.6): every publication counts fully for
* each segment its plan used. Rows are per currency so revenue never mixes.

View File

@ -968,6 +968,21 @@ export const FunnelRowSchema = z.object({
});
export type FunnelRow = z.infer<typeof FunnelRowSchema>;
/** Results per teaser segment; one row per currency so revenue never mixes. */
export const SegmentFunnelRowSchema = z.object({
segmentId: z.string(),
assetId: Id,
startMs: z.number().int(),
endMs: z.number().int(),
publications: z.number().int(),
views: z.number().int(),
clicks: z.number().int(),
conversions: z.number().int(),
revenueCents: z.number().int(),
currency: z.string(),
});
export type SegmentFunnelRow = z.infer<typeof SegmentFunnelRowSchema>;
export const MotionTemplateInfoSchema = z.object({
id: SafeId,
kind: z.enum(["intro", "outro", "lower_third", "caption", "end_card"]),
@ -1298,7 +1313,7 @@ export const mediaRpcShapes = {
list: { input: z.object({}).default({}), output: z.array(BrandKitRecordSchema) },
upsert: {
input: z.object({ id: Id.optional(), kit: BrandKitSchema }),
output: BrandKitRecordSchema,
output: z.object({ record: BrandKitRecordSchema, warnings: z.array(z.string()) }),
},
remove: { input: z.object({ id: Id }), output: ok },
templates: { input: z.object({}).default({}), output: z.array(MotionTemplateInfoSchema) },
@ -1405,6 +1420,11 @@ export const mediaRpcShapes = {
input: z.object({ projectId: Id.optional() }),
output: z.array(FunnelRowSchema),
},
segments: {
input: z.object({ projectId: Id.optional() }),
output: z.array(SegmentFunnelRowSchema),
},
resetTeaserModel: { input: z.object({}).default({}), output: ok },
},
} as const;

View File

@ -954,6 +954,8 @@ export const appContract = {
importCsv: rpc(mediaRpcShapes.metrics.importCsv),
record: rpc(mediaRpcShapes.metrics.record),
funnel: rpc(mediaRpcShapes.metrics.funnel),
segments: rpc(mediaRpcShapes.metrics.segments),
resetTeaserModel: rpc(mediaRpcShapes.metrics.resetTeaserModel),
},
},
};