Skip to content

Commit

Permalink
feat(Open Response): Add SpeechToText transcription support (#1642)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan Lim-Breitbart <[email protected]>
  • Loading branch information
hirokiterashima and breity authored Feb 21, 2024
1 parent 133938b commit e28b7e3
Show file tree
Hide file tree
Showing 14 changed files with 4,582 additions and 2,521 deletions.
6,636 changes: 4,128 additions & 2,508 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
"@angular/platform-browser": "^16.2.9",
"@angular/platform-browser-dynamic": "^16.2.9",
"@angular/router": "^16.2.9",
"@aws-sdk/client-cognito-identity": "^3.310.0",
"@aws-sdk/client-comprehend": "^3.310.0",
"@aws-sdk/client-ses": "^3.310.0",
"@aws-sdk/client-transcribe-streaming": "^3.310.0",
"@aws-sdk/client-translate": "^3.310.0",
"@aws-sdk/credential-provider-cognito-identity": "^3.310.0",
"@ng-web-apis/common": "^2.0.1",
"@ng-web-apis/intersection-observer": "^3.0.0",
"@stomp/rx-stomp": "^1.1.4",
Expand All @@ -31,13 +37,15 @@
"@zxcvbn-ts/core": "^2.2.1",
"@zxcvbn-ts/language-en": "^2.1.0",
"@wise-community/angular-password-strength-meter": "^7.0.1",
"buffer": "^6.0.3",
"canvg": "^2.0.0",
"compute-covariance": "^1.0.1",
"core-js": "^3.22.0",
"dom-autoscroller": "^2.3.4",
"eventemitter2": "^5.0.1",
"fabric": "3.6.3",
"file-saver": "^2.0.5",
"get-user-media-promise": "^1.1.4",
"highcharts": "^9.3.3",
"highcharts-angular": "^3.1.2",
"html2canvas": "^0.5.0-beta4",
Expand All @@ -46,12 +54,15 @@
"jwt-decode": "^3.1.2",
"lz-string": "^1.4.4",
"mathjax": "^3.2.2",
"microphone-stream": "^6.0.1",
"ng-file-upload": "^12.2.13",
"ng-recaptcha": "^12.0.2",
"ng2-file-upload": "^5.0.0",
"ngx-filesize": "^3.0.2",
"process": "^0.11.10",
"rxjs": "^7.5.6",
"sockjs-client": "^1.6.0",
"stream": "0.0.2",
"svg.draggable.js": "2.2.0",
"svg.js": "2.7.1",
"tabulator-tables": "^5.4.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('ShowNodeInfoDialogComponent', () => {
spyOn(TestBed.inject(ProjectService), 'getNodeById').and.returnValue(node);
spyOn(TestBed.inject(ProjectService), 'getNodePositionById').and.returnValue('1.1');
spyOn(TestBed.inject(VLEProjectService), 'getSpaces').and.returnValue([]);
spyOn(TestBed.inject(ProjectService), 'getSpeechToTextSettings').and.returnValue({});
spyOn(TestBed.inject(NotebookService), 'isNotebookEnabled').and.returnValue(false);
fixture.detectChanges();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
cdkTextareaAutosize
>
</textarea>
<speech-to-text
*ngIf="speechToTextEnabled"
(newTextEvent)="appendStudentResponse($event)"
></speech-to-text>
</div>
<div *ngIf="isStudentAttachmentEnabled" fxLayout="row wrap" fxLayoutAlign="start center">
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('OpenResponseStudent', () => {
});

beforeEach(() => {
spyOn(TestBed.inject(ProjectService), 'getSpeechToTextSettings').and.returnValue({});
fixture = TestBed.createComponent(OpenResponseStudent);
spyOn(TestBed.inject(ProjectService), 'isSpaceExists').and.returnValue(false);
spyOn(TestBed.inject(ProjectService), 'getThemeSettings').and.returnValue({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class OpenResponseStudent extends ComponentStudent {
cRaterTimeout: number = 40000;
isPublicSpaceExist: boolean = false;
isStudentAudioRecordingEnabled: boolean = false;
protected speechToTextEnabled: boolean;
studentResponse: string = '';

constructor(
Expand Down Expand Up @@ -61,6 +62,7 @@ export class OpenResponseStudent extends ComponentStudent {
StudentAssetService,
StudentDataService
);
this.speechToTextEnabled = this.ProjectService.getSpeechToTextSettings()?.enabled;
}

ngOnInit(): void {
Expand Down Expand Up @@ -190,6 +192,11 @@ export class OpenResponseStudent extends ComponentStudent {
return this.studentResponse;
}

protected appendStudentResponse(text: string): void {
this.studentResponse += text;
this.studentDataChanged();
}

/**
* Create a new component state populated with the student data
* @param action the action that is triggering creating of this component state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { StudentTeacherCommonModule } from '../../../../../app/student-teacher-c
import { StudentComponentModule } from '../../../../../app/student/student.component.module';
import { AudioRecorderComponent } from '../audio-recorder/audio-recorder.component';
import { OpenResponseStudent } from './open-response-student.component';
import { SpeechToTextComponent } from '../speech-to-text/speech-to-text.component';
import { TranscribeService } from '../../../../wise5/services/transcribeService';

@NgModule({
declarations: [AudioRecorderComponent, OpenResponseStudent],
imports: [StudentTeacherCommonModule, StudentComponentModule],
imports: [StudentTeacherCommonModule, StudentComponentModule, SpeechToTextComponent],
providers: [TranscribeService],
exports: [OpenResponseStudent]
})
export class OpenResponseStudentModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<span class="mat-caption" i18n>Speak to add to your response:</span>
<button
mat-stroked-button
[matMenuTriggerFor]="languageSelect"
[disabled]="recording()"
matTooltip="Select language"
i18n-mat-tooltip
>
<mat-icon>translate</mat-icon>
<span *ngIf="selectedLanguage()">{{ selectedLanguage().name }}</span>
</button>
<mat-menu #languageSelect="matMenu">
<button
*ngFor="let language of languages"
[value]="language"
mat-menu-item
(click)="changeLanguage(language)"
[class]="{ primary: language.code === selectedLanguage().code }"
>
{{ language.name }}
</button>
</mat-menu>
<button
*ngIf="!(recording() && recordingRequester)"
[disabled]="recording()"
mat-icon-button
color="primary"
(click)="toggleRecording()"
matTooltip="Record voice"
i18n-mat-tooltip
>
<mat-icon>mic</mat-icon>
</button>
<ng-container *ngIf="recording() && recordingRequester">
<button
mat-icon-button
(click)="toggleRecording()"
matTooltip="Stop recording"
i18n-mat-tooltip
>
<mat-icon color="warn">stop_circle</mat-icon>
</button>
<span class="mat-caption text-secondary" i18n>Recording...</span>
</ng-container>
</div>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SpeechToTextComponent } from './speech-to-text.component';
import { TranscribeService } from '../../../services/transcribeService';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ProjectService } from '../../../services/projectService';
import { ConfigService } from '../../../services/configService';
import { HttpClientTestingModule } from '@angular/common/http/testing';

describe('SpeechToTextComponent', () => {
let component: SpeechToTextComponent;
let fixture: ComponentFixture<SpeechToTextComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, HttpClientTestingModule, SpeechToTextComponent],
providers: [
ConfigService,
{
provide: ProjectService,
useValue: {
project: { speechToText: { defaultLanguage: 'en-US', supportedLanguages: [] } }
}
},
TranscribeService
]
});
fixture = TestBed.createComponent(SpeechToTextComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Component, EventEmitter, Output, Signal } from '@angular/core';
import { Language, TranscribeService } from '../../../services/transcribeService';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu';
import { FlexLayoutModule } from '@angular/flex-layout';

@Component({
standalone: true,
selector: 'speech-to-text',
imports: [
CommonModule,
FormsModule,
FlexLayoutModule,
MatButtonModule,
MatFormFieldModule,
MatIconModule,
MatMenuModule,
MatTooltipModule
],
templateUrl: './speech-to-text.component.html',
styleUrls: ['./speech-to-text.component.scss']
})
export class SpeechToTextComponent {
protected languages: Language[] = this.transcribeService.languages;
@Output() newTextEvent: EventEmitter<string> = new EventEmitter<string>();
protected recording: Signal<boolean> = this.transcribeService.recording;
protected recordingRequester = false;
protected selectedLanguage: Signal<Language> = this.transcribeService.selectedLanguage;

constructor(private transcribeService: TranscribeService) {}

ngOnDestroy(): void {
this.stopRecording();
}

protected toggleRecording(): void {
if (!this.recording()) {
this.startRecording();
} else {
this.stopRecording();
}
}

protected changeLanguage(language: Language): void {
this.transcribeService.setSelectedLanguage(language);
}

private async startRecording(): Promise<void> {
try {
this.recordingRequester = true;
await this.transcribeService.startRecording(
this.selectedLanguage().code,
this.processTranscriptionText.bind(this)
);
} catch (error) {
alert($localize`An error occurred while recording: ${error.message}`);
this.stopRecording();
}
}

private stopRecording(): void {
this.transcribeService.stopRecording();
this.recordingRequester = false;
}

private processTranscriptionText(text: string): void {
this.newTextEvent.emit(text);
}
}
4 changes: 4 additions & 0 deletions src/assets/wise5/services/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1998,4 +1998,8 @@ export class ProjectService {
const componentId = content.getReferenceComponentId();
return new Component(this.getComponent(nodeId, componentId), nodeId);
}

getSpeechToTextSettings(): any {
return this.project.speechToText;
}
}
Loading

0 comments on commit e28b7e3

Please sign in to comment.