import React, { useState } from 'react'; import { Music, RefreshCw, Plus, Trash2, Copy, Check } from 'lucide-react'; const SongwritingTool = () => { const [singleLine, setSingleLine] = useState({ syllables: 8, text: '' }); const [couplet, setCouplet] = useState({ line1Syllables: 8, line2Syllables: 8, line1: '', line2: '', makeRhyme: false }); const [quatrain, setQuatrain] = useState({ syllablePattern: [8, 8, 8, 8], lines: ['', '', '', ''], rhymeScheme: 'ABAB', makeRhyme: false }); const [copiedStates, setCopiedStates] = useState({}); const [rhymeCache, setRhymeCache] = useState({}); const [isGenerating, setIsGenerating] = useState(false); // Expanded word banks const wordBanks = { emotions: [ 'love', 'heart', 'soul', 'dreams', 'hope', 'pain', 'joy', 'fear', 'light', 'dark', 'fire', 'stars', 'passion', 'desire', 'longing', 'yearning', 'bliss', 'ecstasy', 'rapture', 'euphoria', 'melancholy', 'sorrow', 'grief', 'anguish', 'despair', 'rage', 'fury', 'wrath', 'serenity', 'peace', 'tranquil', 'calm', 'serene', 'gentle', 'tender', 'fierce', 'wild', 'savage', 'untamed', 'free', 'bound', 'trapped', 'liberated', 'elated', 'dejected', 'forlorn', 'abandoned', 'cherished', 'beloved' ], nature: [ 'sky', 'ocean', 'mountain', 'river', 'moon', 'sun', 'wind', 'rain', 'flower', 'tree', 'storm', 'dawn', 'sunset', 'sunrise', 'twilight', 'dusk', 'midnight', 'starlight', 'moonbeam', 'sunbeam', 'rainbow', 'thunder', 'lightning', 'hurricane', 'tornado', 'blizzard', 'avalanche', 'earthquake', 'volcano', 'forest', 'meadow', 'valley', 'canyon', 'cliff', 'waterfall', 'cascade', 'rapids', 'brook', 'creek', 'stream', 'lake', 'pond', 'marsh', 'swamp', 'desert', 'oasis', 'mirage', 'glacier', 'iceberg' ], actions: [ 'run', 'fly', 'dance', 'sing', 'cry', 'laugh', 'fight', 'hold', 'break', 'shine', 'fall', 'rise', 'soar', 'glide', 'float', 'drift', 'swim', 'dive', 'climb', 'crawl', 'leap', 'bound', 'skip', 'stumble', 'tumble', 'roll', 'slide', 'slip', 'grip', 'grasp', 'clutch', 'release', 'surrender', 'embrace', 'caress', 'stroke', 'touch', 'feel', 'sense', 'perceive', 'witness', 'observe', 'watch' ], descriptive: [ 'ancient', 'eternal', 'infinite', 'boundless', 'endless', 'timeless', 'ageless', 'immortal', 'mortal', 'fragile', 'delicate', 'tender', 'soft', 'smooth', 'rough', 'coarse', 'jagged', 'sharp', 'blunt', 'pointed', 'curved', 'straight', 'crooked', 'twisted', 'bent', 'broken', 'whole', 'complete', 'perfect', 'flawed', 'imperfect', 'beautiful', 'gorgeous', 'stunning', 'magnificent', 'splendid', 'glorious', 'radiant', 'brilliant', 'dazzling', 'sparkling' ], abstract: [ 'freedom', 'liberty', 'justice', 'truth', 'wisdom', 'knowledge', 'understanding', 'awareness', 'consciousness', 'reality', 'existence', 'being', 'nothingness', 'void', 'emptiness', 'fullness', 'completeness', 'wholeness', 'unity', 'harmony', 'discord', 'chaos', 'order', 'balance', 'equilibrium', 'stability', 'change', 'transformation', 'evolution', 'growth' ], connectors: [ 'and', 'but', 'when', 'where', 'through', 'beyond', 'within', 'beneath', 'above', 'until', 'while', 'like', 'across', 'along', 'around', 'between', 'among', 'inside', 'outside', 'beside', 'behind', 'before' ], endings: [ 'away', 'tonight', 'forever', 'alone', 'again', 'somehow', 'maybe', 'always', 'never', 'together', 'apart', 'aside', 'beyond', 'behind', 'before', 'above', 'below', 'around', 'between', 'today' ] }; // Improved syllable counter with better accuracy const countSyllables = (word) => { if (!word) return 0; // Handle common multi-syllable patterns const syllableMap = { 'the': 1, 'a': 1, 'an': 1, 'and': 1, 'or': 1, 'but': 1, 'in': 1, 'on': 1, 'at': 1, 'to': 1, 'for': 1, 'of': 1, 'with': 1, 'by': 1, 'through': 1, 'around': 2, 'about': 2, 'above': 2, 'below': 2, 'behind': 2, 'before': 2, 'between': 2, 'beautiful': 3, 'wonderful': 3, 'powerful': 3, 'terrible': 3, 'horrible': 3, 'incredible': 4, 'every': 3, 'everything': 3, 'everyone': 3, 'everywhere': 3, 'forever': 3, 'however': 3, 'ocean': 2, 'mountain': 2, 'forest': 2, 'river': 2, 'flower': 2, 'power': 2, 'tower': 2, 'fire': 2, 'desire': 3, 'inspire': 3, 'require': 3, 'entire': 3, 'retire': 3, 'love': 1, 'heart': 1, 'soul': 1, 'dream': 1, 'hope': 1, 'pain': 1, 'joy': 1, 'fear': 1 }; word = word.toLowerCase().replace(/[^\w]/g, ''); if (syllableMap[word]) return syllableMap[word]; if (word.length <= 3) return 1; // Remove silent endings word = word.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, ''); word = word.replace(/^y/, ''); // Count vowel groups const matches = word.match(/[aeiouy]{1,2}/g); return matches ? Math.max(1, matches.length) : 1; }; const countLineSyllables = (line) => { if (!line.trim()) return 0; return line.split(/\s+/).reduce((total, word) => { const cleanWord = word.replace(/[^\w]/g, ''); return total + countSyllables(cleanWord); }, 0); }; // Generate a random line with better word selection and flow const generateLine = (targetSyllables, avoidWords = [], endWord = null) => { const getAllWords = () => [...Object.values(wordBanks).flat()]; const getRandomFromCategory = (category) => category[Math.floor(Math.random() * category.length)]; const line = []; let currentSyllables = 0; // If we have an end word, account for its syllables const endWordSyllables = endWord ? countSyllables(endWord) : 0; const targetForGeneration = endWord ? targetSyllables - endWordSyllables : targetSyllables; while (currentSyllables < targetForGeneration) { const remaining = targetForGeneration - currentSyllables; let word; // Smart word selection based on position and remaining syllables if (line.length === 0) { // First word - prefer descriptive or emotional words const categoryChoice = Math.random(); if (categoryChoice < 0.3) word = getRandomFromCategory(wordBanks.descriptive); else if (categoryChoice < 0.6) word = getRandomFromCategory(wordBanks.emotions); else word = getRandomFromCategory(wordBanks.nature); } else if (remaining <= 2) { // Near the end - prefer endings or short words const allWords = getAllWords(); const suitableWords = allWords.filter(w => countSyllables(w) <= remaining && !avoidWords.includes(w) && !line.includes(w) ); word = suitableWords.length > 0 ? suitableWords[Math.floor(Math.random() * suitableWords.length)] : 'love'; } else { // Middle words - mix of categories with connectors const categoryChoice = Math.random(); if (categoryChoice < 0.2) word = getRandomFromCategory(wordBanks.connectors); else if (categoryChoice < 0.35) word = getRandomFromCategory(wordBanks.actions); else if (categoryChoice < 0.5) word = getRandomFromCategory(wordBanks.nature); else if (categoryChoice < 0.65) word = getRandomFromCategory(wordBanks.emotions); else if (categoryChoice < 0.8) word = getRandomFromCategory(wordBanks.descriptive); else word = getRandomFromCategory(wordBanks.abstract); } const syllableCount = countSyllables(word); if (currentSyllables + syllableCount <= targetForGeneration && !avoidWords.includes(word) && !line.includes(word)) { line.push(word); currentSyllables += syllableCount; avoidWords.push(word); } else if (remaining === 1) { // Force completion with a single syllable word const singleSyllableWords = getAllWords().filter(w => countSyllables(w) === 1 && !avoidWords.includes(w) && !line.includes(w) ); if (singleSyllableWords.length > 0) { word = singleSyllableWords[Math.floor(Math.random() * singleSyllableWords.length)]; line.push(word); currentSyllables += 1; } break; } // Prevent infinite loops if (line.length > 10) break; } // Add end word if specified if (endWord && line.length > 0) { line.push(endWord); } else if (endWord && line.length === 0) { line.push(endWord); } // Capitalize first word if (line.length > 0) { line[0] = line[0].charAt(0).toUpperCase() + line[0].slice(1); } return line.join(' '); }; // Consolidated rhyme API function with better error handling const getRhymes = async (word, useCache = true) => { const cleanWord = word.split(' ').pop().replace(/[^\w]/g, '').toLowerCase(); if (!cleanWord) return []; // Check cache first if (useCache && rhymeCache[cleanWord]) { console.log(`Using cached rhymes for "${cleanWord}"`); return rhymeCache[cleanWord]; } try { console.log(`Fetching rhymes for "${cleanWord}"`); // Try perfect rhymes first const perfectResponse = await fetch(`https://api.datamuse.com/words?rel_rhy=${encodeURIComponent(cleanWord)}&max=50`); if (perfectResponse.ok) { const perfectRhymes = await perfectResponse.json(); const perfectWords = perfectRhymes .map(item => item.word) .filter(w => w !== cleanWord && w.length > 0 && /^[a-zA-Z]+$/.test(w)) .slice(0, 30); if (perfectWords.length > 0) { // Cache and return perfect rhymes if (useCache) setRhymeCache(prev => ({ ...prev, [cleanWord]: perfectWords })); console.log(`Found ${perfectWords.length} perfect rhymes for "${cleanWord}"`); return perfectWords; } } // Fallback to near rhymes if no perfect rhymes const nearResponse = await fetch(`https://api.datamuse.com/words?rel_nry=${encodeURIComponent(cleanWord)}&max=30`); if (nearResponse.ok) { const nearRhymes = await nearResponse.json(); const nearWords = nearRhymes .map(item => item.word) .filter(w => w !== cleanWord && w.length > 0 && /^[a-zA-Z]+$/.test(w)) .slice(0, 20); if (useCache) setRhymeCache(prev => ({ ...prev, [cleanWord]: nearWords })); console.log(`Found ${nearWords.length} near rhymes for "${cleanWord}"`); return nearWords; } } catch (error) { console.error('API error fetching rhymes for', cleanWord, ':', error); } // Ultimate fallback - return empty array console.log(`No rhymes found for "${cleanWord}"`); return []; }; // Improved rhyme word selection with syllable constraints const selectBestRhyme = (rhymes, targetSyllables, maxOptions = 10) => { if (!rhymes || rhymes.length === 0) return null; // First preference: rhymes that fit exactly in syllable count const perfectFit = rhymes.filter(rhyme => countSyllables(rhyme) <= targetSyllables); if (perfectFit.length > 0) { const optionsToConsider = perfectFit.slice(0, maxOptions); return optionsToConsider[Math.floor(Math.random() * optionsToConsider.length)]; } // Second preference: any rhyme from the first few options const fallbackOptions = rhymes.slice(0, Math.min(maxOptions, rhymes.length)); return fallbackOptions[Math.floor(Math.random() * fallbackOptions.length)]; }; // Fallback rhyme generation for when API fails const generateFallbackRhyme = (word) => { const commonRhymes = { 'ight': ['night', 'light', 'bright', 'sight', 'flight', 'right'], 'eart': ['heart', 'part', 'start', 'art', 'smart'], 'ove': ['love', 'above', 'dove'], 'ay': ['way', 'day', 'stay', 'play', 'say'], 'ime': ['time', 'rhyme', 'climb', 'prime'], 'ound': ['sound', 'found', 'ground', 'around'], 'ire': ['fire', 'desire', 'wire', 'higher'], 'ream': ['dream', 'stream', 'beam', 'gleam'] }; const lastWord = word.split(' ').pop().toLowerCase(); for (const [ending, rhymes] of Object.entries(commonRhymes)) { if (lastWord.endsWith(ending.slice(1))) { const availableRhymes = rhymes.filter(r => r !== lastWord); if (availableRhymes.length > 0) { return availableRhymes[Math.floor(Math.random() * availableRhymes.length)]; } } } // Return a word from endings category as last resort return wordBanks.endings[Math.floor(Math.random() * wordBanks.endings.length)]; }; // Main generation functions const handleSingleLineGenerate = () => { const generated = generateLine(singleLine.syllables); setSingleLine(prev => ({ ...prev, text: generated })); }; const handleCoupletGenerate = async () => { if (!couplet.makeRhyme) { // Simple generation without rhyming const line1 = generateLine(couplet.line1Syllables); const line2 = generateLine(couplet.line2Syllables); setCouplet(prev => ({ ...prev, line1, line2 })); return; } setIsGenerating(true); try { // Generate line 1 const line1 = generateLine(couplet.line1Syllables); // Get rhymes for line 1 const rhymes = await getRhymes(line1); let line2; if (rhymes.length > 0) { const selectedRhyme = selectBestRhyme(rhymes, couplet.line2Syllables); if (selectedRhyme) { line2 = generateLine(couplet.line2Syllables, [line1], selectedRhyme); } else { line2 = generateLine(couplet.line2Syllables); } } else { // Fallback rhyming const fallbackRhyme = generateFallbackRhyme(line1); line2 = generateLine(couplet.line2Syllables, [line1], fallbackRhyme); } setCouplet(prev => ({ ...prev, line1, line2 })); } catch (error) { console.error('Error generating couplet:', error); // Generate without rhyming as fallback const line1 = generateLine(couplet.line1Syllables); const line2 = generateLine(couplet.line2Syllables); setCouplet(prev => ({ ...prev, line1, line2 })); } finally { setIsGenerating(false); } }; const handleQuatrainGenerate = async () => { if (!quatrain.makeRhyme) { // Simple generation without rhyming const newLines = quatrain.syllablePattern.map(syllables => generateLine(syllables)); setQuatrain(prev => ({ ...prev, lines: newLines })); return; } setIsGenerating(true); const newLines = ['', '', '', '']; try { if (quatrain.rhymeScheme === 'ABAB') { // Generate lines 1 and 3, then find rhymes for lines 2 and 4 // Step 1: Generate line 1 console.log('Generating line 1...'); newLines[0] = generateLine(quatrain.syllablePattern[0]); // Step 2: Generate line 2 console.log('Generating line 2...'); newLines[1] = generateLine(quatrain.syllablePattern[1]); // Step 3: Get rhymes for line 1 and generate line 3 console.log('Getting rhymes for line 1...'); const rhymes1 = await getRhymes(newLines[0]); if (rhymes1.length > 0) { const selectedRhyme1 = selectBestRhyme(rhymes1, quatrain.syllablePattern[2]); if (selectedRhyme1) { newLines[2] = generateLine(quatrain.syllablePattern[2], [newLines[0], newLines[1]], selectedRhyme1); } else { newLines[2] = generateLine(quatrain.syllablePattern[2]); } } else { const fallbackRhyme1 = generateFallbackRhyme(newLines[0]); newLines[2] = generateLine(quatrain.syllablePattern[2], [newLines[0], newLines[1]], fallbackRhyme1); } // Step 4: Get rhymes for line 2 and generate line 4 console.log('Getting rhymes for line 2...'); const rhymes2 = await getRhymes(newLines[1]); if (rhymes2.length > 0) { const selectedRhyme2 = selectBestRhyme(rhymes2, quatrain.syllablePattern[3]); if (selectedRhyme2) { newLines[3] = generateLine(quatrain.syllablePattern[3], [newLines[0], newLines[1], newLines[2]], selectedRhyme2); } else { newLines[3] = generateLine(quatrain.syllablePattern[3]); } } else { const fallbackRhyme2 = generateFallbackRhyme(newLines[1]); newLines[3] = generateLine(quatrain.syllablePattern[3], [newLines[0], newLines[1], newLines[2]], fallbackRhyme2); } } else { // AABB scheme // Generate pairs of rhyming lines // Step 1: Generate line 1 console.log('Generating line 1...'); newLines[0] = generateLine(quatrain.syllablePattern[0]); // Step 2: Get rhymes for line 1 and generate line 2 console.log('Getting rhymes for line 1...'); const rhymes1 = await getRhymes(newLines[0]); if (rhymes1.length > 0) { const selectedRhyme1 = selectBestRhyme(rhymes1, quatrain.syllablePattern[1]); if (selectedRhyme1) { newLines[1] = generateLine(quatrain.syllablePattern[1], [newLines[0]], selectedRhyme1); } else { newLines[1] = generateLine(quatrain.syllablePattern[1]); } } else { const fallbackRhyme1 = generateFallbackRhyme(newLines[0]); newLines[1] = generateLine(quatrain.syllablePattern[1], [newLines[0]], fallbackRhyme1); } // Step 3: Generate line 3 console.log('Generating line 3...'); newLines[2] = generateLine(quatrain.syllablePattern[2]); // Step 4: Get rhymes for line 3 and generate line 4 console.log('Getting rhymes for line 3...'); const rhymes3 = await getRhymes(newLines[2]); if (rhymes3.length > 0) { const selectedRhyme3 = selectBestRhyme(rhymes3, quatrain.syllablePattern[3]); if (selectedRhyme3) { newLines[3] = generateLine(quatrain.syllablePattern[3], [newLines[0], newLines[1], newLines[2]], selectedRhyme3); } else { newLines[3] = generateLine(quatrain.syllablePattern[3]); } } else { const fallbackRhyme3 = generateFallbackRhyme(newLines[2]); newLines[3] = generateLine(quatrain.syllablePattern[3], [newLines[0], newLines[1], newLines[2]], fallbackRhyme3); } } setQuatrain(prev => ({ ...prev, lines: newLines })); } catch (error) { console.error('Error generating quatrain:', error); // Fallback to simple generation const fallbackLines = quatrain.syllablePattern.map(syllables => generateLine(syllables)); setQuatrain(prev => ({ ...prev, lines: fallbackLines })); } finally { setIsGenerating(false); } }; const copyToClipboard = async (text, id) => { try { await navigator.clipboard.writeText(text); setCopiedStates(prev => ({ ...prev, [id]: true })); setTimeout(() => { setCopiedStates(prev => ({ ...prev, [id]: false })); }, 2000); } catch (err) { console.error('Failed to copy text: ', err); } }; return (

Improved Syllabic Songwriting Tool

{/* Single Line Generator */}

Single Line Generator

setSingleLine(prev => ({ ...prev, syllables: parseInt(e.target.value) || 8 }))} className="w-20 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent" />