|
1 | | - |
2 | 1 | import React, { useState, useCallback, useEffect, useRef } from 'react'; |
3 | 2 | import Spinner from './components/Spinner'; |
4 | 3 | import type { ToolProps } from './Layout'; |
5 | 4 | import { resolveSunoUrlToPotentialSongId } from './services/sunoService'; |
6 | 5 | import { fetchSunoClipById } from './services/sunoService'; |
7 | 6 | import { fetchRiffusionSongData, extractRiffusionSongId } from './services/riffusionService'; |
| 7 | +import { countSyllablesInLine } from './utils/lyricUtils'; |
8 | 8 |
|
9 | 9 | // Simplified InputField for this tool |
10 | 10 | const InputField: React.FC<{ |
@@ -111,23 +111,6 @@ const InfoIcon: React.FC<{tooltip: string, className?: string}> = ({tooltip, cla |
111 | 111 | const LinkIcon: React.FC<{ className?: string }> = ({ className = "w-4 h-4" }) => (<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}><path strokeLinecap="round" strokeLinejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg>); |
112 | 112 |
|
113 | 113 |
|
114 | | -// Moved outside the component for stability |
115 | | -const countSyllablesForWord = (word: string): number => { |
116 | | - if (!word) return 0; |
117 | | - word = word.toLowerCase().trim(); |
118 | | - const cleanWord = word.replace(/^[^a-z']+|[^a-z']+$|(?<=\s)[^a-z']+|[^a-z']+(?=\s)/g, '').replace(/[^a-z']/g, ''); |
119 | | - if (cleanWord.length === 0) return 0; |
120 | | - if (['the', 'a', 'i', 'is', 'of', 'to', 'in', 'it', 'on', 'at', 'me', 'my', 'he', 'she', 'we', 'so', 'no', 'go', 'by'].includes(cleanWord)) return 1; |
121 | | - if (cleanWord.length <= 3 && cleanWord.match(/[aeiouy]/)) return 1; |
122 | | - const exceptions: Record<string, number> = { "because": 2, "amazing": 3, "beautiful": 3, "chocolate": 2, "every": 2, "everything": 3, "different": 3, "interesting": 3, "usually": 3, "family": 3, "probably": 3, "finally": 3, "actually": 3, "sometimes": 2, "something": 2, "anything": 3, "especially": 4, "business": 2, "problem": 2, "example": 3, "however": 3, "really": 2, "area": 3, "genuine": 3, "create": 2, "naive": 2, "idea": 3, "evening": 2, "people": 2, "inspire": 2, "desire": 2, "fire": 1, "hour": 1, "choir": 1, "quiet": 2, "science": 2, "video": 3, "audio": 3, "radio": 3, }; |
123 | | - if (exceptions[cleanWord]) return exceptions[cleanWord]; |
124 | | - let syllableCount = 0; const vowels = "aeiouy"; let lastCharWasVowel = false; |
125 | | - for (let i = 0; i < cleanWord.length; i++) { const char = cleanWord[i]; const isVowel = vowels.includes(char); if (isVowel && !lastCharWasVowel) { syllableCount++; } lastCharWasVowel = isVowel; } |
126 | | - if (cleanWord.endsWith('e') && !cleanWord.endsWith('le') && syllableCount > 1) { const charBeforeE = cleanWord.charAt(cleanWord.length - 2); if (charBeforeE && !vowels.includes(charBeforeE)) { syllableCount--; } } |
127 | | - if (syllableCount === 0 && cleanWord.match(/[aeiouy]/)) { syllableCount = 1; } |
128 | | - return Math.max(0, syllableCount); |
129 | | -}; |
130 | | - |
131 | 114 | const structuralKeywordsArray = [ "Verse", "Chorus", "Intro", "Outro", "Bridge", "Pre-Chorus", "Post-Chorus", "Instrumental", "Guitar Solo", "Keyboard Solo", "Drum Solo", "Bass Solo", "Sax Solo", "Trumpet Solo", "Violin Solo", "Cello Solo", "Flute Solo", "Solo", "Hook", "Refrain", "Interlude", "Skit", "Spoken", "Adlib", "Vamp", "Coda", "Pre-Verse", "Post-Verse", "Pre-Bridge", "Post-Bridge", "Breakdown", "Build-up", "Drop", "Section", "Part", "Prelude", "Segway" ]; |
132 | 115 | const structuralMarkerPattern = new RegExp( `^(${structuralKeywordsArray.join('|')})(?:\\s+[A-Za-z0-9#]+)*(?:\\s*x\\d+)?$`, 'i' ); |
133 | 116 |
|
@@ -342,7 +325,7 @@ const LyricProcessorTool: React.FC<ToolProps> = ({ trackLocalEvent }) => { |
342 | 325 | const words = trimmedLine.split(/\s+/); |
343 | 326 | const wordCount = words.filter(Boolean).length; |
344 | 327 | const charCount = trimmedLine.length; |
345 | | - const lineSyllableCount = words.reduce((sum, word) => sum + countSyllablesForWord(word), 0); |
| 328 | + const lineSyllableCount = countSyllablesInLine(trimmedLine); |
346 | 329 | return `${trimmedLine} (${lineSyllableCount} syllables, ${wordCount} words, ${charCount} chars)`; |
347 | 330 | } |
348 | 331 | }); |
@@ -520,7 +503,7 @@ const LyricProcessorTool: React.FC<ToolProps> = ({ trackLocalEvent }) => { |
520 | 503 | </div> |
521 | 504 |
|
522 | 505 | <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-6 border-t border-gray-700"> |
523 | | - <button type="button" onClick={handleCountSyllables} disabled={isLoading || !canProcess} className="w-full flex justify-center items-center py-3 px-4 border border-green-600 rounded-md shadow-sm text-sm font-medium text-green-300 bg-transparent hover:bg-green-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 focus:ring-offset-gray-900 disabled:bg-gray-800 disabled:text-gray-500 disabled:border-gray-700 disabled:cursor-not-allowed transition-colors" aria-live="polite"> {isLoading && currentAction === 'syllables' ? ( <><Spinner size="w-5 h-5 mr-2" color="text-green-300" /> COUNTING...</> ) : ( 'COUNT SYLLABLES PER LINE' )} </button> |
| 506 | + <button type="button" onClick={handleCountSyllables} disabled={isLoading || !canProcess} className="w-full flex justify-center items-center py-3 px-4 border border-green-600 rounded-md shadow-sm text-sm font-medium text-green-300 bg-transparent hover:bg-green-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 focus:ring-offset-gray-900 disabled:bg-gray-800 disabled:text-gray-500 disabled:border-gray-700 disabled:cursor-not-allowed transition-colors" aria-live="polite"> {isLoading && currentAction === 'syllables' ? ( <><Spinner size="w-5 h-5" color="text-green-300" /> COUNTING...</> ) : ( 'COUNT SYLLABLES PER LINE' )} </button> |
524 | 507 | <button type="button" onClick={handleCleanAndFormat} disabled={isLoading && (!lyricsInput.trim() && !creatorName.trim() && !songTitle.trim())} className="w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-black bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-400 focus:ring-offset-gray-900 disabled:bg-gray-700 disabled:text-gray-400 disabled:border-gray-600 disabled:cursor-not-allowed transition-colors" aria-live="polite"> {isLoading && currentAction === 'clean' ? ( <><Spinner size="w-5 h-5 mr-2" color="text-black" /> CLEANING...</> ) : ( 'CLEAN & FORMAT LYRICS' )} </button> |
525 | 508 | </div> |
526 | 509 | </div> |
|
0 commit comments