diff --git a/apps/api/src/media-test-fakes.ts b/apps/api/src/media-test-fakes.ts index 69c8872..bc073b6 100644 --- a/apps/api/src/media-test-fakes.ts +++ b/apps/api/src/media-test-fakes.ts @@ -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" }, }; } diff --git a/apps/api/src/media.ts b/apps/api/src/media.ts index 038b421..192b2c6 100644 --- a/apps/api/src/media.ts +++ b/apps/api/src/media.ts @@ -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 }; + }), }, }); } diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index f6a9368..fbc87eb 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -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"; diff --git a/packages/adapters/src/media-metrics.ts b/packages/adapters/src/media-metrics.ts index 8f212fe..3317513 100644 --- a/packages/adapters/src/media-metrics.ts +++ b/packages/adapters/src/media-metrics.ts @@ -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. diff --git a/packages/contracts/src/media.ts b/packages/contracts/src/media.ts index b70e5b8..9eef713 100644 --- a/packages/contracts/src/media.ts +++ b/packages/contracts/src/media.ts @@ -968,6 +968,21 @@ export const FunnelRowSchema = z.object({ }); export type FunnelRow = z.infer; +/** 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; + 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; diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 638a943..f13901c 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -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), }, }, };