All files / src/lib/api social-drafts.ts

100% Statements 10/10
100% Branches 0/0
100% Functions 9/9
100% Lines 10/10

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189                                                                                                                                                                                                                                                                                        94x 1x         1x     2x               1x   1x         1x           1x         1x               1x          
// Social Drafts API - Social Studio feature
import { request } from "./base";
 
export type SocialDraftStatus =
	| "pending"
	| "processing"
	| "cancelled"
	| "ready"
	| "failed";
 
export type MusicTrackCategory = "upbeat" | "calm" | "dramatic" | "modern" | "traditional";
 
export type VoiceoverVoice = "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer";
 
export interface MusicTrack {
	id: string;
	name: string;
	category: MusicTrackCategory;
	durationS: number;
	r2Key: string;
}
 
export type BadgePosition =
	| "top-left"
	| "top-center"
	| "top-right"
	| "middle-left"
	| "center"
	| "middle-right"
	| "bottom-left"
	| "bottom-center"
	| "bottom-right";
 
export type BadgeStyle = "certified" | "featured" | "new" | "custom";
 
export interface Badge {
	id: string; // client-generated uuid for React key
	label: string; // user text, 1-40 chars
	style?: BadgeStyle; // default: "custom"
	position: BadgePosition;
	customColor?: string; // hex "#RRGGBB" when style="custom"
}
 
export interface OverlayConfig {
	introEnabled?: boolean;
	introDurationS?: number;
	outroEnabled?: boolean;
	outroDurationS?: number;
	badges?: Badge[];
	// Deprecated: kept for backwards-compat
	badgeType?: "certified" | "featured" | "new" | null;
	badgePosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
}
 
export interface VoiceoverConfig {
	enabled: boolean;
	voice: VoiceoverVoice;
	script?: string;
	autoGenerate: boolean;
	// Set by the Worker on partial-success: pro requested voiceover but TTS
	// failed (any cause — missing API key, rate limit, network). The draft
	// card reads this to show a "Narration failed — Regenerate" badge.
	failed?: boolean;
	// C1: set by the Worker when GPT-4o caption generation hit a permanent
	// failure. The pro received a template-fallback caption instead of
	// AI-generated copy. Shares this column rather than introducing a parallel
	// captionConfig column (plan: zero schema migrations). The draft card reads
	// this to show an "AI caption failed — Regenerate" badge.
	captionFailed?: boolean;
}
 
export interface TemplatePhotoSlot {
	slotIndex: number;
	durationS: number;
	transition: "fade" | "slide_left" | "slide_right" | "zoom" | "none";
	label?: string;
	photoType?: "before" | "after" | null;
}
 
export interface TemplateTextOverlay {
	slotIndex: number;
	text: string;
	position: "top-center" | "bottom-center" | "center";
	fontSizePx: number;
	startS?: number;
	durationS?: number;
}
 
export interface TemplateSlots {
	introSlide?: { durationS: number; layout: "centered" | "left-aligned" };
	photoSlots: TemplatePhotoSlot[];
	outroSlide?: { durationS: number; layout: "centered" | "left-aligned" };
	textOverlays?: TemplateTextOverlay[];
}
 
export interface SocialStudioTemplate {
	id: string;
	name: string;
	description: string | null;
	thumbnailR2Key: string | null;
	category: string | null;
	slots: string | null; // JSON string of TemplateSlots
	defaultMusicId: string | null;
	defaultOverlay: string | null; // JSON string of OverlayConfig
	brandingRequired: boolean;
	isSystem: boolean;
	createdByProId: string | null;
	dateCreated: string;
}
 
export interface SocialDraft {
	id: string;
	proId: string;
	projectId: string;
	projectTitle: string | null;
	status: SocialDraftStatus;
	template: "before_after" | "highlight" | null;
	reelR2Key: string | null;
	captionEn: string | null;
	expiresAt: string | null; // ISO date string (Drizzle serializes timestamp integers as ISO via JSON)
	dateCreated: string;
	dateUpdated: string;
	// Parsed from the JSON blob stored in social_drafts.voiceover_config.
	// Present when the pro enabled voiceover. When voiceoverConfig.failed is
	// true, the draft card shows the partial-success "Voice failed" badge.
	voiceoverConfig?: VoiceoverConfig;
}
 
export interface CreateDraftInput {
	projectId: string;
	selectedPhotoIds?: number[];
	triggerSource?: "manual" | "auto";
	musicTrackId?: string;
	brandingEnabled?: boolean;
	overlayConfig?: OverlayConfig;
	templateId?: string;
	slotAssignments?: Record<number, number>;
	voiceoverConfig?: VoiceoverConfig;
}
 
export const socialDraftsApi = {
	list: (proId: string) =>
		request<{ drafts: SocialDraft[]; pendingCount: number }>(
			`/api/pro/${proId}/social-drafts`,
		),
 
	get: (proId: string, draftId: string) =>
		request<SocialDraft>(`/api/pro/${proId}/social-drafts/${draftId}`),
 
	create: (proId: string, input: CreateDraftInput) =>
		request<{ draft: SocialDraft }>(`/api/pro/${proId}/social-drafts`, {
			method: "POST",
			body: input,
		}),
 
	/** @deprecated Use create() instead. Kept for backward compatibility. */
	regenerate: (proId: string, projectId: string) =>
		socialDraftsApi.create(proId, { projectId, triggerSource: "auto" }),
 
	download: (proId: string, draftId: string) =>
		request<{ url: string }>(
			`/api/pro/${proId}/social-drafts/${draftId}/download`,
		),
 
	updateCaption: (proId: string, draftId: string, captionEn: string) =>
		request<SocialDraft>(`/api/pro/${proId}/social-drafts/${draftId}`, {
			method: "PATCH",
			body: { captionEn },
		}),
 
	listMusicTracks: (proId: string) =>
		request<{ tracks: MusicTrack[] }>(
			`/api/pro/${proId}/social-studio/music-tracks`,
		),
 
	listTemplates: (proId: string) =>
		request<{ templates: SocialStudioTemplate[] }>(
			`/api/pro/${proId}/social-studio/templates`,
		),
 
	// Preview-generate a voiceover script so the pro can edit it in an editable
	// textarea before rendering. Returns deterministic text for the same
	// (projectId, pro) pair — safe to cache on the Portal side.
	getVoiceoverPreviewScript: (proId: string, projectId: string) =>
		request<{ script: string }>(
			`/api/pro/${proId}/social-drafts/preview-script?projectId=${encodeURIComponent(projectId)}`,
		),
};