/** Setup and search readiness are independent: media downloads need not block text. */ export type SemanticCoverage = "text_only" | "full_media"; export interface SemanticSetupStatus { ready: boolean; generation: number | null; pending: number; downloading: boolean; warming: boolean; embedding: boolean; error: string | null; indexing_ready?: boolean; text_model_ready?: boolean; media_model_ready?: boolean; media_model_error?: string | null; requested_coverage?: SemanticCoverage; speech?: { ready: boolean; pending: boolean; requested_policy: string | null; corpus_revision: number; }; } export function semanticIndexReady(status: SemanticSetupStatus) { return Boolean( status.ready && !status.warming && !status.embedding && (status.indexing_ready ?? !status.downloading), ); } /** Promotion must receive its own automatic attempt even with unchanged counts. */ export function semanticIndexKey( status: SemanticSetupStatus, folderRevision: number, ) { if (status.speech?.ready && status.speech.pending) { // Failed optional speech can still publish raw media: publication must not // create an infinite retry loop for the same source corpus and policy. return `speech:${status.speech.requested_policy}:${status.speech.corpus_revision}:${folderRevision}`; } return `${status.generation}:${status.pending}:${folderRevision}:${status.requested_coverage ?? "full_media"}`; } export function semanticRetryVisible(status: SemanticSetupStatus) { return Boolean(status.media_model_error || (status.error && !status.ready)); } export function semanticMediaReady(status: SemanticSetupStatus) { // Legacy capture fixtures and older full-pair status did not expose this field. return status.media_model_ready ?? (status.ready && !status.downloading); }