openvibers/openvibe.mediahub Commits

The Clip menu starts where the video is (start at the current second, end 30 s on), and says an empty title is named by AI (#145)

OP OpenVibers committed 17 days ago · a0572ae5f7 · 3 files, +30 −4

Changes

ddiff --git a/public/js/video.js b/public/js/video.js
iindex ccb1571..4f6e22a 100644
---- a/public/js/video.js
++++ b/public/js/video.js
@@@ -334,7 +334,26 @@
transcript(video);
if (!video) return;
-- // "Make a clip": with the player here, mark the start and end at the current time.
++ // "Make a clip": opening the Clip menu starts it where the video is (owner, 2026-10-27: "it should have the
++ // start/end time set based on the current timestamp the user is on rather than being at 0"): the start at the
++ // current second, the end 30 s on, both inside the video. Opened again later, it follows the video again, unless
++ // the person typed their own times.
++ const clipper = document.querySelector('details.ovv-clipper');
++ const startIn = document.querySelector('[data-ovv-clip-start]');
++ const endIn = document.querySelector('[data-ovv-clip-end]');
++ if (clipper && startIn && endIn) {
++ let typed = false;
++ [startIn, endIn].forEach(function (i) { i.addEventListener('input', function () { typed = true; }); });
++ clipper.addEventListener('toggle', function () {
++ if (!clipper.open || typed) return;
++ const total = Number(startIn.getAttribute('data-ovv-clip-duration')) || video.duration || 0;
++ let a = Math.max(0, Math.floor(video.currentTime || 0));
++ let b = a + 30;
++ if (total > 1 && b > total) { b = Math.floor(total); a = Math.max(0, b - 30); }
++ startIn.value = clockOf(a);
++ endIn.value = clockOf(b);
++ });
++ }
const now = document.querySelector('[data-ovv-clip-now]');
if (now) {
now.hidden = false;
@@@ -342,7 +361,7 @@
b.addEventListener('click', function () {
const which = b.getAttribute('data-ovv-clip-mark');
const input = document.querySelector(which === 'start' ? '[data-ovv-clip-start]' : '[data-ovv-clip-end]');
-- if (input) input.value = clockOf(video.currentTime);
++ if (input) { input.value = clockOf(video.currentTime); input.dispatchEvent(new Event('input')); }
if (which === 'start') {
const end = document.querySelector('[data-ovv-clip-end]');
if (end && !end.value) end.value = clockOf(video.currentTime + 30);
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 2b040c8..afb2908 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -601,11 +601,12 @@ function createVideoRoutes(ctx) {
return actionMenu({ icon: 'fa-solid fa-scissors', label: 'Clip', title: `Make a clip of up to ${CLIP_MAX_SECONDS} seconds`, cls: 'ovv-clipper', body: html`<p class="ovv-pop-title">Make a clip</p>
<form class="ovv-owner-form" method="post" action="/v/${id}/clip">
<div class="ovv-clip-times">
--<label class="ovv-field"><span>Start</span><input name="start" type="text" inputmode="numeric" placeholder="0:00" pattern="[0-9:.]{1,12}" required data-ovv-clip-start></label>
++<label class="ovv-field"><span>Start</span><input name="start" type="text" inputmode="numeric" placeholder="0:00" pattern="[0-9:.]{1,12}" required data-ovv-clip-start${dur ? html` data-ovv-clip-duration="${dur}"` : ''}></label>
<label class="ovv-field"><span>End</span><input name="end" type="text" inputmode="numeric" placeholder="0:30" pattern="[0-9:.]{1,12}" required data-ovv-clip-end></label>
</div>
<p class="ovv-clip-now" hidden data-ovv-clip-now><button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="start">Start here</button> <button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="end">End here</button></p>
--<label class="ovv-field"><span>Title <span class="ovv-fine">(optional)</span></span><input name="title" type="text" maxlength="100"></label>
++<label class="ovv-field"><span>Title <span class="ovv-fine">(optional)</span></span><input name="title" type="text" maxlength="100" placeholder="Leave empty: AI names it" aria-describedby="ovv-clip-title-note"></label>
++<p class="ovv-fine" id="ovv-clip-title-note">With no title, AI names the clip from what happens and what is said in it, a minute or so after it is cut.</p>
<p class="ovv-fine">Up to ${CLIP_MAX_SECONDS} seconds${dur ? `, within ${clock(dur)}` : ''}. Times are m:ss or h:mm:ss. It is cut on OpenVibe.Media and credited to you.</p>
<button class="ovv-btn ovv-btn--primary" type="submit">Make the clip</button>
</form>` });
ddiff --git a/test/video-clip.test.js b/test/video-clip.test.js
iindex 9d0ca4d..b6ddd93 100644
---- a/test/video-clip.test.js
++++ b/test/video-clip.test.js
@@@ -74,6 +74,12 @@ function vreq(base, path, { method = 'GET', cookie, form, same = true } = {}) {
assert.ok(r.text.includes('action="/v/101/clip"'), 'any signed-in viewer, not only the channel');
assert.ok(r.text.includes('within 1:00:00'), 'the recording length');
assert.ok(r.text.includes('data-ovv-clip-now'), 'the Start here / End here buttons (shown by the script)');
++ // Owner, 2026-10-27: the times start where the video is, and an empty title is named by AI (said on the form).
++ assert.ok(r.text.includes('data-ovv-clip-duration="3600"'), 'the script keeps the times inside the video');
++ assert.ok(r.text.includes('placeholder="Leave empty: AI names it"') && r.text.includes('With no title, AI names the clip from what happens and what is said in it'));
++ const js = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'js', 'video.js'), 'utf8');
++ assert.ok(js.includes("clipper.addEventListener('toggle'") && js.includes('Math.floor(video.currentTime || 0)'), 'opening the menu starts at the current time');
++ assert.ok(js.includes('if (!clipper.open || typed) return;'), 'times the person typed are kept');
});
await check('a clip is made on Live as the person, with their address, then its page says it is being cut', async () => {