feat: build reproducible dataset editor

This commit is contained in:
Codex
2026-08-28 10:46:33 +09:00
parent 6a1197d60e
commit 08fcf578de
16 changed files with 2579 additions and 11 deletions
+105
View File
@@ -0,0 +1,105 @@
export const DEFAULT_VIEW_PLANE = Object.freeze({
xMin: -0.6,
xMax: 0.6,
yMin: -0.45,
yMax: 0.45,
});
export const SOURCE_COLORS = ["#65f2bf", "#58c7e8", "#ffc56e", "#ff8f87", "#be9bff", "#8ee36b"];
export function prepareSceneForEditing(scene) {
const prepared = structuredClone(scene);
prepared.assets ??= [];
prepared.ambient ??= [];
prepared.mix ??= { normalization: "none", targetPeak: 0.95 };
return prepared;
}
export function createSceneId(now = Date.now(), randomValue = crypto.getRandomValues(new Uint32Array(1))[0]) {
return `scene-${now.toString(36)}-${randomValue.toString(36)}`;
}
export function uniqueId(base, existingIds) {
const normalized = String(base)
.toLowerCase()
.normalize("NFKD")
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "") || "asset";
let candidate = normalized;
let suffix = 2;
while (existingIds.has(candidate)) {
candidate = `${normalized}-${suffix}`;
suffix += 1;
}
return candidate;
}
export function assetDescriptorForFile(file, kind, existingIds) {
const baseName = file.name.replace(/\.[^.]+$/, "") || kind;
return {
id: uniqueId(`${kind}-${baseName}`, existingIds),
kind,
fileName: file.name,
mimeType: file.type || (kind === "image" ? "image/*" : "audio/*"),
};
}
export function matchAssetDescriptor(assets, boundAssetIds, fileName, kind, hash) {
const candidates = assets.filter((asset) => asset.kind === kind);
const hashMatches = candidates.filter((asset) => asset.sha256 === hash);
const hashMatch = hashMatches.find((asset) => !boundAssetIds.has(asset.id)) ?? hashMatches[0];
if (hashMatch) return { descriptor: hashMatch, conflict: false };
const baseName = (path) => path.split(/[\\/]/).pop();
const nameCandidates = candidates.filter((asset) => baseName(asset.fileName) === fileName);
const compatible = nameCandidates.find((asset) => !asset.sha256 && !boundAssetIds.has(asset.id));
if (compatible) return { descriptor: compatible, conflict: false };
return { descriptor: null, conflict: nameCandidates.some((asset) => asset.sha256 !== hash) };
}
export function createSource(scene) {
const existingIds = new Set(scene.sources.map((source) => source.id));
const ordinal = scene.sources.length + 1;
return {
id: uniqueId(`source-${ordinal}`, existingIds),
name: `음원 ${ordinal}`,
position: [0, 0, scene.image?.plane?.z ?? scene.sources[0]?.position?.[2] ?? 2],
gainDb: -6,
startTime: 0,
duration: scene.duration,
signal: { type: "tone", frequency: Math.min(1000, scene.sampleRate / 4), phaseDeg: 0 },
};
}
export function changeSignalType(source, type, firstAudioAssetId = "") {
if (type === "tone") {
source.signal = { type: "tone", frequency: 1000, phaseDeg: 0 };
} else if (type === "noise") {
source.signal = { type: "noise", color: "white", seedOffset: 0 };
} else if (type === "asset") {
source.signal = { type: "asset", assetId: firstAudioAssetId, channel: 0, loop: false };
}
}
export function describeSignal(signal) {
if (signal.type === "tone") return `${signal.frequency} Hz tone`;
if (signal.type === "noise") return `${signal.color} noise`;
if (signal.type === "asset") return `asset · ${signal.assetId || "미선택"}`;
return "알 수 없는 신호";
}
export function referencedAssetIds(scene) {
const ids = new Set();
if (scene.image?.assetId) ids.add(scene.image.assetId);
for (const entry of [...scene.sources, ...(scene.ambient ?? [])]) {
if (entry.signal?.type === "asset" && entry.signal.assetId) ids.add(entry.signal.assetId);
}
return ids;
}
export function isAssetReferenced(scene, assetId) {
return referencedAssetIds(scene).has(assetId);
}
export function safeDownloadStem(scene) {
return uniqueId(scene.id || scene.name || "scene", new Set());
}