openvibers/openvibe.live Commits

Clip cuts follow their Media job (WS-G task 3): a clip the server cuts answers with its clip.cut job id; the channel page and the broadcast dashboard follow it (public/js/ov-clip-jobs.js) and open the trim editor or say it failed, with a notice per retry; a reload reattaches (pending cuts in localStorage, resumed when the player starts); GET /api/clips/:id/job for the clip's maker or manager, only that clip's cut job; re-cut passes its job id

OP OpenVibers committed 26 Sept 2026 · 73c8a39b78 · 7 files, +306 −10


Changes

ddiff --git a/public/features.json b/public/features.json
iindex 5bb25335..8e71b38b 100644
---- a/public/features.json
++++ b/public/features.json
@@@ -51,7 +51,15 @@
"feeds"
]
},
++ "clipJobs": {
++ "js": [
++ "/js/ov-clip-jobs.js"
++ ]
++ },
"player": {
++ "deps": [
++ "clipJobs"
++ ],
"js": [
"/js/stream-player.js",
"/js/stream-pip.js"
@@@ -192,7 +200,8 @@
"deps": [
"chatExtras",
"guide",
-- "aiViewers"
++ "aiViewers",
++ "clipJobs"
],
"js": [
"/js/broadcast-state.js",
ddiff --git a/public/js/broadcast.js b/public/js/broadcast.js
iindex 4a414085..9a83641b 100644
---- a/public/js/broadcast.js
++++ b/public/js/broadcast.js
@@@ -5795,7 +5795,17 @@ async function createBroadcastClip() {
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to create clip');
-- showToast(`Clip created! "${title}"`, 'success');
++ // The server cuts it (Media's clip.cut job): say when it is ready, even after a reload.
++ const made = data.clip || {};
++ if (made.job_id && made.status === 'processing' && window.OVClipJobs) {
++ showToast(`Clip "${title}" created — cutting it now…`, 'success');
++ window.OVClipJobs.follow(made.id, made.job_id, {
++ onReady: () => showToast(`Clip "${title}" is ready`, 'success'),
++ onFailed: (msg) => showToast(`Clip "${title}" could not be cut: ${msg}`, 'error'),
++ });
++ } else {
++ showToast(`Clip created! "${title}"`, 'success');
++ }
// Reset panel
const titleInput = document.getElementById('bc-clip-title');
ddiff --git a/public/js/ov-clip-jobs.js b/public/js/ov-clip-jobs.js
nnew file mode 100644
iindex 00000000..fc86f202
---- /dev/null
++++ b/public/js/ov-clip-jobs.js
@@@ -0,0 +1,64 @@
++/**
++ * Following a clip cut on the server (OpenVibe.Media's clip.cut job; roadmap WS-G task 3).
++ *
++ * A clip the server cuts answers with its job id. The page follows the job until the clip is ready
++ * and survives a reload: pending cuts are kept in localStorage (ov_clip_jobs, 30 minutes at most) and
++ * the next page that loads this reattaches to them.
++ *
++ * OVClipJobs.follow(clipId, jobId, { onReady(clip), onFailed(message), onRetry(job) })
++ * OVClipJobs.resume({ onReady, onFailed, onRetry }) follow what an earlier page left pending
++ * OVClipJobs.pending() [{ clipId, jobId, at }]
++ */
++(function () {
++ 'use strict';
++ var KEY = 'ov_clip_jobs';
++ var MAX_AGE_MS = 30 * 60 * 1000;
++ var followed = {};
++
++ function load() {
++ try {
++ var list = JSON.parse(localStorage.getItem(KEY) || '[]');
++ return Array.isArray(list) ? list.filter(function (e) { return e && e.clipId && e.jobId && Date.now() - e.at < MAX_AGE_MS; }) : [];
++ } catch (e) { return []; }
++ }
++ function save(list) { try { localStorage.setItem(KEY, JSON.stringify(list.slice(-10))); } catch (e) { /* storage off: this page still follows */ } }
++ function forget(jobId) { save(load().filter(function (e) { return e.jobId !== jobId; })); delete followed[jobId]; }
++ function remember(clipId, jobId) {
++ var list = load().filter(function (e) { return e.jobId !== jobId; });
++ list.push({ clipId: clipId, jobId: jobId, at: Date.now() });
++ save(list);
++ }
++ function headers() {
++ var t = null;
++ try { t = localStorage.getItem('token'); } catch (e) { /* none */ }
++ return t ? { Authorization: 'Bearer ' + t } : {};
++ }
++
++ function follow(clipId, jobId, h) {
++ h = h || {};
++ if (!clipId || !jobId || followed[jobId]) return;
++ followed[jobId] = true;
++ remember(clipId, jobId);
++ var started = Date.now(), tries = 0, retried = 0;
++ (function poll() {
++ if (Date.now() - started > MAX_AGE_MS) { forget(jobId); return; }
++ fetch('/api/clips/' + encodeURIComponent(clipId) + '/job?job=' + encodeURIComponent(jobId), { headers: headers(), credentials: 'same-origin' })
++ .then(function (r) { return r.status === 404 || r.status === 403 ? { gone: true } : (r.ok ? r.json() : null); })
++ .then(function (d) {
++ if (d && d.gone) { forget(jobId); return; }
++ var job = d && d.job;
++ if (job && job.status === 'succeeded') { forget(jobId); if (h.onReady) h.onReady(d.clip || { id: clipId }); return; }
++ if (job && (job.status === 'failed' || job.status === 'cancelled')) { forget(jobId); if (h.onFailed) h.onFailed(job.error || 'The clip could not be cut'); return; }
++ // A failed attempt waits for its retry (the server backs off): say so once per attempt.
++ if (job && job.status === 'queued' && job.attempts > retried) { retried = job.attempts; if (h.onRetry) h.onRetry(job); }
++ tries++;
++ setTimeout(poll, job && job.attempts > 0 ? 5000 : Math.min(4000, 800 + tries * 200));
++ })
++ .catch(function () { tries++; setTimeout(poll, Math.min(15000, 2000 * tries)); });
++ })();
++ }
++
++ function resume(h) { load().forEach(function (e) { follow(e.clipId, e.jobId, h); }); }
++
++ window.OVClipJobs = { follow: follow, resume: resume, pending: load };
++})();
ddiff --git a/public/js/stream-player.js b/public/js/stream-player.js
iindex 2b92dad1..7aacb5e7 100644
---- a/public/js/stream-player.js
++++ b/public/js/stream-player.js
@@@ -899,6 +899,8 @@ function _applyLoaderThumb(stream) {
}
function initPlayer(stream) {
++ // Clips this browser asked for before a reload are still being cut: reattach to them.
++ if (window.OVClipJobs && !initPlayer._clipsResumed) { initPlayer._clipsResumed = true; window.OVClipJobs.resume(_clipCutHandlers); }
destroyPlayer();
// Camera overlay is a separate viewer session against another slot's live stream;
// attach it alongside whichever protocol the main stream uses, and let it fail
@@@ -3442,10 +3444,17 @@ async function _serverLiveClipAttempt(isRetry) {
toast(data.error || 'Failed to create clip', 'error');
return 'handled';
}
-- const clipId = data.clip && data.clip.id;
-- if (data.deduplicated) toast('That moment is already clipped — opening it', 'info');
-- else toast('Clip created! — Trim & title it', 'success');
-- if (clipId) openClipTrimEditor(clipId);
++ const clip = data.clip || {};
++ const clipId = clip.id;
++ const dedup = data.deduplicated || clip.deduplicated;
++ if (dedup) toast('That moment is already clipped — opening it', 'info');
++ if (clipId && clip.job_id && clip.status === 'processing' && !dedup && window.OVClipJobs) {
++ toast('Clip created — cutting it on the server…', 'success');
++ _followClipCut(clipId, clip.job_id);
++ } else {
++ if (!dedup) toast('Clip created! — Trim & title it', 'success');
++ if (clipId) openClipTrimEditor(clipId);
++ }
return 'ok';
} catch (err) {
clearSlow();
@@@ -3454,6 +3463,15 @@ async function _serverLiveClipAttempt(isRetry) {
}
}
++// A clip the server cuts (Media's clip.cut job, WS-G task 3): follow the job, then open the trim editor.
++// A reload reattaches: ov-clip-jobs.js keeps the pending cut and initPlayer resumes it.
++const _clipCutHandlers = {
++ onReady: (clip) => { toast('Your clip is ready — Trim & title it', 'success'); openClipTrimEditor(clip.id); },
++ onFailed: (msg) => toast('The clip could not be cut: ' + msg, 'error'),
++ onRetry: (job) => toast(`Still working on your clip (attempt ${job.attempts + 1} of ${job.max_attempts})…`, 'info'),
++};
++function _followClipCut(clipId, jobId) { window.OVClipJobs.follow(clipId, jobId, _clipCutHandlers); }
++
/**
* Show a modal prompt for the user to title their clip after creation.
* @param {number} clipId - The newly created clip's ID
ddiff --git a/server/media-client.js b/server/media-client.js
iindex 6beb6ea2..f02dfbff 100644
---- a/server/media-client.js
++++ b/server/media-client.js
@@@ -24,6 +24,7 @@ const MEDIA_APP_ID = process.env.MEDIA_APP_ID || 'live';
const MEDIA_API_KEY = process.env.MEDIA_API_KEY || '';
const API_BASE = `${MEDIA_URL}/api/v1/${MEDIA_APP_ID}`;
++const API_V2_BASE = `${MEDIA_URL}/api/v2/${MEDIA_APP_ID}`; // media jobs (GET /jobs/:id)
class MediaApiError extends Error {
constructor(message, status, body) {
@@@ -66,8 +67,8 @@ function _qs(query) {
* Core request helper. `body` may be a plain object (JSON) or FormData (multipart).
* Returns parsed JSON (or null for empty responses). Throws MediaApiError on !ok.
*/
--async function request(method, apiPath, { body, query, actingUser, headers = {}, timeoutMs = 30000 } = {}) {
-- const url = `${API_BASE}${apiPath}${_qs(query)}`;
++async function request(method, apiPath, { body, query, actingUser, headers = {}, timeoutMs = 30000, base = API_BASE } = {}) {
++ const url = `${base}${apiPath}${_qs(query)}`;
const opts = {
method,
headers: { Accept: 'application/json', ..._authHeader({ actingUser }), ...headers },
@@@ -196,6 +197,11 @@ function deleteVod(vodId, opts = {}) {
// ── Clips ────────────────────────────────────────────────────────────────────
/** POST /clips { vod_id, start_s, end_s, title?, user_id? } → { id, status } */
++/** A media job of this app (e.g. a clip's clip.cut, whose id createClip/recutClip answer) → { job } */
++function getJob(jobId, opts = {}) {
++ return request('GET', `/jobs/${encodeURIComponent(jobId)}`, { ...opts, base: API_V2_BASE });
++}
++
function createClip({ vod_id, start_s, end_s, title, user_id, ...extra } = {}, opts = {}) {
return request('POST', '/clips', { body: { vod_id, start_s, end_s, title, user_id, ...extra }, ...opts });
}
@@@ -400,7 +406,7 @@ module.exports = {
uploadVodChunk, completeVodChunks, finalizeVod,
getVod, listVods, updateVod, deleteVod,
// clips
-- createClip, getClip, listClips, updateClip, deleteClip, recutClip,
++ createClip, getClip, listClips, updateClip, deleteClip, recutClip, getJob,
// pastes
createPaste, getPaste, listPastes, listPastesNeedingAi, setPasteAi, deletePaste,
// files + thumbnails
ddiff --git a/server/media-proxy/clips.js b/server/media-proxy/clips.js
iindex dc81e042..d99dd072 100644
---- a/server/media-proxy/clips.js
++++ b/server/media-proxy/clips.js
@@@ -374,11 +374,39 @@ router.post('/:id/recut', requireAuth, async (req, res) => {
if (!clip) return res.status(404).json({ error: 'Clip not found' });
if (!(await canActorDeleteClip(req.user, clip))) return refuse(req, res, clip, 'Not authorized to re-cut this clip');
const out = await media.recutClip(clip.id);
-- res.json({ message: 'Re-cutting clip', status: out?.status || 'processing' });
++ res.json({ message: 'Re-cutting clip', status: out?.status || 'processing', job_id: out?.job_id || null });
} catch (err) {
mediaErr(res, err, 'Failed to re-cut clip');
}
});
++/**
++ * The cut of a clip the server makes (Media's clip.cut job, WS-G task 3): what the clip UI follows and
++ * reattaches to after a reload (public/js/ov-clip-jobs.js). Whoever made the clip, or may manage it, sees
++ * its job, and only a clip.cut job of that very clip; anything else is 404.
++ * GET /api/clips/:id/job?job=<mjob_…> → { job: { id, status, attempts, max_attempts, error, run_after }, clip: { id, status, title, duration_seconds } }
++ */
++router.get('/:id/job', requireAuth, async (req, res) => {
++ try {
++ const jobId = String(req.query.job || '');
++ if (!/^mjob_[0-9A-Za-z]{10,40}$/.test(jobId)) return res.status(400).json({ error: 'job is required' });
++ let clip;
++ try { clip = await media.getClip(req.params.id); } catch (err) { return mediaErr(res, err, 'Clip not found'); }
++ if (!clip) return res.status(404).json({ error: 'Clip not found' });
++ const mine = clip.user_id != null && String(clip.user_id) === String(req.user.id);
++ if (!mine && !(await canActorDeleteClip(req.user, clip))) return res.status(404).json({ error: 'Clip not found' });
++ let out;
++ try { out = await media.getJob(jobId); } catch (err) { return mediaErr(res, err, 'Job not found'); }
++ const job = out && out.job;
++ if (!job || job.type !== 'clip.cut' || Number(job.params && job.params.clip_id) !== Number(clip.id)) return res.status(404).json({ error: 'Job not found' });
++ res.set('Cache-Control', 'private, no-store').json({
++ job: { id: job.id, status: job.status, attempts: job.attempts, max_attempts: job.max_attempts, error: job.status === 'failed' ? job.error || null : null, run_after: job.run_after || null },
++ clip: { id: clip.id, status: clip.status, title: clip.title, duration_seconds: clip.duration_seconds },
++ });
++ } catch (err) {
++ mediaErr(res, err, 'Failed to read the clip job');
++ }
++});
++
module.exports = router;
module.exports._internals = { clipChannelOwnerId };
ddiff --git a/test/clip-cut-job.test.js b/test/clip-cut-job.test.js
nnew file mode 100644
iindex 00000000..2c561f5e
---- /dev/null
++++ b/test/clip-cut-job.test.js
@@@ -0,0 +1,161 @@
++/**
++ * Following a clip cut on the server (Media's clip.cut job, roadmap WS-G task 3).
++ *
++ * - GET /api/clips/:id/job?job= answers the job of that clip's cut to whoever made the clip (or may
++ * manage it): status, attempts, the error once it failed. Anyone else, a job of another clip, or
++ * a job that is not a clip.cut gets 404; a malformed job id 400.
++ * - public/js/ov-clip-jobs.js (run here with a stand-in fetch, localStorage and timers) follows a job
++ * to ready or failed, says so once per retry, keeps the pending cut across a "reload" and resumes it,
++ * and forgets a job the server no longer knows.
++ *
++ * node test/clip-cut-job.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++const vm = require('vm');
++const tmp = path.join(os.tmpdir(), `ov-clip-cut-job-${process.pid}.db`);
++process.env.DB_PATH = tmp;
++process.env.NODE_ENV = 'test';
++const quiet = console.log;
++console.log = (...a) => { if (!/^\[/.test(String(a[0]))) quiet(...a); };
++console.warn = () => {};
++console.error = () => {};
++const db = require('../server/db/database');
++db.initDb();
++const raw = db.getDb();
++const auth = require('../server/auth/auth');
++const signIn = (req) => {
++ const id = Number(req.headers['x-test-user'] || 0);
++ const u = id ? db.getUserById(id) : null;
++ if (u) { req.user = u; req.authSource = 'network'; }
++ return u;
++};
++auth.requireAuth = (req, res, next) => (signIn(req) ? next() : res.status(401).json({ error: 'Authentication required' }));
++auth.optionalAuth = (req, res, next) => { signIn(req); next(); };
++const addUser = (id, username) => raw.prepare(`INSERT INTO users (id, username, display_name, email, password_hash, role, created_at)
++ VALUES (?, ?, ?, ?, 'x', 'user', '2025-01-01 00:00:00')`).run(id, username, username, `${username}@x`);
++addUser(3, 'maker');
++addUser(4, 'stranger');
++
++// ── OpenVibe.Media stand-in ──
++const media = require('../server/media-client');
++const CLIPS = { 10: { id: 10, user_id: 3, channel_user_id: 9, status: 'processing', title: 'Mine', duration_seconds: 0 }, 11: { id: 11, user_id: 3, channel_user_id: 9, status: 'ready', title: 'Other', duration_seconds: 7 } };
++const JOBS = {
++ mjob_01M3AAAAAAAAAAAAAAAAAAAAAA: { id: 'mjob_01M3AAAAAAAAAAAAAAAAAAAAAA', type: 'clip.cut', status: 'running', attempts: 0, max_attempts: 4, params: { clip_id: 10 }, error: null },
++ mjob_01M3BBBBBBBBBBBBBBBBBBBBBB: { id: 'mjob_01M3BBBBBBBBBBBBBBBBBBBBBB', type: 'object.sprite', status: 'succeeded', attempts: 1, max_attempts: 3, params: {}, error: null },
++};
++media.getClip = async (id) => { const c = CLIPS[Number(id)]; if (!c) throw new media.MediaApiError('not found', 404, null); return c; };
++media.getJob = async (id) => { const j = JOBS[id]; if (!j) throw new media.MediaApiError('No such job', 404, null); return { job: j }; };
++
++const express = require('express');
++const app = express();
++app.use(express.json());
++app.use('/api/clips', require('../server/media-proxy/clips'));
++const server = http.createServer(app).listen(0);
++const base = () => `http://127.0.0.1:${server.address().port}`;
++const get = async (p, user) => { const r = await fetch(base() + p, { headers: user ? { 'x-test-user': String(user) } : {} }); return { status: r.status, body: await r.json().catch(() => null) }; };
++
++(async () => {
++ await new Promise((r) => server.once('listening', r));
++ const A = 'mjob_01M3AAAAAAAAAAAAAAAAAAAAAA', B = 'mjob_01M3BBBBBBBBBBBBBBBBBBBBBB';
++
++ // ── The route ──
++ let r = await get(`/api/clips/10/job?job=${A}`, 3);
++ assert.strictEqual(r.status, 200, JSON.stringify(r.body));
++ assert.deepStrictEqual(r.body.job, { id: A, status: 'running', attempts: 0, max_attempts: 4, error: null, run_after: null });
++ assert.deepStrictEqual(r.body.clip, { id: 10, status: 'processing', title: 'Mine', duration_seconds: 0 });
++ assert.strictEqual((await get(`/api/clips/10/job?job=${A}`, 4)).status, 404, 'someone else: as if missing');
++ assert.strictEqual((await get(`/api/clips/10/job?job=${A}`)).status, 401);
++ assert.strictEqual((await get(`/api/clips/11/job?job=${A}`, 3)).status, 404, 'the job of another clip');
++ assert.strictEqual((await get(`/api/clips/10/job?job=${B}`, 3)).status, 404, 'not a clip.cut');
++ assert.strictEqual((await get('/api/clips/10/job?job=mjob_01M3CCCCCCCCCCCCCCCCCCCCCC', 3)).status, 404, 'unknown job');
++ assert.strictEqual((await get('/api/clips/10/job?job=../x', 3)).status, 400);
++ JOBS[A] = { ...JOBS[A], status: 'failed', attempts: 1, error: 'No decodable footage in that window' };
++ r = await get(`/api/clips/10/job?job=${A}`, 3);
++ assert.strictEqual(r.body.job.error, 'No decodable footage in that window', 'a failed cut says why');
++
++ // ── The client helper, against a scripted server ──
++ const src = fs.readFileSync(path.join(__dirname, '../public/js/ov-clip-jobs.js'), 'utf8');
++ const store = new Map();
++ const script = {}; // jobId -> list of answers, one per poll
++ const polls = [];
++ const timers = [];
++ const makeWindow = () => {
++ const w = {
++ localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) },
++ fetch: async (url) => {
++ const m = /\/api\/clips\/(\d+)\/job\?job=([\w]+)/.exec(url);
++ polls.push(m[2]);
++ const next = (script[m[2]] || []).shift() || { status: 200, body: { job: { status: 'running', attempts: 0 } } };
++ return { status: next.status, ok: next.status < 300, json: async () => next.body };
++ },
++ setTimeout: (fn) => { timers.push(fn); return timers.length; },
++ Date,
++ };
++ w.window = w;
++ return w;
++ };
++ const drain = async () => { for (let i = 0; i < 50 && timers.length; i++) { const fn = timers.shift(); fn(); await new Promise((r2) => setImmediate(r2)); await new Promise((r2) => setImmediate(r2)); } };
++ const plain = (v) => JSON.parse(JSON.stringify(v)); // values from the page's realm
++ const settle = () => new Promise((r2) => setImmediate(() => setImmediate(r2)));
++
++ // A cut that retries once, then is ready.
++ let w = makeWindow();
++ vm.runInNewContext(src, w);
++ const seen = [];
++ script.mjob_1 = [
++ { status: 200, body: { job: { status: 'running', attempts: 0 } } },
++ { status: 200, body: { job: { status: 'queued', attempts: 1, max_attempts: 4 } } },
++ { status: 200, body: { job: { status: 'queued', attempts: 1, max_attempts: 4 } } },
++ { status: 200, body: { job: { status: 'succeeded', attempts: 2 }, clip: { id: 10, status: 'ready' } } },
++ ];
++ w.OVClipJobs.follow(10, 'mjob_1', { onReady: (c) => seen.push(['ready', c.id]), onFailed: (m) => seen.push(['failed', m]), onRetry: (j) => seen.push(['retry', j.attempts]) });
++ assert.deepStrictEqual(plain(w.OVClipJobs.pending().map((e) => e.jobId)), ['mjob_1'], 'kept while pending');
++ await settle(); await drain();
++ assert.deepStrictEqual(seen, [['retry', 1], ['ready', 10]], 'one retry notice, then ready');
++ assert.deepStrictEqual(plain(w.OVClipJobs.pending()), [], 'forgotten once done');
++
++ // A reload: the new page resumes what the old one left pending, and a failure is reported.
++ w = makeWindow();
++ vm.runInNewContext(src, w);
++ script.mjob_2 = [{ status: 200, body: { job: { status: 'running', attempts: 0 } } }];
++ w.OVClipJobs.follow(12, 'mjob_2', {});
++ await settle();
++ timers.length = 0; // the page goes away mid-cut
++ assert.deepStrictEqual(JSON.parse(store.get('ov_clip_jobs')).map((e) => e.jobId), ['mjob_2']);
++ const w2 = makeWindow();
++ vm.runInNewContext(src, w2);
++ script.mjob_2 = [{ status: 200, body: { job: { status: 'failed', attempts: 4, error: 'gave up' } } }];
++ const seen2 = [];
++ w2.OVClipJobs.resume({ onFailed: (m) => seen2.push(m) });
++ await settle(); await drain();
++ assert.deepStrictEqual(seen2, ['gave up'], 'the resumed cut reports its failure');
++ assert.deepStrictEqual(plain(w2.OVClipJobs.pending()), []);
++
++ // A job the server no longer knows (or no longer shows this person) is dropped silently.
++ const w3 = makeWindow();
++ vm.runInNewContext(src, w3);
++ script.mjob_3 = [{ status: 404, body: { error: 'Job not found' } }];
++ const seen3 = [];
++ w3.OVClipJobs.follow(13, 'mjob_3', { onReady: () => seen3.push('ready'), onFailed: () => seen3.push('failed') });
++ await settle(); await drain();
++ assert.deepStrictEqual([seen3, plain(w3.OVClipJobs.pending())], [[], []]);
++
++ // Following the same job twice polls it once.
++ const w4 = makeWindow();
++ vm.runInNewContext(src, w4);
++ polls.length = 0;
++ script.mjob_4 = [{ status: 200, body: { job: { status: 'succeeded' }, clip: { id: 14 } } }];
++ w4.OVClipJobs.follow(14, 'mjob_4', {});
++ w4.OVClipJobs.follow(14, 'mjob_4', {});
++ await settle(); await drain();
++ assert.deepStrictEqual(polls, ['mjob_4']);
++
++ server.close();
++ try { fs.unlinkSync(tmp); } catch { /* */ }
++ quiet('clip cut job: all checks passed');
++})().catch((err) => { quiet(err); process.exit(1); });