Avanzamento con riconoscimento facciale
This commit is contained in:
@@ -45,8 +45,7 @@
|
||||
[class.full-screen-container]="settingsService.fullscreenMode()"
|
||||
(touchstart)="onTouchStart($event)"
|
||||
(touchmove)="onTouchMove($event)"
|
||||
(touchend)="onTouchEnd()"
|
||||
(wheel)="onWheel($event)">
|
||||
(touchend)="onTouchEnd()">
|
||||
|
||||
|
||||
<!-- Landscape Side Controls (Scrollable) -->
|
||||
@@ -137,20 +136,6 @@
|
||||
</div>
|
||||
</ion-toolbar>
|
||||
|
||||
<!-- Visualizzatore Vocale & Log del Riconoscimento in Tempo Reale -->
|
||||
<div class="transcript-area" *ngIf="settingsService.enableAcousticAutoscroll() && audioEngine.isListening()" style="background: rgba(0, 0, 0, 0.55); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); padding: 10px 14px; border-top: 1px solid rgba(255,255,255,0.08); display: flex; flex-direction: column; gap: 8px; align-items: center; justify-content: center; position: relative;">
|
||||
<div class="energy-bar-container" style="width: 100%; height: 5px; background: rgba(255,255,255,0.12); border-radius: 3px; overflow: hidden;">
|
||||
<div class="energy-bar" [style.width.%]="math.min(100, audioEngine.energyLevel() * 3)" style="height: 100%; background: linear-gradient(90deg, var(--ion-color-secondary) 0%, #20e9ff 100%); border-radius: 3px; transition: width 0.08s ease-out;"></div>
|
||||
</div>
|
||||
|
||||
<div class="speech-log-row" style="display: flex; align-items: center; justify-content: center; width: 100%;">
|
||||
<div class="acoustic-counter-badge" style="display: inline-flex; align-items: center; gap: 6px; background: rgba(32, 233, 255, 0.12); border: 1px solid rgba(32, 233, 255, 0.25); color: #20e9ff; padding: 4px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 700;">
|
||||
<span style="width: 6px; height: 6px; background: #20e9ff; border-radius: 50%; display: inline-block; box-shadow: 0 0 8px #20e9ff; animation: pulse 1.5s infinite;"></span>
|
||||
<span>Parole Rilevate: {{ wordsSpokenInCurrentPage }} / {{ getPageTargetWordCount() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-toolbar class="bg-gradient slim-toolbar">
|
||||
<div class="slim-controls">
|
||||
<!-- Autoscroll Standard in Portrait Footer -->
|
||||
@@ -167,9 +152,9 @@
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<!-- Karaoke Toggle in Portrait -->
|
||||
<ion-button *ngIf="settingsService.enableAcousticAutoscroll()" fill="clear" size="small" (click)="toggleListening()" [color]="audioEngine.isListening() ? 'danger' : 'secondary'" class="mic-btn-portrait">
|
||||
<ion-icon slot="icon-only" [name]="audioEngine.isListening() ? 'mic' : 'mic-off'"></ion-icon>
|
||||
<!-- Camera Head Gestures Toggle in Portrait -->
|
||||
<ion-button fill="clear" size="small" (click)="toggleCameraNavigation()" [color]="enableCameraNavigation() ? 'success' : 'secondary'">
|
||||
<ion-icon slot="icon-only" [name]="enableCameraNavigation() ? 'videocam' : 'videocam-off-outline'"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
|
||||
@@ -220,21 +205,12 @@
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
<!-- Vertical Sensitivity Slider Overlay -->
|
||||
<div class="mic-sensitivity-overlay" *ngIf="settingsService.enableAcousticAutoscroll() && showSensitivitySlider() && audioEngine.isListening()">
|
||||
<div class="slider-card glass">
|
||||
<ion-button fill="clear" color="secondary" (click)="toggleSensitivitySlider($event)" class="close-slider-btn">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<div class="slider-wrapper"
|
||||
(touchstart)="handleSensitivityTouch($event)"
|
||||
(touchmove)="handleSensitivityTouch($event)">
|
||||
<div class="custom-vertical-slider">
|
||||
<div class="slider-track"></div>
|
||||
<div class="slider-fill" [style.height.%]="(audioEngine.sensitivity() - 50) * 2"></div>
|
||||
<div class="slider-knob" [style.bottom.%]="(audioEngine.sensitivity() - 50) * 2"></div>
|
||||
</div>
|
||||
<!-- Floating Camera Preview for Head Navigation -->
|
||||
<div class="camera-preview-floating glass" *ngIf="enableCameraNavigation()">
|
||||
<video id="face-preview-video" muted playsinline autoplay></video>
|
||||
<div class="camera-preview-overlay">
|
||||
<div class="angle-indicator" [class.tilted]="faceDetector.isTilted()">
|
||||
{{ faceDetector.currentTiltAngle() }}°
|
||||
</div>
|
||||
<span class="sensitivity-label outfit-font">{{ audioEngine.sensitivity() }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -455,6 +455,113 @@ ion-content.full-screen-content {
|
||||
}
|
||||
}
|
||||
|
||||
.voice-threshold-overlay {
|
||||
position: fixed;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.slider-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 6px 12px 6px;
|
||||
background: rgba(18, 18, 18, 0.85) !important;
|
||||
backdrop-filter: blur(25px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(46, 213, 115, 0.4);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
gap: 8px;
|
||||
animation: slideInLeft 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.close-slider-btn {
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
margin: 0;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
--color: var(--ion-color-secondary);
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-wrapper {
|
||||
height: 180px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.custom-vertical-slider {
|
||||
position: relative;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
|
||||
.slider-track {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: linear-gradient(to top, #2ed573, #7bed9f);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px rgba(46, 213, 115, 0.3);
|
||||
transition: height 0.05s linear;
|
||||
}
|
||||
|
||||
.slider-knob {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: #2ed573;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5), 0 0 15px rgba(46, 213, 115, 0.5);
|
||||
border: 2px solid #fff;
|
||||
transition: bottom 0.05s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.sensitivity-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
color: #2ed573;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.floating-autoscroll-bar {
|
||||
position: fixed;
|
||||
bottom: 120px;
|
||||
@@ -612,3 +719,54 @@ ion-content.full-screen-content {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.camera-preview-floating {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
right: 15px;
|
||||
width: 90px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.4);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #000;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
bottom: 15px;
|
||||
right: 80px;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scaleX(-1); // Mirror camera preview
|
||||
}
|
||||
|
||||
.camera-preview-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 2px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.angle-indicator {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
|
||||
&.tilted {
|
||||
color: #2ed573;
|
||||
text-shadow: 0 0 5px #2ed573;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AlertController, ToastController, GestureController } from '@ionic/angular';
|
||||
import { CantiService, Canto } from '../../services/canti.service';
|
||||
import { AudioEngineService } from '../../services/audio-engine.service';
|
||||
import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser.service';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
@@ -13,6 +12,7 @@ import { YoutubePlayerService } from '../../services/youtube-player.service';
|
||||
import { MyCantiService } from '../../services/my-canti.service';
|
||||
import { ComunitaService } from '../../services/comunita.service';
|
||||
import { StatsService } from '../../services/stats.service';
|
||||
import { FaceDetectorService } from '../../services/face-detector.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-player',
|
||||
@@ -26,7 +26,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
/** true = show chords (accordi mode), false = text only */
|
||||
public showChords = signal<boolean>(false);
|
||||
public showSensitivitySlider = signal<boolean>(false);
|
||||
|
||||
/** Autoscroll standard */
|
||||
public isAutoscrolling = signal<boolean>(false);
|
||||
@@ -98,7 +97,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
private lastAdvanceTimestamp: number = 0; // For safety cooldown
|
||||
private readonly ADVANCE_COOLDOWN = 1500; // 1.5 seconds min
|
||||
|
||||
public wordsSpokenInCurrentPage: number = 0;
|
||||
public lastScrollBlock = signal<'start' | 'center'>('start');
|
||||
private lastProcessedTranscript: string = '';
|
||||
private initialStartTime: number = 0;
|
||||
@@ -106,7 +104,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
private route = inject(ActivatedRoute);
|
||||
public router = inject(Router);
|
||||
public cantiService = inject(CantiService);
|
||||
public audioEngine = inject(AudioEngineService);
|
||||
private lyricsParser = inject(LyricsParserService);
|
||||
private alertCtrl = inject(AlertController);
|
||||
private toastCtrl = inject(ToastController);
|
||||
@@ -120,6 +117,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
private gestureCtrl = inject(GestureController);
|
||||
private el = inject(ElementRef);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
public faceDetector = inject(FaceDetectorService);
|
||||
|
||||
public enableCameraNavigation = signal<boolean>(false);
|
||||
|
||||
private songStartTime: number = 0;
|
||||
|
||||
@@ -135,23 +135,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.showChords.set(this.settingsService.showChordsDefault());
|
||||
}, { allowSignalWrites: true });
|
||||
|
||||
// Avanzamento guidato dai picchi acustici con conteggio parole della riga in corso
|
||||
effect(() => {
|
||||
const count = this.audioEngine.linesDetected();
|
||||
if (count > 0 && this.audioEngine.isListening()) {
|
||||
this.handleAcousticPeak();
|
||||
}
|
||||
});
|
||||
|
||||
// Avanzamento in tempo reale tramite riconoscimento vocale di prossimità (driver primario veloce)
|
||||
effect(() => {
|
||||
const transcript = this.audioEngine.backgroundTranscript();
|
||||
const isSpeechActive = this.audioEngine.speechRecognitionActive();
|
||||
if (transcript && this.audioEngine.isListening() && isSpeechActive) {
|
||||
this.checkWordAdvancement();
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-scroll logic: keep active line in view
|
||||
effect(() => {
|
||||
const idx = this.currentLineIndex();
|
||||
@@ -275,26 +258,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
gestureX.enable();
|
||||
|
||||
const gestureY = this.gestureCtrl.create({
|
||||
el: this.el.nativeElement,
|
||||
direction: 'y',
|
||||
gestureName: 'swipe-song-y',
|
||||
canStart: (ev) => {
|
||||
const target = ev.event.target as HTMLElement;
|
||||
return !target.closest('ion-footer') && !target.closest('ion-header');
|
||||
},
|
||||
onEnd: (ev) => {
|
||||
if (Math.abs(ev.deltaY) > 50) {
|
||||
if (ev.deltaY > 0) {
|
||||
this.prev();
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
gestureY.enable();
|
||||
}
|
||||
|
||||
|
||||
@@ -361,25 +324,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.initialPinchDistance = null;
|
||||
}
|
||||
|
||||
private lastWheelTime = 0;
|
||||
private readonly WHEEL_COOLDOWN = 400; // ms to avoid too rapid advancement
|
||||
|
||||
onWheel(event: WheelEvent) {
|
||||
event.preventDefault();
|
||||
const now = Date.now();
|
||||
if (now - this.lastWheelTime < this.WHEEL_COOLDOWN) {
|
||||
return;
|
||||
}
|
||||
if (Math.abs(event.deltaY) > 5) {
|
||||
if (event.deltaY > 0) {
|
||||
this.next();
|
||||
} else {
|
||||
this.prev();
|
||||
}
|
||||
this.lastWheelTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
private getDistance(t1: Touch, t2: Touch): number {
|
||||
return Math.sqrt(Math.pow(t1.clientX - t2.clientX, 2) + Math.pow(t1.clientY - t2.clientY, 2));
|
||||
}
|
||||
@@ -434,40 +378,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
toggleListening() {
|
||||
if (this.audioEngine.isListening()) {
|
||||
this.audioEngine.stopListening();
|
||||
this.showSensitivitySlider.set(false);
|
||||
} else {
|
||||
this.audioEngine.startListening();
|
||||
this.showSensitivitySlider.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
toggleSensitivitySlider(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.showSensitivitySlider.update(v => !v);
|
||||
}
|
||||
|
||||
onSensitivityChange(event: any) {
|
||||
this.audioEngine.sensitivity.set(event.detail.value);
|
||||
}
|
||||
|
||||
handleSensitivityTouch(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
const touch = event.touches[0];
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
// Calculate percentage based on Y position (bottom is 50%, top is 100%)
|
||||
const rawPercentage = 100 - ((touch.clientY - rect.top) / rect.height * 100);
|
||||
// Map 0-100 raw to 50-100 range
|
||||
let percentage = 50 + (rawPercentage * 0.5);
|
||||
percentage = Math.max(50, Math.min(100, Math.round(percentage)));
|
||||
|
||||
this.audioEngine.sensitivity.set(percentage);
|
||||
}
|
||||
|
||||
toggleAudio() {
|
||||
if (this.youtubePlayerService.currentCantoId() === this.canto()?.id) {
|
||||
this.youtubePlayerService.togglePlayPause();
|
||||
@@ -492,10 +402,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
restart() {
|
||||
this.currentLineIndex.set(0);
|
||||
this.audioEngine.resetWordCount();
|
||||
this.youtubePlayerService.seekTo(0);
|
||||
this.channel.postMessage({ type: 'SYNC_INDEX', index: 0 });
|
||||
this.wordsSpokenInCurrentPage = 0;
|
||||
this.lastScrollBlock.set('center');
|
||||
}
|
||||
|
||||
@@ -549,26 +457,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private getLineWords(lineIndex: number): string[] {
|
||||
const allLines = this.getAllLines();
|
||||
const lineText = allLines[lineIndex]?.text || '';
|
||||
return lineText
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.map((w: string) => w.replace(/[^a-zA-Z0-9àèìòùáéíóú]/g, ''))
|
||||
.filter((w: string) => w.length >= 3);
|
||||
}
|
||||
|
||||
public getPageWords(startIndex: number, pageSize: number): string[] {
|
||||
const allWords: string[] = [];
|
||||
const totalLines = this.getTotalLines();
|
||||
const end = Math.min(totalLines, startIndex + pageSize);
|
||||
for (let i = startIndex; i < end; i++) {
|
||||
allWords.push(...this.getLineWords(i));
|
||||
}
|
||||
return allWords;
|
||||
}
|
||||
|
||||
public getVisibleLineIndices(): number[] {
|
||||
try {
|
||||
const container = document.querySelector('.lyrics-container') as HTMLElement;
|
||||
@@ -607,127 +495,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public getPageTargetWordCount(): number {
|
||||
const visibleIndices = this.getVisibleLineIndices();
|
||||
let totalRawWords = 0;
|
||||
const currentIdx = this.currentLineIndex();
|
||||
|
||||
if (visibleIndices.length > 0) {
|
||||
if (currentIdx === 0) {
|
||||
// Sulla prima pagina, contiamo solo la prima metà delle righe visibili
|
||||
// per far avanzare l'applicazione non appena si arriva a metà della prima schermata
|
||||
const halfLength = Math.max(1, Math.ceil(visibleIndices.length / 2));
|
||||
for (let i = 0; i < halfLength; i++) {
|
||||
const idx = visibleIndices[i];
|
||||
totalRawWords += this.getLineRawWordCount(idx);
|
||||
}
|
||||
} else {
|
||||
// Dalla seconda pagina in poi, contiamo solo le righe attive e future (escludendo il testo già cantato in alto)
|
||||
for (const idx of visibleIndices) {
|
||||
if (idx >= currentIdx) {
|
||||
totalRawWords += this.getLineRawWordCount(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: calcolo teorico basato sulla pagina corrente
|
||||
const pageSize = this.calculatePageStepSize();
|
||||
const totalLines = this.getTotalLines();
|
||||
|
||||
let end: number;
|
||||
if (currentIdx === 0) {
|
||||
// Fallback prima pagina: metà delle righe della pagina teorica
|
||||
const halfPageSize = Math.max(1, Math.ceil(pageSize / 2));
|
||||
end = Math.min(totalLines, currentIdx + halfPageSize);
|
||||
} else {
|
||||
end = Math.min(totalLines, currentIdx + pageSize);
|
||||
}
|
||||
|
||||
for (let i = currentIdx; i < end; i++) {
|
||||
totalRawWords += this.getLineRawWordCount(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Soglia al 70% delle parole contate nella schermata visibile
|
||||
return Math.max(1, Math.ceil(totalRawWords * 0.70));
|
||||
}
|
||||
|
||||
private checkWordAdvancement() {
|
||||
const now = Date.now();
|
||||
|
||||
// Cooldown per prevenire avanzamenti multipli troppo veloci
|
||||
if (now - this.lastAdvanceTimestamp < this.ADVANCE_COOLDOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIdx = this.currentLineIndex();
|
||||
let nextPageIdx: number;
|
||||
const totalLines = this.getTotalLines();
|
||||
|
||||
if (currentIdx === 0) {
|
||||
// Sulla prima pagina, la "pagina successiva" è in realtà la seconda metà della prima schermata
|
||||
const visibleIndices = this.getVisibleLineIndices();
|
||||
if (visibleIndices.length > 0) {
|
||||
const halfLength = Math.max(1, Math.ceil(visibleIndices.length / 2));
|
||||
nextPageIdx = visibleIndices[halfLength] !== undefined ? visibleIndices[halfLength] : halfLength;
|
||||
} else {
|
||||
const pageSize = this.calculatePageStepSize();
|
||||
const halfPageSize = Math.max(1, Math.ceil(pageSize / 2));
|
||||
nextPageIdx = currentIdx + halfPageSize;
|
||||
}
|
||||
} else {
|
||||
nextPageIdx = this.calculateNextPageStartIndex();
|
||||
}
|
||||
|
||||
if (nextPageIdx >= totalLines) return;
|
||||
|
||||
const pageSize = this.calculatePageStepSize();
|
||||
|
||||
// Raccoglie le parole del blocco successivo (metà pagina se siamo all'inizio, altrimenti intera pagina)
|
||||
const nextPageWords = this.getPageWords(nextPageIdx, currentIdx === 0 ? Math.max(1, Math.ceil(pageSize / 2)) : pageSize);
|
||||
if (nextPageWords.length === 0) return;
|
||||
|
||||
// Parole rilevate di recente (con tolleranza di ritardo della Speech API)
|
||||
const recentWords = this.audioEngine.getRecentWordsSince(this.lastAdvanceTimestamp);
|
||||
|
||||
// Controlla se l'utente ha cantato parole della pagina successiva -> Avanzamento istantaneo immediato
|
||||
const matchNext = nextPageWords.some(word => recentWords.includes(word));
|
||||
if (matchNext) {
|
||||
console.log(`[SmartKaraoke] Parola chiave del blocco successivo rilevata! Avanzamento immediato a: ${nextPageIdx}`);
|
||||
this.lastScrollBlock.set('center');
|
||||
this.currentLineIndex.set(nextPageIdx);
|
||||
this.channel.postMessage({ type: 'SYNC_INDEX', index: nextPageIdx });
|
||||
this.lastAdvanceTimestamp = now;
|
||||
this.audioEngine.resetWordCount();
|
||||
this.wordsSpokenInCurrentPage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public getLineRawWordCount(lineIndex: number): number {
|
||||
const allLines = this.getAllLines();
|
||||
const lineText = allLines[lineIndex]?.text || '';
|
||||
const words = lineText.trim().split(/\s+/).filter((w: string) => w.length > 0);
|
||||
return words.length;
|
||||
}
|
||||
|
||||
public getLineTargetWordCount(lineIndex: number): number {
|
||||
const rawCount = this.getLineRawWordCount(lineIndex);
|
||||
// Soglia proporzionale tollerante (65% del conteggio parole effettivo) per compensare legature melodiche
|
||||
return Math.max(1, Math.ceil(rawCount * 0.65));
|
||||
}
|
||||
|
||||
private handleAcousticPeak() {
|
||||
const targetWordCount = this.getPageTargetWordCount();
|
||||
|
||||
this.wordsSpokenInCurrentPage++;
|
||||
console.log(`[PeakAdvance] Peak detected. Spoken words on page: ${this.wordsSpokenInCurrentPage}/${targetWordCount}`);
|
||||
|
||||
if (this.wordsSpokenInCurrentPage >= targetWordCount) {
|
||||
console.log(`[PeakAdvance] Target page word count reached (${targetWordCount}). Advancing page.`);
|
||||
this.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateNextPageStartIndex(): number {
|
||||
try {
|
||||
const container = document.querySelector('.lyrics-container') as HTMLElement;
|
||||
@@ -781,8 +548,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
next(isAutomatic: boolean = false) {
|
||||
this.lastAdvanceTimestamp = Date.now();
|
||||
this.audioEngine.resetWordCount();
|
||||
this.wordsSpokenInCurrentPage = 0;
|
||||
|
||||
const totalLines = this.getTotalLines();
|
||||
const prevIdx = this.currentLineIndex();
|
||||
@@ -825,8 +590,6 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
prev() {
|
||||
this.lastAdvanceTimestamp = Date.now();
|
||||
this.audioEngine.resetWordCount();
|
||||
this.wordsSpokenInCurrentPage = 0;
|
||||
|
||||
const prevIdx = this.currentLineIndex();
|
||||
if (prevIdx > 0) {
|
||||
@@ -1064,10 +827,46 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
toggleCameraNavigation() {
|
||||
if (this.enableCameraNavigation()) {
|
||||
this.stopCameraNavigation();
|
||||
} else {
|
||||
this.startCameraNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
startCameraNavigation() {
|
||||
this.enableCameraNavigation.set(true);
|
||||
|
||||
setTimeout(async () => {
|
||||
const videoEl = document.querySelector('#face-preview-video') as HTMLVideoElement;
|
||||
if (videoEl) {
|
||||
try {
|
||||
await this.faceDetector.start(videoEl, (direction) => {
|
||||
console.log(`[PlayerPage] Head tilt trigger received: ${direction}`);
|
||||
if (direction === 'next') {
|
||||
this.next(false);
|
||||
} else {
|
||||
this.prev();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.enableCameraNavigation.set(false);
|
||||
alert('Impossibile accedere alla fotocamera. Assicurati di aver concesso i permessi e di usare HTTPS.');
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
stopCameraNavigation() {
|
||||
this.enableCameraNavigation.set(false);
|
||||
this.faceDetector.stop();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.stopAutoscroll();
|
||||
this.logPreviousSongTime();
|
||||
this.audioEngine.stopListening();
|
||||
this.stopCameraNavigation();
|
||||
this.channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user