Added lyrics to scores

This commit is contained in:
marc
2025-04-05 15:10:56 +02:00
parent d5eb40e300
commit 211a1fbb05
7 changed files with 89 additions and 9 deletions

View File

@@ -4,12 +4,12 @@
** TODO Ordenar els resultats de la cerca de temes ** TODO Ordenar els resultats de la cerca de temes
** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat) ** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat)
** TODO Usuaris i permisos granulars ** TODO Usuaris i permisos granulars
** TODO Lilypond support (o similar)
*** TODO Fer cançoners "en directe"
*** TODO Suport de caràcters especials (al títol i més llocs?)
*** TODO Mostrar partitura als enllaços (resultats de cerca)
** DONE Arreglar estadístiques de temes (dos temes tocats a la mateixa sessió compten un cop)
** TODO Arreglar visualitzador de pdf, suportar més d'un visualitzador per pàgina ** TODO Arreglar visualitzador de pdf, suportar més d'un visualitzador per pàgina
** Lilypond support (o similar)
*** TODO Fer cançoners "en directe"
*** DONE Suport de caràcters especials (al títol i més llocs?)
*** DONE Mostrar partitura als enllaços (resultats de cerca)
** DONE Arreglar estadístiques de temes (dos temes tocats a la mateixa sessió compten un cop)
* Idees * Idees
** Jams ** Jams
*** Properes jams *** Properes jams

View File

@@ -59,3 +59,8 @@
\bar "|." \bar "|."
} }
} }
\addlyrics {
% -----------------------------------------------------
% Lletra
% -----------------------------------------------------
}

View File

@@ -1,3 +1,4 @@
\version "2.24.4"
\paper { \paper {
top-margin = 10 top-margin = 10
left-margin = 15 left-margin = 15
@@ -7,9 +8,9 @@
\header { \header {
title = \markup { "{{ tema.title | safe }}" } title = \markup { "{{ tema.title | safe }}" }
{% if tema.composer() %} {% if tema.composer() %}
composer = "{{ tema.composer() }}" composer = "{{ tema.composer() | safe }}"
{% elif tema.origin() %} {% elif tema.origin() %}
composer = "{{ tema.origin() }}" composer = "{{ tema.origin() | safe }}"
{% endif %} {% endif %}
tagline = "Partitura generada amb LilyPond" tagline = "Partitura generada amb LilyPond"
@@ -25,3 +26,21 @@
{{ score_source | safe }} {{ score_source | safe }}
>> >>
} }
{% if lyrics_text is not none %}
\markup {
\fill-line {
\hspace #1
{% for column in lyrics_text.columns %}
\center-column {
{% for paragraph in column.paragraphs %}
{% for line in paragraph.lines %}
\line { {{ line | safe }} }
{% endfor %}
\vspace #1
{% endfor %}
}
\hspace #1
{% endfor %}
}
}
{% endif %}

View File

@@ -22,3 +22,18 @@ class RenderError:
pos=str(self.pos), pos=str(self.pos),
error=self.error, error=self.error,
) )
@dataclasses.dataclass
class LyricsParagraph:
lines: list[str]
@dataclasses.dataclass
class LyricsColumn:
paragraphs: list[LyricsParagraph]
@dataclasses.dataclass
class LyricsText:
columns: list[LyricsColumn]

View File

@@ -1,12 +1,14 @@
import asyncio import asyncio
import dataclasses import dataclasses
import itertools
import os import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
import aiofiles import aiofiles
from folkugat_web.model.lilypond import RenderError from folkugat_web.model.lilypond import (LyricsColumn, LyricsParagraph,
LyricsText, RenderError)
from folkugat_web.model.temes import Tema from folkugat_web.model.temes import Tema
from folkugat_web.services import files as files_service from folkugat_web.services import files as files_service
from folkugat_web.templates import templates from folkugat_web.templates import templates
@@ -17,10 +19,15 @@ RenderFormat = Literal["png"] | Literal["pdf"]
def source_to_single_score(source: str, tema: Tema) -> str: def source_to_single_score(source: str, tema: Tema) -> str:
if tema.lyrics:
lyrics_text = build_lyrics(tema.lyrics[0].content)
else:
lyrics_text = None
return templates.get_template("lilypond/single_score.ly").render( return templates.get_template("lilypond/single_score.ly").render(
score_beginning=SCORE_BEGINNING, score_beginning=SCORE_BEGINNING,
score_source=source, score_source=source,
tema=tema, tema=tema,
lyrics_text=lyrics_text,
) )
@@ -68,3 +75,15 @@ async def render(source: str, fmt: RenderFormat, output_filename: Path | None =
output_filename = output_filename.with_suffix(".pdf") output_filename = output_filename.with_suffix(".pdf")
return output_filename, [] return output_filename, []
def build_lyrics(text: str, max_cols: int = 2, min_pars: int = 2) -> LyricsText:
paragraphs = [LyricsParagraph(lines=par_str.splitlines()) for par_str in text.split("\n\n")]
n_cols = next(filter(lambda nc: len(paragraphs) // nc >= min_pars, range(max_cols, 0, -1)), 1)
n_long_cols = len(paragraphs) % n_cols
pars_per_col = [len(paragraphs) // n_cols + 1] * n_long_cols + [len(paragraphs) // n_cols] * (n_cols - n_long_cols)
acc_pars_per_col = [0] + list(itertools.accumulate(pars_per_col))
return LyricsText(columns=[
LyricsColumn(paragraphs=paragraphs[acc_pars_per_col[i]:acc_pars_per_col[i+1]])
for i in range(n_cols)
])

View File

@@ -1,7 +1,26 @@
import dataclasses
import re
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
from typing import Callable
from folkugat_web.dal.sql.temes import lyrics as lyrics_dal from folkugat_web.dal.sql.temes import lyrics as lyrics_dal
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.utils import FnChain
def _sub(pattern: str, sub: str) -> Callable[[str], str]:
def _inner_sub(text: str) -> str:
return re.sub(pattern, sub, text)
return _inner_sub
def _clean_string(lyrics_str: str) -> str:
return (
FnChain.transform(lyrics_str) |
_sub(r" *", " ") |
_sub(r"\s*\n\s*", "\n") |
_sub(r"\n*", "\n")
).result()
def add_lyrics_to_tema(tema: model.Tema) -> model.Tema: def add_lyrics_to_tema(tema: model.Tema) -> model.Tema:
@@ -19,6 +38,7 @@ def get_lyric_by_id(lyric_id: int, tema_id: int | None = None) -> model.Lyrics |
def update_lyric(lyric: model.Lyrics): def update_lyric(lyric: model.Lyrics):
lyric = dataclasses.replace(lyric, content=_clean_string(lyric.content))
lyrics_dal.update_lyric(lyric=lyric) lyrics_dal.update_lyric(lyric=lyric)

View File

@@ -4,6 +4,7 @@ from fastapi import HTTPException
from folkugat_web.dal.sql.temes import scores as scores_dal from folkugat_web.dal.sql.temes import scores as scores_dal
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.services import lilypond from folkugat_web.services import lilypond
from folkugat_web.services.temes import lyrics as lyrics_service
from folkugat_web.services.temes import properties as properties_service from folkugat_web.services.temes import properties as properties_service
from folkugat_web.services.temes import query as temes_q from folkugat_web.services.temes import query as temes_q
from folkugat_web.utils import FnChain from folkugat_web.utils import FnChain
@@ -51,6 +52,7 @@ def build_single_tune_full_source(tema_id: int, source: str) -> str:
raise HTTPException(status_code=404, detail="Could not find tema!") raise HTTPException(status_code=404, detail="Could not find tema!")
tema = ( tema = (
FnChain.transform(tema) | FnChain.transform(tema) |
properties_service.add_properties_to_tema properties_service.add_properties_to_tema |
lyrics_service.add_lyrics_to_tema
).result() ).result()
return lilypond.source_to_single_score(source=source, tema=tema) return lilypond.source_to_single_score(source=source, tema=tema)