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",
|
foreign: "not_found",
|
||||||
},
|
},
|
||||||
"metrics/funnel": { input: { projectId: ids.projectId }, 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 { implement, ORPCError } from "@orpc/server";
|
||||||
import { UnsupportedMediaOperationError } from "@rakazo/adapter-kit";
|
import { UnsupportedMediaOperationError } from "@rakazo/adapter-kit";
|
||||||
import type { MediaServices } from "@rakazo/adapters";
|
import type { MediaServices } from "@rakazo/adapters";
|
||||||
|
import { recordMetric, resetTeaserModel, saveBrandKit, segmentFunnel } from "@rakazo/adapters";
|
||||||
import type { Actor, BrandKit, MediaCapabilities, MediaSourceKind } from "@rakazo/contracts";
|
import type { Actor, BrandKit, MediaCapabilities, MediaSourceKind } from "@rakazo/contracts";
|
||||||
import { appContract, MediaInferenceOperationSchema } from "@rakazo/contracts";
|
import { appContract, MediaInferenceOperationSchema } from "@rakazo/contracts";
|
||||||
import type { PrismaClient } from "@rakazo/db";
|
import type { PrismaClient } from "@rakazo/db";
|
||||||
@ -389,7 +390,7 @@ export function createMediaRouter(deps: MediaRouterDeps) {
|
|||||||
context.actor.spaceId,
|
context.actor.spaceId,
|
||||||
brandKitArtifactIds(input.kit),
|
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 }) => {
|
remove: media.brandKits.remove.handler(async ({ context, input }) => {
|
||||||
await context.services.deps.repos.removeBrandKit(context.actor.spaceId, input.id);
|
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);
|
return context.services.marketing.importMetricsCsv(context.actor, input);
|
||||||
}),
|
}),
|
||||||
record: media.metrics.record.handler(({ context, input }) =>
|
record: media.metrics.record.handler(({ context, input }) =>
|
||||||
context.services.deps.repos.recordMetric(context.actor.spaceId, {
|
recordMetric(context.services.deps, context.actor, input),
|
||||||
...input,
|
|
||||||
source: "manual",
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
funnel: media.metrics.funnel.handler(({ context, input }) =>
|
funnel: media.metrics.funnel.handler(({ context, input }) =>
|
||||||
context.services.deps.repos.funnel(context.actor.spaceId, input.projectId),
|
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-oauth.js";
|
||||||
export * from "./mcp-server-tool.js";
|
export * from "./mcp-server-tool.js";
|
||||||
export * from "./mcp-transport.js";
|
export * from "./mcp-transport.js";
|
||||||
|
export * from "./media-brand.js";
|
||||||
export * from "./media-context.js";
|
export * from "./media-context.js";
|
||||||
export * from "./media-emulator.js";
|
export * from "./media-emulator.js";
|
||||||
export * from "./media-emulator-clips.js";
|
export * from "./media-emulator-clips.js";
|
||||||
export * from "./media-emulator-source.js";
|
export * from "./media-emulator-source.js";
|
||||||
export * from "./media-factory.js";
|
export * from "./media-factory.js";
|
||||||
|
export * from "./media-ffmpeg-renderer.js";
|
||||||
export * from "./media-folder-source.js";
|
export * from "./media-folder-source.js";
|
||||||
export * from "./media-folder-source-probe.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 {
|
export type {
|
||||||
MotionFrameContext,
|
MotionFrameContext,
|
||||||
MotionTemplate,
|
MotionTemplate,
|
||||||
@ -93,7 +101,17 @@ export {
|
|||||||
MotionTemplateError,
|
MotionTemplateError,
|
||||||
ResvgMotionGraphicsRenderer,
|
ResvgMotionGraphicsRenderer,
|
||||||
} from "./media-motion-renderer.js";
|
} 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-sidecar.js";
|
||||||
|
export * from "./media-zip.js";
|
||||||
export * from "./memory-provider-factory.js";
|
export * from "./memory-provider-factory.js";
|
||||||
export * from "./messaging-context.js";
|
export * from "./messaging-context.js";
|
||||||
export * from "./messaging-delivery.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 { TEASER_FEATURES, TEASER_LEARNING_MIN_SAMPLES } from "@rakazo/contracts";
|
||||||
import type { NewMediaMetric, TeaserModelRecord } from "@rakazo/db";
|
import type { NewMediaMetric, TeaserModelRecord } from "@rakazo/db";
|
||||||
import { InvalidMediaInputError } from "@rakazo/db";
|
import { InvalidMediaInputError } from "@rakazo/db";
|
||||||
@ -87,19 +93,6 @@ async function withPlans(
|
|||||||
return { samples, segments: await repos.listSegments(spaceId, assetIds) };
|
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
|
* 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.
|
* 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>;
|
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({
|
export const MotionTemplateInfoSchema = z.object({
|
||||||
id: SafeId,
|
id: SafeId,
|
||||||
kind: z.enum(["intro", "outro", "lower_third", "caption", "end_card"]),
|
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) },
|
list: { input: z.object({}).default({}), output: z.array(BrandKitRecordSchema) },
|
||||||
upsert: {
|
upsert: {
|
||||||
input: z.object({ id: Id.optional(), kit: BrandKitSchema }),
|
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 },
|
remove: { input: z.object({ id: Id }), output: ok },
|
||||||
templates: { input: z.object({}).default({}), output: z.array(MotionTemplateInfoSchema) },
|
templates: { input: z.object({}).default({}), output: z.array(MotionTemplateInfoSchema) },
|
||||||
@ -1405,6 +1420,11 @@ export const mediaRpcShapes = {
|
|||||||
input: z.object({ projectId: Id.optional() }),
|
input: z.object({ projectId: Id.optional() }),
|
||||||
output: z.array(FunnelRowSchema),
|
output: z.array(FunnelRowSchema),
|
||||||
},
|
},
|
||||||
|
segments: {
|
||||||
|
input: z.object({ projectId: Id.optional() }),
|
||||||
|
output: z.array(SegmentFunnelRowSchema),
|
||||||
|
},
|
||||||
|
resetTeaserModel: { input: z.object({}).default({}), output: ok },
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@ -954,6 +954,8 @@ export const appContract = {
|
|||||||
importCsv: rpc(mediaRpcShapes.metrics.importCsv),
|
importCsv: rpc(mediaRpcShapes.metrics.importCsv),
|
||||||
record: rpc(mediaRpcShapes.metrics.record),
|
record: rpc(mediaRpcShapes.metrics.record),
|
||||||
funnel: rpc(mediaRpcShapes.metrics.funnel),
|
funnel: rpc(mediaRpcShapes.metrics.funnel),
|
||||||
|
segments: rpc(mediaRpcShapes.metrics.segments),
|
||||||
|
resetTeaserModel: rpc(mediaRpcShapes.metrics.resetTeaserModel),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user