Merge commit 'refs/pull/8619/head' of https://github.com/nexu-io/open-design into feat/motion-design-skill

This commit is contained in:
bone3deep1962-collab
2026-10-12 18:07:00 +08:00
8 changed files with 62 additions and 8 deletions
+12 -1
View File
@@ -58,10 +58,17 @@ export function hyperFramesAudioEncodingArgs(tracks: HyperFramesAudioTrack[], du
// Filters stay within FFmpeg 4.1 (the bundled Linux build): every track is
// made stereo so one per-channel delay covers it (no `adelay all=1`), and
// amix's input averaging is undone with a gain (no `amix normalize=0`).
// Each track fades out where it stops sounding: its own authored end or the
// film's end, whichever comes first. A track that already ends quietly is
// unchanged; one cut short (a remake made shorter than its music) no longer
// stops on a click.
const filters = tracks.map((track, index) => {
const delayMs = Math.round(track.start * 1000);
const audible = Math.min(track.duration, duration - track.start);
const fade = audible > 0 ? Math.min(0.6, audible / 4) : 0;
const fadeOut = fade > 0 ? `afade=t=out:st=${round(audible - fade)}:d=${round(fade)},` : '';
return `[${index + 1}:a]atrim=duration=${track.duration},asetpts=PTS-STARTPTS,`
+ `aformat=channel_layouts=stereo,volume=${track.volume},adelay=${delayMs}|${delayMs}[a${index}]`;
+ `aformat=channel_layouts=stereo,${fadeOut}volume=${track.volume},adelay=${delayMs}|${delayMs}[a${index}]`;
});
const mixed = tracks.length === 1
? '[a0]'
@@ -70,3 +77,7 @@ export function hyperFramesAudioEncodingArgs(tracks: HyperFramesAudioTrack[], du
filters.push(`${mixed}apad,atrim=duration=${duration}[audio]`);
return ['-filter_complex', filters.join(';'), '-map', '0:v', '-map', '[audio]', '-c:a', 'aac', '-b:a', '192k'];
}
function round(seconds: number): number {
return Math.round(seconds * 1000) / 1000;
}
@@ -2,7 +2,7 @@ import { mkdtemp, writeFile, rm, symlink } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, expect, it } from 'vitest';
import { prepareHyperFramesAudio } from '../../src/media/hyperframes-audio.js';
import { hyperFramesAudioEncodingArgs, prepareHyperFramesAudio } from '../../src/media/hyperframes-audio.js';
let root: string;
beforeEach(async () => {
@@ -33,3 +33,17 @@ it.each(['data-start="intro + 2"', 'data-volume="2"', 'loop', 'data-media-start=
await expect(prepareHyperFramesAudio(`<audio src="music.mp3" data-duration="3" ${attribute}></audio>`, root)).rejects.toThrow();
},
);
it('fades each track out where it stops sounding, including a cut at the film end', () => {
const track = { file: 'music.mp3', format: 'mp3', volume: 1 };
const graph = (tracks: Parameters<typeof hyperFramesAudioEncodingArgs>[0], duration: number) =>
hyperFramesAudioEncodingArgs(tracks, duration)[1];
// Music authored for 15 s in a film cut to 10 s fades over the film's last 0.6 s.
expect(graph([{ ...track, start: 0, duration: 15 }], 10)).toContain('afade=t=out:st=9.4:d=0.6');
// A track that ends inside the film fades at its own end, offset by its start.
expect(graph([{ ...track, start: 2, duration: 4 }], 10)).toContain('afade=t=out:st=3.4:d=0.6');
// Short tracks keep most of their sound: the fade is at most a quarter of what plays.
expect(graph([{ ...track, start: 0, duration: 1 }], 10)).toContain('afade=t=out:st=0.75:d=0.25');
// A track that starts after the film ends contributes no fade.
expect(graph([{ ...track, start: 12, duration: 3 }], 10)).not.toContain('afade');
});
+5 -1
View File
@@ -94,4 +94,8 @@ Every template carries an original procedural soundtrack in
used and no audio-generation model was involved. HTML preview sound starts with
the film; when the browser blocks autoplay, the first click or key press turns
it on, and Sound off mutes it. Sound follows the master timeline. The desktop
export path mixes the timed local audio into the MP4 with FFmpeg.
export path mixes the timed local audio into the MP4 with FFmpeg and fades each
track out where it stops, so a remake made shorter than its music ends cleanly.
Make-same keeps the template's soundtrack by default, because the music is part
of the look being remade; a brief that asks for no music or different music
takes precedence.
@@ -65,7 +65,12 @@ frame expensive and slow the MP4 export.
## Sound
Carry `assets/soundtrack.mp3` with the reference files. It is an original
procedural soundtrack authored for this example and timed to its beats; no
audio from the source video is used. Keep the `<audio>` element inside the
audio from the source video is used. The soundtrack is part of the look the
user is remaking: keep it by default, and follow the brief when it asks for no
music or different music. The music lands on this film's beats, so keep the
film close to this length; when the brief changes the length, keep
`data-duration` within the film, and the export fades the track out where it
stops. Keep the `<audio>` element inside the
composition root, with numeric `data-start`, `data-duration` and `data-volume`;
the desktop exporter mixes that local file into the MP4. Preview sound
starts with the film; when the browser blocks autoplay, the first click or key
@@ -68,7 +68,12 @@ frame expensive and slow the MP4 export.
## Sound
Carry `assets/soundtrack.mp3` with the reference files. It is an original
procedural soundtrack authored for this example and timed to its beats; no
audio from the source video is used. Keep the `<audio>` element inside the
audio from the source video is used. The soundtrack is part of the look the
user is remaking: keep it by default, and follow the brief when it asks for no
music or different music. The music lands on this film's beats, so keep the
film close to this length; when the brief changes the length, keep
`data-duration` within the film, and the export fades the track out where it
stops. Keep the `<audio>` element inside the
composition root, with numeric `data-start`, `data-duration` and `data-volume`;
the desktop exporter mixes that local file into the MP4. Preview sound
starts with the film; when the browser blocks autoplay, the first click or key
@@ -59,7 +59,12 @@ frame expensive and slow the MP4 export.
## Sound
Carry `assets/soundtrack.mp3` with the reference files. It is an original
procedural soundtrack authored for this example and timed to its beats; no
audio from the source video is used. Keep the `<audio>` element inside the
audio from the source video is used. The soundtrack is part of the look the
user is remaking: keep it by default, and follow the brief when it asks for no
music or different music. The music lands on this film's beats, so keep the
film close to this length; when the brief changes the length, keep
`data-duration` within the film, and the export fades the track out where it
stops. Keep the `<audio>` element inside the
composition root, with numeric `data-start`, `data-duration` and `data-volume`;
the desktop exporter mixes that local file into the MP4. Preview sound
starts with the film; when the browser blocks autoplay, the first click or key
@@ -66,7 +66,12 @@ frame expensive and slow the MP4 export.
## Sound
Carry `assets/soundtrack.mp3` with the reference files. It is an original
procedural soundtrack authored for this example and timed to its beats; no
audio from the source video is used. Keep the `<audio>` element inside the
audio from the source video is used. The soundtrack is part of the look the
user is remaking: keep it by default, and follow the brief when it asks for no
music or different music. The music lands on this film's beats, so keep the
film close to this length; when the brief changes the length, keep
`data-duration` within the film, and the export fades the track out where it
stops. Keep the `<audio>` element inside the
composition root, with numeric `data-start`, `data-duration` and `data-volume`;
the desktop exporter mixes that local file into the MP4. Preview sound
starts with the film; when the browser blocks autoplay, the first click or key
@@ -60,7 +60,12 @@ frame expensive and slow the MP4 export.
## Sound
Carry `assets/soundtrack.mp3` with the reference files. It is an original
procedural soundtrack authored for this example and timed to its beats; no
audio from the source video is used. Keep the `<audio>` element inside the
audio from the source video is used. The soundtrack is part of the look the
user is remaking: keep it by default, and follow the brief when it asks for no
music or different music. The music lands on this film's beats, so keep the
film close to this length; when the brief changes the length, keep
`data-duration` within the film, and the export fades the track out where it
stops. Keep the `<audio>` element inside the
composition root, with numeric `data-start`, `data-duration` and `data-volume`;
the desktop exporter mixes that local file into the MP4. Preview sound
starts with the film; when the browser blocks autoplay, the first click or key