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:
parent
f29e6379cb
commit
21edc4add7
@ -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" },
|
||||
};
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user