Initial commit

This commit is contained in:
2026-04-08 10:01:19 -07:00
commit 6657125a1e
68 changed files with 15886 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@goodgrief/shared-types",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"zod": "^3.24.4"
}
}
+374
View File
@@ -0,0 +1,374 @@
export type SubmissionSource = "live" | "pre_show" | "invite" | "admin_upload" | "library_import";
export type SubmissionStatus =
| "uploaded"
| "processing"
| "pending_moderation"
| "approved_partial"
| "approved_all"
| "rejected"
| "archived";
export type ProcessingStatus = "queued" | "ready" | "failed";
export type ModerationStatus = "pending" | "approved" | "hold" | "rejected" | "archived";
export type ModerationDecisionType = "approved" | "hold" | "rejected" | "archive_only";
export type CollectionKind = "bank" | "playlist" | "moment" | "favorites" | "archive_set";
export type SceneRenderMode = "2d" | "3d" | "hybrid" | "shader_overlay";
export type CueTriggerMode = "manual" | "follow" | "hold" | "armed";
export type OperatorSessionMode = "rehearsal" | "tech" | "show" | "archive_review";
export type OutputSurfaceRole = "program" | "preview" | "aux";
export type SceneTier = "mvp" | "v1" | "stretch";
export type SceneFamily = "hero" | "chorus" | "floor_paint" | "arrival" | "rupture" | "safe";
export type TextTreatmentMode = "off" | "edge_whispers" | "relay_ticker" | "anchor_caption";
export type SceneCategory =
| "memory_elegy"
| "humor_rupture"
| "choir_swell"
| "abstract_grief"
| "photo_collage"
| "immersive_3d"
| "transition"
| "audience_reactive";
export interface ContributorConsent {
id: string;
submissionId: string;
hasRights: boolean;
allowProjection: boolean;
acknowledgePublicPerformance: boolean;
agreedAt: string;
allowArchive?: boolean;
contactEmail?: string;
guardianConfirmed?: boolean;
}
export interface Submission {
id: string;
source: SubmissionSource;
submittedAt: string;
status: SubmissionStatus;
consentId: string;
displayName?: string;
caption?: string;
promptAnswer?: string;
sessionToken?: string;
notes?: string;
}
export interface QualityFlags {
tooSmall?: boolean;
blurry?: boolean;
lowContrast?: boolean;
unusualAspectRatio?: boolean;
}
export interface PhotoAsset {
id: string;
submissionId: string;
originalKey: string;
thumbKey?: string;
previewKey?: string;
renderKey?: string;
mimeType: string;
width?: number;
height?: number;
orientation?: "portrait" | "landscape" | "square";
sha256?: string;
pHash?: string;
dominantColor?: string;
processingStatus: ProcessingStatus;
moderationStatus: ModerationStatus;
createdAt: string;
qualityFlags?: QualityFlags;
approvedAt?: string;
rejectionReason?: string;
}
export interface Tag {
id: string;
label: string;
category: "tone" | "era" | "subject" | "palette" | "scene_fit" | "quality" | "show_moment";
color?: string;
description?: string;
}
export interface Collection {
id: string;
name: string;
kind: CollectionKind;
createdAt: string;
description?: string;
coverAssetId?: string;
locked?: boolean;
assetIds: string[];
tagIds: string[];
}
export interface PhotoTreatmentParams {
contrast: number;
saturation: number;
blackPoint: number;
whitePoint: number;
paletteMix: number;
clarity: number;
edgeLight: number;
}
export interface ScenicTreatmentParams {
washIntensity: number;
spill: number;
floorMix: number;
paletteBias: number;
vignette: number;
fillHue: number;
fillSaturation: number;
fillLightness: number;
}
export interface CompositionParams {
motion: number;
density: number;
depth: number;
focus: number;
crop: number;
emphasis: number;
bands?: number;
columns?: number;
shutters?: number;
tiles?: number;
lanes?: number;
edge?: "left" | "right";
}
export interface TextTreatmentParams {
mode: TextTreatmentMode;
opacity: number;
density: number;
scale: number;
}
export interface SceneParamGroups {
photoTreatment: PhotoTreatmentParams;
scenicTreatment: ScenicTreatmentParams;
composition: CompositionParams;
textTreatment: TextTreatmentParams;
}
export interface SceneParamPatch {
photoTreatment?: Partial<PhotoTreatmentParams>;
scenicTreatment?: Partial<ScenicTreatmentParams>;
composition?: Partial<CompositionParams>;
textTreatment?: Partial<TextTreatmentParams>;
}
export interface PhotoTreatmentPreset {
id: string;
name: string;
params: Partial<PhotoTreatmentParams>;
}
export interface ScenicTreatmentPreset {
id: string;
name: string;
params: Partial<ScenicTreatmentParams>;
}
export interface SceneDefinition {
id: string;
sceneKey: string;
sceneFamily: SceneFamily;
name: string;
category: SceneCategory;
tier: SceneTier;
visualDescription: string;
emotionalUseCase: string;
renderMode: SceneRenderMode;
complexity: "low" | "medium" | "high";
performanceRisk: "low" | "medium" | "high";
inputRules: {
minAssets: number;
maxAssets?: number;
recommendedTags: string[];
};
defaultParams: SceneParamGroups;
defaultPresetId: string;
supportedPresetIds: string[];
operatorControls: string[];
metadataHints: string[];
}
export interface CueTransition {
style: "cut" | "dissolve" | "veil_wipe" | "luma_hold" | "rupture_offset";
durationMs: number;
}
export interface Cue {
id: string;
showConfigId: string;
orderIndex: number;
sceneDefinitionId: string;
triggerMode: CueTriggerMode;
transitionIn: CueTransition;
transitionOut: CueTransition;
collectionId?: string;
assetIds?: string[];
durationMs?: number;
effectPresetId?: string;
parameterOverrides?: SceneParamPatch;
notes?: string;
nextCueId?: string;
}
export interface CueUpsertPayload {
id?: string;
showConfigId?: string;
orderIndex?: number;
sceneDefinitionId: string;
triggerMode: CueTriggerMode;
transitionIn: CueTransition;
transitionOut: CueTransition;
collectionId?: string;
assetIds?: string[];
durationMs?: number;
effectPresetId?: string;
parameterOverrides?: SceneParamPatch;
notes?: string;
nextCueId?: string;
}
export interface CueMovePayload {
direction: "up" | "down";
}
export interface CueGeneratePayload {
sceneDefinitionId?: string;
preferredAssetIds?: string[];
includeRupture?: boolean;
}
export interface EffectPreset {
id: string;
modeKey: string;
compatibleSceneFamilies: SceneFamily[];
name: string;
category:
| "compositing"
| "temporal"
| "spatial"
| "color"
| "depth"
| "particles"
| "reveal"
| "audio"
| "performer"
| "projection";
artisticPurpose: string;
operatorControls: string[];
implementationLevel: "css" | "canvas" | "webgl" | "custom_shader";
performanceNotes: string;
paramDefaults: SceneParamPatch;
safeRanges: Record<string, { min: number; max: number }>;
}
export interface OperatorSession {
id: string;
startedAt: string;
mode: OperatorSessionMode;
operatorName: string;
showConfigId: string;
endedAt?: string;
venueName?: string;
incidentNotes?: string;
}
export interface ModerationDecision {
id: string;
assetId: string;
operatorSessionId: string;
decision: ModerationDecisionType;
decidedAt: string;
reasonCode?: string;
note?: string;
}
export interface OutputSurface {
id: string;
name: string;
role: OutputSurfaceRole;
width: number;
height: number;
aspectRatio: string;
screenIndex?: number;
maskShape?: "full_frame" | "letterbox" | "pillarbox" | "custom";
safeMargin?: number;
colorProfile?: string;
fullscreenBounds?: {
x: number;
y: number;
width: number;
height: number;
};
}
export interface ShowConfig {
id: string;
showName: string;
venueName: string;
defaultOutputSurfaceId: string;
safeSceneCueId: string;
retentionDays: number;
ingestPolicy: "fully_live" | "pre_show_plus_live" | "pre_show_only";
theme?: string;
projectionNotes?: string;
operatorShortcuts?: Record<string, string>;
}
export interface SessionEvent {
id: string;
sessionId: string;
timestamp: string;
type:
| "cue_fired"
| "cue_skipped"
| "blackout"
| "safe_scene"
| "submission_received"
| "asset_approved"
| "asset_rejected";
payload: Record<string, string | number | boolean | null>;
}
export interface SubmissionPayload {
displayName?: string;
caption?: string;
promptAnswer?: string;
allowArchive?: boolean;
hasRights: boolean;
allowProjection: boolean;
acknowledgePublicPerformance: boolean;
source?: SubmissionSource;
}
export interface ModerationActionPayload {
decision: ModerationDecisionType;
reasonCode?: string;
note?: string;
tagIds?: string[];
collectionIds?: string[];
}
export interface RepositoryState {
submissions: Submission[];
consents: ContributorConsent[];
photoAssets: PhotoAsset[];
tags: Tag[];
collections: Collection[];
scenes: SceneDefinition[];
cues: Cue[];
effectPresets: EffectPreset[];
operatorSessions: OperatorSession[];
moderationDecisions: ModerationDecision[];
outputSurfaces: OutputSurface[];
showConfig: ShowConfig;
sessionEvents: SessionEvent[];
}
+21
View File
@@ -0,0 +1,21 @@
import type { ModerationDecisionType } from "./entities";
export type ApiEvent =
| {
type: "submission.received";
submissionId: string;
assetId: string;
}
| {
type: "asset.moderated";
assetId: string;
decision: ModerationDecisionType;
}
| {
type: "cue.fired";
cueId: string;
}
| {
type: "cue.safe";
cueId: string;
};
+5
View File
@@ -0,0 +1,5 @@
export * from "./entities";
export * from "./events";
export * from "./mock";
export * from "./scene-params";
export * from "./scenes";
+113
View File
@@ -0,0 +1,113 @@
import type {
Collection,
OperatorSession,
OutputSurface,
RepositoryState,
ShowConfig,
Tag
} from "./entities";
import { defaultCueStack, defaultEffectPresets, defaultSceneDefinitions } from "./scenes";
export const defaultTags: Tag[] = [
{ id: "tag-quiet", label: "quiet", category: "tone", color: "#9ea29f" },
{ id: "tag-family", label: "family", category: "subject", color: "#d09d74" },
{ id: "tag-portrait", label: "portrait", category: "subject", color: "#6f8579" },
{ id: "tag-live", label: "live", category: "show_moment", color: "#e27f66" },
{ id: "tag-choir", label: "choir", category: "show_moment", color: "#7d7098" },
{ id: "tag-archive", label: "archive", category: "scene_fit", color: "#c8c0b3" }
];
export const defaultCollections: Collection[] = [
{
id: "collection-curated-library",
name: "Curated Library",
kind: "bank",
createdAt: new Date().toISOString(),
description: "Imported operator-managed seed and rehearsal media.",
locked: true,
assetIds: [],
tagIds: ["tag-archive", "tag-portrait"]
},
{
id: "collection-favorites",
name: "Favorites",
kind: "favorites",
createdAt: new Date().toISOString(),
description: "Operator-trusted images for flexible live use.",
locked: false,
assetIds: [],
tagIds: ["tag-portrait", "tag-quiet"]
},
{
id: "collection-choir-swell",
name: "Choir Swell",
kind: "moment",
createdAt: new Date().toISOString(),
description: "Assets suitable for collective or musical lift.",
locked: false,
assetIds: [],
tagIds: ["tag-choir", "tag-family"]
}
];
export const defaultOutputSurfaces: OutputSurface[] = [
{
id: "surface-program",
name: "Program",
role: "program",
width: 1920,
height: 1080,
aspectRatio: "16:9",
safeMargin: 0.06
},
{
id: "surface-preview",
name: "Preview",
role: "preview",
width: 1280,
height: 720,
aspectRatio: "16:9"
}
];
export const defaultShowConfig: ShowConfig = {
id: "show-good-grief",
showName: "Good Grief",
venueName: "Studio Black Box",
defaultOutputSurfaceId: "surface-program",
safeSceneCueId: "cue-safe-hold",
retentionDays: 21,
ingestPolicy: "fully_live",
theme: "Tender collage for live projection.",
projectionNotes: "Maintain center-safe composition and low white clip.",
operatorShortcuts: {
Space: "take cue",
KeyB: "blackout",
KeyS: "safe scene",
ArrowDown: "next cue"
}
};
export const defaultOperatorSession: OperatorSession = {
id: "session-default",
startedAt: new Date().toISOString(),
mode: "rehearsal",
operatorName: "Operator",
showConfigId: "show-good-grief"
};
export const createEmptyRepositoryState = (): RepositoryState => ({
submissions: [],
consents: [],
photoAssets: [],
tags: defaultTags,
collections: defaultCollections,
scenes: defaultSceneDefinitions,
cues: defaultCueStack,
effectPresets: defaultEffectPresets,
operatorSessions: [defaultOperatorSession],
moderationDecisions: [],
outputSurfaces: defaultOutputSurfaces,
showConfig: defaultShowConfig,
sessionEvents: []
});
+108
View File
@@ -0,0 +1,108 @@
import type {
CompositionParams,
PhotoTreatmentParams,
ScenicTreatmentParams,
SceneParamGroups,
SceneParamPatch,
TextTreatmentParams
} from "./entities";
const defaultTextTreatment = (): TextTreatmentParams => ({
mode: "off",
opacity: 0.2,
density: 0.35,
scale: 0.8
});
export type SceneParamScalar = number | string | boolean;
export const createSceneParams = (input: {
photoTreatment: PhotoTreatmentParams;
scenicTreatment: ScenicTreatmentParams;
composition: CompositionParams;
textTreatment?: TextTreatmentParams;
}): SceneParamGroups => ({
photoTreatment: { ...input.photoTreatment },
scenicTreatment: { ...input.scenicTreatment },
composition: { ...input.composition },
textTreatment: {
...defaultTextTreatment(),
...input.textTreatment
}
});
export const mergeSceneParams = (base: SceneParamGroups, ...patches: Array<SceneParamPatch | undefined>): SceneParamGroups => {
const merged = createSceneParams(base);
for (const patch of patches) {
if (!patch) {
continue;
}
if (patch.photoTreatment) {
merged.photoTreatment = {
...merged.photoTreatment,
...patch.photoTreatment
};
}
if (patch.scenicTreatment) {
merged.scenicTreatment = {
...merged.scenicTreatment,
...patch.scenicTreatment
};
}
if (patch.composition) {
merged.composition = {
...merged.composition,
...patch.composition
};
}
if (patch.textTreatment) {
merged.textTreatment = {
...merged.textTreatment,
...patch.textTreatment
};
}
}
return merged;
};
export const getSceneParamValue = (params: SceneParamGroups | SceneParamPatch, path: string): SceneParamScalar | undefined => {
const [group, key] = path.split(".");
if (!group || !key) {
return undefined;
}
const container = params[group as keyof SceneParamGroups] as Record<string, SceneParamScalar> | undefined;
return container?.[key];
};
export const setSceneParamValue = (params: SceneParamGroups, path: string, value: SceneParamScalar): SceneParamGroups => {
const [group, key] = path.split(".");
if (!group || !key) {
return params;
}
if (!(group in params)) {
return params;
}
return {
...params,
[group]: {
...((params[group as keyof SceneParamGroups] as unknown as Record<string, SceneParamScalar>) ?? {}),
[key]: value
}
} as SceneParamGroups;
};
export const flattenSceneParams = (params: SceneParamGroups | SceneParamPatch) =>
Object.fromEntries(
Object.entries(params).flatMap(([group, values]) =>
Object.entries(values ?? {}).map(([key, value]) => [`${group}.${key}`, value] as const)
)
) as Record<string, SceneParamScalar>;
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"src"
]
}