|
|
@@ -1,7 +1,7 @@
|
|
|
|
import { Component, OnInit, ViewChild, ElementRef, inject } from '@angular/core';
|
|
|
|
import { Component, OnInit, ViewChild, ElementRef, inject } from '@angular/core';
|
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
import { IonicModule, ToastController, IonTextarea, PopoverController, NavController } from '@ionic/angular';
|
|
|
|
import { IonicModule, ToastController, IonTextarea, PopoverController, NavController, AlertController } from '@ionic/angular';
|
|
|
|
import { createWorker } from 'tesseract.js';
|
|
|
|
import { createWorker } from 'tesseract.js';
|
|
|
|
import { CantiService } from '../../services/canti.service';
|
|
|
|
import { CantiService } from '../../services/canti.service';
|
|
|
|
import { MyCantiService } from '../../services/my-canti.service';
|
|
|
|
import { MyCantiService } from '../../services/my-canti.service';
|
|
|
@@ -20,6 +20,7 @@ import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser
|
|
|
|
export class ProposeCantoPage implements OnInit {
|
|
|
|
export class ProposeCantoPage implements OnInit {
|
|
|
|
@ViewChild('contentTextarea', { static: false }) contentTextarea!: IonTextarea;
|
|
|
|
@ViewChild('contentTextarea', { static: false }) contentTextarea!: IonTextarea;
|
|
|
|
@ViewChild('cameraInput', { static: false }) cameraInput!: ElementRef;
|
|
|
|
@ViewChild('cameraInput', { static: false }) cameraInput!: ElementRef;
|
|
|
|
|
|
|
|
@ViewChild('fileInput', { static: false }) fileInput!: ElementRef;
|
|
|
|
|
|
|
|
|
|
|
|
public cantiService = inject(CantiService);
|
|
|
|
public cantiService = inject(CantiService);
|
|
|
|
private myCantiService = inject(MyCantiService);
|
|
|
|
private myCantiService = inject(MyCantiService);
|
|
|
@@ -29,6 +30,7 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
private route = inject(ActivatedRoute);
|
|
|
|
private route = inject(ActivatedRoute);
|
|
|
|
private router = inject(Router);
|
|
|
|
private router = inject(Router);
|
|
|
|
public lyricsParser = inject(LyricsParserService);
|
|
|
|
public lyricsParser = inject(LyricsParserService);
|
|
|
|
|
|
|
|
private alertCtrl = inject(AlertController);
|
|
|
|
|
|
|
|
|
|
|
|
showChordsPreview: boolean = true;
|
|
|
|
showChordsPreview: boolean = true;
|
|
|
|
|
|
|
|
|
|
|
@@ -189,6 +191,10 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
this.cameraInput.nativeElement.click();
|
|
|
|
this.cameraInput.nativeElement.click();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chooseFile() {
|
|
|
|
|
|
|
|
this.fileInput.nativeElement.click();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onDragOver(event: DragEvent) {
|
|
|
|
onDragOver(event: DragEvent) {
|
|
|
|
event.preventDefault();
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
event.stopPropagation();
|
|
|
@@ -212,11 +218,12 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
|
|
|
|
|
|
|
|
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
|
|
|
|
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
|
|
|
|
const file = event.dataTransfer.files[0];
|
|
|
|
const file = event.dataTransfer.files[0];
|
|
|
|
if (file.type.indexOf('image') !== -1) {
|
|
|
|
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
|
|
|
|
await this.processImageFile(file);
|
|
|
|
if (file.type.indexOf('image') !== -1 || isPdf) {
|
|
|
|
|
|
|
|
await this.processFile(file);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const toast = await this.toastController.create({
|
|
|
|
const toast = await this.toastController.create({
|
|
|
|
message: 'Per favore, trascina un file immagine valido.',
|
|
|
|
message: 'Per favore, trascina un file immagine o PDF valido.',
|
|
|
|
duration: 3000,
|
|
|
|
duration: 3000,
|
|
|
|
color: 'warning'
|
|
|
|
color: 'warning'
|
|
|
|
});
|
|
|
|
});
|
|
|
@@ -231,7 +238,7 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
console.log('[OCR-Capture] Nessun file selezionato.');
|
|
|
|
console.log('[OCR-Capture] Nessun file selezionato.');
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await this.processImageFile(file);
|
|
|
|
await this.processFile(file);
|
|
|
|
event.target.value = '';
|
|
|
|
event.target.value = '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -239,50 +246,335 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
const items = event.clipboardData?.items;
|
|
|
|
const items = event.clipboardData?.items;
|
|
|
|
if (!items) return;
|
|
|
|
if (!items) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let hasImage = false;
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
if (items[i].type.indexOf('image') !== -1) {
|
|
|
|
if (items[i].type.indexOf('image') !== -1) {
|
|
|
|
|
|
|
|
hasImage = true;
|
|
|
|
event.preventDefault(); // Prevent pasting the image representation as text
|
|
|
|
event.preventDefault(); // Prevent pasting the image representation as text
|
|
|
|
const blob = items[i].getAsFile();
|
|
|
|
const blob = items[i].getAsFile();
|
|
|
|
if (blob) {
|
|
|
|
if (blob) {
|
|
|
|
const file = new File([blob], 'pasted-image.png', { type: blob.type });
|
|
|
|
const file = new File([blob], 'pasted-image.png', { type: blob.type });
|
|
|
|
await this.processImageFile(file);
|
|
|
|
await this.processFile(file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasImage) {
|
|
|
|
|
|
|
|
const pastedText = event.clipboardData?.getData('text');
|
|
|
|
|
|
|
|
if (pastedText && this.looksLikeChordSheetOrSong(pastedText)) {
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
const parsed = this.parsePastedChordSheet(pastedText);
|
|
|
|
|
|
|
|
this.insertText(parsed);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async processImageFile(file: File) {
|
|
|
|
isTextLineChords(line: string, isItalian: boolean): boolean {
|
|
|
|
console.log(`[OCR-Capture] File selezionato: nome="${file.name}", tipo="${file.type}", dimensione=${(file.size / (1024 * 1024)).toFixed(2)} MB`);
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
|
|
if (!trimmed) return false;
|
|
|
|
|
|
|
|
const tokens = trimmed.split(/\s+/);
|
|
|
|
|
|
|
|
let chordCount = 0;
|
|
|
|
|
|
|
|
let nonChordWordCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const token of tokens) {
|
|
|
|
|
|
|
|
let clean = token.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
if (this.isChordWord(clean, isItalian)) {
|
|
|
|
|
|
|
|
chordCount++;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if (clean.length > 4) {
|
|
|
|
|
|
|
|
nonChordWordCount++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (tokens.length === 0) return false;
|
|
|
|
|
|
|
|
const ratio = chordCount / tokens.length;
|
|
|
|
|
|
|
|
return ratio >= 0.6 && nonChordWordCount === 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getSectionType(line: string): 'chorus' | 'verse' | 'intro' | 'none' {
|
|
|
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
|
|
if (/^(ritornello|rit|chorus|refrain|coro)/i.test(trimmed)) {
|
|
|
|
|
|
|
|
return 'chorus';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (/^(verso|strofa|verse|strophe|\d+(\.)?)/i.test(trimmed)) {
|
|
|
|
|
|
|
|
return 'verse';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (/^(intro|introduzione|bridge|special|outro|strum)/i.test(trimmed)) {
|
|
|
|
|
|
|
|
return 'intro';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'none';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
looksLikeChordSheetOrSong(text: string): boolean {
|
|
|
|
|
|
|
|
const lines = text.split('\n');
|
|
|
|
|
|
|
|
if (lines.length < 2) return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const words = text.split(/\s+/).map(t => ({ text: t }));
|
|
|
|
|
|
|
|
const isItalian = this.isItalianNotation(words);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let chordLinesCount = 0;
|
|
|
|
|
|
|
|
let sectionMarkersCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
|
|
if (this.getSectionType(line) !== 'none') {
|
|
|
|
|
|
|
|
sectionMarkersCount++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.isTextLineChords(line, isItalian)) {
|
|
|
|
|
|
|
|
chordLinesCount++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return chordLinesCount > 0 || sectionMarkersCount > 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
convertPureChordLine(line: string, isItalian: boolean): string {
|
|
|
|
|
|
|
|
return line.replace(/\S+/g, (match) => {
|
|
|
|
|
|
|
|
let clean = match.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
if (this.isChordWord(clean, isItalian)) {
|
|
|
|
|
|
|
|
return `[${this.convertEnglishChordToItalian(clean, isItalian)}]`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return match;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parsePastedChordSheet(text: string): string {
|
|
|
|
|
|
|
|
const lines = text.split('\n');
|
|
|
|
|
|
|
|
const processedLines: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const words = text.split(/\s+/).map(t => ({ text: t }));
|
|
|
|
|
|
|
|
const isItalian = this.isItalianNotation(words);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let inVerse = false;
|
|
|
|
|
|
|
|
let inChorus = false;
|
|
|
|
|
|
|
|
let hasAccumulatedLines = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const closeSection = () => {
|
|
|
|
|
|
|
|
if (inChorus) {
|
|
|
|
|
|
|
|
processedLines.push('{end_chorus}');
|
|
|
|
|
|
|
|
inChorus = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inVerse) {
|
|
|
|
|
|
|
|
processedLines.push('{end_verse}');
|
|
|
|
|
|
|
|
inVerse = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hasAccumulatedLines = false;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
|
|
|
|
|
|
const line = lines[i];
|
|
|
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!trimmed) {
|
|
|
|
|
|
|
|
if (hasAccumulatedLines) {
|
|
|
|
|
|
|
|
closeSection();
|
|
|
|
|
|
|
|
processedLines.push('');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sectionType = this.getSectionType(line);
|
|
|
|
|
|
|
|
if (sectionType !== 'none') {
|
|
|
|
|
|
|
|
closeSection();
|
|
|
|
|
|
|
|
if (sectionType === 'chorus') {
|
|
|
|
|
|
|
|
processedLines.push('{start_chorus}');
|
|
|
|
|
|
|
|
inChorus = true;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
processedLines.push('{start_verse}');
|
|
|
|
|
|
|
|
inVerse = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If no section is open, open a default one (verse)
|
|
|
|
|
|
|
|
if (!inChorus && !inVerse) {
|
|
|
|
|
|
|
|
processedLines.push('{start_verse}');
|
|
|
|
|
|
|
|
inVerse = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this line is chords
|
|
|
|
|
|
|
|
const isChords = this.isTextLineChords(line, isItalian);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isChords) {
|
|
|
|
|
|
|
|
// Look ahead to see if the next line is lyrics (not empty, not chords, not section marker)
|
|
|
|
|
|
|
|
let nextLine = '';
|
|
|
|
|
|
|
|
let nextLineIndex = i + 1;
|
|
|
|
|
|
|
|
while (nextLineIndex < lines.length) {
|
|
|
|
|
|
|
|
const nextTrimmed = lines[nextLineIndex].trim();
|
|
|
|
|
|
|
|
if (nextTrimmed) {
|
|
|
|
|
|
|
|
if (this.getSectionType(lines[nextLineIndex]) === 'none' && !this.isTextLineChords(lines[nextLineIndex], isItalian)) {
|
|
|
|
|
|
|
|
nextLine = lines[nextLineIndex];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
nextLineIndex++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (nextLine) {
|
|
|
|
|
|
|
|
// Merge spatially!
|
|
|
|
|
|
|
|
const chords: { text: string; index: number }[] = [];
|
|
|
|
|
|
|
|
const regex = /\S+/g;
|
|
|
|
|
|
|
|
let match: RegExpExecArray | null;
|
|
|
|
|
|
|
|
while ((match = regex.exec(line)) !== null) {
|
|
|
|
|
|
|
|
let clean = match[0].toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
if (this.isChordWord(clean, isItalian)) {
|
|
|
|
|
|
|
|
chords.push({
|
|
|
|
|
|
|
|
text: `[${this.convertEnglishChordToItalian(clean, isItalian)}]`,
|
|
|
|
|
|
|
|
index: match.index
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
chords.push({
|
|
|
|
|
|
|
|
text: match[0],
|
|
|
|
|
|
|
|
index: match.index
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Merge chords with the next line (lyrics)
|
|
|
|
|
|
|
|
let merged = '';
|
|
|
|
|
|
|
|
let lyricIdx = 0;
|
|
|
|
|
|
|
|
let chordIdx = 0;
|
|
|
|
|
|
|
|
while (lyricIdx < nextLine.length || chordIdx < chords.length) {
|
|
|
|
|
|
|
|
if (chordIdx < chords.length && (lyricIdx === chords[chordIdx].index || lyricIdx >= nextLine.length)) {
|
|
|
|
|
|
|
|
merged += chords[chordIdx].text;
|
|
|
|
|
|
|
|
chordIdx++;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
merged += nextLine[lyricIdx];
|
|
|
|
|
|
|
|
lyricIdx++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processedLines.push(merged);
|
|
|
|
|
|
|
|
hasAccumulatedLines = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Skip the next line since we consumed it
|
|
|
|
|
|
|
|
i = nextLineIndex;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// No lyrics line follows, just convert chords in place
|
|
|
|
|
|
|
|
processedLines.push(this.convertPureChordLine(line, isItalian));
|
|
|
|
|
|
|
|
hasAccumulatedLines = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// Plain text line
|
|
|
|
|
|
|
|
processedLines.push(this.wrapChords(line, this.getChordRegex(isItalian), isItalian));
|
|
|
|
|
|
|
|
hasAccumulatedLines = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
closeSection();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return processedLines.join('\n');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async convertPdfToImages(file: File): Promise<Blob[]> {
|
|
|
|
|
|
|
|
console.log('[OCR-Capture] Caricamento PDF per conversione in immagini...');
|
|
|
|
|
|
|
|
const pdfjsLib = await import('pdfjs-dist');
|
|
|
|
|
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'assets/pdf.worker.min.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
|
|
|
|
|
|
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
|
|
|
|
|
|
|
const pdf = await loadingTask.promise;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] PDF caricato con successo. Numero di pagine: ${pdf.numPages}`);
|
|
|
|
|
|
|
|
const blobs: Blob[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Limit to maximum 5 pages to avoid extreme memory consumption or timeouts
|
|
|
|
|
|
|
|
const pagesToRender = Math.min(pdf.numPages, 5);
|
|
|
|
|
|
|
|
for (let i = 1; i <= pagesToRender; i++) {
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] Rendering pagina ${i}/${pagesToRender}...`);
|
|
|
|
|
|
|
|
const page = await pdf.getPage(i);
|
|
|
|
|
|
|
|
const viewport = page.getViewport({ scale: 2.0 }); // High resolution scale for OCR
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
|
|
canvas.width = viewport.width;
|
|
|
|
|
|
|
|
canvas.height = viewport.height;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
|
|
|
throw new Error('Impossibile ottenere il context 2D per il canvas.');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await page.render({
|
|
|
|
|
|
|
|
canvasContext: ctx,
|
|
|
|
|
|
|
|
viewport: viewport,
|
|
|
|
|
|
|
|
canvas: canvas
|
|
|
|
|
|
|
|
}).promise;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const blob = await new Promise<Blob | null>((resolve) => {
|
|
|
|
|
|
|
|
canvas.toBlob(b => resolve(b), 'image/jpeg', 0.95);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (blob) {
|
|
|
|
|
|
|
|
blobs.push(blob);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return blobs;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async processFile(file: File) {
|
|
|
|
|
|
|
|
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] File selezionato: nome="${file.name}", tipo="${file.type}", dimensione=${(file.size / (1024 * 1024)).toFixed(2)} MB, è PDF: ${isPdf}`);
|
|
|
|
|
|
|
|
|
|
|
|
this.isProcessingOCR = true;
|
|
|
|
this.isProcessingOCR = true;
|
|
|
|
this.ocrProgress = 0;
|
|
|
|
this.ocrProgress = 0;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
console.log('[OCR-Capture] Immagine/Fotocamera rilevata. Avvio ridimensionamento...');
|
|
|
|
if (isPdf) {
|
|
|
|
const compressedBlob = await this.resizeImage(file);
|
|
|
|
const pageBlobs = await this.convertPdfToImages(file);
|
|
|
|
const compressedFile = new File([compressedBlob], file.name, { type: 'image/jpeg' });
|
|
|
|
console.log(`[OCR-Capture] PDF convertito in ${pageBlobs.length} immagini.`);
|
|
|
|
console.log(`[OCR-Capture] Dimensioni dopo compressione: ${(compressedFile.size / 1024).toFixed(1)} KB`);
|
|
|
|
|
|
|
|
|
|
|
|
for (let idx = 0; idx < pageBlobs.length; idx++) {
|
|
|
|
console.log('[OCR-Capture] Avvio motore Tesseract OCR local...');
|
|
|
|
const pageBlob = pageBlobs[idx];
|
|
|
|
const extractedText = await this.processImageOCR(compressedFile);
|
|
|
|
const pageFile = new File([pageBlob], `page_${idx + 1}.jpg`, { type: 'image/jpeg' });
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] Elaborazione pagina ${idx + 1}/${pageBlobs.length} via OCR...`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.ocrProgress = (idx / pageBlobs.length);
|
|
|
|
|
|
|
|
const extractedText = await this.processImageOCR(pageFile);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (extractedText) {
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] Pagina ${idx + 1} estratta con successo.`);
|
|
|
|
|
|
|
|
this.content += (this.content ? '\n\n' : '') + extractedText;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (extractedText) {
|
|
|
|
|
|
|
|
console.log('[OCR-Capture] Testo estratto con successo! Inserimento nell\'editor...');
|
|
|
|
|
|
|
|
this.content += (this.content ? '\n\n' : '') + extractedText;
|
|
|
|
|
|
|
|
const toast = await this.toastController.create({
|
|
|
|
const toast = await this.toastController.create({
|
|
|
|
message: 'Scansione completata!',
|
|
|
|
message: 'Scansione PDF completata!',
|
|
|
|
duration: 2000,
|
|
|
|
duration: 2000,
|
|
|
|
color: 'success'
|
|
|
|
color: 'success'
|
|
|
|
});
|
|
|
|
});
|
|
|
|
toast.present();
|
|
|
|
toast.present();
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
console.warn('[OCR-Capture] Nessun testo estratto dal file.');
|
|
|
|
console.log('[OCR-Capture] Immagine/Fotocamera rilevata. Avvio ridimensionamento...');
|
|
|
|
|
|
|
|
const compressedBlob = await this.resizeImage(file);
|
|
|
|
|
|
|
|
const compressedFile = new File([compressedBlob], file.name, { type: 'image/jpeg' });
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] Dimensioni dopo compressione: ${(compressedFile.size / 1024).toFixed(1)} KB`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[OCR-Capture] Avvio motore Tesseract OCR local...');
|
|
|
|
|
|
|
|
const extractedText = await this.processImageOCR(compressedFile);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (extractedText) {
|
|
|
|
|
|
|
|
console.log('[OCR-Capture] Testo estratto con successo! Inserimento nell\'editor...');
|
|
|
|
|
|
|
|
this.content += (this.content ? '\n\n' : '') + extractedText;
|
|
|
|
|
|
|
|
const toast = await this.toastController.create({
|
|
|
|
|
|
|
|
message: 'Scansione completata!',
|
|
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
|
|
color: 'success'
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
toast.present();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
console.warn('[OCR-Capture] Nessun testo estratto dal file.');
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('[OCR-Capture] Errore durante l\'elaborazione del file:', error);
|
|
|
|
console.error('[OCR-Capture] Errore durante l\'elaborazione del file:', error);
|
|
|
|
const errorToast = await this.toastController.create({
|
|
|
|
const errorToast = await this.toastController.create({
|
|
|
|
message: 'Errore durante la scansione dell\'immagine.',
|
|
|
|
message: 'Errore durante la scansione del file.',
|
|
|
|
duration: 3000,
|
|
|
|
duration: 3000,
|
|
|
|
color: 'danger'
|
|
|
|
color: 'danger'
|
|
|
|
});
|
|
|
|
});
|
|
|
@@ -383,6 +675,13 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
|
|
|
|
|
|
|
|
sanitizeOcrChord(text: string): string {
|
|
|
|
sanitizeOcrChord(text: string): string {
|
|
|
|
if (!text) return text;
|
|
|
|
if (!text) return text;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First, heal separators like I, 1, l, |, \ between chords/notes to '/'
|
|
|
|
|
|
|
|
const separatorRegex = /\b([A-G]|DO|RE|MI|FA|SOL|LA|SI)(#|b|♭)?(m|min|maj|dim|aug|sus)?(\d*)([I1l|\\\/])([A-G]|DO|RE|MI|FA|SOL|LA|SI|\d+)(#|b|♭)?\b/gi;
|
|
|
|
|
|
|
|
text = text.replace(separatorRegex, (match, p1, p2, p3, p4, sep, p5, p6) => {
|
|
|
|
|
|
|
|
return `${p1}${p2 || ''}${p3 || ''}${p4 || ''}/${p5}${p6 || ''}`;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (text.includes('/')) {
|
|
|
|
if (text.includes('/')) {
|
|
|
|
return text.split('/').map(part => this.sanitizeOcrChord(part.trim())).join('/');
|
|
|
|
return text.split('/').map(part => this.sanitizeOcrChord(part.trim())).join('/');
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -397,13 +696,137 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
cleaned = cleaned.replace(/([CDEFGAB]|DO|RE|MI|FA|SOL|LA|SI)H/gi, '$1#');
|
|
|
|
cleaned = cleaned.replace(/([CDEFGAB]|DO|RE|MI|FA|SOL|LA|SI)H/gi, '$1#');
|
|
|
|
// Clean duplicate sharps (e.g. ## -> #)
|
|
|
|
// Clean duplicate sharps (e.g. ## -> #)
|
|
|
|
cleaned = cleaned.replace(/##+/g, '#');
|
|
|
|
cleaned = cleaned.replace(/##+/g, '#');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Clean duplicate chord letters at start (e.g. Ff#m -> F#m)
|
|
|
|
|
|
|
|
cleaned = cleaned.replace(/^([CDEFGAB])\1/gi, '$1');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Convert Italian chords ending in 'M' or 'N' (e.g. LAM -> LAm, LAN -> LAm, LAN7 -> LAm7) to lowercase 'm'
|
|
|
|
|
|
|
|
cleaned = cleaned.replace(/\b(DO|RE|MI|FA|SOL|LA|SI)(#|b|♭)?[mMnN](\d*)\b/g, (match, p1, p2, p3) => {
|
|
|
|
|
|
|
|
return p1 + (p2 || '') + 'm' + (p3 || '');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return cleaned;
|
|
|
|
return cleaned;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
convertEnglishChordToItalian(chord: string): string {
|
|
|
|
getChordRegex(isItalian: boolean): RegExp {
|
|
|
|
|
|
|
|
if (isItalian) {
|
|
|
|
|
|
|
|
return /^(DO|RE|MI|FA|SOL|LA|SI)(#|B)?(M|-|MIN|MAJ|AUG|DIM|4|5|6)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|\d+)(#|B|b)?)?$/i;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return /^(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM|4|5|6)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B|\d+)(#|B|b)?)?$/i;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getMultiChordRegex(isItalian: boolean): RegExp {
|
|
|
|
|
|
|
|
if (isItalian) {
|
|
|
|
|
|
|
|
return /((?:DO|RE|MI|FA|SOL|LA|SI)(?:#|B)?(?:MIN|MAJ|AUG|DIM|M|-|4|5|6)?(?:7|9|11|13)?(?:\/(?:DO|RE|MI|FA|SOL|LA|SI|\d+)(?:#|B|b)?)?)/gi;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return /((?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?(?:MIN|MAJ|AUG|DIM|M|-|4|5|6)?(?:7|9|11|13)?(?:\/(?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B|\d+)(?:#|B|b)?)?)/gi;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isItalianNotation(words: any[]): boolean {
|
|
|
|
|
|
|
|
if (!words || words.length === 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Group words into horizontal lines
|
|
|
|
|
|
|
|
const heights = words.map(w => w.bbox ? (w.bbox.y1 - w.bbox.y0) : 10);
|
|
|
|
|
|
|
|
const avgHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
|
|
|
|
|
|
|
|
const verticalTolerance = avgHeight * 1.0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const lines: any[][] = [];
|
|
|
|
|
|
|
|
words.forEach(word => {
|
|
|
|
|
|
|
|
if (!word.text) return;
|
|
|
|
|
|
|
|
const wordYCenter = word.bbox ? ((word.bbox.y0 + word.bbox.y1) / 2) : 0;
|
|
|
|
|
|
|
|
let added = false;
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
|
|
const avgLineYCenter = line.reduce((sum, w) => sum + (w.bbox ? ((w.bbox.y0 + w.bbox.y1) / 2) : 0), 0) / line.length;
|
|
|
|
|
|
|
|
if (Math.abs(wordYCenter - avgLineYCenter) < verticalTolerance) {
|
|
|
|
|
|
|
|
line.push(word);
|
|
|
|
|
|
|
|
added = true;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!added) {
|
|
|
|
|
|
|
|
lines.push([word]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const genericChordRegex = /^(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM|4|5|6)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B|\d+)(#|B|b)?)?$/i;
|
|
|
|
|
|
|
|
let englishChordsCount = 0;
|
|
|
|
|
|
|
|
let italianChordsCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lines.forEach(line => {
|
|
|
|
|
|
|
|
let chordCount = 0;
|
|
|
|
|
|
|
|
line.forEach(w => {
|
|
|
|
|
|
|
|
let clean = w.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
if (genericChordRegex.test(clean)) {
|
|
|
|
|
|
|
|
chordCount++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ratio = line.length > 0 ? chordCount / line.length : 0;
|
|
|
|
|
|
|
|
if (ratio >= 0.5) {
|
|
|
|
|
|
|
|
line.forEach(w => {
|
|
|
|
|
|
|
|
let clean = w.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isEnglish = /^(C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM|4|5|6)?(7|9|11|13)?(\/(C|D|E|F|G|A|B|\d+)(#|B|b)?)?$/i.test(clean);
|
|
|
|
|
|
|
|
const isItalian = /^(DO|RE|MI|FA|SOL|LA|SI)(#|B)?(M|-|MIN|MAJ|AUG|DIM|4|5|6)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|\d+)(#|B|b)?)?$/i.test(clean);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isEnglish && !isItalian) {
|
|
|
|
|
|
|
|
englishChordsCount++;
|
|
|
|
|
|
|
|
} else if (isItalian && !isEnglish) {
|
|
|
|
|
|
|
|
italianChordsCount++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (englishChordsCount === 0 && italianChordsCount === 0) {
|
|
|
|
|
|
|
|
// Fallback: search the entire words list for unambiguous chords
|
|
|
|
|
|
|
|
words.forEach(w => {
|
|
|
|
|
|
|
|
if (!w.text) return;
|
|
|
|
|
|
|
|
let clean = w.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isUnambiguousEnglish =
|
|
|
|
|
|
|
|
/^(C|D|G|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM|4|5|6)?(7|9|11|13)?(\/(C|D|E|F|G|A|B|\d+)(#|B|b)?)?$/i.test(clean) ||
|
|
|
|
|
|
|
|
/^(A|E|F)(#|B|M|-|MIN|MAJ|AUG|DIM|4|5|6|7|9|11|13)/i.test(clean);
|
|
|
|
|
|
|
|
const isUnambiguousItalian =
|
|
|
|
|
|
|
|
/^(DO|RE|MI|FA|SOL|LA|SI)(#|B|M|-|MIN|MAJ|AUG|DIM|4|5|6|7|9|11|13)/i.test(clean);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isUnambiguousEnglish) englishChordsCount++;
|
|
|
|
|
|
|
|
if (isUnambiguousItalian) italianChordsCount++;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] Chord line analysis - English chords: ${englishChordsCount}, Italian chords: ${italianChordsCount}`);
|
|
|
|
|
|
|
|
return italianChordsCount > englishChordsCount;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isChordWord(text: string, isItalian: boolean): boolean {
|
|
|
|
|
|
|
|
if (!text || text.length === 0) return false;
|
|
|
|
|
|
|
|
const lower = text.toLowerCase();
|
|
|
|
|
|
|
|
// Skip common valid words written purely in lowercase
|
|
|
|
|
|
|
|
if (text[0] === lower[0] && ['la', 'mi', 're', 'do', 'si', 'fa', 'e'].includes(lower)) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let clean = text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
const chordRegex = this.getChordRegex(isItalian);
|
|
|
|
|
|
|
|
if (clean.includes('_')) {
|
|
|
|
|
|
|
|
const parts = clean.split('_').filter(p => p.length > 0);
|
|
|
|
|
|
|
|
if (parts.length === 0) return false;
|
|
|
|
|
|
|
|
return parts.every(p => chordRegex.test(p));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return chordRegex.test(clean);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
convertEnglishChordToItalian(chord: string, isItalian: boolean = false): string {
|
|
|
|
|
|
|
|
if (isItalian) return chord;
|
|
|
|
if (!chord) return chord;
|
|
|
|
if (!chord) return chord;
|
|
|
|
if (chord.includes('/')) {
|
|
|
|
if (chord.includes('/')) {
|
|
|
|
return chord.split('/').map(part => this.convertEnglishChordToItalian(part.trim())).join('/');
|
|
|
|
return chord.split('/').map(part => this.convertEnglishChordToItalian(part.trim(), isItalian)).join('/');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const upper = chord.toUpperCase();
|
|
|
|
const upper = chord.toUpperCase();
|
|
|
|
if (upper.startsWith('DO')) {
|
|
|
|
if (upper.startsWith('DO')) {
|
|
|
@@ -449,6 +872,9 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
return '';
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isItalian = this.isItalianNotation(words);
|
|
|
|
|
|
|
|
console.log(`[OCR-Capture] Rilevata notazione italiana: ${isItalian}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Preprocess words to split run-together chords like BA
|
|
|
|
// Preprocess words to split run-together chords like BA
|
|
|
|
const preprocessedWords: any[] = [];
|
|
|
|
const preprocessedWords: any[] = [];
|
|
|
|
words.forEach(w => {
|
|
|
|
words.forEach(w => {
|
|
|
@@ -478,16 +904,17 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
// Calculate average word height to set vertical tolerance
|
|
|
|
// Calculate average word height to set vertical tolerance
|
|
|
|
const heights = validWords.map(w => w.bbox.y1 - w.bbox.y0);
|
|
|
|
const heights = validWords.map(w => w.bbox.y1 - w.bbox.y0);
|
|
|
|
const avgHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
|
|
|
|
const avgHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
|
|
|
|
const verticalTolerance = avgHeight * 0.85;
|
|
|
|
const verticalTolerance = avgHeight * 1.0;
|
|
|
|
console.log(`[OCR-Capture] Altezza media carattere: ${avgHeight.toFixed(1)}px, tolleranza verticale: ${verticalTolerance.toFixed(1)}px`);
|
|
|
|
console.log(`[OCR-Capture] Altezza media carattere: ${avgHeight.toFixed(1)}px, tolleranza verticale: ${verticalTolerance.toFixed(1)}px`);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Group words into horizontal lines
|
|
|
|
// 2. Group words into horizontal lines
|
|
|
|
const lines: any[][] = [];
|
|
|
|
const lines: any[][] = [];
|
|
|
|
validWords.forEach(word => {
|
|
|
|
validWords.forEach(word => {
|
|
|
|
|
|
|
|
const wordYCenter = (word.bbox.y0 + word.bbox.y1) / 2;
|
|
|
|
let added = false;
|
|
|
|
let added = false;
|
|
|
|
for (const line of lines) {
|
|
|
|
for (const line of lines) {
|
|
|
|
const avgLineY0 = line.reduce((sum, w) => sum + w.bbox.y0, 0) / line.length;
|
|
|
|
const avgLineYCenter = line.reduce((sum, w) => sum + (w.bbox.y0 + w.bbox.y1) / 2, 0) / line.length;
|
|
|
|
if (Math.abs(word.bbox.y0 - avgLineY0) < verticalTolerance) {
|
|
|
|
if (Math.abs(wordYCenter - avgLineYCenter) < verticalTolerance) {
|
|
|
|
line.push(word);
|
|
|
|
line.push(word);
|
|
|
|
added = true;
|
|
|
|
added = true;
|
|
|
|
break;
|
|
|
|
break;
|
|
|
@@ -509,32 +936,14 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Classify lines as Chords vs. Text
|
|
|
|
// 3. Classify lines as Chords vs. Text
|
|
|
|
const chordRegex = /^(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?)?$/i;
|
|
|
|
const chordRegex = this.getChordRegex(isItalian);
|
|
|
|
const isChordWord = (text: string): boolean => {
|
|
|
|
|
|
|
|
if (!text || text.length === 0) return false;
|
|
|
|
|
|
|
|
const lower = text.toLowerCase();
|
|
|
|
|
|
|
|
// Skip common valid words written purely in lowercase
|
|
|
|
|
|
|
|
if (text[0] === lower[0] && ['la', 'mi', 're', 'do', 'si', 'fa', 'e'].includes(lower)) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let clean = text.toUpperCase().replace(/\s+/g, '');
|
|
|
|
|
|
|
|
clean = clean.replace(/\((.*?)\)/g, '/$1');
|
|
|
|
|
|
|
|
clean = clean.replace(/[\.\,]$/g, '');
|
|
|
|
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
|
|
|
|
if (clean.includes('_')) {
|
|
|
|
|
|
|
|
const parts = clean.split('_').filter(p => p.length > 0);
|
|
|
|
|
|
|
|
if (parts.length === 0) return false;
|
|
|
|
|
|
|
|
return parts.every(p => chordRegex.test(p));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return chordRegex.test(clean);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const classifiedLines = lines.map(line => {
|
|
|
|
const classifiedLines = lines.map(line => {
|
|
|
|
let chordCount = 0;
|
|
|
|
let chordCount = 0;
|
|
|
|
let hasLongNonChord = false;
|
|
|
|
let hasLongNonChord = false;
|
|
|
|
|
|
|
|
|
|
|
|
line.forEach(w => {
|
|
|
|
line.forEach(w => {
|
|
|
|
if (isChordWord(w.text)) {
|
|
|
|
if (this.isChordWord(w.text, isItalian)) {
|
|
|
|
chordCount++;
|
|
|
|
chordCount++;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const clean = w.text.replace(/[.,:;!\?]/g, '').trim();
|
|
|
|
const clean = w.text.replace(/[.,:;!\?]/g, '').trim();
|
|
|
@@ -574,7 +983,7 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
const nextText = next ? next.words.map(w => w.text).join(' ') : '';
|
|
|
|
const nextText = next ? next.words.map(w => w.text).join(' ') : '';
|
|
|
|
if (next && !next.isChords && !this.isLabelLine(nextText)) {
|
|
|
|
if (next && !next.isChords && !this.isLabelLine(nextText)) {
|
|
|
|
// Merge spatially!
|
|
|
|
// Merge spatially!
|
|
|
|
const merged = this.mergeChordsAndLyrics(current.words, next.words);
|
|
|
|
const merged = this.mergeChordsAndLyrics(current.words, next.words, isItalian);
|
|
|
|
const lineText = next.words.map(w => w.text).join(' ');
|
|
|
|
const lineText = next.words.map(w => w.text).join(' ');
|
|
|
|
const isChorus = chorusStartRegex.test(lineText);
|
|
|
|
const isChorus = chorusStartRegex.test(lineText);
|
|
|
|
|
|
|
|
|
|
|
@@ -590,7 +999,7 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
i++; // Skip next line because we consumed it!
|
|
|
|
i++; // Skip next line because we consumed it!
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
// Chord line but no text below it: just wrap and print
|
|
|
|
// Chord line but no text below it: just wrap and print
|
|
|
|
const multiChordRegex = /((?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?(?:MIN|MAJ|AUG|DIM|M|-)?(?:7|9|11|13)?(?:\/(?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?)?)/gi;
|
|
|
|
const multiChordRegex = this.getMultiChordRegex(isItalian);
|
|
|
|
const expandedChords: string[] = [];
|
|
|
|
const expandedChords: string[] = [];
|
|
|
|
current.words.forEach(w => {
|
|
|
|
current.words.forEach(w => {
|
|
|
|
const parts = w.text.split(/(_)/);
|
|
|
|
const parts = w.text.split(/(_)/);
|
|
|
@@ -603,13 +1012,13 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
cleanText = cleanText.replace(/[\.\,]$/g, '');
|
|
|
|
cleanText = cleanText.replace(/[\.\,]$/g, '');
|
|
|
|
cleanText = this.sanitizeOcrChord(cleanText);
|
|
|
|
cleanText = this.sanitizeOcrChord(cleanText);
|
|
|
|
|
|
|
|
|
|
|
|
if (chordRegex.test(cleanText)) {
|
|
|
|
if (this.isChordWord(cleanText, isItalian)) {
|
|
|
|
return `[${this.convertEnglishChordToItalian(cleanText)}]`;
|
|
|
|
return `[${this.convertEnglishChordToItalian(cleanText, isItalian)}]`;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const matches = [...cleanText.matchAll(multiChordRegex)];
|
|
|
|
const matches = [...cleanText.matchAll(multiChordRegex)];
|
|
|
|
const fullMatchStr = matches.map(m => m[0]).join('');
|
|
|
|
const fullMatchStr = matches.map(m => m[0]).join('');
|
|
|
|
if (matches.length > 0 && fullMatchStr === cleanText) {
|
|
|
|
if (matches.length > 0 && fullMatchStr === cleanText) {
|
|
|
|
return matches.map((m: any) => `[${this.convertEnglishChordToItalian(m[0].toUpperCase())}]`).join(' ');
|
|
|
|
return matches.map((m: any) => `[${this.convertEnglishChordToItalian(m[0].toUpperCase(), isItalian)}]`).join(' ');
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
return part;
|
|
|
|
return part;
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -634,7 +1043,7 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
inVerse = true;
|
|
|
|
inVerse = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
processedLines.push(this.wrapChords(lineText, chordRegex));
|
|
|
|
processedLines.push(this.wrapChords(lineText, chordRegex, isItalian));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If we see a large vertical gap, close open blocks
|
|
|
|
// If we see a large vertical gap, close open blocks
|
|
|
@@ -655,7 +1064,7 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
return processedLines.join('\n');
|
|
|
|
return processedLines.join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mergeChordsAndLyrics(chordWords: any[], textWords: any[]): string {
|
|
|
|
mergeChordsAndLyrics(chordWords: any[], textWords: any[], isItalian: boolean = false): string {
|
|
|
|
// Pre-process chordWords to merge fragmented bass notes like 'Re', '(f', 'fa#)'
|
|
|
|
// Pre-process chordWords to merge fragmented bass notes like 'Re', '(f', 'fa#)'
|
|
|
|
let mergedChordWords: any[] = [];
|
|
|
|
let mergedChordWords: any[] = [];
|
|
|
|
for (let i = 0; i < chordWords.length; i++) {
|
|
|
|
for (let i = 0; i < chordWords.length; i++) {
|
|
|
@@ -711,13 +1120,16 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
if (!part.trim()) return part;
|
|
|
|
if (!part.trim()) return part;
|
|
|
|
let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
clean = this.sanitizeOcrChord(clean);
|
|
|
|
return `[${this.convertEnglishChordToItalian(clean)}]`;
|
|
|
|
if (this.isChordWord(part, isItalian)) {
|
|
|
|
|
|
|
|
return `[${this.convertEnglishChordToItalian(clean, isItalian)}]`;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return part;
|
|
|
|
|
|
|
|
}
|
|
|
|
}).join('');
|
|
|
|
}).join('');
|
|
|
|
}).join(' ');
|
|
|
|
}).join(' ');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const chordRegex = /^(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?)?$/i;
|
|
|
|
const multiChordRegex = this.getMultiChordRegex(isItalian);
|
|
|
|
const multiChordRegex = /((?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?(?:MIN|MAJ|AUG|DIM|M|-)?(?:7|9|11|13)?(?:\/(?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?)?)/gi;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const expandedChordWords: any[] = [];
|
|
|
|
const expandedChordWords: any[] = [];
|
|
|
|
mergedChordWords.forEach(chord => {
|
|
|
|
mergedChordWords.forEach(chord => {
|
|
|
@@ -822,7 +1234,11 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
|
|
|
|
|
|
|
|
let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
|
|
|
|
let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
|
|
|
|
cleanChord = this.sanitizeOcrChord(cleanChord);
|
|
|
|
cleanChord = this.sanitizeOcrChord(cleanChord);
|
|
|
|
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord)}]`;
|
|
|
|
if (this.isChordWord(chord.text, isItalian)) {
|
|
|
|
|
|
|
|
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord, isItalian)}]`;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
wordResult += wordText.substring(lastCharIndex, charIndex);
|
|
|
|
|
|
|
|
}
|
|
|
|
lastCharIndex = charIndex;
|
|
|
|
lastCharIndex = charIndex;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
@@ -851,7 +1267,12 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
smartProcessOCR(text: string): string {
|
|
|
|
smartProcessOCR(text: string): string {
|
|
|
|
let lines = text.split('\n');
|
|
|
|
let lines = text.split('\n');
|
|
|
|
let processedLines: string[] = [];
|
|
|
|
let processedLines: string[] = [];
|
|
|
|
const chordRegex = /\b(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|b)?(m|-|min|maj|aug|dim)?(7|9|11|13)?\b/gi;
|
|
|
|
|
|
|
|
|
|
|
|
const words = text.split(/\s+/).map(t => ({ text: t }));
|
|
|
|
|
|
|
|
const isItalian = this.isItalianNotation(words);
|
|
|
|
|
|
|
|
const chordRegex = isItalian
|
|
|
|
|
|
|
|
? /(DO|RE|MI|FA|SOL|LA|SI)(#|b)?(m|-|min|maj|aug|dim)?(7|9|11|13)?/i
|
|
|
|
|
|
|
|
: /(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|b)?(m|-|min|maj|aug|dim)?(7|9|11|13)?/i;
|
|
|
|
let inChorus = false;
|
|
|
|
let inChorus = false;
|
|
|
|
let inVerse = false;
|
|
|
|
let inVerse = false;
|
|
|
|
|
|
|
|
|
|
|
@@ -869,13 +1290,13 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; }
|
|
|
|
if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; }
|
|
|
|
processedLines.push('{start_chorus}');
|
|
|
|
processedLines.push('{start_chorus}');
|
|
|
|
inChorus = true;
|
|
|
|
inChorus = true;
|
|
|
|
if (trimmed.length > 12) processedLines.push(this.wrapChords(trimmed, chordRegex));
|
|
|
|
if (trimmed.length > 12) processedLines.push(this.wrapChords(trimmed, chordRegex, isItalian));
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
if (!inVerse && !inChorus && trimmed.length > 5) {
|
|
|
|
if (!inVerse && !inChorus && trimmed.length > 5) {
|
|
|
|
processedLines.push('{start_verse}');
|
|
|
|
processedLines.push('{start_verse}');
|
|
|
|
inVerse = true;
|
|
|
|
inVerse = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
processedLines.push(this.wrapChords(trimmed, chordRegex));
|
|
|
|
processedLines.push(this.wrapChords(trimmed, chordRegex, isItalian));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
@@ -884,21 +1305,24 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
return processedLines.join('\n');
|
|
|
|
return processedLines.join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private wrapChords(line: string, regex: RegExp): string {
|
|
|
|
private wrapChords(line: string, regex: RegExp, isItalian: boolean): string {
|
|
|
|
const globalRegex = new RegExp(regex.source.replace(/^\^/, '').replace(/\$$/, ''), 'gi');
|
|
|
|
let source = regex.source.replace(/^\^/, '').replace(/\$$/, '');
|
|
|
|
|
|
|
|
if (!source.startsWith('\\b')) source = '\\b' + source;
|
|
|
|
|
|
|
|
if (!source.endsWith('\\b')) source = source + '\\b';
|
|
|
|
|
|
|
|
const globalRegex = new RegExp(source, 'gi');
|
|
|
|
const parts = line.split(/(_)/);
|
|
|
|
const parts = line.split(/(_)/);
|
|
|
|
return parts.map((part: string) => {
|
|
|
|
return parts.map((part: string) => {
|
|
|
|
if (part === '_') return ' _ ';
|
|
|
|
if (part === '_') return ' _ ';
|
|
|
|
return part.replace(globalRegex, (match) => {
|
|
|
|
return part.replace(globalRegex, (match) => {
|
|
|
|
const lower = match.toLowerCase();
|
|
|
|
const lower = match.toLowerCase();
|
|
|
|
if (match === 'a' || match === 'e' || match === 'o' || match === 'i') {
|
|
|
|
if (['a', 'e', 'o', 'i'].includes(lower)) {
|
|
|
|
return match;
|
|
|
|
return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (['la', 'mi', 're', 'do', 'si', 'fa', 'sol'].includes(lower) && match === lower) {
|
|
|
|
if (['la', 'mi', 're', 'do', 'si', 'fa', 'sol'].includes(lower)) {
|
|
|
|
return match;
|
|
|
|
return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const sanitized = this.sanitizeOcrChord(match.toUpperCase());
|
|
|
|
const sanitized = this.sanitizeOcrChord(match.toUpperCase());
|
|
|
|
return `[${this.convertEnglishChordToItalian(sanitized)}]`;
|
|
|
|
return `[${this.convertEnglishChordToItalian(sanitized, isItalian)}]`;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}).join('');
|
|
|
|
}).join('');
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -908,6 +1332,15 @@ export class ProposeCantoPage implements OnInit {
|
|
|
|
async saveToMyCanti() {
|
|
|
|
async saveToMyCanti() {
|
|
|
|
if (!this.title || !this.content) return;
|
|
|
|
if (!this.title || !this.content) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Automatically convert any [LAM]/[LAN], [REM]/[REN] etc. to [LAm], [REm] in content before saving
|
|
|
|
|
|
|
|
this.content = this.content.replace(/\[(DO|RE|MI|FA|SOL|LA|SI)(#|b|♭)?[mMnN](\d*)\]/g, (match, p1, p2, p3) => {
|
|
|
|
|
|
|
|
return `[${p1}${p2 || ''}m${p3 || ''}]`;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await this.proceedSaveToMyCanti();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async proceedSaveToMyCanti() {
|
|
|
|
// Combine lit and tematico for id_momenti
|
|
|
|
// Combine lit and tematico for id_momenti
|
|
|
|
const id_momenti = [...this.selectedLiturgico, ...this.selectedTematico];
|
|
|
|
const id_momenti = [...this.selectedLiturgico, ...this.selectedTematico];
|
|
|
|
|
|
|
|
|
|
|
|