/** * C166 — pure geometry for the acervo excerpt picker: whole-second * [start, end] ranges snapped to transcript segment boundaries, clamped to the * 5 s minimum and to the speech duration (C170: no upper cap — the speech * itself is the limit). No DOM and no React: * the client controls only translate pointers/keys into these calls. */ /** * C174 — the empty-state CTA of "Cortes desta fala" lives outside the player * but must turn the picker on. The player owns the selection state, so the CTA * only announces the intent on `window`; the name lives here (client-safe, no * DOM) so the two ends cannot drift into a silent no-op. */ export const SPEECH_EXCERPT_REQUEST_EVENT = 'speech:excerpt-request' export type ExcerptSegment = { startSeconds: number endSeconds: number } export type ExcerptRange = { startSeconds: number endSeconds: number } export type ExcerptEdge = 'start' | 'end' /** C173 — the presentation phase of the excerpt preview, shared by the player and the card. */ export type ExcerptPreviewPhase = 'idle' | 'playing' | 'ended' export const MIN_EXCERPT_SECONDS = 5 /** * C174 — the duration the picker can work with: the stored duration, falling * back to the last transcript segment's end. Single owner of the rule so the * player and the empty "Cortes desta fala" block agree on availability. */ export const excerptSelectionDuration = ( durationSeconds: number | null | undefined, segments: readonly ExcerptSegment[], ): number | null => durationSeconds ?? segments.at(-1)?.endSeconds ?? null /** Whether the picker is offered at all (the speech holds the 5 s minimum). */ export const isExcerptSelectionAvailable = ( durationSeconds: number | null | undefined, segments: readonly ExcerptSegment[], ): boolean => { const duration = excerptSelectionDuration(durationSeconds, segments) return duration !== null && duration >= MIN_EXCERPT_SECONDS } export const rangeDurationSeconds = (range: ExcerptRange): number => range.endSeconds - range.startSeconds /** * C217 — whether a transcript segment is inside the picked window. Shared by * the Câmara and the web players so the marked phrases can never disagree * (ASR timestamps are fractional; the overlap is strict on both edges). */ export const segmentInExcerpt = (segment: ExcerptSegment, range: ExcerptRange): boolean => segment.endSeconds > range.startSeconds && segment.startSeconds < range.endSeconds /** * C173 — the stop target of the excerpt preview: the range end once * `currentSeconds` reaches it (inclusive). The `