Skip to content

Commit 365aa87

Browse files
zubeydecivelekzzacharo
authored andcommitted
feature: follow and search transcriptions
1 parent 98f30cd commit 365aa87

7 files changed

Lines changed: 312 additions & 7 deletions

File tree

cds/modules/deposit/static/templates/cds_deposit/types/video/uploader.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ <h5 class="text-muted"><strong>Tips and suggestions</strong></h5>
282282
ngf-model-options="{allowInvalid: false}"
283283
ngf-max-size="500GB"
284284
ngf-multiple="true"
285+
ngf-validate-fn="$ctrl.validateAdditionalFiles($file)"
285286
>
286287
<div class="pa-10 cds-deposit-box-upload-wrapper text-center">
287288
<p class="cds-deposit-box-upload-icon mb-20">

cds/modules/previewer/templates/cds_previewer/macros/player.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
{% endif %}
5252
initialRendition: 'first'
5353
});
54+
55+
window.top.player = player;
5456
// Preload
5557
player.source = {
5658
sources: [

cds/modules/records/static/templates/cds_records/video/detail.html

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ <h1 class="mb-0 mt-0 f4 fw-n t-b">
6464
<i class="fa fa-info-circle"></i>
6565
</a>
6666
</h1>
67-
<modal-dialog show="showTranslations" dialog-title="Available transations" >
67+
<modal-dialog show="showTranslations" dialog-title="Available translations" >
6868
<div ng-repeat="translation in record.metadata.translations">
6969
<strong>{{translation.language | isoToLanguage}}</strong>
7070
<h4>{{translation.title.title}}</h4>
@@ -201,6 +201,24 @@ <h4>{{translation.title.title}}</h4>
201201
<!-- /Description -->
202202
<!-- More -->
203203
<div class="cds-detail-more cds-detail-video-more px-20">
204+
<!-- Show Transcriptions -->
205+
<div class="mb-20"
206+
ng-show="(record.metadata._files | filter:{context_type:'subtitle', content_type:'vtt'}).length > 0"
207+
>
208+
<hr />
209+
<div class="transcription-text">
210+
<p><strong>Transcriptions</strong></p>
211+
<small class="text-muted">Follow along or search within the transcript.</small>
212+
</div>
213+
214+
<div class="transcription-button-wrapper pt-10">
215+
<button class="btn btn-sm btn-default transcription-button"
216+
ng-click="toggleTranscript()">
217+
{{ showTranscript ? 'Hide Transcriptions' : 'Show Transcriptions' }}
218+
</button>
219+
</div>
220+
</div>
221+
<!-- Show Transcriptions -->
204222
<ng-include src="'/static/templates/cds_records/keywords.html'"></ng-include>
205223
<ng-include
206224
ng-if="(record.metadata.related_identifiers | filter:{scheme:'indico'}).length > 0"
@@ -216,10 +234,59 @@ <h3 class="cds-title-section-decoration bt bw-1 pt-10 mb-20"><i class="fa fa-sha
216234
<ng-include src="'/static/templates/cds_records/video/share.html'"></ng-include>
217235
</div>
218236
</div>
219-
<!-- /Section -->
220237
<!-- /Left Column -->
221238
<!-- Right Column -->
222239
<div class="col-md-4">
240+
<!-- Section Transcriptions -->
241+
<div
242+
id="transcriptionsSection"
243+
class="cds-detail cds-detail-video cds-div-shadow bg-w pt-10 px-20 pb-10 mb-30 mt-20 t-b"
244+
ng-show="(record.metadata._files | filter:{context_type:'subtitle', content_type:'vtt'}).length > 0 && showTranscript" >
245+
<div class="cds-detail-title cds-detail-video-title pb-20">
246+
<h3 class="cds-title-section-decoration bt bw-1 pt-10 mb-0">
247+
<i class="fa fa-align-left"></i> Transcription
248+
</h3>
249+
</div>
250+
<input type="text"
251+
class="form-control mb-10"
252+
placeholder="Search transcript..."
253+
ng-model="transcriptSearch"
254+
ng-change="filterTranscript()">
255+
<div>
256+
<div
257+
class="panel panel-default"
258+
style="max-height: 300px; overflow-y: auto;"
259+
ng-show="filteredTranscript.length > 0">
260+
<div class="panel-body p-0">
261+
<ul class="list-unstyled mb-0">
262+
<li ng-repeat="line in filteredTranscript"
263+
ng-click="seekTo(line.start)"
264+
class="mb-10 transcript-line"
265+
ng-class="{ 'active': currentTranscriptLine === line }"
266+
style="cursor: pointer;">
267+
<a>
268+
<strong>{{ convertToMinutesSeconds(line.start) }} - {{ convertToMinutesSeconds(line.end) }}</strong>
269+
</a>
270+
<div class="cds-deposit-frame-thumbnail-label">
271+
{{ line.text }}
272+
</div>
273+
</li>
274+
</ul>
275+
</div>
276+
</div>
277+
<div class="form-group mb-10">
278+
<label for="languageSelect">Transcript Language:</label>
279+
<select
280+
id="languageSelect"
281+
class="form-control"
282+
ng-model="selectedTranscriptLanguage"
283+
ng-options="lang as (lang | isoToLanguage) for (lang, _) in transcriptsByLanguage"
284+
ng-change="setTranscriptLanguage(selectedTranscriptLanguage)"
285+
></select>
286+
</div>
287+
</div>
288+
</div>
289+
<!-- /Section Transcriptions -->
223290
<!-- Section Download -->
224291
<div class="cds-detail cds-detail-video cds-div-shadow bg-w pt-10 px-20 pb-10 mb-30 mt-20 t-b">
225292
<ng-include src="'/static/templates/cds_records/video/downloads.html'"></ng-include>

cds/modules/theme/assets/bootstrap3/js/cds_deposit/avc/components/cdsUploader.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,12 @@ function cdsUploaderCtrl(
314314
if (extraHeaders) {
315315
file.headers = extraHeaders;
316316
}
317-
file.headers = {
318-
"X-Invenio-File-Tags": "context_type=additional_file",
319-
};
317+
318+
if (!extraHeaders || !("X-Invenio-File-Tags" in extraHeaders)) {
319+
file.headers = {
320+
"X-Invenio-File-Tags": "context_type=additional_file",
321+
};
322+
}
320323
});
321324

322325
// Find if any of the existing files has been replaced
@@ -545,6 +548,15 @@ function cdsUploaderCtrl(
545548
return match.length > 1 && match[1] in isoLanguages;
546549
};
547550

551+
this.validateAdditionalFiles = function (_file) {
552+
// If it's a .vtt file, validate as subtitle
553+
if (_file.name.toLowerCase().endsWith(".vtt")) {
554+
return this.validateSubtitles(_file);
555+
}
556+
// Accept other types
557+
return true;
558+
};
559+
548560
this.updateFile = function (key, data, force) {
549561
var index = this.findFileIndex(that.files, key);
550562
if (index != -1) {

cds/modules/theme/assets/bootstrap3/js/cds_records/cdsRecord.js

Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
*/
2727

2828
import angular from "angular";
29+
import { WebVTT } from "vtt.js";
2930

3031
import { getCookie } from "../getCookie";
3132

@@ -38,7 +39,7 @@ import { getCookie } from "../getCookie";
3839
* @description
3940
* CDS record controller.
4041
*/
41-
function cdsRecordController($scope, $sce, $http) {
42+
function cdsRecordController($scope, $sce, $http, $timeout) {
4243
// Parameters
4344

4445
// Assign the controller to `vm`
@@ -53,11 +54,216 @@ function cdsRecordController($scope, $sce, $http) {
5354
// Record Warn - if the cdsRecord has any warning
5455
vm.cdsRecordWarning = null;
5556

57+
$scope.transcriptsByLanguage = {};
58+
$scope.transcript = [];
59+
$scope.filteredTranscript = [];
60+
$scope.selectedTranscriptLanguage = null;
61+
5662
const REQUEST_HEADERS = {
5763
"Content-Type": "application/json",
5864
"X-CSRFToken": getCookie("csrftoken"),
5965
};
6066

67+
$scope.seekTo = function (timecode) {
68+
const player = window.top.player;
69+
if (player) {
70+
player.currentTime = timecode;
71+
if (player.paused) {
72+
player.play().catch(function (err) {
73+
console.warn("Autoplay might be blocked by the browser:", err);
74+
});
75+
}
76+
} else {
77+
console.warn("Player not available");
78+
}
79+
};
80+
81+
$scope.toggleTranscript = function () {
82+
$scope.showTranscript = !$scope.showTranscript;
83+
84+
// Jump to Transcriptions section
85+
if ($scope.showTranscript) {
86+
setTimeout(function () {
87+
const el = document.getElementById("transcriptionsSection");
88+
if (el) {
89+
const rect = el.getBoundingClientRect();
90+
const isVisible =
91+
rect.top >= 0 &&
92+
rect.bottom <=
93+
(window.innerHeight || document.documentElement.clientHeight);
94+
95+
if (!isVisible) {
96+
const topOffset = rect.top + window.scrollY - 60;
97+
window.scrollTo({ top: topOffset, behavior: "smooth" });
98+
}
99+
}
100+
}, 100);
101+
}
102+
};
103+
104+
$scope.parseVttFromUrl = function (url, type, lang) {
105+
fetch(url)
106+
.then((res) => res.text())
107+
.then(function (vttText) {
108+
const parser = new WebVTT.Parser(window, WebVTT.StringDecoder());
109+
const cues = {};
110+
111+
parser.oncue = function (cue) {
112+
cues[cue.text] = {
113+
start: cue.startTime,
114+
end: cue.endTime,
115+
text: cue.text,
116+
};
117+
};
118+
119+
parser.parse(vttText);
120+
parser.flush();
121+
122+
$timeout(function () {
123+
if (type === "transcript") {
124+
$scope.transcriptsByLanguage[lang] = cues;
125+
126+
// Use the first one that loads
127+
if (!$scope.selectedTranscriptLanguage) {
128+
$scope.transcript = cues;
129+
$scope.filterTranscript();
130+
$scope.selectedTranscriptLanguage = lang;
131+
}
132+
} else {
133+
console.warn("Unknown type for VTT parsing:", type);
134+
}
135+
});
136+
})
137+
.catch(function (err) {
138+
console.error("VTT parsing failed", err);
139+
});
140+
};
141+
142+
$scope.transcriptSearch = "";
143+
144+
$scope.filterTranscript = function () {
145+
var searchTerm = $scope.transcriptSearch.toLowerCase();
146+
$scope.filteredTranscript = Object.values($scope.transcript).filter(
147+
function (line) {
148+
return (
149+
!searchTerm ||
150+
(line.text && line.text.toLowerCase().indexOf(searchTerm) !== -1)
151+
);
152+
}
153+
);
154+
};
155+
156+
$scope.$watch("transcript", function (newVal) {
157+
if (newVal) $scope.filterTranscript();
158+
});
159+
160+
$scope.$watch("record", function (newVal) {
161+
if (newVal) {
162+
$scope.initVttLoad(newVal);
163+
}
164+
});
165+
166+
$scope.initVttLoad = function (record) {
167+
console.log("Initializing VTT load for record:", record);
168+
const files = record.metadata._files || [];
169+
170+
// Subtitles (transcripts)
171+
const transcriptVttFiles = files.filter(
172+
(f) => f.context_type === "subtitle" && f.content_type === "vtt"
173+
);
174+
175+
// Step 2: If found, load it
176+
transcriptVttFiles.forEach((file) => {
177+
const lang = file.tags.language || "unknown";
178+
if (file.links?.self) {
179+
$scope.parseVttFromUrl(file.links.self, "transcript", lang);
180+
} else {
181+
console.warn("No subtitle file found.");
182+
}
183+
});
184+
};
185+
186+
$scope.setTranscriptLanguage = function (lang) {
187+
if ($scope.transcriptsByLanguage[lang]) {
188+
$scope.transcript = $scope.transcriptsByLanguage[lang];
189+
$scope.filterTranscript();
190+
} else {
191+
console.warn("Transcript not found for language:", lang);
192+
}
193+
};
194+
195+
// Follow transcriptions
196+
$scope.currentTranscriptLine = null;
197+
function getScrollableParent(el) {
198+
while (el && el !== document.body) {
199+
const style = window.getComputedStyle(el);
200+
const overflowY = style.overflowY;
201+
if (overflowY === "auto" || overflowY === "scroll") {
202+
return el;
203+
}
204+
el = el.parentElement;
205+
}
206+
return null;
207+
}
208+
function updateTranscriptHighlight() {
209+
const player = window.top.player;
210+
if (!player || !$scope.transcript) return;
211+
212+
const currentTime = player.currentTime;
213+
const lines = Object.values($scope.transcript);
214+
215+
for (let i = 0; i < lines.length; i++) {
216+
const line = lines[i];
217+
if (currentTime >= line.start && currentTime <= line.end) {
218+
if ($scope.currentTranscriptLine !== line) {
219+
$scope.currentTranscriptLine = line;
220+
$scope.$applyAsync(); // Trigger Angular update
221+
222+
// Auto-scroll to the active line
223+
setTimeout(() => {
224+
const el = document.querySelector(".transcript-line.active");
225+
const container = getScrollableParent(el);
226+
227+
if (el && container) {
228+
const elRect = el.getBoundingClientRect();
229+
const containerRect = container.getBoundingClientRect();
230+
231+
const currentScroll = container.scrollTop;
232+
const topOffset = elRect.top - containerRect.top;
233+
234+
const targetScroll = currentScroll + topOffset - 10;
235+
236+
container.scrollTo({
237+
top: targetScroll,
238+
behavior: "smooth",
239+
});
240+
}
241+
}, 50);
242+
}
243+
return;
244+
}
245+
}
246+
247+
$scope.currentTranscriptLine = null;
248+
$scope.$applyAsync();
249+
}
250+
251+
let transcriptTimer = setInterval(updateTranscriptHighlight, 100);
252+
253+
$scope.$on("$destroy", function () {
254+
clearInterval(transcriptTimer);
255+
});
256+
257+
$scope.convertToMinutesSeconds = function (seconds) {
258+
const minutes = Math.floor(seconds / 60);
259+
const secs = Math.floor(seconds % 60);
260+
261+
// Pad with zero if needed
262+
const paddedSecs = secs < 10 ? "0" + secs : secs;
263+
264+
return `${minutes}:${paddedSecs}`;
265+
};
266+
61267
/**
62268
* Trust iframe url
63269
* @memberof cdsRecordController
@@ -180,7 +386,7 @@ function cdsRecordController($scope, $sce, $http) {
180386
$scope.$on("cds.record.loading.stop", cdsRecordLoadingStop);
181387
}
182388

183-
cdsRecordController.$inject = ["$scope", "$sce", "$http"];
389+
cdsRecordController.$inject = ["$scope", "$sce", "$http", "$timeout"];
184390

185391
////////////
186392

0 commit comments

Comments
 (0)