mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio): timeline leaf helpers — audio inspector, zoom math, UI prefs (#2192)
What: extends three leaf modules to their final NLE-stack form, tests in the same change: timelineInspector (isAudioTimelineElement, resolveBeatSourceTrack), timelineZoom (zoom/pps math incl. computePinnedZoomPercent), and studioUiPreferences (persisted editor prefs). Why: leaf dependencies of the NLE timeline stack; landing them first keeps the later glue PRs to wiring. How: additive from the consumer side — every export main already uses is unchanged (typecheck against main's consumers passes untouched); every new export is exercised by a test in this PR. Test plan: bunx vitest run on the three test files; tsc --noEmit in packages/studio; fallow audit --base origin/main clean.
This commit is contained in:
@@ -4,7 +4,7 @@ const AUDIO_TIMELINE_TAGS = new Set(["audio", "music", "sfx", "sound", "narratio
|
||||
const AUDIO_SOURCE_EXT_RE = /\.(aac|flac|m4a|mp3|ogg|opus|wav)(?:[?#].*)?$/i;
|
||||
const MUSIC_ID_RE = /\b(music|bgm|soundtrack|background[-_]?music)\b/i;
|
||||
|
||||
function isAudioTimelineElement(
|
||||
export function isAudioTimelineElement(
|
||||
element: Pick<TimelineElement, "tag" | "src"> | null | undefined,
|
||||
): boolean {
|
||||
if (!element) return false;
|
||||
@@ -29,3 +29,34 @@ export function isMusicTrack(
|
||||
const id = element.domId ?? element.id ?? "";
|
||||
return MUSIC_ID_RE.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the best audio source for beat analysis. An explicitly tagged or
|
||||
* named music track wins; when none is present (e.g. an audio file dropped
|
||||
* from Finder with a generic id), the LONGEST untagged audio clip is used as a
|
||||
* fallback. Ties on duration resolve to the FIRST such clip encountered (the loop
|
||||
* keeps the current best on `>` only), i.e. discovery/DOM order wins.
|
||||
* Returns the element and whether it was found via the fallback path.
|
||||
*
|
||||
* The `isMusicTrack` predicate is unchanged so beat-snap and drag-exclusion
|
||||
* logic remain unaffected by this fallback.
|
||||
*/
|
||||
export function resolveBeatSourceTrack(
|
||||
elements: readonly Pick<
|
||||
TimelineElement,
|
||||
"tag" | "src" | "id" | "domId" | "timelineRole" | "duration"
|
||||
>[],
|
||||
): { element: (typeof elements)[number]; isFallback: boolean } | null {
|
||||
const explicit = elements.find(isMusicTrack);
|
||||
if (explicit) return { element: explicit, isFallback: false };
|
||||
|
||||
// Fallback: pick the longest audio clip (skipping explicitly non-music roles
|
||||
// like "sfx" or "voiceover" to avoid triggering beat analysis on those).
|
||||
let best: (typeof elements)[number] | null = null;
|
||||
for (const el of elements) {
|
||||
if (!isAudioTimelineElement(el)) continue;
|
||||
if (el.timelineRole && el.timelineRole !== "music") continue;
|
||||
if (!best || el.duration > best.duration) best = el;
|
||||
}
|
||||
return best ? { element: best, isFallback: true } : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user