From 29fd9d5194d4886775a64bc25765fc94c41ccfa5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:22:32 +0000 Subject: [PATCH 1/3] Initial plan From 331b77f5dd8c1c1396086aff2bbc9ed0e95af5b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:30:06 +0000 Subject: [PATCH 2/3] Add rhyme dictionary page with navigation and utilities Co-authored-by: lucis <18706156+lucis@users.noreply.github.com> --- view/src/components/site-header.tsx | 14 ++ view/src/lib/rhymes.ts | 200 +++++++++++++++++ view/src/main.tsx | 2 + view/src/routes/rimas.tsx | 327 ++++++++++++++++++++++++++++ 4 files changed, 543 insertions(+) create mode 100644 view/src/lib/rhymes.ts create mode 100644 view/src/routes/rimas.tsx diff --git a/view/src/components/site-header.tsx b/view/src/components/site-header.tsx index 5deb57f..c829ab7 100644 --- a/view/src/components/site-header.tsx +++ b/view/src/components/site-header.tsx @@ -60,6 +60,13 @@ export function SiteHeader({ showBackButton, backTo = "/", backLabel = "Voltar" > Cantadores + + Rimas + @@ -94,6 +101,13 @@ export function SiteHeader({ showBackButton, backTo = "/", backLabel = "Voltar" > Cantadores + + Rimas + diff --git a/view/src/lib/rhymes.ts b/view/src/lib/rhymes.ts new file mode 100644 index 0000000..1c53a09 --- /dev/null +++ b/view/src/lib/rhymes.ts @@ -0,0 +1,200 @@ +/** + * Utilitário para extrair e agrupar rimas do acervo + */ + +import type { Cantoria, Estrofe } from "./types"; + +export interface RhymeWord { + word: string; // Palavra final normalizada + original: string; // Palavra original (com acentos, maiúsculas) + verse: string; // Verso completo + cantoriaId: string; + cantoriaTitle: string; + cantadorName: string; + estilo: string; + estrofeNumero: number; + versoNumero: number; // Posição do verso na estrofe (0-indexed) +} + +export interface RhymeGroup { + rhyme: string; // Terminação da rima (últimas 2-3 sílabas) + words: Map; // Agrupado por palavra completa + count: number; // Total de ocorrências +} + +/** + * Extrai a palavra final de um verso (remove pontuação) + */ +function extractLastWord(verse: string): string { + // Remove pontuação final e espaços + const cleaned = verse.trim().replace(/[.,!?;:"'»«""]$/g, ''); + const words = cleaned.split(/\s+/); + return words[words.length - 1] || ''; +} + +/** + * Normaliza palavra para comparação (remove acentos, lowercase) + */ +function normalizeWord(word: string): string { + return word + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); +} + +/** + * Extrai a terminação da rima (últimas 2-4 letras dependendo do tamanho) + */ +function extractRhymeSuffix(word: string): string { + const normalized = normalizeWord(word); + + // Para palavras muito curtas, usar a palavra inteira + if (normalized.length <= 3) { + return normalized; + } + + // Para palavras médias (4-6 letras), pegar últimas 3 letras + if (normalized.length <= 6) { + return normalized.slice(-3); + } + + // Para palavras longas, pegar últimas 4 letras + return normalized.slice(-4); +} + +/** + * Extrai todas as rimas de uma cantoria + */ +export function extractRhymesFromCantoria(cantoria: Cantoria): RhymeWord[] { + const rhymes: RhymeWord[] = []; + + cantoria.estrofes.forEach((estrofe: Estrofe) => { + estrofe.versos.forEach((verso: string, index: number) => { + const original = extractLastWord(verso); + if (!original) return; + + const word = normalizeWord(original); + + rhymes.push({ + word, + original, + verse: verso, + cantoriaId: cantoria.id, + cantoriaTitle: cantoria.titulo, + cantadorName: estrofe.cantador, + estilo: cantoria.estilo.nome, + estrofeNumero: estrofe.numero, + versoNumero: index, + }); + }); + }); + + return rhymes; +} + +/** + * Agrupa rimas por terminação + */ +export function groupRhymesByEnding(rhymeWords: RhymeWord[]): Map { + const groups = new Map(); + + rhymeWords.forEach((rhymeWord) => { + const suffix = extractRhymeSuffix(rhymeWord.word); + + if (!groups.has(suffix)) { + groups.set(suffix, { + rhyme: suffix, + words: new Map(), + count: 0, + }); + } + + const group = groups.get(suffix)!; + + if (!group.words.has(rhymeWord.word)) { + group.words.set(rhymeWord.word, []); + } + + group.words.get(rhymeWord.word)!.push(rhymeWord); + group.count++; + }); + + return groups; +} + +/** + * Extrai todas as rimas de um conjunto de cantorias + */ +export function extractAllRhymes(cantorias: Cantoria[]): RhymeWord[] { + const allRhymes: RhymeWord[] = []; + + cantorias.forEach((cantoria) => { + const rhymes = extractRhymesFromCantoria(cantoria); + allRhymes.push(...rhymes); + }); + + return allRhymes; +} + +/** + * Obtém estatísticas das rimas + */ +export function getRhymeStats(rhymeGroups: Map) { + let totalRhymes = 0; + let totalWords = 0; + const styleCount = new Map(); + + rhymeGroups.forEach((group) => { + totalRhymes += group.count; + totalWords += group.words.size; + + group.words.forEach((words) => { + words.forEach((word) => { + styleCount.set(word.estilo, (styleCount.get(word.estilo) || 0) + 1); + }); + }); + }); + + return { + totalRhymeGroups: rhymeGroups.size, + totalRhymes, + totalUniqueWords: totalWords, + styleCount, + }; +} + +/** + * Filtra grupos de rimas por letra inicial + */ +export function filterByLetter( + rhymeGroups: Map, + letter: string +): Map { + const filtered = new Map(); + + rhymeGroups.forEach((group, key) => { + if (key.startsWith(letter.toLowerCase())) { + filtered.set(key, group); + } + }); + + return filtered; +} + +/** + * Ordena grupos de rimas por número de ocorrências + */ +export function sortByFrequency( + rhymeGroups: Map +): [string, RhymeGroup][] { + return Array.from(rhymeGroups.entries()).sort((a, b) => b[1].count - a[1].count); +} + +/** + * Ordena grupos de rimas alfabeticamente + */ +export function sortAlphabetically( + rhymeGroups: Map +): [string, RhymeGroup][] { + return Array.from(rhymeGroups.entries()).sort((a, b) => a[0].localeCompare(b[0])); +} diff --git a/view/src/main.tsx b/view/src/main.tsx index 9c5accd..7564eaa 100644 --- a/view/src/main.tsx +++ b/view/src/main.tsx @@ -15,6 +15,7 @@ import CantadoresRoute from "./routes/cantadores.tsx"; import CantadorRoute from "./routes/cantador.tsx"; import MusicasRoute from "./routes/musicas.tsx"; import MusicaRoute from "./routes/musica.tsx"; +import RimasRoute from "./routes/rimas.tsx"; import { Toaster } from "sonner"; import "./styles.css"; @@ -34,6 +35,7 @@ const routeTree = rootRoute.addChildren([ CantadorRoute(rootRoute), MusicasRoute(rootRoute), MusicaRoute(rootRoute), + RimasRoute(rootRoute), ]); const queryClient = new QueryClient(); diff --git a/view/src/routes/rimas.tsx b/view/src/routes/rimas.tsx new file mode 100644 index 0000000..6d91f73 --- /dev/null +++ b/view/src/routes/rimas.tsx @@ -0,0 +1,327 @@ +import { createRoute, Link, type RootRoute } from "@tanstack/react-router"; +import { useState, useMemo } from "react"; +import { SiteHeader } from "../components/site-header"; +import { SiteFooter } from "../components/site-footer"; +import { Music, Search, Filter, ArrowUpDown } from "lucide-react"; +import acervoData from "../lib/acervoCompat"; +import type { Cantoria } from "../lib/types"; +import { + extractAllRhymes, + groupRhymesByEnding, + getRhymeStats, + sortByFrequency, + sortAlphabetically, +} from "../lib/rhymes"; + +function RimasPage() { + const cantorias = acervoData.repentes as Cantoria[]; + + // Estados de filtro e ordenação + const [selectedLetter, setSelectedLetter] = useState("todas"); + const [selectedEstilo, setSelectedEstilo] = useState("todos"); + const [sortBy, setSortBy] = useState<"alfabetica" | "frequencia">("frequencia"); + const [searchTerm, setSearchTerm] = useState(""); + const [expandedRhyme, setExpandedRhyme] = useState(null); + + // Extrair todas as rimas + const allRhymes = useMemo(() => { + return extractAllRhymes(cantorias); + }, [cantorias]); + + // Agrupar por terminação + const rhymeGroups = useMemo(() => { + let rhymes = allRhymes; + + // Filtrar por estilo + if (selectedEstilo !== "todos") { + rhymes = rhymes.filter((r) => r.estilo === selectedEstilo); + } + + return groupRhymesByEnding(rhymes); + }, [allRhymes, selectedEstilo]); + + // Filtrar e ordenar grupos + const filteredAndSortedGroups = useMemo(() => { + let groups = rhymeGroups; + + // Filtrar por letra + if (selectedLetter !== "todas") { + const filtered = new Map(); + groups.forEach((group, key) => { + if (key.startsWith(selectedLetter.toLowerCase())) { + filtered.set(key, group); + } + }); + groups = filtered; + } + + // Filtrar por busca + if (searchTerm) { + const filtered = new Map(); + const search = searchTerm.toLowerCase(); + groups.forEach((group, key) => { + // Buscar na terminação ou nas palavras + if ( + key.includes(search) || + Array.from(group.words.keys()).some((word) => word.includes(search)) + ) { + filtered.set(key, group); + } + }); + groups = filtered; + } + + // Ordenar + const sorted = + sortBy === "alfabetica" + ? sortAlphabetically(groups) + : sortByFrequency(groups); + + return sorted; + }, [rhymeGroups, selectedLetter, sortBy, searchTerm]); + + // Estatísticas + const stats = useMemo(() => getRhymeStats(rhymeGroups), [rhymeGroups]); + + // Estilos únicos + const estilos = useMemo(() => { + const unique = new Set(cantorias.map((c) => c.estilo.nome)); + return Array.from(unique).sort(); + }, [cantorias]); + + // Alfabeto + const alphabet = "abcdefghijklmnopqrstuvwxyz".split(""); + + return ( +
+ + + {/* Hero */} +
+
+

+ Dicionário de Rimas +

+ +

+ Navegue pelas rimas do repente nordestino. Descubra versos agrupados por suas terminações poéticas. +

+ + {/* Estatísticas */} +
+
+
+ {stats.totalRhymeGroups} +
+
Terminações de Rimas
+
+
+
+ {stats.totalUniqueWords} +
+
Palavras Únicas
+
+
+
+ {stats.totalRhymes} +
+
Versos Catalogados
+
+
+
+ {cantorias.filter((c) => c.estrofes.length > 0).length} +
+
Cantorias Analisadas
+
+
+
+
+ + {/* Filtros */} +
+
+ {/* Busca */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-3 border-2 border-[#8B6F47] rounded-lg focus:outline-none focus:border-[#C84B31] bg-[#F5EBE0]" + /> +
+ + {/* Filtros principais */} +
+ {/* Filtro por estilo */} +
+ + +
+ + {/* Ordenação */} +
+ + +
+ + {/* Contador de resultados */} +
+ {filteredAndSortedGroups.length} terminaç{filteredAndSortedGroups.length === 1 ? "ão" : "ões"} +
+
+ + {/* Alfabeto */} +
+ + {alphabet.map((letter) => ( + + ))} +
+
+
+ + {/* Lista de rimas */} +
+
+ {filteredAndSortedGroups.length === 0 ? ( +
+ +

Nenhuma rima encontrada com esses filtros

+
+ ) : ( +
+ {filteredAndSortedGroups.map(([rhymeKey, group]) => { + const isExpanded = expandedRhyme === rhymeKey; + const wordEntries = Array.from(group.words.entries()).sort( + (a, b) => b[1].length - a[1].length + ); + + return ( +
+ {/* Header */} + + + {/* Palavras e versos */} + {isExpanded && ( +
+ {wordEntries.map(([word, rhymeWords]) => ( +
+ {/* Palavra */} +
+ + {rhymeWords[0].original} + + + ({rhymeWords.length} vez{rhymeWords.length !== 1 ? "es" : ""}) + +
+ + {/* Versos */} +
+ {rhymeWords.map((rhyme, idx) => ( +
+

+ "{rhyme.verse}" +

+
+ c.id === rhyme.cantoriaId)?.slug || "" }} + className="hover:text-[#C84B31] hover:underline" + > + {rhyme.cantoriaTitle} + + + {rhyme.cantadorName} + + {rhyme.estilo} +
+
+ ))} +
+
+ ))} +
+ )} +
+ ); + })} +
+ )} +
+
+ + +
+ ); +} + +export default function RimasRoute(rootRoute: RootRoute) { + return createRoute({ + getParentRoute: () => rootRoute, + path: "/rimas", + component: RimasPage, + }); +} From eb32a265285ddf28feca48f34ca0b9ebe343c1f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:34:10 +0000 Subject: [PATCH 3/3] Add documentation for rhyme dictionary feature Co-authored-by: lucis <18706156+lucis@users.noreply.github.com> --- DICIONARIO-RIMAS.md | 127 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 DICIONARIO-RIMAS.md diff --git a/DICIONARIO-RIMAS.md b/DICIONARIO-RIMAS.md new file mode 100644 index 0000000..0bed42f --- /dev/null +++ b/DICIONARIO-RIMAS.md @@ -0,0 +1,127 @@ +# 📖 Dicionário de Rimas - Documentação + +## Visão Geral + +O **Dicionário de Rimas** é uma página interativa que visualiza e agrupa as palavras finais dos versos de todas as cantorias do acervo Vilanova. É uma ferramenta única para poetas, estudantes e entusiastas do repente descobrirem padrões de rima e explorarem o vocabulário dos cantadores. + +## Funcionalidades + +### 🎯 Agrupamento Inteligente + +As rimas são agrupadas por suas **terminações fonéticas** (últimas 2-4 letras normalizadas), permitindo descobrir: +- Palavras que rimam entre si +- Frequência de uso de cada rima +- Contexto dos versos onde aparecem + +**Exemplo:** +- Terminação `-ente`: independente (10x), valente (4x), serpente (3x) +- Terminação `-oano`: alagoano (32x) +- Terminação `-mar`: mar (23x) + +### 🔍 Filtros Disponíveis + +1. **Por Letra**: Navegue pelas rimas começando com cada letra do alfabeto +2. **Por Estilo**: Filtre rimas de um estilo específico (Martelo Alagoano, Galope à Beira Mar, etc.) +3. **Busca**: Pesquise por palavra ou terminação específica +4. **Ordenação**: Por frequência (mais usadas) ou ordem alfabética + +### 📊 Estatísticas em Tempo Real + +A página mostra: +- Total de terminações de rimas diferentes +- Total de palavras únicas +- Total de versos catalogados +- Número de cantorias analisadas + +### 🎨 Interface Interativa + +- **Cards Expansíveis**: Clique em uma terminação para ver todas as palavras e versos +- **Contexto Rico**: Cada verso mostra: + - Texto completo do verso + - Link para a cantoria + - Nome do cantador + - Estilo da cantoria +- **Responsivo**: Funciona perfeitamente em desktop e mobile + +## Como Usar + +### Para Poetas e Estudantes + +1. **Encontrar Rimas**: Digite uma palavra na busca para ver com que ela rima +2. **Inspiração**: Navegue pelas terminações mais frequentes para descobrir vocabulário comum +3. **Estudar Padrões**: Compare como diferentes cantadores usam as mesmas rimas + +### Para Pesquisadores + +1. **Análise de Vocabulário**: Veja quais palavras são mais usadas em cada estilo +2. **Padrões de Rima**: Identifique preferências de rima por estilo ou cantador +3. **Contexto Histórico**: Veja como palavras são usadas em diferentes contextos + +## Exemplo de Uso + +``` +Buscar: "amor" +Resultado: + - Terminação "-amor" + • amor (15 ocorrências) + - "Sei que acima da morte está o amor." (Pensamentos e Pensadores) + - "O amor é mais nobre, é mais humano" (Ivanildo Vilanova) +``` + +## Dados Processados + +Atualmente o dicionário analisa: +- **12 cantorias** com versos completos +- **104 estrofes** catalogadas +- **980 versos** transcritos +- **436 terminações** de rima diferentes + +Os dados são extraídos em tempo real do acervo, então novos repentes adicionados aparecem automaticamente. + +## Navegação + +Acesse: `/rimas` ou clique em "Rimas" no menu de navegação + +## Implementação Técnica + +### Arquivos + +- **Página**: `/view/src/routes/rimas.tsx` +- **Utilitários**: `/view/src/lib/rhymes.ts` +- **Navegação**: Adicionado em `/view/src/components/site-header.tsx` + +### Algoritmo de Agrupamento + +1. **Extração**: Remove pontuação e extrai palavra final de cada verso +2. **Normalização**: Remove acentos e converte para minúsculas +3. **Terminação**: Extrai últimas 2-4 letras dependendo do tamanho da palavra +4. **Agrupamento**: Agrupa palavras com mesma terminação + +### Performance + +- Processamento em memória usando `useMemo` para cache +- Filtros aplicados de forma eficiente com Maps +- Interface responsiva e rápida + +## Futuras Melhorias + +- [ ] Visualização de esquemas de rima (AABB, ABAB, etc.) +- [ ] Exportar lista de rimas como PDF +- [ ] Sons de rima (fonética completa, não apenas terminação) +- [ ] Estatísticas de evolução temporal das rimas +- [ ] API pública para consulta de rimas + +## Contribuindo + +Para adicionar novas cantorias que aparecerão no dicionário: +1. Adicione o arquivo JSON em `public/data/cantorias/` +2. Inclua as estrofes completas com todos os versos +3. O dicionário atualizará automaticamente + +## Créditos + +Desenvolvido para o **Projeto Vilanova** - preservação digital da cantoria nordestina. + +--- + +**Feito com ❤️ para o repente nordestino**