Skip to content

Commit 358ad69

Browse files
Anishyoubassner
andauthored
Lectures: Allow trigger transcription for existing videos (#11675)
Co-authored-by: Patrick Bassner <[email protected]>
1 parent 0735a29 commit 358ad69

File tree

13 files changed

+525
-22
lines changed

13 files changed

+525
-22
lines changed

docs/user/lectures.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,6 @@ If your Artemis instance is connected to Nebula (see the :doc:`Nebula setup guid
136136
3. Enable the checkbox and save the lecture unit. Artemis sends the job to Nebula and shows a toast confirming that processing started.
137137
4. Nebula processes the lecture asynchronously. Once finished, the transcription is attached to the unit and becomes visible the next time you open the editor.
138138

139-
If the recording cannot be processed automatically (for example due to missing credentials), you can still paste an existing transcription JSON into the **Video Transcription**
140-
field before saving.
141-
142139
|create-attachment-video-unit|
143140

144141
Either all Attachment Video Units of a lecture or specific Attachment Video Units can be sent to Iris, over the ingestion button in the lecture unit overview.

src/main/webapp/app/lecture/manage/lecture-units/attachment-video-unit-form/attachment-video-unit-form.component.spec.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ describe('AttachmentVideoUnitFormComponent', () => {
129129
attachmentVideoUnitFormComponentFixture.detectChanges();
130130

131131
attachmentVideoUnitFormComponentFixture.componentRef.setInput('formData', formData);
132-
attachmentVideoUnitFormComponent.ngOnChanges();
132+
attachmentVideoUnitFormComponentFixture.detectChanges();
133133

134134
expect(attachmentVideoUnitFormComponent.nameControl?.value).toEqual(formData.formProperties.name);
135135
expect(attachmentVideoUnitFormComponent.releaseDateControl?.value).toEqual(formData.formProperties.releaseDate);
@@ -150,15 +150,15 @@ describe('AttachmentVideoUnitFormComponent', () => {
150150
transcriptionStatus: TranscriptionStatus.PENDING,
151151
};
152152
attachmentVideoUnitFormComponentFixture.componentRef.setInput('formData', formDataWithStatus);
153-
attachmentVideoUnitFormComponent.ngOnChanges();
153+
attachmentVideoUnitFormComponentFixture.detectChanges();
154154
expect(attachmentVideoUnitFormComponent.transcriptionStatus()).toBe(TranscriptionStatus.PENDING);
155155

156156
const formDataWithoutStatus: AttachmentVideoUnitFormData = {
157157
formProperties: {},
158158
fileProperties: {},
159159
};
160160
attachmentVideoUnitFormComponentFixture.componentRef.setInput('formData', formDataWithoutStatus);
161-
attachmentVideoUnitFormComponent.ngOnChanges();
161+
attachmentVideoUnitFormComponentFixture.detectChanges();
162162
expect(attachmentVideoUnitFormComponent.transcriptionStatus()).toBeUndefined();
163163
expect(attachmentVideoUnitFormComponent.showTranscriptionPendingWarning()).toBeFalse();
164164
});
@@ -173,13 +173,13 @@ describe('AttachmentVideoUnitFormComponent', () => {
173173
transcriptionStatus: TranscriptionStatus.PENDING,
174174
};
175175
attachmentVideoUnitFormComponentFixture.componentRef.setInput('formData', formDataWithStatus);
176-
attachmentVideoUnitFormComponent.ngOnChanges();
176+
attachmentVideoUnitFormComponentFixture.detectChanges();
177177

178178
expect(attachmentVideoUnitFormComponent.transcriptionStatus()).toBe(TranscriptionStatus.PENDING);
179179
expect(attachmentVideoUnitFormComponent.showTranscriptionPendingWarning()).toBeTrue();
180180

181181
attachmentVideoUnitFormComponentFixture.componentRef.setInput('isEditMode', false);
182-
attachmentVideoUnitFormComponent.ngOnChanges();
182+
attachmentVideoUnitFormComponentFixture.detectChanges();
183183

184184
expect(attachmentVideoUnitFormComponent.transcriptionStatus()).toBeUndefined();
185185
expect(attachmentVideoUnitFormComponent.showTranscriptionPendingWarning()).toBeFalse();
@@ -694,4 +694,55 @@ describe('AttachmentVideoUnitFormComponent', () => {
694694

695695
emitSpy.mockRestore();
696696
});
697+
698+
it('should set playlist URL from formData in edit mode via effect', () => {
699+
attachmentVideoUnitFormComponentFixture.componentRef.setInput('isEditMode', true);
700+
attachmentVideoUnitFormComponentFixture.detectChanges();
701+
702+
const playlistUrl = 'https://live.rbg.tum.de/playlist.m3u8';
703+
const formDataWithPlaylist: AttachmentVideoUnitFormData = {
704+
formProperties: {
705+
name: 'test',
706+
},
707+
fileProperties: {},
708+
playlistUrl: playlistUrl,
709+
};
710+
711+
attachmentVideoUnitFormComponentFixture.componentRef.setInput('formData', formDataWithPlaylist);
712+
attachmentVideoUnitFormComponentFixture.detectChanges();
713+
714+
// Effect should have triggered and set the playlist URL
715+
expect(attachmentVideoUnitFormComponent.playlistUrl()).toBe(playlistUrl);
716+
expect(attachmentVideoUnitFormComponent.canGenerateTranscript()).toBeTrue();
717+
});
718+
719+
it('should update playlist URL when formData changes in edit mode', () => {
720+
attachmentVideoUnitFormComponentFixture.componentRef.setInput('isEditMode', true);
721+
attachmentVideoUnitFormComponentFixture.detectChanges();
722+
723+
// Initial formData without playlist
724+
const formDataWithoutPlaylist: AttachmentVideoUnitFormData = {
725+
formProperties: {
726+
name: 'test',
727+
},
728+
fileProperties: {},
729+
};
730+
attachmentVideoUnitFormComponentFixture.componentRef.setInput('formData', formDataWithoutPlaylist);
731+
attachmentVideoUnitFormComponentFixture.detectChanges();
732+
733+
expect(attachmentVideoUnitFormComponent.playlistUrl()).toBeUndefined();
734+
735+
// Update formData with playlist URL
736+
const playlistUrl = 'https://live.rbg.tum.de/playlist.m3u8';
737+
const formDataWithPlaylist: AttachmentVideoUnitFormData = {
738+
...formDataWithoutPlaylist,
739+
playlistUrl: playlistUrl,
740+
};
741+
attachmentVideoUnitFormComponentFixture.componentRef.setInput('formData', formDataWithPlaylist);
742+
attachmentVideoUnitFormComponentFixture.detectChanges();
743+
744+
// Effect should have updated the playlist URL
745+
expect(attachmentVideoUnitFormComponent.playlistUrl()).toBe(playlistUrl);
746+
expect(attachmentVideoUnitFormComponent.canGenerateTranscript()).toBeTrue();
747+
});
697748
});

src/main/webapp/app/lecture/manage/lecture-units/attachment-video-unit-form/attachment-video-unit-form.component.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ElementRef, OnChanges, ViewChild, computed, inject, input, output, signal, viewChild } from '@angular/core';
1+
import { Component, ElementRef, ViewChild, computed, effect, inject, input, output, signal, viewChild } from '@angular/core';
22
import dayjs from 'dayjs/esm';
33
import { AbstractControl, FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
44
import urlParser from 'js-video-url-parser';
@@ -112,7 +112,7 @@ function validJsonOrEmpty(control: AbstractControl): ValidationErrors | null {
112112
templateUrl: './attachment-video-unit-form.component.html',
113113
imports: [FormsModule, ReactiveFormsModule, TranslateDirective, FaIconComponent, NgbTooltip, FormDateTimePickerComponent, CompetencySelectionComponent, ArtemisTranslatePipe],
114114
})
115-
export class AttachmentVideoUnitFormComponent implements OnChanges {
115+
export class AttachmentVideoUnitFormComponent {
116116
protected readonly faQuestionCircle = faQuestionCircle;
117117
protected readonly faTimes = faTimes;
118118
protected readonly faArrowLeft = faArrowLeft;
@@ -153,6 +153,25 @@ export class AttachmentVideoUnitFormComponent implements OnChanges {
153153

154154
readonly shouldShowTranscriptionCreation = computed(() => this.accountService.isAdmin());
155155

156+
constructor() {
157+
effect(() => {
158+
const formData = this.formData();
159+
if (this.isEditMode() && formData) {
160+
this.setFormValues(formData);
161+
const newStatus = formData.transcriptionStatus ? (formData.transcriptionStatus as TranscriptionStatus) : undefined;
162+
this.transcriptionStatus.set(newStatus);
163+
164+
// Set playlist URL if available from formData (for existing videos)
165+
if (formData.playlistUrl) {
166+
this.playlistUrl.set(formData.playlistUrl);
167+
this.canGenerateTranscript.set(true);
168+
}
169+
} else {
170+
this.transcriptionStatus.set(undefined);
171+
}
172+
});
173+
}
174+
156175
form: FormGroup = this.formBuilder.group({
157176
name: [undefined as string | undefined, [Validators.required, Validators.maxLength(255)]],
158177
description: [undefined as string | undefined, [Validators.maxLength(1000)]],
@@ -211,17 +230,6 @@ export class AttachmentVideoUnitFormComponent implements OnChanges {
211230
return this.statusChanges() === 'VALID' && !this.isFileTooBig() && this.datePickerComponent()?.isValid() && (!!this.fileName() || !!this.videoSourceSignal());
212231
});
213232

214-
ngOnChanges() {
215-
const formData = this.formData();
216-
if (this.isEditMode() && formData) {
217-
this.setFormValues(formData);
218-
const newStatus = formData.transcriptionStatus ? (formData.transcriptionStatus as TranscriptionStatus) : undefined;
219-
this.transcriptionStatus.set(newStatus);
220-
} else {
221-
this.transcriptionStatus.set(undefined);
222-
}
223-
}
224-
225233
onFileChange(event: Event): void {
226234
const input = event.target as HTMLInputElement;
227235
if (!input.files?.length) {

src/main/webapp/app/lecture/manage/lecture-units/edit-attachment-video-unit/edit-attachment-video-unit.component.spec.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('EditAttachmentVideoUnitComponent', () => {
3030
let router: Router;
3131
let navigateSpy: jest.SpyInstance;
3232
let updateAttachmentVideoUnitSpy: jest.SpyInstance;
33+
let fetchAndUpdatePlaylistUrlSpy: jest.SpyInstance;
3334
let attachment: Attachment;
3435
let attachmentVideoUnit: AttachmentVideoUnit;
3536
let baseFormData: FormData;
@@ -121,6 +122,7 @@ describe('EditAttachmentVideoUnitComponent', () => {
121122

122123
jest.spyOn(lectureTranscriptionService, 'getTranscription').mockReturnValue(of(undefined));
123124
jest.spyOn(lectureTranscriptionService, 'getTranscriptionStatus').mockReturnValue(of(undefined));
125+
fetchAndUpdatePlaylistUrlSpy = jest.spyOn(attachmentVideoUnitService, 'fetchAndUpdatePlaylistUrl').mockImplementation((_, formData) => of(formData));
124126
});
125127

126128
afterEach(() => {
@@ -387,6 +389,120 @@ describe('EditAttachmentVideoUnitComponent', () => {
387389
expect(updateAttachmentVideoUnitSpy).toHaveBeenCalledWith(1, 1, expect.any(FormData), undefined);
388390
expect(navigateSpy).toHaveBeenCalledOnce();
389391
});
392+
it('should fetch playlist URL when editing existing video with videoSource', () => {
393+
const playlistUrl = 'https://live.rbg.tum.de/playlist.m3u8';
394+
395+
const expectedFormData: AttachmentVideoUnitFormData = {
396+
formProperties: {
397+
name: attachmentVideoUnit.name,
398+
description: attachmentVideoUnit.description,
399+
releaseDate: attachmentVideoUnit.releaseDate,
400+
version: attachmentVideoUnit.attachment?.version,
401+
videoSource: attachmentVideoUnit.videoSource,
402+
updateNotificationText: undefined,
403+
},
404+
fileProperties: {
405+
fileName: attachmentVideoUnit.attachment?.link,
406+
},
407+
transcriptionProperties: {
408+
videoTranscription: undefined,
409+
},
410+
transcriptionStatus: undefined,
411+
playlistUrl: playlistUrl,
412+
};
413+
414+
fetchAndUpdatePlaylistUrlSpy.mockReturnValue(of(expectedFormData));
415+
416+
fixture.detectChanges();
417+
418+
expect(fetchAndUpdatePlaylistUrlSpy).toHaveBeenCalledWith(attachmentVideoUnit.videoSource, expect.anything());
419+
420+
// Wait for async operation
421+
return fixture.whenStable().then(() => {
422+
const formComponent: AttachmentVideoUnitFormComponent = fixture.debugElement.query(By.directive(AttachmentVideoUnitFormComponent)).componentInstance;
423+
expect(formComponent.formData()?.playlistUrl).toBe(playlistUrl);
424+
});
425+
});
426+
427+
it('should not fetch playlist URL when videoSource is missing', () => {
428+
attachmentVideoUnit.videoSource = undefined;
429+
jest.spyOn(attachmentVideoUnitService, 'findById').mockReturnValue(
430+
of(
431+
new HttpResponse({
432+
body: attachmentVideoUnit,
433+
status: 200,
434+
}),
435+
),
436+
);
437+
438+
const fetchAndUpdatePlaylistUrlSpy = jest.spyOn(attachmentVideoUnitService, 'fetchAndUpdatePlaylistUrl');
439+
440+
fixture.detectChanges();
441+
442+
// It is called with undefined, but returns original form data (mock needed if strict)
443+
// But wait, if we don't mock it, it might return undefined if it's a mock service.
444+
// We should mock it to return the form data.
445+
446+
const expectedFormData: AttachmentVideoUnitFormData = {
447+
formProperties: {
448+
name: attachmentVideoUnit.name,
449+
description: attachmentVideoUnit.description,
450+
releaseDate: attachmentVideoUnit.releaseDate,
451+
version: attachmentVideoUnit.attachment?.version,
452+
videoSource: undefined,
453+
updateNotificationText: undefined,
454+
},
455+
fileProperties: {
456+
fileName: attachmentVideoUnit.attachment?.link,
457+
},
458+
transcriptionProperties: {
459+
videoTranscription: undefined,
460+
},
461+
transcriptionStatus: undefined,
462+
};
463+
464+
// fetchAndUpdatePlaylistUrlSpy is already mocked in beforeEach, no need to re-spy
465+
// We just need to ensure it returns the expected form data for this specific test case.
466+
467+
fixture.detectChanges();
468+
469+
fetchAndUpdatePlaylistUrlSpy.mockReturnValue(of(expectedFormData));
470+
471+
expect(fetchAndUpdatePlaylistUrlSpy).toHaveBeenCalledWith(undefined, expect.anything());
472+
});
473+
474+
it('should handle playlist URL fetch failure gracefully', () => {
475+
// When fetch fails (or returns null), it returns the original form data
476+
const originalFormData: AttachmentVideoUnitFormData = {
477+
formProperties: {
478+
name: attachmentVideoUnit.name,
479+
description: attachmentVideoUnit.description,
480+
releaseDate: attachmentVideoUnit.releaseDate,
481+
version: attachmentVideoUnit.attachment?.version,
482+
videoSource: attachmentVideoUnit.videoSource,
483+
updateNotificationText: undefined,
484+
},
485+
fileProperties: {
486+
fileName: attachmentVideoUnit.attachment?.link,
487+
},
488+
transcriptionProperties: {
489+
videoTranscription: undefined,
490+
},
491+
transcriptionStatus: undefined,
492+
};
493+
494+
fetchAndUpdatePlaylistUrlSpy.mockReturnValue(of(originalFormData));
495+
496+
fixture.detectChanges();
497+
498+
expect(fetchAndUpdatePlaylistUrlSpy).toHaveBeenCalledWith(attachmentVideoUnit.videoSource, expect.anything());
499+
500+
// Should still initialize form data without playlist URL
501+
return fixture.whenStable().then(() => {
502+
const formComponent: AttachmentVideoUnitFormComponent = fixture.debugElement.query(By.directive(AttachmentVideoUnitFormComponent)).componentInstance;
503+
expect(formComponent.formData()?.playlistUrl).toBeUndefined();
504+
});
505+
});
390506

391507
it('should trigger transcript generation when generateTranscript is true', () => {
392508
fixture.detectChanges();

src/main/webapp/app/lecture/manage/lecture-units/edit-attachment-video-unit/edit-attachment-video-unit.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ export class EditAttachmentVideoUnitComponent implements OnInit {
8787
},
8888
transcriptionStatus: transcriptionStatus,
8989
};
90+
// Check if playlist URL is available for existing video to enable transcription generation
91+
this.attachmentVideoUnitService.fetchAndUpdatePlaylistUrl(this.attachmentVideoUnit.videoSource, this.formData).subscribe({
92+
next: (updatedFormData) => {
93+
this.formData = updatedFormData;
94+
},
95+
});
9096
},
9197
error: (res: HttpErrorResponse) => onError(this.alertService, res),
9298
});

0 commit comments

Comments
 (0)