Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
47 lines
2.7 KiB
JavaScript
47 lines
2.7 KiB
JavaScript
// The project as lanes of clips: what the timeline stage draws. Pure.
|
|
import { segments, isVideoClip, effFps } from "../../shell/scenes.js";
|
|
|
|
const first = (t) => ((t || "").split("\n")[0] || "").slice(0, 80);
|
|
|
|
const LIKED = new Set(["Perfect", "Nailed it", "Missing action", "Missing details", "Wrong details"]);
|
|
const DISLIKED = new Set(["Missing quality", "Missing action + quality", "Missing details + quality", "Wrong action", "Wrong action + quality", "Awful"]);
|
|
|
|
/** "liked" / "disliked" / "" from a saved rating word (numbers: 6+ is liked; "-Just forget it-" is none). */
|
|
export function mood(rating) {
|
|
const v = String(rating || "").replace(/\|loved$/, "").trim();
|
|
if (!v || v.startsWith("-")) return "";
|
|
if (/^\d+$/.test(v)) return Number(v) >= 6 ? "liked" : "disliked";
|
|
return LIKED.has(v) ? "liked" : DISLIKED.has(v) || v.toLowerCase().startsWith("disliked") ? "disliked" : ""; // v4's category words, by the sign of their reward
|
|
}
|
|
|
|
export function videoLane(p, picked, focus, actions = () => [], ghostActions = () => [], tags = () => []) {
|
|
const renders = p.scene_renders || {};
|
|
return {
|
|
id: "video", label: "Video", kind: "video", reorder: true, slip: true,
|
|
clips: segments(p).map((seg) => {
|
|
if (seg.kind === "gap") return { id: seg.id, start: seg.start, dur: seg.dur, title: "pause", ghost: true };
|
|
if (seg.kind === "ghost") return { id: seg.id, start: seg.start, dur: seg.dur, title: first(seg.ghost.text) || "removed", ghost: true, actions: ghostActions(seg.ghost) };
|
|
const sc = seg.scene;
|
|
const rendered = Boolean(renders[sc.id]);
|
|
return {
|
|
id: sc.id, start: seg.start, dur: seg.dur, trim: true,
|
|
rating: mood(sc.rating) || undefined,
|
|
head: [isVideoClip(sc) ? "▶ video" : rendered ? "✓" : "◌", ...(sc.source_image ? ["◐"] : []), `${seg.dur.toFixed(1)}s`, ...(mood(sc.rating) ? [mood(sc.rating) === "liked" ? "★" : "✕"] : []), ...tags(sc)],
|
|
thumbs: sc.source_image ? [`/funpack/api/media/${encodeURIComponent(sc.source_image)}/thumb`] : undefined, // the picture it starts from
|
|
title: first(sc.text) || "(empty scene)",
|
|
selected: picked.includes(sc.id), focus: sc.id === focus, excluded: Boolean(sc.excluded),
|
|
actions: actions(sc),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
/** Frames at `sec` into scene `sc`, for Split. */
|
|
export const framesAt = (sc, p, sec) => Math.round(sec * effFps(sc, p));
|
|
|
|
/** The sound that came with the picture, one block under each clip. */
|
|
export const audioLane = (p, picked) => ({
|
|
id: "audio", label: "Original", kind: "audio",
|
|
clips: segments(p).filter((s) => s.kind === "scene").map((s, i) => ({ id: `a:${s.id}`, start: s.start, dur: s.dur, title: `S${i + 1}`, selected: picked.includes(s.id), excluded: Boolean(s.scene.excluded) })),
|
|
});
|