Skip to content

Commit

Permalink
provider settings WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
jptrsn committed Feb 2, 2025
1 parent aa1bc46 commit 0e260fc
Show file tree
Hide file tree
Showing 25 changed files with 287 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<label class="form-control" [formGroup]="group">
<span class="text-accent label" translate>Engine</span>
<span class="text-accent label" translate>Provider</span>
<select class="select select-secondary select-lg mt-1 w-full" formControlName="provider">
<option *ngFor="let p of providers" [value]="p.value">{{p.label}}</option>
</select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class AzureRecognitionService {
private azureSttEndpoint: string;
private readonly MAX_RECOGNITION_LENGTH = 15;
private transcriptionEnabled: Signal<boolean | undefined>;
private readonly STT_CREDITS_PER_MINUTE = 60;

constructor(private store: Store<AppState>,
private http: HttpClient
Expand All @@ -34,7 +35,6 @@ export class AzureRecognitionService {
return this._getToken().pipe(map((auth) => {
if (!auth) throw new Error('Azure Recognition Service missing token');
const speechConfig = sdk.SpeechConfig.fromAuthorizationToken(auth.token, auth.region);
console.log('language', language);
speechConfig.speechRecognitionLanguage = language;

const audioConfig = sdk.AudioConfig.fromDefaultMicrophoneInput();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AvailableLanguages } from '../../models/settings.model';
selector: 'app-language-selector',
templateUrl: './language-selector.component.html',
styleUrls: ['./language-selector.component.scss'],
standalone: false
})
export class LanguageSelectorComponent {
@Input() group!: FormGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { ActivatedRoute, Router } from '@angular/router';
]
})
export class SettingsComponent {

public acceptedCookies: Signal<boolean | undefined>;
public showUnsavedChangesModal?: boolean;
public modalClosed$: Subject<boolean> = new Subject<boolean>();
Expand All @@ -42,12 +42,12 @@ export class SettingsComponent {
'sharing': 'heroShare',
'obs': 'obsStudioLogo'
}

constructor(private fb: FormBuilder,
private store: Store<AppState>,
private router: Router,
private route: ActivatedRoute) {

this.acceptedCookies = toSignal(this.store.pipe(
select(selectAppAppearance),
map((appearance: AppAppearanceState) => appearance.cookiesAccepted)
Expand All @@ -57,7 +57,7 @@ export class SettingsComponent {
const initialTabIndex = this.route.snapshot.queryParams['tabIndex'] % this.tabNames.length || 0;
this.tabsControl = this.fb.control(initialTabIndex);
this.tabIndex = toSignal(this.tabsControl.valueChanges.pipe(
takeUntilDestroyed(),
takeUntilDestroyed(),
tap((index) => {
const params = { tabIndex: index };
this.router.navigate([], { relativeTo: this.route, queryParams: params, queryParamsHandling: 'merge'})
Expand All @@ -66,14 +66,6 @@ export class SettingsComponent {
)) as Signal<number>;
}

canDeactivate(): boolean | Observable<boolean> {
// if (this.formGroup.dirty) {
// this.showUnsavedChangesModal = true;
// return this.modalClosed$.asObservable().pipe(take(1));
// }
return true;
}

modalClosed(value: boolean): void {
this.showUnsavedChangesModal = false;
this.modalClosed$.next(value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="card bg-base-100 shadow-xl flex flex-col">
<div class="card-body">
<div class="card-title"><span translate>USER.PROVIDER.title</span></div>
<div class="text-sm mb-3" translate>USER.PROVIDER.description</div>

<app-recognition-engine-select></app-recognition-engine-select>
<ng-container *ngIf="provider() as p">
<div class="text-md mt-3">{{'USER.PROVIDER.' + p + '.label' | translate}}</div>
<p>{{'USER.PROVIDER.' + p + '.description' | translate }}</p>

</ng-container>
</div>
</div>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProviderComponent } from './provider.component';

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

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProviderComponent],
}).compileComponents();

fixture = TestBed.createComponent(ProviderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Component, Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Store, select } from '@ngrx/store';
import { AppState } from '../../../../models/app.model';
import { RecognitionEngineState } from '../../../../models/recognition.model';
import { selectRecognitionEngine } from '../../../../selectors/recognition.selector';
import { map } from 'rxjs';

@Component({
selector: 'app-provider',
templateUrl: './provider.component.html',
styleUrls: ['./provider.component.scss'],
})
export class ProviderComponent {
public provider: Signal<RecognitionEngineState['provider'] | undefined>;
constructor(private store: Store<AppState>) {
this.provider = toSignal(this.store.pipe(
select(selectRecognitionEngine),
map((e) => e?.provider )
));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
<div class="flex flex-col items-center justify-center basis-full overflow-y-auto p-6 gap-6 flex-grow-0 bg-base-300 text-base-content">
<app-user-profile class="sm:basis-auto basis-full"></app-user-profile>
<!-- Tabs header -->
<form class="flex flex-row items-center justify-center gap-3 self-stretch tabs-bordered">
<div class="relative border-b-2" [ngClass]="{'text-secondary border-secondary': tabIndex() === i}" *ngFor="let name of tabNames, index as i">
<input [formControl]="tabsControl" type="radio" role="tab" class="tab" [attr.aria-label]="'USER.TABS.' + name | translate" [value]="i" [id]="name">
<label [for]="name" class="absolute top-0 left-0 right-0 cursor-pointer sm:!hidden flex items-center justify-center">
<ng-icon [name]="tabIcons[name]" size="24"></ng-icon>
</label>
</div>
</form>

<div class="text-lg text-bold text-secondary mt-2 sm:hidden">
{{ 'USER.TABS.' + tabNames[tabIndex()] | translate }}
</div>

<div class="basis-full overflow-y-auto p-3" [ngSwitch]="tabNames[tabIndex()]">
<app-user-profile class="sm:basis-auto basis-full" *ngSwitchCase="'account'"></app-user-profile>
<app-provider class="sm:basis-auto basis-full" *ngSwitchCase="'provider'"></app-provider>
</div>

<div class="flex flex-row gap-3" *ngIf="!loggedIn()">
<button class="btn btn-primary" [routerLink]="['..']" translate>ROUTES.home</button>
<button class="btn btn-secondary" [routerLink]="['..', 'auth', 'login']" translate>BUTTONS.login</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ export class UserHomeComponent implements OnInit {
public tabIndex: Signal<number>;
public tabNames = [
'account',
'transcripts'
'provider'
]
public tabIcons: {[key: string]: string} = {
'account': 'heroUser',
'provider': 'heroWrenchScrewdriver'
}
constructor(private store: Store<AppState>,
private router: Router,
private fb: FormBuilder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
</div>
<ng-container *ngIf="patron.status === 'active_patron'">
<div class="text-xs max-w-xs" translate>USER.PATREON.thanksMessage</div>
<app-recognition-engine-select></app-recognition-engine-select>
</ng-container>
<div class="text-sm mt-1"><span translate>USER.PATREON.usernameLabel</span>: <span>{{patron.displayName}}</span></div>
<div class="text-sm mt-1"><span translate>USER.PATREON.startDateLabel</span>: <span>{{patron.startDate | timeago }}</span></div>
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/app/modules/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ import { TranscriptsListComponent } from './components/transcripts-list/transcri
import { ViewTranscriptComponent } from './components/view-transcript/view-transcript.component';
import { EditableStringComponent } from '../../standalone/editable-string/editable-string.component';
import { RecognitionEngineSelectComponent } from '../../components/recognition-engine-select/recognition-engine-select.component';

import { ProviderComponent } from './components/provider/provider.component';

@NgModule({
declarations: [
UserHomeComponent,
UserProfileComponent,
TranscriptsListComponent,
ViewTranscriptComponent,
ProviderComponent,
],
imports: [
CommonModule,
Expand All @@ -39,7 +40,7 @@ import { RecognitionEngineSelectComponent } from '../../components/recognition-e
EffectsModule.forFeature([UserEffects, AuthEffects]),
StoreModule.forFeature('user', userReducer),
EditableStringComponent,
RecognitionEngineSelectComponent,
RecognitionEngineSelectComponent,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
Expand Down
20 changes: 19 additions & 1 deletion packages/client/src/app/reducers/user.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface UserProfile {
googleConnected?: boolean;
azureConnected?: boolean;
syncUiSettings?: boolean;
creditBalance?: number;
}

export interface UserRoom {
Expand All @@ -36,13 +37,30 @@ export interface SupporterProfile {
updatedAt?: Date;
}

export interface CreditAdd {
id: string;
createdAt: Date;
provider: string;
creditsAdded: number;
}

export interface CreditExpenditure {
id: string;
createdAt: Date;
serviceName: string;
durationMs?: number;
creditsUsed: number;
}

export interface UserState {
id?: string;
profile?: UserProfile;
supporter?: SupporterProfile;
uiSettings?: SettingsState;
rooms?: UserRoom[];
error?: string;
creditAcquisitions?: CreditAdd[];
creditExpenditures?: CreditExpenditure[];
}

export const defaultUserState: UserState = {
Expand All @@ -59,7 +77,7 @@ export const userReducer = createReducer(

on(UserActions.saveSettingsStateSuccess, (state: UserState, action: { settings: SettingsState }) => ({...state, uiSettings: action.settings})),
on(UserActions.saveSettingsStateFailure, (state: UserState, action: { error: string }) => ({...state, profile: undefined, error: action.error })),

on(UserActions.getSettingsSuccess, (state: UserState, action: { settings: SettingsState }) => ({...state, uiSettings: action.settings})),
on(UserActions.getSettingsFailure, (state: UserState, action: { error: string }) => ({...state, profile: undefined, error: action.error })),

Expand Down
22 changes: 21 additions & 1 deletion packages/client/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,10 @@
"loginWithPatreon": "Sign in with Patreon"
},
"USER": {
"TABS": {
"account": "Account",
"provider": "Provider"
},
"PROFILE": {
"title": "Welcome",
"blurb": "Here's what we know about you:",
Expand Down Expand Up @@ -616,7 +620,23 @@
"usernameLabel": "Patreon user name",
"startDateLabel": "Joined",
"thanksMessage": "Thank you for supporting us - we couldn't do this without your help!"
}
},
"PROVIDER": {
"title": "Recognition Provider",
"description": "Select which service you would like to process audio and return results. Different engines will vary in speed, quality, and cost. We do our best to bring you as much choice as possible while keeping things as free as we can.",
"web": {
"label": "Browser (Chrome/Safari)",
"description": "Using the browser provider, Zip Captions will send audio data from your device directly to your browser provider using the Web Speech API.",
"cost": "free",
"link": "https://learn.microsoft.com/en-us/javascript/api/overview/azure/microsoft-cognitiveservices-speech-sdk-readme?view=azure-node-latest"
},
"azure": {
"label": "Microsoft Azure Cognitive Services",
"description": "Using the Azure Cognitive Services provider, Zip Captions will send audio data from your device directly to Microsoft's API using a secure web socket.",
"cost": "60 credits per minute",
"link": "https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition"
}
}
},
"TRANSCRIPT": {
"titlePlural": "Transcripts",
Expand Down
23 changes: 19 additions & 4 deletions packages/server/src/app/modules/azure-stt/azure-stt.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import { Controller, Get, Req } from "@nestjs/common";
import { Controller, Get, HttpException, HttpStatus, Req, UseGuards } from "@nestjs/common";
import { AzureSttService } from "./services/azure-stt.service";
import { NoCache } from "../../decorators/no-cache.decorator";
import { JwtAuthGuard } from "../../guards/jwt-auth.guard";
import { UserService } from "../user/services/user.service";
import { SupporterService } from "../../services/supporter/supporter.service";

@Controller('azure-stt')
export class AzureSttController {
constructor(private azureSttService: AzureSttService) {}
constructor(
private azureSttService: AzureSttService,
private userService: UserService,
private supporterService: SupporterService,
) {}

@Get('get-token')
@NoCache()
// @UseGuards(JwtAuthGuard)
async getSpeechToken(@Req() req): Promise<any> {
@UseGuards(JwtAuthGuard)
async getSpeechToken(@Req() req): Promise<{token: string; region: string}> {
const user = await this.userService.findOne({ id: req.user.id });
if (!user) {
throw new HttpException(`User not found`, HttpStatus.NOT_FOUND)
}
const supporter = await this.supporterService.findOne({ email: user.primaryEmail });
if (!supporter || !supporter.amountCents) {
throw new HttpException(`User is not a supporter`, HttpStatus.BAD_REQUEST);
}
return this.azureSttService.getToken();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { User, UserSchema } from '../user/models/user.model';
import { Supporter, SupporterSchema } from '../../models/supporter.model';
import { AzureSttController } from './azure-stt.controller';
import { AzureSttService } from './services/azure-stt.service';
import { UserService } from '../user/services/user.service';
import { SupporterService } from '../../services/supporter/supporter.service';

@Module({
imports: [
Expand All @@ -24,7 +26,9 @@ import { AzureSttService } from './services/azure-stt.service';
],
providers: [
JwtStrategy,
AzureSttService
AzureSttService,
UserService,
SupporterService,
],
})
export class AzureSttModule {}
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { User } from "../../user/models/user.model";
import { Supporter } from "../../../models/supporter.model";
import { HttpService } from "@nestjs/axios";
import { Injectable } from "@nestjs/common";
import { firstValueFrom } from "rxjs";

@Injectable()
export class AzureSttService {
private speechKey = process.env.AZURE_SPEECH_KEY;
private speechRegion = process.env.AZURE_SPEECH_REGION;
constructor(@InjectModel(User.name) private userModel: Model<User>,
@InjectModel(Supporter.name) private supporterModel: Model<Supporter>,
private http: HttpService) { }
constructor(private http: HttpService) { }

async getToken(): Promise<any> {
async getToken(): Promise<{token: string; region: string}> {
const headers = {
headers: {
'Ocp-Apim-Subscription-Key': this.speechKey,
Expand Down
Loading

0 comments on commit 0e260fc

Please sign in to comment.