tercul-backend/internal/enrich/poetic_analyzer.go
Damir Mukimov 4957117cb6 Initial commit: Tercul Go project with comprehensive architecture
- Core Go application with GraphQL API using gqlgen
- Comprehensive data models for literary works, authors, translations
- Repository pattern with caching layer
- Authentication and authorization system
- Linguistics analysis capabilities with multiple adapters
- Vector search integration with Weaviate
- Docker containerization support
- Python data migration and analysis scripts
- Clean architecture with proper separation of concerns
- Production-ready configuration and middleware
- Proper .gitignore excluding vendor/, database files, and build artifacts
2025-08-13 07:42:32 +02:00

291 lines
6.5 KiB
Go

package enrich
import (
"strings"
"unicode"
)
// PoeticAnalyzer analyzes the poetic structure of text
type PoeticAnalyzer struct{}
// NewPoeticAnalyzer creates a new PoeticAnalyzer
func NewPoeticAnalyzer() *PoeticAnalyzer {
return &PoeticAnalyzer{}
}
// Analyse analyzes the poetic structure of text and returns metrics
func (a *PoeticAnalyzer) Analyse(text Text) (PoeticMetrics, error) {
// This is a simplified implementation
// In a real-world scenario, you would use a more sophisticated approach
content := text.Body
// Split into lines
lines := strings.Split(content, "\n")
// Count non-empty lines
var nonEmptyLines []string
for _, line := range lines {
if strings.TrimSpace(line) != "" {
nonEmptyLines = append(nonEmptyLines, line)
}
}
// Count stanzas (groups of lines separated by blank lines)
stanzaCount := 0
inStanza := false
for _, line := range lines {
if strings.TrimSpace(line) == "" {
if inStanza {
inStanza = false
}
} else {
if !inStanza {
inStanza = true
stanzaCount++
}
}
}
// Ensure at least one stanza if there are lines
if len(nonEmptyLines) > 0 && stanzaCount == 0 {
stanzaCount = 1
}
// Detect rhyme scheme
rhymeScheme := detectRhymeScheme(nonEmptyLines)
// Detect meter type
meterType := detectMeterType(nonEmptyLines)
// Determine structure
structure := determineStructure(stanzaCount, len(nonEmptyLines), rhymeScheme, meterType)
metrics := PoeticMetrics{
RhymeScheme: rhymeScheme,
MeterType: meterType,
StanzaCount: stanzaCount,
LineCount: len(nonEmptyLines),
Structure: structure,
}
return metrics, nil
}
// detectRhymeScheme detects the rhyme scheme of a poem
func detectRhymeScheme(lines []string) string {
// This is a simplified implementation
// In a real-world scenario, you would use phonetic analysis
if len(lines) < 2 {
return "Unknown"
}
// Extract last word of each line
lastWords := make([]string, len(lines))
for i, line := range lines {
words := strings.Fields(line)
if len(words) > 0 {
lastWords[i] = strings.ToLower(words[len(words)-1])
// Remove punctuation
lastWords[i] = strings.TrimFunc(lastWords[i], func(r rune) bool {
return !unicode.IsLetter(r)
})
}
}
// Check for common rhyme schemes
// Check for AABB pattern
if len(lines) >= 4 {
if endsMatch(lastWords[0], lastWords[1]) && endsMatch(lastWords[2], lastWords[3]) {
return "AABB"
}
}
// Check for ABAB pattern
if len(lines) >= 4 {
if endsMatch(lastWords[0], lastWords[2]) && endsMatch(lastWords[1], lastWords[3]) {
return "ABAB"
}
}
// Check for ABBA pattern
if len(lines) >= 4 {
if endsMatch(lastWords[0], lastWords[3]) && endsMatch(lastWords[1], lastWords[2]) {
return "ABBA"
}
}
// Check for AAAA pattern
if len(lines) >= 4 {
if endsMatch(lastWords[0], lastWords[1]) && endsMatch(lastWords[1], lastWords[2]) && endsMatch(lastWords[2], lastWords[3]) {
return "AAAA"
}
}
// Default
return "Irregular"
}
// detectMeterType detects the meter type of a poem
func detectMeterType(lines []string) string {
// This is a simplified implementation
// In a real-world scenario, you would use syllable counting and stress patterns
if len(lines) == 0 {
return "Unknown"
}
// Count syllables in each line
syllableCounts := make([]int, len(lines))
for i, line := range lines {
syllableCounts[i] = countSyllables(line)
}
// Check for common meter types
// Check for iambic pentameter (around 10 syllables per line)
pentameterCount := 0
for _, count := range syllableCounts {
if count >= 9 && count <= 11 {
pentameterCount++
}
}
if float64(pentameterCount)/float64(len(lines)) > 0.7 {
return "Iambic Pentameter"
}
// Check for tetrameter (around 8 syllables per line)
tetrameterCount := 0
for _, count := range syllableCounts {
if count >= 7 && count <= 9 {
tetrameterCount++
}
}
if float64(tetrameterCount)/float64(len(lines)) > 0.7 {
return "Tetrameter"
}
// Check for trimeter (around 6 syllables per line)
trimeterCount := 0
for _, count := range syllableCounts {
if count >= 5 && count <= 7 {
trimeterCount++
}
}
if float64(trimeterCount)/float64(len(lines)) > 0.7 {
return "Trimeter"
}
// Default
return "Free Verse"
}
// determineStructure determines the overall structure of a poem
func determineStructure(stanzaCount, lineCount int, rhymeScheme, meterType string) string {
// This is a simplified implementation
// Check for common poetic forms
// Sonnet
if lineCount == 14 && (rhymeScheme == "ABAB" || rhymeScheme == "ABBA") && meterType == "Iambic Pentameter" {
return "Sonnet"
}
// Haiku
if lineCount == 3 && stanzaCount == 1 {
return "Haiku"
}
// Limerick
if lineCount == 5 && stanzaCount == 1 && rhymeScheme == "AABBA" {
return "Limerick"
}
// Quatrain
if lineCount%4 == 0 && stanzaCount == lineCount/4 {
return "Quatrain"
}
// Tercet
if lineCount%3 == 0 && stanzaCount == lineCount/3 {
return "Tercet"
}
// Couplet
if lineCount%2 == 0 && stanzaCount == lineCount/2 {
return "Couplet"
}
// Default
return "Free Form"
}
// endsMatch checks if two words have matching endings (simplified rhyme detection)
func endsMatch(word1, word2 string) bool {
// This is a simplified implementation
// In a real-world scenario, you would use phonetic analysis
if len(word1) < 2 || len(word2) < 2 {
return false
}
// Check if the last 2 characters match
return word1[len(word1)-2:] == word2[len(word2)-2:]
}
// countSyllables counts the syllables in a line of text
func countSyllables(line string) int {
// This is a simplified implementation
// In a real-world scenario, you would use a dictionary or ML model
words := strings.Fields(line)
syllableCount := 0
for _, word := range words {
// Clean the word
word = strings.ToLower(word)
word = strings.TrimFunc(word, func(r rune) bool {
return !unicode.IsLetter(r)
})
// Count vowel groups
inVowelGroup := false
for i, r := range word {
isVowelChar := isVowel(r)
// Start of vowel group
if isVowelChar && !inVowelGroup {
inVowelGroup = true
syllableCount++
}
// End of vowel group
if !isVowelChar {
inVowelGroup = false
}
// Handle silent e at the end
if i == len(word)-1 && r == 'e' && i > 0 {
prevChar := rune(word[i-1])
if !isVowel(prevChar) {
syllableCount--
}
}
}
// Ensure at least one syllable per word
if syllableCount == 0 {
syllableCount = 1
}
}
return syllableCount
}
// isVowel checks if a character is a vowel
func isVowel(r rune) bool {
return r == 'a' || r == 'e' || r == 'i' || r == 'o' || r == 'u' || r == 'y'
}