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 (
{/* Single Line Generator */}
{/* Couplet Generator */}
{(couplet.line1 || couplet.line2) && (
)}
{/* Quatrain Generator */}
);
};
export default SongwritingTool;
Improved Syllabic Songwriting Tool
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"
/>
Couplet Generator
setCouplet(prev => ({ ...prev, line1Syllables: parseInt(e.target.value) || 8 }))}
className="w-20 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
setCouplet(prev => ({ ...prev, line2Syllables: parseInt(e.target.value) || 8 }))}
className="w-20 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
setCouplet(prev => ({ ...prev, makeRhyme: e.target.checked }))}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
Quatrain Generator
{quatrain.syllablePattern.map((syllables, index) => (
{
const newPattern = [...quatrain.syllablePattern];
newPattern[index] = parseInt(e.target.value) || 8;
setQuatrain(prev => ({ ...prev, syllablePattern: newPattern }));
}}
className="w-16 px-2 py-1 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent text-center"
/>
))}
setQuatrain(prev => ({ ...prev, makeRhyme: e.target.checked }))}
className="w-4 h-4 text-green-600 bg-gray-100 border-gray-300 rounded focus:ring-green-500"
/>
{quatrain.lines.map((line, index) => (
))}
{quatrain.lines.some(line => line.trim()) && (
)}
Tip: Edit any generated lines to perfect your lyrics. The syllable counter updates automatically!
When "Make this rhyme" is enabled, the tool uses the Datamuse API to find real rhymes with improved fallback handling.