Skip to content

Commit

Permalink
feat: eaaaaaaaaaaaaster egg πŸ˜‡
Browse files Browse the repository at this point in the history
  • Loading branch information
Wassim CHEGHAM committed Apr 19, 2018
1 parent 486595f commit 7b2054e
Show file tree
Hide file tree
Showing 18 changed files with 327 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -31,6 +31,7 @@
"@angular/router": "^6.0.0-rc.1",
"algoliasearch": "^3.26.0",
"algoliasearch-helper": "^2.24.0",
"api-ai-javascript": "^2.0.0-beta.21",
"core-js": "^2.5.4",
"hammerjs": "^2.0.8",
"rxjs": "^6.0.0-rc.0",
Expand Down
8 changes: 8 additions & 0 deletions src/app/app.component.css
Expand Up @@ -2,3 +2,11 @@
margin-bottom: 30px;
display: block;
}

.easter {
display: block;
height: 140px;
width: 100%;
position: absolute;
top: 0;
}
3 changes: 2 additions & 1 deletion src/app/app.component.html
@@ -1 +1,2 @@
<router-outlet></router-outlet>
<div class="easter" (click)="hitMe()"></div>
<router-outlet></router-outlet>
14 changes: 13 additions & 1 deletion src/app/app.component.ts
@@ -1,10 +1,22 @@
import { Component } from '@angular/core';
import { EastereggService } from '@app/core/easteregg.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
hits = 0;
constructor(private easter: EastereggService) {}

hitMe() {
this.hits++;
if (this.hits > 5) {
this.hits = 0;
this.easter.surprise();
} else {
console.log('One more time...');
}
}
}
13 changes: 0 additions & 13 deletions src/app/core/algolia/algolia.module.spec.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/app/core/algolia/algolia.module.ts
Expand Up @@ -10,7 +10,6 @@ export interface AlgoliaConfiguration {
}

@NgModule({
imports: [CommonModule],
providers: [AlgoliaService]
})
export class AlgoliaModule {
Expand Down
8 changes: 8 additions & 0 deletions src/app/core/core.module.ts
@@ -1,7 +1,11 @@
import { EastereggService } from './easteregg.service';
import { environment } from './../../environments/environment';
import { NgModule } from '@angular/core';
import { MaterialModule } from '@app/core/material/material.module';
import { AlgoliaModule } from '@app/core/algolia/algolia.module';
import { TextToSpeechService } from '@app/core/nlp/text-to-speech.service';
import { SpeechToTextService } from '@app/core/nlp/speech-to-text.service';
import { NlpModule } from '@app/core/nlp/nlp.module';

@NgModule({
imports: [
Expand All @@ -10,8 +14,12 @@ import { AlgoliaModule } from '@app/core/algolia/algolia.module';
applicationId: environment.algolia.applicationId,
searchApiKey: environment.algolia.searchApiKey,
indexName: environment.algolia.indexName
}),
NlpModule.forRoot({
accessToken: environment.dialogflow.accessToken
})
],
providers: [SpeechToTextService, TextToSpeechService, EastereggService],
exports: [MaterialModule, AlgoliaModule]
})
export class CoreModule {}
65 changes: 65 additions & 0 deletions src/app/core/easteregg.service.ts
@@ -0,0 +1,65 @@
import { debounceTime, distinctUntilChanged, switchMap, filter } from 'rxjs/operators';
import { NlpService } from './nlp/nlp.service';
import { Injectable, NgZone } from '@angular/core';
import { SpeechToTextService } from '@app/core/nlp/speech-to-text.service';
import { TextToSpeechService } from '@app/core/nlp/text-to-speech.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { from } from 'rxjs/internal/observable/from';

@Injectable({
providedIn: 'root'
})
export class EastereggService {
constructor(
private zone: NgZone,
private snackBar: MatSnackBar,
private stt: SpeechToTextService,
private tts: TextToSpeechService,
private nlp: NlpService
) {}

surprise() {
console.log('Activating speach recognition...');
this.stt.listen();
this.setup();
}

private setup() {
let sb = null;
this.stt.onstart$.subscribe(() => {
this.zone.run(() => {
sb = this.snackBar.open('Listening...', 'STOP');
sb.onAction().subscribe(() => {
this.stt.stop();
sb.dismiss();
});
});
});
this.stt.onresult$
.pipe(
// prettier-ignore
debounceTime(200),
distinctUntilChanged(),
filter(transcription => !!transcription),
switchMap(transcription => from(this.nlp.process(transcription)))
)
.subscribe(response => {
this.stt.stop();
this.tts.say(response.speech);
});
this.stt.onend$.subscribe(() => {
this.zone.run(() => {
sb.dismiss();
});
});
this.stt.onerror$.subscribe(error => {
console.error('STT error', error);
this.zone.run(() => {
sb.dismiss();
});
});
this.tts.onend$.subscribe(() => {
this.stt.start();
});
}
}
2 changes: 2 additions & 0 deletions src/app/core/nlp/inject-tokens.ts
@@ -0,0 +1,2 @@
import { InjectionToken } from '@angular/core';
export const DIALOGFLOW_TOKEN = new InjectionToken<string>('DIALOGFLOW_TOKEN');
25 changes: 25 additions & 0 deletions src/app/core/nlp/nlp.module.ts
@@ -0,0 +1,25 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DIALOGFLOW_TOKEN } from '@app/core/nlp/inject-tokens';
import { NlpService } from './nlp.service';

export interface DialogflowTokenInterface {
accessToken: string;
}

@NgModule({
providers: [NlpService]
})
export class NlpModule {
static forRoot(config: DialogflowTokenInterface): ModuleWithProviders {
return {
ngModule: NlpModule,
providers: [
{
provide: DIALOGFLOW_TOKEN,
useValue: config.accessToken
}
]
};
}
}
27 changes: 27 additions & 0 deletions src/app/core/nlp/nlp.service.ts
@@ -0,0 +1,27 @@
import { ApiAiClient, IServerResponse } from 'api-ai-javascript/index.js';
import { Injectable, Inject, InjectionToken } from '@angular/core';
import { DIALOGFLOW_TOKEN } from '@app/core/nlp/inject-tokens';

@Injectable({
providedIn: 'root'
})
export class NlpService {
client: ApiAiClient;

constructor(@Inject(DIALOGFLOW_TOKEN) private accessToken) {
this.client = new ApiAiClient({ accessToken });
}

async process(message: string) {
try {
const response = (await this.client.textRequest(message)) as any;
const url = response.result.contexts && response.result.contexts[0] && response.result.contexts[0].parameters.url;
return {
speech: response.result.fulfillment.speech,
url
};
} catch (error) {
console.error(error);
}
}
}
124 changes: 124 additions & 0 deletions src/app/core/nlp/speech-to-text.service.ts
@@ -0,0 +1,124 @@
import { Injectable, NgZone } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable } from 'rxjs/internal/Observable';
import { NlpService } from '@app/core/nlp/nlp.service';

@Injectable({
providedIn: 'root'
})
export class SpeechToTextService {
recognizing = false;
recognition;
start_timestamp = 0;
onresult$: Observable<string>;
onend$: Observable<{}>;
onerror$: Observable<{}>;
onstart$: Observable<{}>;
constructor() {}

listen() {
if (!('webkitSpeechRecognition' in window)) {
console.error('Your device is not compatible with this easter egg. sorry!');
} else {
this.recognition = new window['webkitSpeechRecognition']();
this.recognition.continuous = true;
this.recognition.interimResults = true;

this.setupListeners();
}

if (this.recognizing) {
this.recognition.stop();
return;
}
this.recognition.lang = 'en-US';
this.recognition.start();
this.start_timestamp = Date.now();
}

private setupListeners() {
this.onstart$ = new Observable(observer => {
this.recognition.onstart = () => {
this.handleOnStart();
observer.next(event);
};
return () => (this.recognition.onstart = null);
});

this.onerror$ = new Observable(observer => {
this.recognition.onerror = event => {
this.handleOnError(event);
observer.next(event.error);
};
return () => (this.recognition.onerror = null);
});

this.onend$ = new Observable(observer => {
this.recognition.onend = event => {
this.handleOnEnd(event);
observer.next(event);
};
return () => (this.recognition.onend = null);
});

this.onresult$ = new Observable(observer => {
this.recognition.onresult = event => {
const transcript = this.handleOnResult(event);
observer.next(transcript);
};
return () => (this.recognition.onresult = null);
});
}

stop() {
this.recognition.stop();
}
start() {
this.recognition.start();
}

private handleOnStart() {
this.recognizing = true;
}
private handleOnResult(event) {
let interim_transcript = '';
let final_transcript = '';
if (typeof event.results === 'undefined') {
this.recognition.onend = null;
this.recognition.stop();
console.error(`Sorry, your device is not compatible with Speech-To-Text technology.`);

return;
}
for (let i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
final_transcript += event.results[i][0].transcript;
} else {
interim_transcript += event.results[i][0].transcript;
}
}

return final_transcript;
}

private handleOnEnd(event) {
this.recognizing = false;
}

private handleOnError(event) {
if (event.error === 'no-speech') {
console.log('info_no_speech');
console.error(`Sorry, I didn't hear anything.`);
}
if (event.error === 'audio-capture') {
console.error(`Sorry, your mic isn't available.`);
}
if (event.error === 'not-allowed') {
if (event.timeStamp - this.start_timestamp < 100) {
console.error(`Sorry, I was blocked from access your mic.`);
} else {
console.error(`Sorry, I couln't access your mic.`);
}
}
}
}
36 changes: 36 additions & 0 deletions src/app/core/nlp/text-to-speech.service.ts
@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { Observer } from 'rxjs/internal/types';

@Injectable({
providedIn: 'root'
})
export class TextToSpeechService {
onend$: Observable<{}>;
_observer: Observer<{}>;
constructor() {
this.onend$ = new Observable(observer => {
this._observer = observer;
return () => {};
});
}

say(text) {
const msg = new SpeechSynthesisUtterance();
// const voices = window.speechSynthesis.getVoices();
// console.log(voices);

// msg.voice = voices[49]; // Note: some voices don't support altering params
// msg['voiceURI'] = 'native';
// msg.volume = 1; // 0 to 1
// msg.rate = 1; // 0.1 to 10
// msg.pitch = 0; // 0 to 2
msg.text = text;
msg.lang = 'en-US';

msg.onend = event => {
this._observer.next(event);
};
speechSynthesis.speak(msg);
}
}
3 changes: 3 additions & 0 deletions src/app/search-ui/search/search.component.html
Expand Up @@ -9,6 +9,9 @@
<mat-card-content class="card_content" [class.no-result]="applications.length === 0">

<div class="card_sorting-options">

<span class="card_applications-count">{{ applicationsCount }}</span>

<mat-button-toggle-group (change)="onSortOptionChange($event)" #group="matButtonToggleGroup" *ngIf="applications.length > 0">
<mat-button-toggle [checked]="sortOption" value="relevance_desc">
Most Relevant
Expand Down

0 comments on commit 7b2054e

Please sign in to comment.