Create standalone browser port of LTX Athena with authenticated native API transport
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import { persistedKeyframeSchema } from '../types/project-model.ts'
|
||||
import { pickFreeFrameIndex } from './keyframe-timeline.ts'
|
||||
import {
|
||||
appendKeyframePaths,
|
||||
applyKeyframeImagePaths,
|
||||
DEFAULT_KEYFRAME_STRENGTH,
|
||||
MISSING_KEYFRAME_STRENGTH,
|
||||
fromPersistedKeyframes,
|
||||
toPersistedKeyframes,
|
||||
videoGenerationModeFromInputs,
|
||||
enhanceKeyframesPayload,
|
||||
type KeyframeItem,
|
||||
} from './multi-keyframe.ts'
|
||||
|
||||
function item(
|
||||
id: string,
|
||||
path: string,
|
||||
frameIndex: number,
|
||||
strength = DEFAULT_KEYFRAME_STRENGTH,
|
||||
): KeyframeItem {
|
||||
return { id, path, frameIndex, strength }
|
||||
}
|
||||
|
||||
describe('rapid keyframe placement', () => {
|
||||
it('never assigns the same frame index twice', () => {
|
||||
const placed: { frameIndex: number }[] = []
|
||||
|
||||
for (let count = 0; count < 5; count++) {
|
||||
const frameIndex = pickFreeFrameIndex(placed, 120, 0)
|
||||
assert.notEqual(frameIndex, null)
|
||||
placed.push({ frameIndex: frameIndex! })
|
||||
}
|
||||
|
||||
assert.equal(new Set(placed.map(({ frameIndex }) => frameIndex)).size, placed.length)
|
||||
})
|
||||
})
|
||||
|
||||
describe('appendKeyframePaths', () => {
|
||||
it('spreads a burst of images across free frames', () => {
|
||||
let nextId = 0
|
||||
const added = appendKeyframePaths(
|
||||
[],
|
||||
['/a.png', '/b.png', '/c.png'],
|
||||
120,
|
||||
0,
|
||||
5,
|
||||
() => `id-${nextId++}`,
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
added.map(({ path, frameIndex, strength }) => ({ path, frameIndex, strength })),
|
||||
[
|
||||
{ path: '/a.png', frameIndex: 0, strength: DEFAULT_KEYFRAME_STRENGTH },
|
||||
{ path: '/b.png', frameIndex: 60, strength: DEFAULT_KEYFRAME_STRENGTH },
|
||||
{ path: '/c.png', frameIndex: 90, strength: DEFAULT_KEYFRAME_STRENGTH },
|
||||
],
|
||||
)
|
||||
assert.equal(new Set(added.map(({ frameIndex }) => frameIndex)).size, 3)
|
||||
})
|
||||
|
||||
it('stops at the remaining cap', () => {
|
||||
const existing: KeyframeItem[] = [
|
||||
item('opening', '/opening.png', 0),
|
||||
item('closing', '/closing.png', 10),
|
||||
]
|
||||
|
||||
const added = appendKeyframePaths(
|
||||
existing,
|
||||
['/a.png', '/b.png', '/c.png'],
|
||||
20,
|
||||
0,
|
||||
3,
|
||||
() => 'extra',
|
||||
)
|
||||
|
||||
assert.equal(added.length, 3)
|
||||
assert.equal(added.at(-1)?.path, '/a.png')
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyKeyframeImagePaths', () => {
|
||||
const existing: KeyframeItem[] = [
|
||||
item('opening', '/opening.png', 0),
|
||||
]
|
||||
|
||||
it('replaces one marker and appends leftover files', () => {
|
||||
let nextId = 0
|
||||
const next = applyKeyframeImagePaths({
|
||||
keyframes: existing,
|
||||
paths: ['/new-opening.png', '/middle.png'],
|
||||
replaceId: 'opening',
|
||||
lastFrame: 80,
|
||||
preferredFrame: 0,
|
||||
maxCount: 5,
|
||||
createId: () => `id-${nextId++}`,
|
||||
})
|
||||
|
||||
assert.equal(next[0]?.path, '/new-opening.png')
|
||||
assert.equal(next[0]?.frameIndex, 0)
|
||||
assert.equal(next[1]?.path, '/middle.png')
|
||||
assert.notEqual(next[1]?.frameIndex, 0)
|
||||
})
|
||||
|
||||
it('replaces without appending when duration is unknown', () => {
|
||||
const next = applyKeyframeImagePaths({
|
||||
keyframes: existing,
|
||||
paths: ['/new-opening.png', '/ignored.png'],
|
||||
replaceId: 'opening',
|
||||
lastFrame: null,
|
||||
preferredFrame: 0,
|
||||
maxCount: 5,
|
||||
})
|
||||
|
||||
assert.deepEqual(next, [item('opening', '/new-opening.png', 0)])
|
||||
})
|
||||
|
||||
it('keeps the still\'s owned strength when replacing its image', () => {
|
||||
const next = applyKeyframeImagePaths({
|
||||
keyframes: [item('opening', '/opening.png', 0, 0.7)],
|
||||
paths: ['/new-opening.png'],
|
||||
replaceId: 'opening',
|
||||
lastFrame: 80,
|
||||
preferredFrame: 0,
|
||||
maxCount: 5,
|
||||
})
|
||||
|
||||
assert.deepEqual(next, [item('opening', '/new-opening.png', 0, 0.7)])
|
||||
})
|
||||
})
|
||||
|
||||
describe('persisted keyframes', () => {
|
||||
it('drops UI ids when snapshotting for generationParams', () => {
|
||||
assert.deepEqual(
|
||||
toPersistedKeyframes([
|
||||
item('opening', '/opening.png', 0),
|
||||
item('closing', '/closing.png', 80, 0.7),
|
||||
]),
|
||||
[
|
||||
{ path: '/opening.png', frameIndex: 0, strength: DEFAULT_KEYFRAME_STRENGTH },
|
||||
{ path: '/closing.png', frameIndex: 80, strength: 0.7 },
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
it('restores items with fresh ids and owned strength', () => {
|
||||
let nextId = 0
|
||||
const restored = fromPersistedKeyframes(
|
||||
[{ path: '/opening.png', frameIndex: 0, strength: 0.7 }],
|
||||
() => `id-${nextId++}`,
|
||||
)
|
||||
assert.deepEqual(restored, [item('id-0', '/opening.png', 0, 0.7)])
|
||||
})
|
||||
|
||||
it('fills missing persisted strength with a full lock, not the new-still default', () => {
|
||||
let nextId = 0
|
||||
const restored = fromPersistedKeyframes(
|
||||
[{ path: '/opening.png', frameIndex: 0 }],
|
||||
() => `id-${nextId++}`,
|
||||
)
|
||||
assert.deepEqual(restored, [item('id-0', '/opening.png', 0, MISSING_KEYFRAME_STRENGTH)])
|
||||
assert.notEqual(MISSING_KEYFRAME_STRENGTH, DEFAULT_KEYFRAME_STRENGTH)
|
||||
})
|
||||
|
||||
it('persists and restores a zero lock instead of treating it as missing', () => {
|
||||
assert.deepEqual(
|
||||
toPersistedKeyframes([item('opening', '/opening.png', 0, 0)]),
|
||||
[{ path: '/opening.png', frameIndex: 0, strength: 0 }],
|
||||
)
|
||||
let nextId = 0
|
||||
const restored = fromPersistedKeyframes(
|
||||
[{ path: '/opening.png', frameIndex: 0, strength: 0 }],
|
||||
() => `id-${nextId++}`,
|
||||
)
|
||||
assert.deepEqual(restored, [item('id-0', '/opening.png', 0, 0)])
|
||||
})
|
||||
|
||||
it('clamps restored strength to the 0-1 lock range', () => {
|
||||
let nextId = 0
|
||||
const restored = fromPersistedKeyframes(
|
||||
[
|
||||
{ path: '/hi.png', frameIndex: 0, strength: 1.5 },
|
||||
{ path: '/lo.png', frameIndex: 40, strength: -0.2 },
|
||||
],
|
||||
() => `id-${nextId++}`,
|
||||
)
|
||||
assert.deepEqual(restored, [
|
||||
item('id-0', '/hi.png', 0, 1),
|
||||
item('id-1', '/lo.png', 40, 0),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('videoGenerationModeFromInputs', () => {
|
||||
it('prefers multi-keyframe over image and audio inputs', () => {
|
||||
assert.equal(
|
||||
videoGenerationModeFromInputs({
|
||||
keyframes: [{ path: '/opening.png', frameIndex: 0 }],
|
||||
audioUrl: '/clip.mp3',
|
||||
imageUrl: '/still.png',
|
||||
}),
|
||||
'multi-keyframe',
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to text-to-video when nothing is attached', () => {
|
||||
assert.equal(videoGenerationModeFromInputs({}), 'text-to-video')
|
||||
})
|
||||
})
|
||||
|
||||
describe('enhanceKeyframesPayload', () => {
|
||||
it('returns undefined when there are no stills', () => {
|
||||
assert.equal(enhanceKeyframesPayload([]), undefined)
|
||||
})
|
||||
|
||||
it('maps a single still onto the enhance keyframe list', () => {
|
||||
assert.deepEqual(
|
||||
enhanceKeyframesPayload([item('opening', '/opening.png', 40)]),
|
||||
[{ imagePath: '/opening.png', frameIndex: 40, strength: DEFAULT_KEYFRAME_STRENGTH }],
|
||||
)
|
||||
})
|
||||
|
||||
it('sends every still, including middle markers, in frame order', () => {
|
||||
assert.deepEqual(
|
||||
enhanceKeyframesPayload([
|
||||
item('closing', '/closing.png', 80),
|
||||
item('opening', '/opening.png', 0),
|
||||
item('middle', '/middle.png', 40),
|
||||
]),
|
||||
[
|
||||
{ imagePath: '/opening.png', frameIndex: 0, strength: DEFAULT_KEYFRAME_STRENGTH },
|
||||
{ imagePath: '/middle.png', frameIndex: 40, strength: DEFAULT_KEYFRAME_STRENGTH },
|
||||
{ imagePath: '/closing.png', frameIndex: 80, strength: DEFAULT_KEYFRAME_STRENGTH },
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards each still\'s owned strength instead of hardcoding the default', () => {
|
||||
assert.deepEqual(
|
||||
enhanceKeyframesPayload([item('opening', '/opening.png', 0, 0.7)]),
|
||||
[{ imagePath: '/opening.png', frameIndex: 0, strength: 0.7 }],
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards a zero lock instead of treating it as missing', () => {
|
||||
assert.deepEqual(
|
||||
enhanceKeyframesPayload([item('opening', '/opening.png', 0, 0)]),
|
||||
[{ imagePath: '/opening.png', frameIndex: 0, strength: 0 }],
|
||||
)
|
||||
})
|
||||
|
||||
it('clamps out-of-range strength on the enhance payload', () => {
|
||||
assert.deepEqual(
|
||||
enhanceKeyframesPayload([
|
||||
item('hi', '/hi.png', 0, 1.5),
|
||||
item('lo', '/lo.png', 40, -0.2),
|
||||
]),
|
||||
[
|
||||
{ imagePath: '/hi.png', frameIndex: 0, strength: 1 },
|
||||
{ imagePath: '/lo.png', frameIndex: 40, strength: 0 },
|
||||
],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistedKeyframeSchema', () => {
|
||||
it('defaults missing strength to a full lock', () => {
|
||||
assert.deepEqual(
|
||||
persistedKeyframeSchema.parse({ path: '/opening.png', frameIndex: 0 }),
|
||||
{ path: '/opening.png', frameIndex: 0, strength: MISSING_KEYFRAME_STRENGTH },
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps a zero lock and clamps out of range so project parse cannot fail', () => {
|
||||
assert.equal(
|
||||
persistedKeyframeSchema.parse({ path: '/opening.png', frameIndex: 0, strength: 0 }).strength,
|
||||
0,
|
||||
)
|
||||
assert.equal(
|
||||
persistedKeyframeSchema.parse({ path: '/opening.png', frameIndex: 0, strength: 1.5 }).strength,
|
||||
1,
|
||||
)
|
||||
assert.equal(
|
||||
persistedKeyframeSchema.parse({ path: '/opening.png', frameIndex: 0, strength: -0.2 }).strength,
|
||||
0,
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user