Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions __TEST__/e2e/caption-import-media.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// A user's workflow: import a local video with its VTT (or SRT), edit, leave
// the project and come back. The importer used to read the video into memory
// before putting it on the player, while the project was created as soon as
// the caption file was read, so with a video large enough to take a moment
// the project was saved with the previous project's media. Coming back, the
// captions were there and the video was not.
import { test, expect } from '@playwright/test';
import fs from 'node:fs';
import { pollPage } from './helpers.mjs';

for (const kind of ['vtt', 'srt']) {
test(`a local video imported with its ${kind.toUpperCase()} is still the project's media after leaving and coming back`, async ({ page }, testInfo) => {
await page.goto('/index.html');
await page.waitForSelector('#hypertranscript [data-m]');
await pollPage(page, async () => (await window.HyperaudioSave.library.list()).length >= 1);
const introId = await page.evaluate(() => window.HyperaudioSave.library.currentId());
// a large video takes a while to read: make any read of it slow
await page.evaluate(() => {
const real = File.prototype.arrayBuffer;
File.prototype.arrayBuffer = function () { return new Promise((r) => setTimeout(r, 1500)).then(() => real.call(this)); };
});
const captions = testInfo.outputPath(`talk.${kind}`);
fs.writeFileSync(captions, kind === 'vtt'
? 'WEBVTT\n\n00:00:00.000 --> 00:00:00.500\nHello there\n\n00:00:00.500 --> 00:00:01.000\nfrom the video\n'
: '1\n00:00:00,000 --> 00:00:00,500\nHello there\n\n2\n00:00:00,500 --> 00:00:01,000\nfrom the video\n');
const video = testInfo.outputPath('my-talk.mp4');
fs.copyFileSync('__TEST__/fixtures/video-640x360.mp4', video);
await page.evaluate((k) => { document.getElementById(`file-import-${k}-dialog`).checked = true; }, kind);
await page.setInputFiles(`#${kind}-file`, video);
await page.setInputFiles(`#${kind}`, captions);
await page.click(`#file-import-${kind}`);
await expect(page.locator('#hypertranscript')).toContainText('Hello there');
// the new project is saved with the video as its media
await pollPage(page, async () => {
const id = window.HyperaudioSave.library.currentId();
const entry = (await window.HyperaudioSave.library.list()).find((p) => p.id === id);
return !!(entry && entry.media && entry.media.filename === 'my-talk.mp4');
});
const bornId = await page.evaluate(() => window.HyperaudioSave.library.currentId());
// away, and back
await page.evaluate((id) => window.HyperaudioSave.library.open(id), introId);
await expect(page.locator('#hypertranscript')).not.toContainText('Hello there');
await page.evaluate((id) => window.HyperaudioSave.library.open(id), bornId);
await expect(page.locator('#hypertranscript')).toContainText('Hello there');
await expect.poll(() => page.evaluate(() => {
const p = document.getElementById('hyperplayer');
return p.src.startsWith('blob:') && p.videoWidth === 640;
})).toBe(true);
const entry = await page.evaluate(async (id) => (await window.HyperaudioSave.library.list()).find((p) => p.id === id), bornId);
expect(entry.media).toMatchObject({ kind: 'original', filename: 'my-talk.mp4' });
});
}
8 changes: 4 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="version" content="1.3.39">
<meta name="version" content="1.3.40">
<!-- The version is bumped with the FIRST change of a release, like the
per-file cache stamps, so a working tree reports the release it is
becoming. The date is set by the release commit and stays empty until
then: Settings shows "unreleased" in its place (#615). -->
<meta name="release-date" content="2026-10-14">
<meta name="release-date" content="2026-10-15">
<title>Hyperaudio Lite Editor</title>
<link rel="apple-touch-icon" href="images/icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon-32x32.png">
Expand Down Expand Up @@ -1304,7 +1304,7 @@ <h3 class="font-bold text-lg">Caption Regeneration </h3>

<script src="js/editor-file-menu.js?v=1.2.5"></script>

<script src="./js/hyperaudio-lite-editor-export.js?v=1.3.25" type="module"> </script>
<script src="./js/hyperaudio-lite-editor-export.js?v=1.3.40" type="module"> </script>
<!-- end of localstorage additions -->

<!-- caption editor additions -->
Expand Down Expand Up @@ -1356,7 +1356,7 @@ <h3 class="font-bold text-lg">Caption Regeneration </h3>
<script type="module" src="js/editor-audio-cut.js?v=1.3.39"></script>

<!-- .hyperaudio project save/open + OPFS autosave (spec: docs/format/) -->
<script src="js/hyperaudio-save.js?v=1.3.39"></script>
<script src="js/hyperaudio-save.js?v=1.3.40"></script>
<script src="js/tpme.js?v=1.3.22"></script>
<script src="js/hyperaudio-library.js?v=1.3.32"></script>
<script src="js/media-first-frame.js?v=1.3.20"></script>
Expand Down
46 changes: 17 additions & 29 deletions js/hyperaudio-lite-editor-export.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* hyperaudio-lite-editor-export.js
* (C) The Hyperaudio Project
* @version 1.3.25 — last changed in release 1.3.25
* @version 1.3.40 — last changed in release 1.3.40
* @license MIT
*
* Export/import custom elements: transcript downloads and the Deepgram JSON,
Expand Down Expand Up @@ -200,23 +200,17 @@ class ImportSrt extends HTMLElement {

confirmSrt() {
let player = document.querySelector("#hyperplayer");
// The media goes on the player NOW, before the caption file is read and
// the new project is created from it. It used to be read into memory
// first (twice over), and a large video only reached the player after the
// project had been created, so the project was saved with whatever media
// the player held before: the previous project's. A File is a Blob, so it
// can go on the player as it is; the save module captures the same File
// from this input for the project's media.
if (document.querySelector('#srt-file').value == ""){
console.log("new src ", document.querySelector('#srt-media').value);
player.src = document.querySelector('#srt-media').value;
} else {
const file = document.querySelector('[name=srt-file]').files[0];
// Create a new FileReader instance
const reader = new FileReader();
reader.readAsArrayBuffer(file);
let blob = null;

reader.addEventListener('load', () => {

file.arrayBuffer().then((arrayBuffer) => {
blob = new Blob([new Uint8Array(arrayBuffer)], {type: file.type });
player.src = URL.createObjectURL(blob);
});
});
player.src = URL.createObjectURL(document.querySelector('[name=srt-file]').files[0]);
}

const file = document.querySelector('[name=srt]').files[0];
Expand Down Expand Up @@ -332,23 +326,17 @@ class ImportVtt extends HTMLElement {

confirmVtt() {
let player = document.querySelector("#hyperplayer");
// The media goes on the player NOW, before the caption file is read and
// the new project is created from it. It used to be read into memory
// first (twice over), and a large video only reached the player after the
// project had been created, so the project was saved with whatever media
// the player held before: the previous project's. A File is a Blob, so it
// can go on the player as it is; the save module captures the same File
// from this input for the project's media.
if (document.querySelector('#vtt-file').value == ""){
console.log("new src ", document.querySelector('#vtt-media').value);
player.src = document.querySelector('#vtt-media').value;
} else {
const file = document.querySelector('[name=vtt-file]').files[0];
// Create a new FileReader instance
const reader = new FileReader();
reader.readAsArrayBuffer(file);
let blob = null;

reader.addEventListener('load', () => {

file.arrayBuffer().then((arrayBuffer) => {
blob = new Blob([new Uint8Array(arrayBuffer)], {type: file.type });
player.src = URL.createObjectURL(blob);
});
});
player.src = URL.createObjectURL(document.querySelector('[name=vtt-file]').files[0]);
}

const file = document.querySelector('[name=vtt]').files[0];
Expand Down
6 changes: 4 additions & 2 deletions js/hyperaudio-save.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* .hyperaudio PROJECT SAVE — format, container, OPFS working copy, UI
* ============================================================================
*
* @version 1.3.39 — last changed in release 1.3.39
* @version 1.3.40 — last changed in release 1.3.40
*
* Implements the .hyperaudio format v1.2 (normative spec:
* docs/hyperaudio-format.md — originated in issue #403). 1.1 added media.kind
Expand Down Expand Up @@ -4013,7 +4013,9 @@
function wireCapture() {
// The original media File, captured at the existing engine file inputs —
// no engine code is touched.
['#file-input', '#parakeet-file-input', '#deepgram-file', '#assemblyai-file', '#parakeet-hf-file']
['#file-input', '#parakeet-file-input', '#deepgram-file', '#assemblyai-file', '#parakeet-hf-file',
// the local media chosen beside an imported caption file
'#srt-file', '#vtt-file']
.forEach((selector) => {
const el = document.querySelector(selector);
if (el === null) return;
Expand Down
2 changes: 1 addition & 1 deletion serviceworker.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
'use strict';

const VERSION = '1.3.39';
const VERSION = '1.3.40';
const PREFIX = 'hyperaudio-editor-';
const CACHE = PREFIX + VERSION;
// the old worker's cache. Finding it means this visitor was pinned.
Expand Down
Loading