vivid-editor-core 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -348,6 +348,10 @@ export function executeAiCommands(commands, snapshot, actions) {
348
348
  for (let i = 0; i < beatsAbsolute.length; i += beatInterval) {
349
349
  cutPoints.push(beatsAbsolute[i]);
350
350
  }
351
+ // Pictures from the first note: the first beat is often a few ms in
352
+ // (60 ms on a MiniMax track), which left a black flash at the start.
353
+ if (mode === 'redistribute')
354
+ cutPoints[0] = audioStart;
351
355
  // Always end at audio end so the last segment fills the music.
352
356
  if (cutPoints[cutPoints.length - 1] < audioEnd - 50)
353
357
  cutPoints.push(audioEnd);
@@ -366,69 +370,70 @@ export function executeAiCommands(commands, snapshot, actions) {
366
370
  const newCanvasObjects = [];
367
371
  const trackId = targetVisuals[0].trackId;
368
372
  const now = Date.now();
369
- // Walk the tape with a moving cursor; for each cut interval,
370
- // peel off a segment of the tape equal to (next - current).
373
+ // Every cut interval gets exactly one piece of its full length, so
374
+ // nothing is left black between two cuts or before the end of the
375
+ // music. Walking the tape with a cursor:
376
+ // - the rest of the current segment covers the interval → take it;
377
+ // - it covers at least MIN_RATE of it → take it slowed down a little
378
+ // (a still image just stays on screen longer);
379
+ // - it is shorter → drop that leftover and try the next segment;
380
+ // - the tape runs out → start again from the first segment.
381
+ // If no segment can fill an interval even after a full loop, the
382
+ // longest one is slowed down as far as the player allows.
383
+ const MIN_RATE = 0.8;
384
+ const EPS = 1;
371
385
  let tapeIdx = 0;
372
- let tapeCursorInSegment = 0;
386
+ let cursor = 0;
387
+ const advance = () => { tapeIdx = (tapeIdx + 1) % tape.length; cursor = 0; };
388
+ const pick = (segmentMs) => {
389
+ for (let tries = 0; tries <= tape.length; tries++) {
390
+ const seg = tape[tapeIdx];
391
+ const avail = seg.durationMs - cursor;
392
+ const still = seg.sourceClip.mediaType === 'image';
393
+ if (avail >= segmentMs - EPS || (avail > EPS && (still || avail >= segmentMs * MIN_RATE))) {
394
+ const used = Math.min(avail, segmentMs);
395
+ const piece = {
396
+ seg,
397
+ offsetMs: seg.offsetMs + cursor,
398
+ rate: still || used >= segmentMs - EPS ? undefined : used / segmentMs,
399
+ };
400
+ cursor += used;
401
+ if (cursor >= seg.durationMs - EPS)
402
+ advance();
403
+ return piece;
404
+ }
405
+ advance();
406
+ }
407
+ const seg = tape.reduce((a, b) => (b.durationMs > a.durationMs ? b : a));
408
+ tapeIdx = tape.indexOf(seg);
409
+ advance();
410
+ return { seg, offsetMs: seg.offsetMs, rate: Math.max(0.25, seg.durationMs / segmentMs) };
411
+ };
373
412
  for (let i = 0; i < cutPoints.length - 1; i++) {
374
413
  const segmentMs = cutPoints[i + 1] - cutPoints[i];
375
414
  if (segmentMs <= 0)
376
415
  continue;
377
- // The piece we lay down references the current tape segment,
378
- // possibly clipping its tail if the tape segment is shorter
379
- // than the beat interval (then we step to next tape segment).
380
- let remaining = segmentMs;
381
- let placed = false;
382
- while (remaining > 0 && tapeIdx < tape.length) {
383
- const seg = tape[tapeIdx];
384
- const available = seg.durationMs - tapeCursorInSegment;
385
- if (available <= 0) {
386
- tapeIdx++;
387
- tapeCursorInSegment = 0;
388
- continue;
389
- }
390
- const take = Math.min(available, remaining);
391
- if (!placed) {
392
- // First piece of this beat segment — emit a clip starting at cutPoints[i].
393
- const newId = `clip-cb-${now}-${i}`;
394
- newClips.push({
395
- id: newId,
396
- assetId: seg.sourceClip.assetId,
397
- name: seg.sourceClip.name,
398
- url: seg.sourceClip.url,
399
- startMs: cutPoints[i],
400
- durationMs: take,
401
- originalDurationMs: seg.sourceClip.originalDurationMs,
402
- sourceOffsetMs: seg.offsetMs + tapeCursorInSegment,
403
- mediaType: seg.sourceClip.mediaType,
404
- trackId,
405
- });
406
- newCanvasObjects.push({
407
- id: `obj-${newId}`,
408
- clipId: newId,
409
- x: 0, y: 0, w: 1, h: 1,
410
- rotation: 0,
411
- });
412
- placed = true;
413
- }
414
- else {
415
- // Subsequent piece within the same beat — extend the
416
- // previous clip's duration if and only if it's the same
417
- // source asset; otherwise start a fresh clip nested in
418
- // the same beat. For simplicity, we only support a
419
- // single asset per beat segment (truncate trailing tape
420
- // and snap to next beat).
421
- break;
422
- }
423
- tapeCursorInSegment += take;
424
- remaining -= take;
425
- if (tapeCursorInSegment >= seg.durationMs) {
426
- tapeIdx++;
427
- tapeCursorInSegment = 0;
428
- }
429
- }
430
- if (tapeIdx >= tape.length)
431
- break; // tape exhausted, stop emitting
416
+ const { seg, offsetMs, rate } = pick(segmentMs);
417
+ const newId = `clip-cb-${now}-${i}`;
418
+ newClips.push({
419
+ id: newId,
420
+ assetId: seg.sourceClip.assetId,
421
+ name: seg.sourceClip.name,
422
+ url: seg.sourceClip.url,
423
+ startMs: cutPoints[i],
424
+ durationMs: segmentMs,
425
+ originalDurationMs: seg.sourceClip.originalDurationMs,
426
+ sourceOffsetMs: offsetMs,
427
+ mediaType: seg.sourceClip.mediaType,
428
+ trackId,
429
+ ...(rate !== undefined ? { playbackRate: rate } : {}),
430
+ });
431
+ newCanvasObjects.push({
432
+ id: `obj-${newId}`,
433
+ clipId: newId,
434
+ x: 0, y: 0, w: 1, h: 1,
435
+ rotation: 0,
436
+ });
432
437
  }
433
438
  // Preserve audio + non-target clips intact.
434
439
  const preservedClips = snapshot.timelineClips.filter((c) => c.mediaType === 'audio' || c.mediaType === 'color' ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vivid-editor-core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "VIVID video editor core: project file schema, editor store and AI command orchestrator — shared by the vividai.tv editor and vivid-mcp.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",