import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule, ToastController, IonTextarea, PopoverController, NavController, AlertController } from '@ionic/angular'; import { createWorker } from 'tesseract.js'; import { CantiService } from '../../services/canti.service'; import { MyCantiService } from '../../services/my-canti.service'; import { PlaylistService } from '../../services/playlist.service'; import { ThemeService } from '../../services/theme.service'; import { ActivatedRoute, RouterModule, Router } from '@angular/router'; import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser.service'; import { YoutubePlayerService } from '../../services/youtube-player.service'; @Component({ selector: 'app-propose-canto', templateUrl: './propose-canto.page.html', styleUrls: ['./propose-canto.page.scss'], standalone: true, imports: [CommonModule, FormsModule, IonicModule, RouterModule] }) export class ProposeCantoPage implements OnInit, OnDestroy { @ViewChild('nativeTextarea', { static: false }) nativeTextarea!: ElementRef; @ViewChild('cameraInput', { static: false }) cameraInput!: ElementRef; @ViewChild('fileInput', { static: false }) fileInput!: ElementRef; public cantiService = inject(CantiService); private myCantiService = inject(MyCantiService); private playlistService = inject(PlaylistService); private navCtrl = inject(NavController); public themeService = inject(ThemeService); private route = inject(ActivatedRoute); private router = inject(Router); public lyricsParser = inject(LyricsParserService); private alertCtrl = inject(AlertController); public youtubePlayerService = inject(YoutubePlayerService); showChordsPreview: boolean = true; activeTab: string = 'editor'; transposeAmount: number = 0; get highlightedHtml(): string { if (!this.content) return ''; let escaped = this.content .replace(/&/g, '&') .replace(//g, '>'); escaped = escaped.replace(/\[([^\]]+)\]/g, (match, chord) => { const transposed = this.transposeAmount !== 0 ? this.lyricsParser.transposeChord(chord, this.transposeAmount) : chord; return `[${transposed}]`; }); const lines = escaped.split('\n'); let htmlLines: string[] = []; let inChorus = false; let inVerse = false; let inVerseNum = false; for (const line of lines) { const trimmed = line.trim(); let currentInChorus = inChorus; let currentInVerse = inVerse; let currentInVerseNum = inVerseNum; let isTagLine = false; let processedContent = line; if (trimmed === '{start_chorus}' || trimmed === '{soc}') { inChorus = true; currentInChorus = true; isTagLine = true; processedContent = `${line}`; } else if (trimmed === '{end_chorus}' || trimmed === '{eoc}') { inChorus = false; currentInChorus = true; isTagLine = true; processedContent = `${line}`; } else if (trimmed === '{start_verse}' || trimmed === '{sov}') { inVerse = true; currentInVerse = true; isTagLine = true; processedContent = `${line}`; } else if (trimmed === '{end_verse}' || trimmed === '{eov}') { inVerse = false; currentInVerse = true; isTagLine = true; processedContent = `${line}`; } else if (trimmed === '{start_verse_num}') { inVerseNum = true; currentInVerseNum = true; isTagLine = true; processedContent = `${line}`; } else if (trimmed === '{end_verse_num}') { inVerseNum = false; currentInVerseNum = true; isTagLine = true; processedContent = `${line}`; } else if (trimmed.startsWith('{') && trimmed.endsWith('}')) { isTagLine = true; processedContent = `${line}`; } let classes = ['editor-line']; if (currentInChorus) classes.push('chorus'); if (currentInVerse) classes.push('verse'); if (currentInVerseNum) classes.push('verse-num'); if (isTagLine) classes.push('tag-line'); const finalContent = processedContent || '​'; htmlLines.push(`
${finalContent}
`); } return htmlLines.join('\n'); } get parsedSections(): ParsedSection[] { const sections = this.lyricsParser.parseAccordi(this.content); if (this.showChordsPreview && this.transposeAmount !== 0) { return this.lyricsParser.transposeSections(sections, this.transposeAmount); } return sections; } transposeUp() { this.transposeAmount = (this.transposeAmount + 1) > 12 ? -11 : this.transposeAmount + 1; } transposeDown() { this.transposeAmount = (this.transposeAmount - 1) < -12 ? 11 : this.transposeAmount - 1; } setDefaultTonalita() { if (this.transposeAmount === 0 || !this.content) return; const chordRegex = /\[([^\]]+)\]/g; this.content = this.content.replace(chordRegex, (match, chord) => { const transposed = this.lyricsParser.transposeChord(chord, this.transposeAmount); return `[${transposed}]`; }); this.transposeAmount = 0; } isAudioLoaded(): boolean { return !!this.youtubeLink && this.cantiService.getYoutubeId(this.youtubeLink) !== null; } loadedYoutubeLink: string = ''; loadEditorAudio() { if (!this.youtubeLink) return; const videoId = this.cantiService.getYoutubeId(this.youtubeLink); if (videoId) { this.youtubePlayerService.initPlayer(this.editId || 'editing_song', 0, undefined, undefined, this.youtubeLink); this.loadedYoutubeLink = this.youtubeLink; } } togglePlayPause() { const currentId = this.editId || 'editing_song'; const isLoaded = this.youtubePlayerService.currentCantoId() === currentId && this.loadedYoutubeLink === this.youtubeLink; if (!isLoaded && this.youtubeLink) { this.loadEditorAudio(); } else { this.youtubePlayerService.togglePlayPause(); } } onSeek(event: any) { const value = event.detail.value; this.youtubePlayerService.seekTo(value); } formatSeconds(seconds: number): string { if (isNaN(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs < 10 ? '0' : ''}${secs}`; } toggleChordsPreview() { this.showChordsPreview = !this.showChordsPreview; } title: string = ''; author: string = ''; youtubeLink: string = ''; selectedLiturgico: number[] = []; selectedTematico: number[] = []; durata: string = ''; bpm: number | null = null; get sortedLiturgico() { return [...this.cantiService.indiceLiturgico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name)); } get sortedTematico() { return [...this.cantiService.indiceTematico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name)); } editId: string | null = null; private _content: string = ''; get content(): string { return this._content; } set content(val: string) { if (this._content !== val) { this.saveToUndoStack(this._content); this._content = val; } } undoStack: string[] = []; isProcessingOCR: boolean = false; ocrProgress: number = 0; isDraggingOver: boolean = false; get isHighContrast(): boolean { return this.themeService.highContrast(); } groupedChords = [ { root: 'DO', chords: ['DO', 'DO-', 'DO#', 'DO#-', 'DO7', 'DO-7', 'DOmaj7', 'DO4', 'DOdim', 'DOm7'] }, { root: 'RE', chords: ['RE', 'RE-', 'RE#', 'RE#-', 'RE7', 'RE-7', 'REmaj7', 'RE4', 'REdim', 'REm7'] }, { root: 'MI', chords: ['MI', 'MI-', 'MI7', 'MI-7', 'MImaj7', 'MI4', 'MIdim'] }, { root: 'FA', chords: ['FA', 'FA-', 'FA#', 'FA#-', 'FA7', 'FAmaj7', 'FA4', 'FAdim', 'FAm7'] }, { root: 'SOL', chords: ['SOL', 'SOL-', 'SOL#', 'SOL#-', 'SOL7', 'SOLmaj7', 'SOL4', 'SOLdim', 'SOLm7'] }, { root: 'LA', chords: ['LA', 'LA-', 'LA#', 'LA#-', 'LA7', 'LA-7', 'LAmaj7', 'LA4', 'LAdim', 'LAm7'] }, { root: 'SI', chords: ['SI', 'SI-', 'SI7', 'SI-7', 'SImaj7', 'SI4', 'SIdim'] } ]; selectedRootChord: string | null = null; selectRoot(root: string) { if (this.selectedRootChord === root) { this.selectedRootChord = null; } else { this.selectedRootChord = root; } } getVariations(): string[] { if (!this.selectedRootChord) return []; const group = this.groupedChords.find(g => g.root === this.selectedRootChord); return group ? group.chords : []; } commonTags = [ { label: 'Ritornello', start: '{start_chorus}', end: '{end_chorus}' }, { label: 'Strofa', start: '{start_verse}', end: '{end_verse}' }, { label: 'Strofa Num.', start: '{start_verse_num}', end: '{end_verse_num}' }, { label: 'Inciso', start: '{start_bridge}', end: '{end_bridge}' }, { label: 'Intro Accordi', start: '{start_chord}', end: '{end_chord}' }, { label: 'Commento', start: '{c: ', end: '}' }, { label: 'ChordPro Strofa', start: '{sov}', end: '{eov}' }, { label: 'ChordPro Rit.', start: '{soc}', end: '{eoc}' } ]; constructor(private toastController: ToastController, private popoverController: PopoverController) { } ngOnInit() { this.route.queryParams.subscribe(params => { const editId = params['editId']; if (editId) { this.editId = editId; // Find the song in standard canti, personal canti, or remote custom canti list const song = [ ...this.cantiService.canti(), ...this.myCantiService.myCanti(), ...this.playlistService.remoteCustomSongs(), ...this.playlistService.remoteShareCanti() ].find(c => c.id === editId); if (song) { this.title = song.titolo; this.author = song.autore || ''; this.youtubeLink = song.link_youtube || ''; this.content = song.accordi || song.testo || ''; this.durata = song.durata || ''; this.bpm = song.bpm !== undefined ? song.bpm : null; // Pre-populate liturgico and tematico lists const litIds = this.cantiService.indiceLiturgico().map(m => m.id); const temIds = this.cantiService.indiceTematico().map(m => m.id); this.selectedLiturgico = song.id_momenti?.filter((id: number) => litIds.includes(id)) || []; this.selectedTematico = song.id_momenti?.filter((id: number) => temIds.includes(id)) || []; } } }); } ngOnDestroy() { this.youtubePlayerService.stop(); } async insertText(tag: string) { const input = this.nativeTextarea.nativeElement; const start = input.selectionStart || 0; const end = input.selectionEnd || 0; this.content = this.content.substring(0, start) + tag + this.content.substring(end); setTimeout(() => { input.focus(); input.setSelectionRange(start + tag.length, start + tag.length); }, 10); } insertChord(chord: string) { this.insertText(`[${chord}]`); } undo() { if (this.undoStack.length > 0) { const previous = this.undoStack.pop(); if (previous !== undefined) { this._content = previous; } } } private saveToUndoStack(val: string) { if (this.undoStack.length >= 30) { this.undoStack.shift(); } this.undoStack.push(val); } takePhoto() { this.cameraInput.nativeElement.click(); } chooseFile() { this.fileInput.nativeElement.click(); } async deduceChords() { if (!this.content) return; // Salva nello stack degli undo this.saveToUndoStack(this.content); const lines = this.content.split('\n'); interface SectionLine { originalIndex: number; text: string; } interface Section { type: 'verse' | 'chorus' | 'other'; startTag: string | null; endTag: string | null; lines: SectionLine[]; hasChords: boolean; } const sections: Section[] = []; let currentSection: Section = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); const startMatch = trimmed.match(/^\{(start_verse|start_chorus|start_verse_num|sov|soc)\}/); const endMatch = trimmed === '{end_verse}' || trimmed === '{eov}' || trimmed === '{end_chorus}' || trimmed === '{eoc}' || trimmed === '{end_verse_num}'; if (startMatch) { if (currentSection.lines.length > 0 || currentSection.startTag) { sections.push(currentSection); } let type: 'verse' | 'chorus' | 'other' = 'other'; const tag = startMatch[1]; if (tag === 'start_verse' || tag === 'start_verse_num' || tag === 'sov') { type = 'verse'; } else if (tag === 'start_chorus' || tag === 'soc') { type = 'chorus'; } currentSection = { type, startTag: line, endTag: null, lines: [], hasChords: false }; } else if (endMatch) { currentSection.endTag = line; sections.push(currentSection); currentSection = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false }; } else { if (trimmed === '' && !currentSection.startTag) { if (currentSection.lines.length > 0) { sections.push(currentSection); } sections.push({ type: 'other', startTag: null, endTag: null, lines: [{ originalIndex: i, text: '' }], hasChords: false }); currentSection = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false }; } else { const hasChords = /\[[^\]]+\]/.test(line); if (hasChords) { currentSection.hasChords = true; } if (currentSection.lines.length === 0 && !currentSection.startTag && trimmed !== '') { currentSection.type = 'verse'; } const isComment = trimmed.startsWith('{c:') || trimmed.startsWith('{comment:'); if (!isComment) { currentSection.lines.push({ originalIndex: i, text: line }); } } } } if (currentSection.lines.length > 0 || currentSection.startTag) { sections.push(currentSection); } const templateVerse = sections.find(s => s.type === 'verse' && s.hasChords); const templateChorus = sections.find(s => s.type === 'chorus' && s.hasChords); let deducedCount = 0; const getWords = (text: string) => { const words: { text: string; start: number; end: number }[] = []; const regex = /\S+/g; let match; while ((match = regex.exec(text)) !== null) { words.push({ text: match[0], start: match.index, end: match.index + match[0].length }); } return words; }; const alignChords = (templateLine: string, targetLine: string): string => { if (/\[[^\]]+\]/.test(targetLine)) { return targetLine; } const parsedTemplate = this.lyricsParser.parseChordLine(templateLine); const templateClean = parsedTemplate.text; const targetClean = targetLine.replace(/\[[^\]]*\]/g, ''); if (!templateClean.trim() || !targetClean.trim()) { return targetLine; } const chords: { chord: string; charIndex: number }[] = []; let charAcc = 0; parsedTemplate.segments.forEach(seg => { if (seg.chord) { chords.push({ chord: seg.chord, charIndex: charAcc }); } charAcc += seg.text.length; }); if (chords.length === 0) { return targetLine; } const templateWords = getWords(templateClean); const targetWords = getWords(targetClean); const insertions: { chord: string; index: number }[] = []; chords.forEach(c => { const ratio = c.charIndex / Math.max(1, templateClean.length); const wordIdx = templateWords.findIndex(w => Math.abs(w.start - c.charIndex) <= 1); let targetIndex = 0; if (wordIdx !== -1 && templateWords.length > 1 && targetWords.length > 1) { const wRatio = wordIdx / (templateWords.length - 1); const targetWIdx = Math.round(wRatio * (targetWords.length - 1)); targetIndex = targetWords[targetWIdx].start; } else { targetIndex = Math.round(ratio * targetClean.length); } insertions.push({ chord: c.chord, index: targetIndex }); }); insertions.sort((a, b) => b.index - a.index); let result = targetClean; insertions.forEach(ins => { result = result.substring(0, ins.index) + `[${ins.chord}]` + result.substring(ins.index); }); return result; }; const newLines = [...lines]; sections.forEach(sec => { if (!sec.hasChords && sec.type !== 'other') { const template = sec.type === 'verse' ? templateVerse : templateChorus; if (template) { for (let j = 0; j < sec.lines.length; j++) { const targetSecLine = sec.lines[j]; if (j < template.lines.length) { const templateSecLine = template.lines[j]; const aligned = alignChords(templateSecLine.text, targetSecLine.text); if (aligned !== targetSecLine.text) { newLines[targetSecLine.originalIndex] = aligned; deducedCount++; } } } } } }); if (deducedCount > 0) { this.content = newLines.join('\n'); const toast = await this.toastController.create({ message: `Accordi dedotti con successo in ${deducedCount} righe!`, duration: 3000, color: 'success' }); toast.present(); } else { const toast = await this.toastController.create({ message: 'Nessuna strofa compatibile trovata o accordi già presenti.', duration: 3000, color: 'warning' }); toast.present(); } } onDragOver(event: DragEvent) { event.preventDefault(); event.stopPropagation(); // Mostra l'overlay solo se si sta trascinando un file if (event.dataTransfer?.types.includes('Files')) { this.isDraggingOver = true; } } onDragLeave(event: DragEvent) { event.preventDefault(); event.stopPropagation(); this.isDraggingOver = false; } async onDrop(event: DragEvent) { event.preventDefault(); event.stopPropagation(); this.isDraggingOver = false; if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) { const file = event.dataTransfer.files[0]; const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'); if (file.type.indexOf('image') !== -1 || isPdf) { await this.processFile(file); } else { const toast = await this.toastController.create({ message: 'Per favore, trascina un file immagine o PDF valido.', duration: 3000, color: 'warning' }); toast.present(); } } } async onFileSelected(event: any, isCamera: boolean) { const file = event.target.files[0]; if (!file) { console.log('[OCR-Capture] Nessun file selezionato.'); return; } await this.processFile(file); event.target.value = ''; } async onPaste(event: ClipboardEvent) { const items = event.clipboardData?.items; if (!items) return; let hasImage = false; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { hasImage = true; event.preventDefault(); // Prevent pasting the image representation as text const blob = items[i].getAsFile(); if (blob) { const file = new File([blob], 'pasted-image.png', { type: blob.type }); await this.processFile(file); } break; } } if (!hasImage) { const pastedText = event.clipboardData?.getData('text'); if (pastedText && this.looksLikeChordSheetOrSong(pastedText)) { event.preventDefault(); const parsed = this.parsePastedChordSheet(pastedText); this.insertText(parsed); } } } isTextLineChords(line: string, isItalian: boolean): boolean { 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 { 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((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.ocrProgress = 0; try { if (isPdf) { const pageBlobs = await this.convertPdfToImages(file); console.log(`[OCR-Capture] PDF convertito in ${pageBlobs.length} immagini.`); for (let idx = 0; idx < pageBlobs.length; idx++) { const pageBlob = pageBlobs[idx]; 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; } } const toast = await this.toastController.create({ message: 'Scansione PDF completata!', duration: 2000, color: 'success' }); toast.present(); } else { 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) { console.error('[OCR-Capture] Errore durante l\'elaborazione del file:', error); const errorToast = await this.toastController.create({ message: 'Errore durante la scansione del file.', duration: 3000, color: 'danger' }); errorToast.present(); } finally { this.isProcessingOCR = false; this.ocrProgress = 0; } } async resizeImage(file: File): Promise { console.log('[OCR-Capture] Caricamento immagine in memoria...'); return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { console.log(`[OCR-Capture] Immagine caricata in memoria. Dimensioni originali: ${img.width}x${img.height}`); const canvas = document.createElement('canvas'); const maxDim = 1200; let width = img.width; let height = img.height; if (width > maxDim || height > maxDim) { if (width > height) { height = Math.round((height * maxDim) / width); width = maxDim; } else { width = Math.round((width * maxDim) / height); height = maxDim; } } console.log(`[OCR-Capture] Ridimensionamento a: ${width}x${height}`); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(img, 0, 0, width, height); canvas.toBlob(blob => { if (blob) { console.log('[OCR-Capture] Compressione in JPEG completata con successo.'); resolve(blob); } else { reject(new Error('Canvas toBlob failed')); } }, 'image/jpeg', 0.85); } else { reject(new Error('Canvas getContext 2d failed')); } }; img.onerror = (err) => { console.error('[OCR-Capture] Impossibile caricare l\'immagine in memoria:', err); reject(err); }; img.src = URL.createObjectURL(file); }); } async processImageOCR(file: File): Promise { console.log('[OCR-Capture] Inizializzazione Worker Tesseract.js...'); const worker = await createWorker('ita', 1, { logger: m => { if (m.status === 'recognizing text') { this.ocrProgress = m.progress; console.log(`[OCR-Capture] Progresso OCR: ${(m.progress * 100).toFixed(0)}%`); } } }); console.log('[OCR-Capture] Avvio riconoscimento caratteri (OCR) con blocks abilitato...'); const { data } = await worker.recognize(file, {}, { blocks: true }); console.log('[OCR-Capture] Riconoscimento caratteri terminato. Spegnimento worker...'); await worker.terminate(); console.log('[OCR-Capture] Spegnimento worker completato. Estrazione parole...'); // Flatten blocks hierarchy to get a flat words list const words: any[] = []; if (data && (data as any).blocks) { const blocks = (data as any).blocks; blocks.forEach((block: any) => { if (block.paragraphs) { block.paragraphs.forEach((paragraph: any) => { if (paragraph.lines) { paragraph.lines.forEach((line: any) => { if (line.words) { line.words.forEach((word: any) => { words.push(word); }); } }); } }); } }); } console.log(`[OCR-Capture] Parole estratte dal blocco gerarchico: ${words.length}`); return this.parseSongSpatially(words); } sanitizeOcrChord(text: string): string { 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('/')) { return text.split('/').map(part => this.sanitizeOcrChord(part.trim())).join('/'); } let cleaned = text; // Replace D0/d0 with DO/do cleaned = cleaned.replace(/^D0/gi, 'DO'); // Replace S0L/s0l with SOL/sol cleaned = cleaned.replace(/^S0L/gi, 'SOL'); // Clean H/sharp mismatches: cleaned = cleaned.replace(/H#/gi, '#'); cleaned = cleaned.replace(/#H/gi, '#'); cleaned = cleaned.replace(/([CDEFGAB]|DO|RE|MI|FA|SOL|LA|SI)H/gi, '$1#'); // Clean duplicate sharps (e.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; } 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, allowLowercase: boolean = false): boolean { if (!text || text.length === 0) return false; const lower = text.toLowerCase(); // Skip common valid words written purely in lowercase unless allowLowercase is true if (!allowLowercase && 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.includes('/')) { return chord.split('/').map(part => this.convertEnglishChordToItalian(part.trim(), isItalian)).join('/'); } const upper = chord.toUpperCase(); if (upper.startsWith('DO')) { return chord; } if (upper.startsWith('FA')) { if (upper.startsWith('FAUG') || upper.startsWith('FADD') || upper.startsWith('FALT')) { return 'FA' + chord.slice(1); } return chord; } if (upper.startsWith('C')) { return 'DO' + chord.slice(1); } if (upper.startsWith('D')) { return 'RE' + chord.slice(1); } if (upper.startsWith('E')) { return 'MI' + chord.slice(1); } if (upper.startsWith('F')) { return 'FA' + chord.slice(1); } if (upper.startsWith('G')) { return 'SOL' + chord.slice(1); } if (upper.startsWith('A')) { return 'LA' + chord.slice(1); } if (upper.startsWith('B')) { return 'SI' + chord.slice(1); } return chord; } isLabelLine(text: string): boolean { return /^(Intro|Strofa|Rit|Special|Coro|Bridge|RIT|CHORUS|VERSE)/i.test(text.trim()); } parseSongSpatially(words: any[]): string { if (!words || words.length === 0) { console.warn('[OCR-Capture] Nessuna parola ricevuta dall\'OCR.'); return ''; } const isItalian = this.isItalianNotation(words); console.log(`[OCR-Capture] Rilevata notazione italiana: ${isItalian}`); // Preprocess words to split run-together chords like BA const preprocessedWords: any[] = []; words.forEach(w => { if (!w.text) return; const match = w.text.match(/^([ABCDEFG])([ABCDEFG])$/i); if (match && !(match[1].toUpperCase() === 'F' && match[2].toUpperCase() === 'A')) { const charWidth = (w.bbox.x1 - w.bbox.x0) / 2; preprocessedWords.push({ text: match[1], bbox: { ...w.bbox, x1: w.bbox.x0 + charWidth } }); preprocessedWords.push({ text: match[2], bbox: { ...w.bbox, x0: w.bbox.x0 + charWidth } }); } else { preprocessedWords.push(w); } }); words = preprocessedWords; console.log(`[OCR-Capture] Parole totali ricevute dall'OCR: ${words.length}`); const validWords = words.filter(w => w.text && w.text.trim().length > 0); console.log(`[OCR-Capture] Parole valide dopo filtraggio: ${validWords.length}`); if (validWords.length === 0) return ''; // Calculate average word height to set vertical tolerance const heights = validWords.map(w => w.bbox.y1 - w.bbox.y0); const avgHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length; const verticalTolerance = avgHeight * 1.0; console.log(`[OCR-Capture] Altezza media carattere: ${avgHeight.toFixed(1)}px, tolleranza verticale: ${verticalTolerance.toFixed(1)}px`); // 2. Group words into horizontal lines const lines: any[][] = []; validWords.forEach(word => { const wordYCenter = (word.bbox.y0 + word.bbox.y1) / 2; let added = false; for (const line of lines) { const avgLineYCenter = line.reduce((sum, w) => sum + (w.bbox.y0 + w.bbox.y1) / 2, 0) / line.length; if (Math.abs(wordYCenter - avgLineYCenter) < verticalTolerance) { line.push(word); added = true; break; } } if (!added) { lines.push([word]); } }); // Sort words horizontally in each line lines.forEach(line => line.sort((a, b) => a.bbox.x0 - b.bbox.x0)); // Sort all lines vertically by average y0 lines.sort((a, b) => { const avgA = a.reduce((sum, w) => sum + w.bbox.y0, 0) / a.length; const avgB = b.reduce((sum, w) => sum + w.bbox.y0, 0) / b.length; return avgA - avgB; }); // 3. Classify lines as Chords vs. Text const chordRegex = this.getChordRegex(isItalian); const classifiedLines = lines.map(line => { let chordCount = 0; let hasLongNonChord = false; line.forEach(w => { if (this.isChordWord(w.text, isItalian, true)) { chordCount++; } else { const clean = w.text.replace(/[.,:;!\?]/g, '').trim(); if (clean.length > 5) { hasLongNonChord = true; } } }); const ratio = line.length > 0 ? chordCount / line.length : 0; let isChords = false; if (ratio >= 0.4 && line.length <= 10) { if (!hasLongNonChord || ratio >= 0.75) { isChords = true; } } return { words: line, isChords: isChords, yCenter: line.reduce((sum, w) => sum + (w.bbox.y0 + w.bbox.y1)/2, 0) / line.length }; }); // 4. Merge chords and text lines const processedLines: string[] = []; let inChorus = false; let inVerse = false; const chorusStartRegex = /^(R:|Rit\.|Rit|Ritornello|Coro)/i; for (let i = 0; i < classifiedLines.length; i++) { const current = classifiedLines[i]; if (current.isChords) { const next = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null; const nextText = next ? next.words.map(w => w.text).join(' ') : ''; if (next && !next.isChords && !this.isLabelLine(nextText)) { // Merge spatially! const merged = this.mergeChordsAndLyrics(current.words, next.words, isItalian); const lineText = next.words.map(w => w.text).join(' '); const isChorus = chorusStartRegex.test(lineText); if (isChorus) { if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; } if (!inChorus) { processedLines.push('{start_chorus}'); inChorus = true; } } else if (!inChorus && !inVerse && lineText.length > 5) { processedLines.push('{start_verse}'); inVerse = true; } processedLines.push(merged); i++; // Skip next line because we consumed it! } else { // Chord line but no text below it: just wrap and print const multiChordRegex = this.getMultiChordRegex(isItalian); const expandedChords: string[] = []; current.words.forEach(w => { const parts = w.text.split(/(_)/); const processedParts = parts.map((part: string) => { if (part === '_') return ' _ '; if (!part.trim()) return part; let cleanText = part.toUpperCase().replace(/\s+/g, ''); cleanText = cleanText.replace(/\((.*?)\)/g, '/$1'); cleanText = cleanText.replace(/[\.\,]$/g, ''); cleanText = this.sanitizeOcrChord(cleanText); if (this.isChordWord(cleanText, isItalian)) { return `[${this.convertEnglishChordToItalian(cleanText, isItalian)}]`; } else { const matches = [...cleanText.matchAll(multiChordRegex)]; const fullMatchStr = matches.map(m => m[0]).join(''); if (matches.length > 0 && fullMatchStr === cleanText) { return matches.map((m: any) => `[${this.convertEnglishChordToItalian(m[0].toUpperCase(), isItalian)}]`).join(' '); } else { return part; } } }); expandedChords.push(processedParts.join('')); }); const wrapped = expandedChords.join(' '); if (wrapped) { processedLines.push(wrapped); } } } else { const lineText = current.words.map(w => w.text).join(' '); const isChorus = chorusStartRegex.test(lineText); if (isChorus) { if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; } if (!inChorus) { processedLines.push('{start_chorus}'); inChorus = true; } } else if (!inChorus && !inVerse && lineText.length > 5) { processedLines.push('{start_verse}'); inVerse = true; } processedLines.push(this.wrapChords(lineText, chordRegex, isItalian)); } // If we see a large vertical gap, close open blocks const lastProcessedLine = classifiedLines[i]; const currentNext = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null; if (currentNext && lastProcessedLine) { const gap = currentNext.yCenter - lastProcessedLine.yCenter; if (gap > avgHeight * 3.5) { if (inChorus) { processedLines.push('{end_chorus}'); inChorus = false; } if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; } processedLines.push(''); } } } if (inChorus) processedLines.push('{end_chorus}'); if (inVerse) processedLines.push('{end_verse}'); return processedLines.join('\n'); } mergeChordsAndLyrics(chordWords: any[], textWords: any[], isItalian: boolean = false): string { // Pre-process chordWords to merge fragmented bass notes like 'Re', '(f', 'fa#)' let mergedChordWords: any[] = []; for (let i = 0; i < chordWords.length; i++) { let cw = chordWords[i]; if (cw.text.startsWith('(') && mergedChordWords.length > 0) { let prev = mergedChordWords[mergedChordWords.length - 1]; prev.text += cw.text; prev.bbox.x1 = Math.max(prev.bbox.x1, cw.bbox.x1); if (!prev.text.includes(')')) { let j = i + 1; while (j < chordWords.length) { prev.text += chordWords[j].text; prev.bbox.x1 = Math.max(prev.bbox.x1, chordWords[j].bbox.x1); if (chordWords[j].text.includes(')')) { i = j; break; } j++; } } } else { let text = cw.text; let bbox = { ...cw.bbox }; if (text.includes('(') && !text.includes(')')) { let j = i + 1; while (j < chordWords.length) { text += chordWords[j].text; bbox.x1 = Math.max(bbox.x1, chordWords[j].bbox.x1); if (chordWords[j].text.includes(')')) { i = j; break; } j++; } } mergedChordWords.push({ text, bbox }); } } // Sanitize common OCR errors in chords (e.g., 'Re(f fa#)' -> 'Re(fa#)') mergedChordWords.forEach(cw => { cw.text = cw.text.replace(/f\s*fa#/gi, 'fa#'); cw.text = cw.text.replace(/ff/gi, 'f'); cw.text = cw.text.replace(/m\s*mi/gi, 'mi'); cw.text = cw.text.replace(/mm/gi, 'm'); }); if (!textWords || textWords.length === 0) { return mergedChordWords.map(c => { const parts = c.text.split(/(_)/); return parts.map((part: string) => { if (part === '_') return ' _ '; if (!part.trim()) return part; let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, ''); clean = this.sanitizeOcrChord(clean); if (this.isChordWord(part, isItalian, true)) { return `[${this.convertEnglishChordToItalian(clean, isItalian)}]`; } else { return part; } }).join(''); }).join(' '); } const multiChordRegex = this.getMultiChordRegex(isItalian); const expandedChordWords: any[] = []; mergedChordWords.forEach(chord => { const parts = chord.text.split(/(_)/); let currentX = chord.bbox.x0; const totalLen = chord.text.length || 1; const widthPerChar = (chord.bbox.x1 - chord.bbox.x0) / totalLen; parts.forEach((part: string) => { const partLen = part.length; const partWidth = partLen * widthPerChar; const partX0 = currentX; const partX1 = currentX + partWidth; currentX = partX1; if (part === '_' || !part.trim()) { return; } let originalText = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, ''); originalText = this.sanitizeOcrChord(originalText); const matches = [...originalText.matchAll(multiChordRegex)]; if (matches.length === 0) { expandedChordWords.push({ text: originalText, bbox: { ...chord.bbox, x0: partX0, x1: partX1 } }); } else { const fullMatchStr = matches.map(m => m[0]).join(''); if (fullMatchStr === originalText) { const matchCharWidth = (partX1 - partX0) / Math.max(1, originalText.length); matches.forEach(match => { const matchIndex = match.index!; const matchLength = match[0].length; const newX0 = partX0 + matchIndex * matchCharWidth; const newX1 = partX0 + (matchIndex + matchLength) * matchCharWidth; expandedChordWords.push({ text: match[0], bbox: { ...chord.bbox, x0: newX0, x1: newX1 } }); }); } else { expandedChordWords.push({ text: originalText, bbox: { ...chord.bbox, x0: partX0, x1: partX1 } }); } } }); }); const chordAssignments = new Map(); expandedChordWords.forEach(chord => { const chordX = (chord.bbox.x0 + chord.bbox.x1) / 2; let closestWord: any = null; let minDistance = Infinity; textWords.forEach(textWord => { let dist = 0; if (chordX < textWord.bbox.x0) { dist = textWord.bbox.x0 - chordX; } else if (chordX > textWord.bbox.x1) { dist = chordX - textWord.bbox.x1; } if (dist < minDistance) { minDistance = dist; closestWord = textWord; } }); if (closestWord) { if (!chordAssignments.has(closestWord)) { chordAssignments.set(closestWord, []); } chordAssignments.get(closestWord)!.push(chord); } }); let result = ''; textWords.forEach((textWord, index) => { const assignedChords = chordAssignments.get(textWord) || []; assignedChords.sort((a, b) => a.bbox.x0 - b.bbox.x0); const wordText = textWord.text; let charWidth = (textWord.bbox.x1 - textWord.bbox.x0) / Math.max(1, wordText.length); if (charWidth <= 0) charWidth = 6; // safe fallback let lastCharIndex = 0; let wordResult = ''; assignedChords.forEach(chord => { const chordX = (chord.bbox.x0 + chord.bbox.x1) / 2; let charIndex = Math.round((chordX - textWord.bbox.x0) / charWidth); if (charIndex < 0) charIndex = 0; if (charIndex > wordText.length) charIndex = wordText.length; let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase(); cleanChord = this.sanitizeOcrChord(cleanChord); if (this.isChordWord(chord.text, isItalian, true)) { wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord, isItalian)}]`; } else { wordResult += wordText.substring(lastCharIndex, charIndex); } lastCharIndex = charIndex; }); wordResult += wordText.substring(lastCharIndex); result += wordResult; if (index < textWords.length - 1) { result += ' '; } }); let finalResult = result.replace(/\]\[/g, '] ['); finalResult = finalResult.replace(/\]_\[/g, '] _ ['); finalResult = finalResult.replace(/\]_/g, '] _ '); finalResult = finalResult.replace(/_\[/g, ' _ ['); // Normalize any duplicate spaces around underscores: finalResult = finalResult.replace(/\s*_\s*/g, ' _ '); console.log(`[OCR-Debug] Linea generata: ${finalResult}`); return finalResult; } smartProcessOCR(text: string): string { let lines = text.split('\n'); let processedLines: string[] = []; 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 inVerse = false; lines.forEach((line) => { let trimmed = line.trim(); if (!trimmed) { if (inChorus) { processedLines.push('{end_chorus}'); inChorus = false; } if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; } processedLines.push(''); return; } const isChorusMarker = /^(Rit|Ritornello|Chorus|CORO)/i.test(trimmed); if (isChorusMarker && !inChorus) { if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; } processedLines.push('{start_chorus}'); inChorus = true; if (trimmed.length > 12) processedLines.push(this.wrapChords(trimmed, chordRegex, isItalian)); } else { if (!inVerse && !inChorus && trimmed.length > 5) { processedLines.push('{start_verse}'); inVerse = true; } processedLines.push(this.wrapChords(trimmed, chordRegex, isItalian)); } }); if (inChorus) processedLines.push('{end_chorus}'); if (inVerse) processedLines.push('{end_verse}'); return processedLines.join('\n'); } private wrapChords(line: string, regex: RegExp, isItalian: boolean): string { 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(/(_)/); return parts.map((part: string) => { if (part === '_') return ' _ '; return part.replace(globalRegex, (match) => { const lower = match.toLowerCase(); if (['a', 'e', 'o', 'i'].includes(lower)) { return match; } if (['la', 'mi', 're', 'do', 'si', 'fa', 'sol'].includes(lower)) { return match; } const sanitized = this.sanitizeOcrChord(match.toUpperCase()); return `[${this.convertEnglishChordToItalian(sanitized, isItalian)}]`; }); }).join(''); } async saveToMyCanti() { 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 const id_momenti = [...this.selectedLiturgico, ...this.selectedTematico]; const activePlaylistId = this.playlistService.activePlaylistId(); const isRemotePlaylist = activePlaylistId && activePlaylistId.startsWith('remote_'); if (isRemotePlaylist) { // 1. Clone the song (generate a brand new my_... ID) const savedCanto = await this.myCantiService.saveCanto({ titolo: this.title, autore: this.author, link_youtube: this.youtubeLink, testo: this.content, accordi: this.content, id_momenti: id_momenti, durata: this.durata || undefined, bpm: this.bpm !== null && this.bpm !== undefined && !isNaN(Number(this.bpm)) ? Number(this.bpm) : undefined }); // 2. Clone/convert remote playlist to local personal playlist const remotePl = this.playlistService.remotePlaylist(); if (remotePl) { const originalIds = remotePl.ids || []; const updatedIds = originalIds.map((id: string) => id === this.editId ? savedCanto.id : id); const songSettings = { ...(remotePl.songSettings || {}) }; if (this.editId && songSettings[this.editId]) { songSettings[savedCanto.id] = { ...songSettings[this.editId] }; delete songSettings[this.editId]; } const localName = remotePl.name.replace('[Remote] ', ''); // Force save as a new local playlist this.playlistService.activePlaylistId.set(null); await this.playlistService.savePlaylist(localName, updatedIds, songSettings); } // Navigate to the player with the new cloned song ID immediately this.router.navigate(['/player'], { queryParams: { id: savedCanto.id }, replaceUrl: true }); } else { // Standard path const savedCanto = await this.myCantiService.saveCanto({ id: this.editId || undefined, titolo: this.title, autore: this.author, link_youtube: this.youtubeLink, testo: this.content, accordi: this.content, // Save to both fields for compatibility id_momenti: id_momenti, durata: this.durata || undefined, bpm: this.bpm !== null && this.bpm !== undefined && !isNaN(Number(this.bpm)) ? Number(this.bpm) : undefined }); if (this.editId && !this.editId.startsWith('my_') && savedCanto && savedCanto.id) { await this.playlistService.replaceSongIdInPlaylists(this.editId, savedCanto.id); } if (this.editId && this.editId.startsWith('remote_share_')) { await this.playlistService.deleteRemoteShareCanto(this.editId); } if (this.editId && savedCanto && savedCanto.id && this.editId !== savedCanto.id) { this.router.navigate(['/player'], { queryParams: { id: savedCanto.id }, replaceUrl: true }); } else { this.navCtrl.back(); } } } }