Files
feuerwerk/src/editor/videoImport.ts
T
adminandClaude Opus 5.5 24cd9fdeaa
feuerwerk / gate (push) Successful in 5m16s
feuerwerk / suite (push) Failing after 19m8s
Let the agent use stored files, presets and usages (#431 #432 #433)
The agent got stuck where the editor has it easy: it could not make a
Sample play without a sound asset, it built instruments node by node
although the Preset menu has them ready, and it could not tell what
names an asset before reworking or deleting it.

- list_files is a new tool. It lists the pictures, fonts, sounds or
  videos the server holds, with size, video state and the asset that
  already wears a file. Bakes and stream copies of songs are left out.
- add_file_asset makes an asset of such a file, as the picker does.
  It reuses the create rules of the import pipeline, so the four
  import kinds are exported now. It refuses a file already in the
  project (naming that asset), a picture without a size, a video not
  yet converted and a file the server does not hold.
- run_commands fetches the file lists its steps need before it runs,
  so the commands themselves stay synchronous.
- apply_preset sets one of the 16 instrument presets or 7 sfxr
  presets, like the Preset menu. Presets take every note, so the
  C4 to B4 range of a new Notes node is no trap there.
- get_asset gains usedBy: every asset naming the asset or one of its
  frames or clips, with the object, node, item or key that does, plus
  start scene, master FX and master routing of the project.
- usesOf in src/core/model/usages.ts walks the project data instead of
  a list of fields, so no new field can be missed. The node every
  asset has in the master routing does not count as a use.
- The relay tells the agent about all three ways.

Found on the way: suite 740 for 2e4c0a0 (#430) was red in e2e 68h
only, a sound clip 61 ms early against 50 allowed. #430 did not touch
sound timing, and e2e 68 on the same tree was green on atx (-4 ms).
Taken as a timing wobble under load until the next suite says more.

Tests:
- agent-files (new, 4 tests) with the other agent and relay tests:
  130/130.
- 8 counter checks, all compiling, all red.
- e2e 71 on atx: 26/26, new 71g9 lists an uploaded sound, makes an
  asset of it and fills an instrument from a preset.
- Gate on atx: CHECK EXIT 0, 326 files, 5021 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:06:15 +02:00

83 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CreateVideoCommand } from '../core/commands/assetCommands';
import type { UploadedVideo, VideoStore } from '../core/store/VideoStore';
import { adopt, type ImportKind, type ImportOutcome, importStored } from './assetImport';
import type { EditorState } from './state';
/** What the file dialog offers and a drop accepts; ffmpeg on the server has the last word. */
export const VIDEO_ACCEPT = '.mp4,.mov,.m4v,.webm,.mkv,.avi';
const VIDEO_EXTENSION = /\.(mp4|mov|m4v|webm|mkv|avi)$/i;
export function isVideoFileName(name: string): boolean {
return VIDEO_EXTENSION.test(name);
}
export interface VideoImportDeps {
state: EditorState;
videos: VideoStore;
}
/**
* A video becomes an asset only once the worker has made it (#402): the asset copies its size,
* length and frame rate, which are not known before.
*/
export const VIDEO_KIND: ImportKind<UploadedVideo, VideoImportDeps> = {
type: 'video',
noun: 'video',
fallbackName: 'Video',
store: (deps) => deps.videos,
prepare: (file) =>
isVideoFileName(file.name)
? Promise.resolve({ blob: file, type: 'application/octet-stream' })
: Promise.reject(new Error('Not a video file – use MP4, MOV, WebM, MKV or AVI.')),
create: (name, video) =>
new CreateVideoCommand(name, video.file, {
width: video.width ?? 0,
height: video.height ?? 0,
duration: video.duration ?? 0,
fps: video.fps ?? 0,
audio: video.audio ?? false,
}),
};
export function importStoredVideo(video: UploadedVideo, deps: VideoImportDeps): ImportOutcome {
return importStored(VIDEO_KIND, video, deps);
}
/** Makes an asset named `name` of a video the server has converted, without opening it (#405). */
export function adoptVideo(video: UploadedVideo, name: string, deps: VideoImportDeps): ImportOutcome {
return adopt(VIDEO_KIND, video, name, name, deps);
}
/**
* Sends the files up one after the other and says how it went; what is done already is made an
* asset at once. The rest is converted on the server – `waiting` gets their names, for the picker
* to watch and adopt when they are ready.
*/
export async function uploadVideos(
files: readonly File[],
deps: VideoImportDeps,
waiting: (files: string[]) => void,
): Promise<void> {
const pending: string[] = [];
const failed: string[] = [];
for (const file of files) {
try {
const { blob, type } = await VIDEO_KIND.prepare(file);
const uploaded = await deps.videos.upload(blob, type, file.name);
if (uploaded.status === 'ready') importStoredVideo(uploaded, deps);
else pending.push(uploaded.file);
} catch (error) {
failed.push(`"${file.name}": ${error instanceof Error ? error.message : String(error)}`);
}
}
if (pending.length > 0) waiting(pending);
if (failed.length > 0) deps.state.setNotice(`Could not upload ${failed.join(', ')}`, 'error');
else if (pending.length > 0)
deps.state.setNotice(
pending.length === 1
? 'Converting the video on the server…'
: `Converting ${pending.length} videos on the server…`,
);
}