mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
- 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
291 lines
6.5 KiB
Go
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'
|
|
}
|