Rendering code refactor
This commit is contained in:
@@ -5,3 +5,5 @@ from .sessio import *
|
|||||||
from .sessions import *
|
from .sessions import *
|
||||||
from .tema import *
|
from .tema import *
|
||||||
from .temes import *
|
from .temes import *
|
||||||
|
|
||||||
|
__all__ = ["router"]
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import Form, Request
|
from fastapi import Form, HTTPException, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
from folkugat_web.api import router
|
from folkugat_web.api import router
|
||||||
from folkugat_web.fragments import live, sessio
|
from folkugat_web.fragments import live, sessio
|
||||||
from folkugat_web.services import auth
|
from folkugat_web.services import auth, files
|
||||||
|
from folkugat_web.services import playlists as playlists_service
|
||||||
|
from folkugat_web.services.lilypond import build as lilypond_build
|
||||||
|
from folkugat_web.services.lilypond import render as lilypond_render
|
||||||
|
from folkugat_web.services.lilypond import source as lilypond_source
|
||||||
|
from folkugat_web.services.temes import scores as scores_service
|
||||||
from folkugat_web.services.temes import write as temes_service
|
from folkugat_web.services.temes import write as temes_service
|
||||||
from folkugat_web.templates import templates
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
@@ -197,3 +203,27 @@ def set_tema_new(
|
|||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
tema_id=new_tema.id,
|
tema_id=new_tema.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/sessio/{session_id}/set/{set_id}/score")
|
||||||
|
async def render(
|
||||||
|
request: Request,
|
||||||
|
_: auth.RequireLogin,
|
||||||
|
session_id: int,
|
||||||
|
set_id: int,
|
||||||
|
):
|
||||||
|
set_entry = playlists_service.get_set(session_id=session_id, set_id=set_id)
|
||||||
|
if not set_entry:
|
||||||
|
raise HTTPException(status_code=404, detail="Could not find set!")
|
||||||
|
tune_set = lilypond_build.set_from_set(set_entry=set_entry)
|
||||||
|
set_source = lilypond_source.set_source(tune_set=tune_set)
|
||||||
|
pdf_result = await lilypond_render.render(
|
||||||
|
source=set_source,
|
||||||
|
output=lilypond_render.RenderOutput.PDF,
|
||||||
|
)
|
||||||
|
if output_filename := pdf_result.result:
|
||||||
|
score_render_url = files.get_db_file_path(output_filename)
|
||||||
|
# return temes.score_render(request=request, score_id=score_id, score_render_url=score_render_url)
|
||||||
|
return HTMLResponse(content=score_render_url)
|
||||||
|
else:
|
||||||
|
return HTMLResponse()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import Form, Request
|
from fastapi import Form, Request
|
||||||
from folkugat_web.api import router
|
from folkugat_web.api import router
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
|||||||
|
|
||||||
@router.delete("/api/tema/{tema_id}")
|
@router.delete("/api/tema/{tema_id}")
|
||||||
def delete_tema(_: auth.RequireLogin, tema_id: int):
|
def delete_tema(_: auth.RequireLogin, tema_id: int):
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
|
if not tema:
|
||||||
|
raise HTTPException(status_code=404, detail="Could not find tune")
|
||||||
|
tema = temes_q.tema_compute_stats(tema=tema)
|
||||||
|
if tema.stats:
|
||||||
|
raise HTTPException(status_code=400, detail="Can't delete a tune that has been played in a set!")
|
||||||
temes_w.delete_tema(tema_id=tema_id)
|
temes_w.delete_tema(tema_id=tema_id)
|
||||||
files_service.clean_orphan_files()
|
files_service.clean_orphan_files()
|
||||||
return HTMLResponse(headers={
|
return HTMLResponse(headers={
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ from fastapi.responses import HTMLResponse
|
|||||||
from folkugat_web.api import router
|
from folkugat_web.api import router
|
||||||
from folkugat_web.fragments import temes
|
from folkugat_web.fragments import temes
|
||||||
from folkugat_web.fragments.tema import scores as scores_fragments
|
from folkugat_web.fragments.tema import scores as scores_fragments
|
||||||
from folkugat_web.services import auth, files, lilypond
|
from folkugat_web.services import auth, files
|
||||||
from folkugat_web.services.temes import properties as properties_service
|
from folkugat_web.services.lilypond import build as lilypond_build
|
||||||
from folkugat_web.services.temes import query as temes_q
|
from folkugat_web.services.lilypond import render as lilypond_render
|
||||||
|
from folkugat_web.services.lilypond import source as lilypond_source
|
||||||
from folkugat_web.services.temes import scores as scores_service
|
from folkugat_web.services.temes import scores as scores_service
|
||||||
from folkugat_web.utils import FnChain
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/tema/{tema_id}/score/{score_id}")
|
@router.get("/api/tema/{tema_id}/score/{score_id}")
|
||||||
@@ -36,10 +36,19 @@ async def set_score(
|
|||||||
if not score:
|
if not score:
|
||||||
raise HTTPException(status_code=404, detail="Could not find lyric!")
|
raise HTTPException(status_code=404, detail="Could not find lyric!")
|
||||||
|
|
||||||
full_source = scores_service.build_single_tune_full_source(tema_id=tema_id, source=source)
|
tune = lilypond_build.tune_from_tema_id(tema_id=tema_id, score_source=source)
|
||||||
pdf_filename = files.create_tema_filename(tema_id=tema_id)
|
tune_source = lilypond_source.tune_source(tune=tune)
|
||||||
pdf_filename, errors = await lilypond.render(source=full_source, fmt="pdf", output_filename=pdf_filename)
|
pdf_result, png_result = await render_tune(tune_source=tune_source, tema_id=tema_id)
|
||||||
if errors:
|
if errors := pdf_result.error:
|
||||||
|
new_score = dataclasses.replace(
|
||||||
|
score,
|
||||||
|
source=source,
|
||||||
|
title=title,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
elif png_result is None:
|
||||||
|
raise RuntimeError(f"Received empty png_result with pdf_result: {pdf_result}")
|
||||||
|
elif errors := png_result.error:
|
||||||
new_score = dataclasses.replace(
|
new_score = dataclasses.replace(
|
||||||
score,
|
score,
|
||||||
source=source,
|
source=source,
|
||||||
@@ -47,21 +56,43 @@ async def set_score(
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
png_filename = files.create_tema_filename(tema_id=tema_id)
|
pdf_file = pdf_result.result
|
||||||
png_filename, errors = await lilypond.render(source=full_source, fmt="png", output_filename=png_filename)
|
png_file = png_result.result
|
||||||
new_score = dataclasses.replace(
|
new_score = dataclasses.replace(
|
||||||
score,
|
score,
|
||||||
source=source,
|
source=source,
|
||||||
title=title,
|
title=title,
|
||||||
pdf_url=files.get_db_file_path(pdf_filename) if pdf_filename else None,
|
pdf_url=files.get_db_file_path(pdf_file) if pdf_file else None,
|
||||||
img_url=files.get_db_file_path(png_filename) if png_filename else None,
|
img_url=files.get_db_file_path(png_file) if png_file else None,
|
||||||
errors=errors,
|
errors=[],
|
||||||
)
|
)
|
||||||
scores_service.update_score(score=new_score)
|
scores_service.update_score(score=new_score)
|
||||||
files.clean_orphan_files()
|
files.clean_orphan_files()
|
||||||
return scores_fragments.score(request=request, logged_in=logged_in, score=new_score)
|
return scores_fragments.score(request=request, logged_in=logged_in, score=new_score)
|
||||||
|
|
||||||
|
|
||||||
|
async def render_tune(
|
||||||
|
tune_source: str,
|
||||||
|
tema_id: int,
|
||||||
|
) -> tuple[lilypond_render.RenderResult, lilypond_render.RenderResult | None]:
|
||||||
|
async with files.tmp_file(content=tune_source) as source_file:
|
||||||
|
pdf_file = files.create_tema_filename(tema_id=tema_id)
|
||||||
|
pdf_result = await lilypond_render.render_file(
|
||||||
|
input_file=source_file,
|
||||||
|
output=lilypond_render.RenderOutput.PDF,
|
||||||
|
output_file=pdf_file,
|
||||||
|
)
|
||||||
|
if pdf_result.error:
|
||||||
|
return pdf_result, None
|
||||||
|
png_file = files.create_tema_filename(tema_id=tema_id)
|
||||||
|
png_result = await lilypond_render.render_file(
|
||||||
|
input_file=source_file,
|
||||||
|
output=lilypond_render.RenderOutput.PNG_CROPPED,
|
||||||
|
output_file=png_file,
|
||||||
|
)
|
||||||
|
return pdf_result, png_result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/tema/{tema_id}/score")
|
@router.post("/api/tema/{tema_id}/score")
|
||||||
def add_score(
|
def add_score(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -108,13 +139,17 @@ async def render(
|
|||||||
score_id: int,
|
score_id: int,
|
||||||
source: Annotated[str, Form()],
|
source: Annotated[str, Form()],
|
||||||
):
|
):
|
||||||
full_source = scores_service.build_single_tune_full_source(tema_id=tema_id, source=source)
|
tune = lilypond_build.tune_from_tema_id(tema_id=tema_id, score_source=source)
|
||||||
output_filename, errors = await lilypond.render(source=full_source, fmt="png")
|
full_source = lilypond_source.tune_source(tune=tune)
|
||||||
if output_filename:
|
png_result = await lilypond_render.render(
|
||||||
|
source=full_source,
|
||||||
|
output=lilypond_render.RenderOutput.PNG_CROPPED,
|
||||||
|
)
|
||||||
|
if output_filename := png_result.result:
|
||||||
score_render_url = files.get_db_file_path(output_filename)
|
score_render_url = files.get_db_file_path(output_filename)
|
||||||
return temes.score_render(request=request, score_id=score_id, score_render_url=score_render_url)
|
return temes.score_render(request=request, score_id=score_id, score_render_url=score_render_url)
|
||||||
else:
|
else:
|
||||||
return temes.score_render(request=request, score_id=score_id, errors=errors)
|
return temes.score_render(request=request, score_id=score_id, errors=png_result.error)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/api/tema/{tema_id}/score/{score_id}/show")
|
@router.put("/api/tema/{tema_id}/score/{score_id}/show")
|
||||||
|
|||||||
@@ -602,6 +602,10 @@ video {
|
|||||||
margin: 0.75rem;
|
margin: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-4 {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.m-6 {
|
.m-6 {
|
||||||
margin: 1.5rem;
|
margin: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -701,6 +705,10 @@ video {
|
|||||||
height: 80%;
|
height: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-auto {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.h-px {
|
.h-px {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
@@ -729,6 +737,10 @@ video {
|
|||||||
max-width: 56rem;
|
max-width: 56rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-w-full {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.max-w-xl {
|
.max-w-xl {
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
}
|
}
|
||||||
@@ -871,6 +883,10 @@ video {
|
|||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded-md {
|
.rounded-md {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
@@ -911,6 +927,11 @@ video {
|
|||||||
background-color: rgb(62 56 52 / var(--tw-bg-opacity, 1));
|
background-color: rgb(62 56 52 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.p-0 {
|
.p-0 {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@@ -961,6 +982,11 @@ video {
|
|||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.py-4 {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pl-5 {
|
.pl-5 {
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
<h5 class="text text-beige"> Previsualització </h5>
|
<h5 class="text text-beige"> Previsualització </h5>
|
||||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
{% if score_render_url %}
|
{% if score_render_url %}
|
||||||
<img class="my-2" src="{{ score_render_url }}" />
|
<div class="flex flex-col items-center
|
||||||
|
bg-white rounded-lg">
|
||||||
|
<img class="m-4" src="{{ score_render_url }}" />
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<ol class="text-red-400">
|
<ol class="text-red-400">
|
||||||
{% for error in errors %}
|
{% for error in errors %}
|
||||||
|
|||||||
@@ -47,22 +47,25 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
{% if score.img_url is not none %}
|
{% if score.img_url is not none %}
|
||||||
|
<div class="flex flex-col items-center
|
||||||
|
bg-white rounded-lg ">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<a href="{{ score.pdf_url or score.img_url }}" target="_blank">
|
<a href="{{ score.pdf_url or score.img_url }}" target="_blank">
|
||||||
<img class="m-2" src="{{ score.img_url }}" />
|
<img class="py-4 px-2 max-w-full h-auto" src="{{ score.img_url }}" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% if score.pdf_url is not none %}
|
{% if score.pdf_url is not none %}
|
||||||
<div class="text-beige border rounded border-beige m-2 p-1">
|
<div class="bg-beige border rounded border-beige m-2 p-1">
|
||||||
<a href="{{ score.pdf_url }}" target="_blank">Obre el PDF</a>
|
<a href="{{ score.pdf_url }}" target="_blank">Obre el PDF</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% elif score.pdf_url is not none %}
|
{% elif score.pdf_url is not none %}
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
{% set pdf_url = score.pdf_url %}
|
{% set pdf_url = score.pdf_url %}
|
||||||
{% include "fragments/pdf_viewer.html" %}
|
{% include "fragments/pdf_viewer.html" %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% include "fragments/tema/visibility.html" %}
|
{% include "fragments/tema/visibility.html" %}
|
||||||
<button title="Canvia el títol"
|
{% if not tema.stats %}
|
||||||
|
<button title="Esborra el tema"
|
||||||
class="text-beige text-2xl mx-2"
|
class="text-beige text-2xl mx-2"
|
||||||
hx-delete="/api/tema/{{ tema.id }}"
|
hx-delete="/api/tema/{{ tema.id }}"
|
||||||
hx-swap="outerHTML">
|
hx-swap="outerHTML">
|
||||||
<i class="fa fa-times" aria-hidden="true"></i>
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,3 @@
|
|||||||
% -----------------------------------------------------
|
|
||||||
% Comandes últils
|
|
||||||
% -----------------------------------------------------
|
|
||||||
% Anacrusa: \partial 2 (2 = blanca)
|
|
||||||
%
|
|
||||||
% Repetició amb caselles:
|
|
||||||
% \repeat volta 2 {
|
|
||||||
% %%%
|
|
||||||
% \alternative {
|
|
||||||
% \volta 1 { %%% }
|
|
||||||
% \volta 2 { %%% }
|
|
||||||
% }
|
|
||||||
% }
|
|
||||||
%
|
|
||||||
% -----------------------------------------------------
|
|
||||||
|
|
||||||
\new ChordNames {
|
\new ChordNames {
|
||||||
\chords {
|
\chords {
|
||||||
\set chordChanges = ##t
|
\set chordChanges = ##t
|
||||||
@@ -50,11 +34,21 @@
|
|||||||
|
|
||||||
% A
|
% A
|
||||||
\mark \default
|
\mark \default
|
||||||
|
\repeat volta 2 {
|
||||||
\bar "||" \break
|
\alternative {
|
||||||
|
\volta 1 { }
|
||||||
|
\volta 2 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
% B
|
% B
|
||||||
\mark \default
|
\mark \default
|
||||||
|
\repeat volta 2 {
|
||||||
|
\alternative {
|
||||||
|
\volta 1 { }
|
||||||
|
\volta 2 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
\bar "|."
|
\bar "|."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
\header {
|
\header {
|
||||||
title = \markup { "{{ tema.title | safe }}" }
|
title = \markup { "{{ tune.header.title | safe }}" }
|
||||||
{% if tema.composer() %}
|
{% if tune.header.composer %}
|
||||||
composer = "{{ tema.composer() | safe }}"
|
composer = "{{ tune.header.composer | safe }}"
|
||||||
{% elif tema.origin() %}
|
|
||||||
composer = "{{ tema.origin() | safe }}"
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
tagline = "Partitura generada amb LilyPond"
|
tagline = "Partitura generada amb LilyPond"
|
||||||
@@ -19,15 +17,17 @@
|
|||||||
|
|
||||||
\markup \vspace #2
|
\markup \vspace #2
|
||||||
|
|
||||||
|
{% if tune.score_source is not none %}
|
||||||
\score {
|
\score {
|
||||||
\language "english"
|
\language "english"
|
||||||
<<
|
<<
|
||||||
{{ score_beginning }}
|
{{ score_beginning }}
|
||||||
{{ score_source | safe }}
|
{{ tune.score_source | safe }}
|
||||||
>>
|
>>
|
||||||
}
|
}
|
||||||
{% if lyrics_text is not none %}
|
{% endif %}
|
||||||
{% for line in lyrics_text.lines %}
|
{% if tune.lyrics is not none %}
|
||||||
|
{% for line in tune.lyrics.lines %}
|
||||||
\markup {
|
\markup {
|
||||||
\vspace #2
|
\vspace #2
|
||||||
\fill-line {
|
\fill-line {
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
\hspace #1
|
\hspace #1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{% if loop.index < lyrics_text.lines|length %}
|
{% if loop.index < tune.lyrics.lines|length %}
|
||||||
\markup {
|
\markup {
|
||||||
\vspace #1
|
\vspace #1
|
||||||
\fill-line {
|
\fill-line {
|
||||||
46
folkugat_web/assets/templates/lilypond/tune_in_set.ly
Normal file
46
folkugat_web/assets/templates/lilypond/tune_in_set.ly
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
\markup {
|
||||||
|
\vspace #2
|
||||||
|
\fill-line {
|
||||||
|
\center-column {
|
||||||
|
\large \bold "{{ tune.header.title | safe }}"
|
||||||
|
{% if tune.header.composer %}
|
||||||
|
"{{ tune.header.composer | safe }}"
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% if tune.score_source is not none %}
|
||||||
|
\score {
|
||||||
|
\language "english"
|
||||||
|
<<
|
||||||
|
{{ tune.score_source | safe }}
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
{% if tune.lyrics is not none %}
|
||||||
|
{% for line in tune.lyrics.lines %}
|
||||||
|
\markup {
|
||||||
|
\vspace #2
|
||||||
|
\fill-line {
|
||||||
|
{% for paragraph in line.paragraphs %}
|
||||||
|
\hspace #1
|
||||||
|
\center-column {
|
||||||
|
{% for line in paragraph.lines %}
|
||||||
|
\line { {{ line | safe }} }
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
|
\hspace #1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% if loop.index < tune.lyrics.lines|length %}
|
||||||
|
\markup {
|
||||||
|
\vspace #1
|
||||||
|
\fill-line {
|
||||||
|
\override #'(span-factor . 4/9)
|
||||||
|
\draw-hline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
31
folkugat_web/assets/templates/lilypond/tune_set.ly
Normal file
31
folkugat_web/assets/templates/lilypond/tune_set.ly
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{{ score_beginning }}
|
||||||
|
\version "2.24.4"
|
||||||
|
\book {
|
||||||
|
\paper {
|
||||||
|
top-margin = 10
|
||||||
|
left-margin = 15
|
||||||
|
right-margin = 15
|
||||||
|
|
||||||
|
scoreTitleMarkup = \markup {
|
||||||
|
\center-column {
|
||||||
|
\fontsize #3 \bold \fromproperty #'header:piece
|
||||||
|
\fill-line {
|
||||||
|
\null
|
||||||
|
\right-column {
|
||||||
|
\fromproperty #'header:composer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\header {
|
||||||
|
title = \markup { "{{ tune_set.title | safe }}" }
|
||||||
|
tagline = "Partitura generada amb LilyPond"
|
||||||
|
copyright = "Folkugat"
|
||||||
|
}
|
||||||
|
|
||||||
|
{% for tune in tune_set.tunes %}
|
||||||
|
{% include "lilypond/tune_in_set.ly" %}
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ def get_playlist_entries(
|
|||||||
id, session_id, set_id, tema_id
|
id, session_id, set_id, tema_id
|
||||||
FROM playlists
|
FROM playlists
|
||||||
WHERE {filter_clause}
|
WHERE {filter_clause}
|
||||||
|
ORDER BY id ASC
|
||||||
"""
|
"""
|
||||||
with get_connection(con) as con:
|
with get_connection(con) as con:
|
||||||
cur = con.cursor()
|
cur = con.cursor()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from folkugat_web.dal.sql import Connection, get_connection
|
from folkugat_web.dal.sql import Connection, get_connection
|
||||||
from folkugat_web.model import search as search_model
|
from folkugat_web.model import search as search_model
|
||||||
from folkugat_web.model import temes as model
|
from folkugat_web.model import temes as model
|
||||||
@@ -22,6 +24,21 @@ def get_tema_by_id(tema_id: int, con: Connection | None = None) -> model.Tema |
|
|||||||
return conversion.row_to_tema(row) if row else None
|
return conversion.row_to_tema(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_temes_by_ids(tema_ids: list[int], con: Connection | None = None) -> list[model.Tema]:
|
||||||
|
placeholders = ", ".join(["?" for _ in tema_ids])
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
id, title, alternatives, creation_date, modification_date, hidden
|
||||||
|
FROM temes
|
||||||
|
WHERE id IN ({placeholders})
|
||||||
|
"""
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
_ = cur.execute(query, tema_ids)
|
||||||
|
rows: Iterable[conversion.TemaRowTuple] = cur.fetchall()
|
||||||
|
return list(map(conversion.row_to_tema, rows))
|
||||||
|
|
||||||
|
|
||||||
def evict_tema_id_to_ngrams_cache():
|
def evict_tema_id_to_ngrams_cache():
|
||||||
global _tema_id_to_ngrams_cache
|
global _tema_id_to_ngrams_cache
|
||||||
_tema_id_to_ngrams_cache = None
|
_tema_id_to_ngrams_cache = None
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from collections.abc import Iterable
|
|||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
from folkugat_web.dal.sql import Connection, get_connection
|
from folkugat_web.dal.sql import Connection, get_connection
|
||||||
from folkugat_web.model import lilypond as lilypond_model
|
|
||||||
from folkugat_web.model import temes as model
|
from folkugat_web.model import temes as model
|
||||||
|
from folkugat_web.model.lilypond.processing import RenderError
|
||||||
|
|
||||||
ScoreRowTuple = tuple[int, int, str, str, str, str | None, str | None, bool]
|
ScoreRowTuple = tuple[int, int, str, str, str, str | None, str | None, bool]
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ def row_to_score(row: ScoreRowTuple) -> model.Score:
|
|||||||
tema_id=row[1],
|
tema_id=row[1],
|
||||||
title=row[2],
|
title=row[2],
|
||||||
source=row[3],
|
source=row[3],
|
||||||
errors=list(map(lilypond_model.RenderError.from_dict, errors_dicts)),
|
errors=list(map(RenderError.from_dict, errors_dicts)),
|
||||||
img_url=row[5],
|
img_url=row[5],
|
||||||
pdf_url=row[6],
|
pdf_url=row[6],
|
||||||
hidden=bool(row[7]),
|
hidden=bool(row[7]),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from folkugat_web.model.pagines import Pages
|
from folkugat_web.model.pagines import Pages
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from folkugat_web.config import calendari as config
|
from folkugat_web.config import calendari as config
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from folkugat_web.model import temes as model
|
from folkugat_web.model import temes as model
|
||||||
from folkugat_web.model.lilypond import RenderError
|
from folkugat_web.model.lilypond.processing import RenderError
|
||||||
from folkugat_web.model.pagines import Pages
|
from folkugat_web.model.pagines import Pages
|
||||||
from folkugat_web.services import sessions as sessions_service
|
from folkugat_web.services import sessions as sessions_service
|
||||||
from folkugat_web.services.temes import links as links_service
|
from folkugat_web.services.temes import links as links_service
|
||||||
|
|||||||
0
folkugat_web/model/lilypond/__init__.py
Normal file
0
folkugat_web/model/lilypond/__init__.py
Normal file
@@ -22,18 +22,3 @@ 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 LyricsLine:
|
|
||||||
paragraphs: list[LyricsParagraph]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class LyricsText:
|
|
||||||
lines: list[LyricsLine]
|
|
||||||
91
folkugat_web/model/lilypond/score.py
Normal file
91
folkugat_web/model/lilypond/score.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import dataclasses
|
||||||
|
import zlib
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from folkugat_web.model import temes
|
||||||
|
from folkugat_web.utils import batched
|
||||||
|
|
||||||
|
|
||||||
|
def get_hash(*v: bytes) -> bytes:
|
||||||
|
return zlib.adler32(b"".join(v)).to_bytes(4)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class LyricsParagraph:
|
||||||
|
lines: list[str]
|
||||||
|
|
||||||
|
def hash(self) -> bytes:
|
||||||
|
return get_hash(*(get_hash(line.encode()) for line in self.lines))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class LyricsLine:
|
||||||
|
paragraphs: list[LyricsParagraph]
|
||||||
|
|
||||||
|
def hash(self) -> bytes:
|
||||||
|
return get_hash(*(paragraph.hash() for paragraph in self.paragraphs))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class LyricsText:
|
||||||
|
lines: list[LyricsLine]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_lyrics(cls, lyrics: temes.Lyrics, max_cols: int = 3) -> Self:
|
||||||
|
paragraphs = [
|
||||||
|
LyricsParagraph(lines=par_str.splitlines())
|
||||||
|
for par_str in lyrics.content.split("\n\n") if par_str
|
||||||
|
]
|
||||||
|
return cls(lines=[
|
||||||
|
LyricsLine(paragraphs=list(line_pars))
|
||||||
|
for line_pars in batched(paragraphs, max_cols)
|
||||||
|
])
|
||||||
|
|
||||||
|
def hash(self) -> bytes:
|
||||||
|
return get_hash(*(line.hash() for line in self.lines))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class HeaderData:
|
||||||
|
title: str
|
||||||
|
composer: str | None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_tema(cls, tema: temes.Tema) -> Self:
|
||||||
|
return cls(
|
||||||
|
title=tema.title,
|
||||||
|
composer=tema.composer() or tema.origin(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def hash(self) -> bytes:
|
||||||
|
return get_hash(
|
||||||
|
self.title.encode(),
|
||||||
|
(self.composer or "").encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class LilypondTune:
|
||||||
|
header: HeaderData
|
||||||
|
score_source: str | None
|
||||||
|
lyrics: LyricsText | None
|
||||||
|
is_unknown: bool = False
|
||||||
|
|
||||||
|
def hash(self) -> bytes:
|
||||||
|
return get_hash(
|
||||||
|
self.header.hash(),
|
||||||
|
(self.score_source or "").encode(),
|
||||||
|
self.lyrics.hash() if self.lyrics else b"",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class LilypondSet:
|
||||||
|
title: str
|
||||||
|
tunes: list[LilypondTune]
|
||||||
|
|
||||||
|
def hash(self) -> bytes:
|
||||||
|
return get_hash(
|
||||||
|
self.title.encode(),
|
||||||
|
*(tune.hash() for tune in self.tunes),
|
||||||
|
)
|
||||||
@@ -51,7 +51,7 @@ class Set:
|
|||||||
raise ValueError("All PlaylistEntries must have the same session_id")
|
raise ValueError("All PlaylistEntries must have the same session_id")
|
||||||
return cls(
|
return cls(
|
||||||
id=set_id,
|
id=set_id,
|
||||||
temes=[TemaInSet.from_playlist_entry(entry) for entry in entries],
|
temes=[TemaInSet.from_playlist_entry(entry) for entry in sorted(entries, key=lambda e: e.id or 0)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import enum
|
|||||||
import itertools
|
import itertools
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
from folkugat_web.model.lilypond import RenderError
|
from folkugat_web.model.lilypond.processing import RenderError
|
||||||
from folkugat_web.model.search import NGrams
|
from folkugat_web.model.search import NGrams
|
||||||
from folkugat_web.model.sessions import Session
|
from folkugat_web.model.sessions import Session
|
||||||
from folkugat_web.services import ngrams
|
from folkugat_web.services import ngrams
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import os
|
|||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
import magic
|
import magic
|
||||||
from fastapi import HTTPException, UploadFile
|
from fastapi import HTTPException, UploadFile
|
||||||
from folkugat_web.config import db
|
from folkugat_web.config import db
|
||||||
@@ -57,8 +59,8 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str:
|
|||||||
|
|
||||||
def create_tema_filename(tema_id: int, extension: str = "") -> Path:
|
def create_tema_filename(tema_id: int, extension: str = "") -> Path:
|
||||||
filename = str(uuid.uuid4().hex) + extension
|
filename = str(uuid.uuid4().hex) + extension
|
||||||
filedir = db.DB_FILES_DIR / str(tema_id)
|
filedir = db.DB_FILES_DIR / "tema" / str(tema_id)
|
||||||
filedir.mkdir(exist_ok=True)
|
filedir.mkdir(parents=True, exist_ok=True)
|
||||||
filepath = filedir / filename
|
filepath = filedir / filename
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
@@ -71,6 +73,18 @@ def create_tmp_filename(extension: str = "") -> Path:
|
|||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def tmp_file(content: str):
|
||||||
|
input_filename = create_tmp_filename(extension=".ly")
|
||||||
|
async with aiofiles.open(input_filename, "w") as f:
|
||||||
|
_ = await f.write(content)
|
||||||
|
try:
|
||||||
|
yield input_filename
|
||||||
|
finally:
|
||||||
|
if input_filename.exists():
|
||||||
|
os.remove(input_filename)
|
||||||
|
|
||||||
|
|
||||||
def list_files(tema_id: str) -> list[str]:
|
def list_files(tema_id: str) -> list[str]:
|
||||||
filedir = db.DB_FILES_DIR / str(tema_id)
|
filedir = db.DB_FILES_DIR / str(tema_id)
|
||||||
return [get_db_file_path(f) for f in filedir.iterdir()]
|
return [get_db_file_path(f) for f in filedir.iterdir()]
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import dataclasses
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
from folkugat_web.model.lilypond import (LyricsLine, LyricsParagraph,
|
|
||||||
LyricsText, RenderError)
|
|
||||||
from folkugat_web.model.temes import Tema
|
|
||||||
from folkugat_web.services import files as files_service
|
|
||||||
from folkugat_web.templates import templates
|
|
||||||
from folkugat_web.utils import batched
|
|
||||||
|
|
||||||
SCORE_BEGINNING = "% --- SCORE BEGINNING --- %"
|
|
||||||
|
|
||||||
RenderFormat = Literal["png"] | Literal["pdf"]
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
print("AAAAAA")
|
|
||||||
print(lyrics_text)
|
|
||||||
print("AAAAAA")
|
|
||||||
return templates.get_template("lilypond/single_score.ly").render(
|
|
||||||
score_beginning=SCORE_BEGINNING,
|
|
||||||
score_source=source,
|
|
||||||
tema=tema,
|
|
||||||
lyrics_text=lyrics_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def render(source: str, fmt: RenderFormat, output_filename: Path | None = None) -> tuple[Path | None, list[RenderError]]:
|
|
||||||
input_filename = files_service.create_tmp_filename(extension=".ly")
|
|
||||||
async with aiofiles.open(input_filename, "w") as f:
|
|
||||||
_ = await f.write(source)
|
|
||||||
|
|
||||||
output_filename = output_filename or files_service.create_tmp_filename()
|
|
||||||
|
|
||||||
if fmt == "png":
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
"lilypond", "--png", "-o", output_filename, input_filename,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
"lilypond", "-o", output_filename, input_filename,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
_stdout, stderr = await proc.communicate()
|
|
||||||
|
|
||||||
if proc.returncode:
|
|
||||||
esc_input_filename = re.escape(str(input_filename))
|
|
||||||
error_re = re.compile(rf"{esc_input_filename}:(?P<line>\d+):(?P<pos>\d+): error: (?P<error>.*)$")
|
|
||||||
error_matches = filter(None, map(error_re.match, stderr.decode("utf-8").splitlines()))
|
|
||||||
errors = map(lambda m: RenderError.from_dict(m.groupdict()), error_matches)
|
|
||||||
line_offset = source.splitlines().index(SCORE_BEGINNING) + 1
|
|
||||||
errors_offset = [dataclasses.replace(e, line=e.line - line_offset) for e in errors if e.line > line_offset]
|
|
||||||
return None, errors_offset
|
|
||||||
|
|
||||||
if fmt == "png":
|
|
||||||
# Check if the output generated multiple pages
|
|
||||||
pages = sorted(output_filename.parent.rglob(f"{output_filename.name}*.png"))
|
|
||||||
if len(pages) > 1:
|
|
||||||
output_filename = pages[0]
|
|
||||||
for page in pages[1:]:
|
|
||||||
os.remove(page)
|
|
||||||
else:
|
|
||||||
output_filename = output_filename.with_suffix(".png")
|
|
||||||
elif fmt == "pdf":
|
|
||||||
output_filename = output_filename.with_suffix(".pdf")
|
|
||||||
|
|
||||||
return output_filename, []
|
|
||||||
|
|
||||||
|
|
||||||
def build_lyrics(text: str, max_cols: int = 3) -> LyricsText:
|
|
||||||
paragraphs = [LyricsParagraph(lines=par_str.splitlines()) for par_str in text.split("\n\n") if par_str]
|
|
||||||
print(list(batched(paragraphs, max_cols)))
|
|
||||||
return LyricsText(lines=[
|
|
||||||
LyricsLine(paragraphs=list(line_pars))
|
|
||||||
for line_pars in batched(paragraphs, max_cols)
|
|
||||||
])
|
|
||||||
0
folkugat_web/services/lilypond/__init__.py
Normal file
0
folkugat_web/services/lilypond/__init__.py
Normal file
102
folkugat_web/services/lilypond/build.py
Normal file
102
folkugat_web/services/lilypond/build.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from collections.abc import Iterable, Iterator
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from folkugat_web.dal.sql.temes import scores as scores_dal
|
||||||
|
from folkugat_web.model import playlists as playlists_model
|
||||||
|
from folkugat_web.model import temes as model
|
||||||
|
from folkugat_web.model.lilypond import score as lilypond_model
|
||||||
|
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 query as temes_q
|
||||||
|
from folkugat_web.services.temes import scores as scores_service
|
||||||
|
from folkugat_web.utils import FnChain
|
||||||
|
|
||||||
|
UNKNOWN_TITLE = "Desconegut"
|
||||||
|
|
||||||
|
|
||||||
|
def unknown_tune() -> lilypond_model.LilypondTune:
|
||||||
|
return lilypond_model.LilypondTune(
|
||||||
|
header=lilypond_model.HeaderData(
|
||||||
|
title=UNKNOWN_TITLE,
|
||||||
|
composer=None,
|
||||||
|
),
|
||||||
|
score_source=None,
|
||||||
|
lyrics=None,
|
||||||
|
is_unknown=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tune_from_tema(tema: model.Tema | None, score_source: str | None = None) -> lilypond_model.LilypondTune:
|
||||||
|
"""
|
||||||
|
The given `tema` is assumed to have properties, lyrics and scores (if source is None)
|
||||||
|
"""
|
||||||
|
if tema is None:
|
||||||
|
return unknown_tune()
|
||||||
|
return lilypond_model.LilypondTune(
|
||||||
|
header=lilypond_model.HeaderData.from_tema(tema=tema),
|
||||||
|
score_source=score_source or (tema.scores[0].source if tema.scores else None),
|
||||||
|
lyrics=lilypond_model.LyricsText.from_lyrics(lyrics=tema.lyrics[0]) if tema.lyrics else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tunes_from_temes(temes: Iterable[model.Tema | None]) -> list[lilypond_model.LilypondTune]:
|
||||||
|
"""
|
||||||
|
All `Tema` in `temes` are assumed to have properties, lyrics and scores
|
||||||
|
"""
|
||||||
|
return [tune_from_tema(tema) for tema in temes]
|
||||||
|
|
||||||
|
|
||||||
|
def tune_from_tema_id(tema_id: int, score_source: str | None = None) -> lilypond_model.LilypondTune:
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
|
if not tema:
|
||||||
|
raise HTTPException(status_code=404, detail="Could not find tema!")
|
||||||
|
tema = (
|
||||||
|
FnChain.transform(tema) |
|
||||||
|
properties_service.add_properties_to_tema |
|
||||||
|
lyrics_service.add_lyrics_to_tema
|
||||||
|
).result()
|
||||||
|
if score_source is None:
|
||||||
|
tema = scores_service.add_scores_to_tema(tema)
|
||||||
|
return tune_from_tema(tema=tema, score_source=score_source)
|
||||||
|
|
||||||
|
|
||||||
|
def tunes_from_tema_ids(tema_ids: list[int]) -> list[lilypond_model.LilypondTune]:
|
||||||
|
temes = (
|
||||||
|
FnChain.transform(temes_q.get_temes_by_ids(tema_ids=tema_ids)) |
|
||||||
|
properties_service.add_properties_to_temes |
|
||||||
|
lyrics_service.add_lyrics_to_temes |
|
||||||
|
scores_service.add_scores_to_temes |
|
||||||
|
list
|
||||||
|
).result()
|
||||||
|
return tunes_from_temes(temes=temes)
|
||||||
|
|
||||||
|
|
||||||
|
def build_set_title(temes: list[model.Tema | None]) -> str:
|
||||||
|
def build_title(tema: model.Tema | None) -> str:
|
||||||
|
return tema.title if tema else UNKNOWN_TITLE
|
||||||
|
return " i ".join(filter(bool, [
|
||||||
|
", ".join([build_title(tema) for tema in temes[:-1]]),
|
||||||
|
build_title(temes[-1])
|
||||||
|
]))
|
||||||
|
|
||||||
|
|
||||||
|
def set_from_set(set_entry: playlists_model.Set) -> lilypond_model.LilypondSet:
|
||||||
|
def _build_temes_by_id(temes: Iterable[model.Tema]) -> dict[int, model.Tema]:
|
||||||
|
return {tema.id: tema for tema in temes if tema.id is not None}
|
||||||
|
tema_ids = [tema_in_set.tema_id for tema_in_set in set_entry.temes]
|
||||||
|
temes_by_id = (
|
||||||
|
FnChain.transform([tema_id for tema_id in tema_ids if tema_id is not None]) |
|
||||||
|
temes_q.get_temes_by_ids |
|
||||||
|
properties_service.add_properties_to_temes |
|
||||||
|
lyrics_service.add_lyrics_to_temes |
|
||||||
|
scores_service.add_scores_to_temes |
|
||||||
|
_build_temes_by_id
|
||||||
|
).result()
|
||||||
|
temes = [temes_by_id[tema_id] if tema_id is not None else None for tema_id in tema_ids]
|
||||||
|
set_title = build_set_title(temes=temes)
|
||||||
|
tunes = tunes_from_temes(temes)
|
||||||
|
return lilypond_model.LilypondSet(
|
||||||
|
title=set_title,
|
||||||
|
tunes=tunes
|
||||||
|
)
|
||||||
105
folkugat_web/services/lilypond/render.py
Normal file
105
folkugat_web/services/lilypond/render.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from folkugat_web.log import logger
|
||||||
|
from folkugat_web.model.lilypond.processing import RenderError
|
||||||
|
from folkugat_web.services import files as files_service
|
||||||
|
from folkugat_web.utils import Result
|
||||||
|
|
||||||
|
from .source import SCORE_BEGINNING
|
||||||
|
|
||||||
|
RenderResult = Result[Path, list[RenderError]]
|
||||||
|
|
||||||
|
|
||||||
|
class RenderOutput(enum.Enum):
|
||||||
|
PDF = "pdf"
|
||||||
|
PNG_CROPPED = "png-cropped"
|
||||||
|
PNG_PREVIEW = "png-preview"
|
||||||
|
|
||||||
|
|
||||||
|
async def render(
|
||||||
|
source: str,
|
||||||
|
output: RenderOutput,
|
||||||
|
output_file: Path | None = None,
|
||||||
|
) -> RenderResult:
|
||||||
|
async with files_service.tmp_file(content=source) as input_file:
|
||||||
|
return await render_file(
|
||||||
|
input_file=input_file,
|
||||||
|
output=output,
|
||||||
|
output_file=output_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def render_file(
|
||||||
|
input_file: Path,
|
||||||
|
output: RenderOutput,
|
||||||
|
output_file: Path | None = None,
|
||||||
|
) -> RenderResult:
|
||||||
|
output_file = output_file or files_service.create_tmp_filename()
|
||||||
|
match output:
|
||||||
|
case RenderOutput.PDF:
|
||||||
|
command = [
|
||||||
|
"lilypond",
|
||||||
|
"-f", "pdf",
|
||||||
|
"-o", str(output_file),
|
||||||
|
str(input_file)
|
||||||
|
]
|
||||||
|
output_file = output_file.with_suffix(".pdf")
|
||||||
|
case RenderOutput.PNG_CROPPED:
|
||||||
|
command = [
|
||||||
|
"lilypond",
|
||||||
|
"-f", "png",
|
||||||
|
"-dcrop=#t",
|
||||||
|
"-dno-print-pages",
|
||||||
|
# "-duse-paper-size-for-page=#f",
|
||||||
|
"-o", str(output_file),
|
||||||
|
str(input_file)
|
||||||
|
]
|
||||||
|
output_file = output_file.with_suffix(".cropped.png")
|
||||||
|
# output_file = output_file.with_suffix(".png")
|
||||||
|
case RenderOutput.PNG_PREVIEW:
|
||||||
|
command = [
|
||||||
|
"lilypond",
|
||||||
|
"-f", "png",
|
||||||
|
"-dpreview=#t",
|
||||||
|
"-dno-print-pages",
|
||||||
|
"-o", str(output_file),
|
||||||
|
str(input_file)
|
||||||
|
]
|
||||||
|
output_file = output_file.with_suffix(".preview.png")
|
||||||
|
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*command,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
_stdout, stderr = await proc.communicate()
|
||||||
|
|
||||||
|
if proc.returncode:
|
||||||
|
errors = await _build_errors(input_file=input_file, stderr=stderr)
|
||||||
|
return Result(error=errors)
|
||||||
|
|
||||||
|
return Result(result=output_file)
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_errors(input_file: Path, stderr: bytes) -> list[RenderError]:
|
||||||
|
stderr_lines = stderr.decode("utf-8").splitlines()
|
||||||
|
logger.warning("Lilypond errors:")
|
||||||
|
for line in stderr_lines:
|
||||||
|
logger.warning(line)
|
||||||
|
async with aiofiles.open(input_file, "r") as f:
|
||||||
|
lines = await f.readlines()
|
||||||
|
|
||||||
|
offset_mark = SCORE_BEGINNING + "\n"
|
||||||
|
line_offset = lines.index(offset_mark) + 1 if offset_mark in lines else 0
|
||||||
|
esc_input_file = re.escape(str(input_file))
|
||||||
|
error_re = re.compile(rf"{esc_input_file}:(?P<line>\d+):(?P<pos>\d+): error: (?P<error>.*)$")
|
||||||
|
error_matches = filter(None, map(error_re.match, stderr_lines))
|
||||||
|
errors = map(lambda m: RenderError.from_dict(m.groupdict()), error_matches)
|
||||||
|
errors_offset = [dataclasses.replace(e, line=e.line - line_offset) for e in errors if e.line > line_offset]
|
||||||
|
return errors_offset
|
||||||
18
folkugat_web/services/lilypond/source.py
Normal file
18
folkugat_web/services/lilypond/source.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from folkugat_web.model.lilypond.score import LilypondSet, LilypondTune
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
SCORE_BEGINNING = "% --- SCORE BEGINNING --- %"
|
||||||
|
|
||||||
|
|
||||||
|
def tune_source(tune: LilypondTune) -> str:
|
||||||
|
return templates.get_template("lilypond/single_tune.ly").render(
|
||||||
|
score_beginning=SCORE_BEGINNING,
|
||||||
|
tune=tune,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_source(tune_set: LilypondSet) -> str:
|
||||||
|
return templates.get_template("lilypond/tune_set.ly").render(
|
||||||
|
score_beginning=SCORE_BEGINNING,
|
||||||
|
tune_set=tune_set,
|
||||||
|
)
|
||||||
@@ -5,7 +5,11 @@ from folkugat_web.model import temes as model
|
|||||||
|
|
||||||
|
|
||||||
def get_tema_by_id(tema_id: int) -> model.Tema | None:
|
def get_tema_by_id(tema_id: int) -> model.Tema | None:
|
||||||
return temes_q.get_tema_by_id(tema_id)
|
return temes_q.get_tema_by_id(tema_id=tema_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_temes_by_ids(tema_ids: list[int]) -> list[model.Tema]:
|
||||||
|
return temes_q.get_temes_by_ids(tema_ids=tema_ids)
|
||||||
|
|
||||||
|
|
||||||
def tema_compute_stats(
|
def tema_compute_stats(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from collections.abc import Iterable, Iterator
|
|||||||
from fastapi import HTTPException
|
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.model.lilypond import score as lilypond_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 lyrics as lyrics_service
|
||||||
from folkugat_web.services.temes import properties as properties_service
|
from folkugat_web.services.temes import properties as properties_service
|
||||||
@@ -46,13 +47,29 @@ def create_score(tema_id: int) -> model.Score:
|
|||||||
return scores_dal.insert_score(score=new_score)
|
return scores_dal.insert_score(score=new_score)
|
||||||
|
|
||||||
|
|
||||||
def build_single_tune_full_source(tema_id: int, source: str) -> str:
|
def build_tune_set_full_source(tema_ids: list[int]) -> str:
|
||||||
tema = temes_q.get_tema_by_id(tema_id)
|
temes = (
|
||||||
if not tema:
|
FnChain.transform(temes_q.get_temes_by_ids(tema_ids=tema_ids)) |
|
||||||
raise HTTPException(status_code=404, detail="Could not find tema!")
|
properties_service.add_properties_to_temes |
|
||||||
tema = (
|
lyrics_service.add_lyrics_to_temes |
|
||||||
FnChain.transform(tema) |
|
add_scores_to_temes |
|
||||||
properties_service.add_properties_to_tema |
|
list
|
||||||
lyrics_service.add_lyrics_to_tema
|
|
||||||
).result()
|
).result()
|
||||||
return lilypond.source_to_single_score(source=source, tema=tema)
|
if not temes:
|
||||||
|
return ""
|
||||||
|
set_title = " i ".join(filter(bool, [
|
||||||
|
", ".join([tema.title for tema in temes[:-1]]),
|
||||||
|
temes[-1].title
|
||||||
|
]))
|
||||||
|
tune_set = lilypond_model.LilypondSet(
|
||||||
|
title=set_title,
|
||||||
|
tunes=[
|
||||||
|
lilypond_model.LilypondTune(
|
||||||
|
header=lilypond_model.HeaderData.from_tema(tema=tema),
|
||||||
|
score_source=tema.scores[0].source if tema.scores else None,
|
||||||
|
lyrics=lilypond_model.LyricsText.from_lyrics(lyrics=tema.lyrics[0]) if tema.lyrics else None,
|
||||||
|
) for tema in temes
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print("TUNE SET HASH: ", tune_set.hash().hex())
|
||||||
|
return lilypond.tune_set_score(tune_set=tune_set)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import itertools
|
import itertools
|
||||||
from collections.abc import Callable, Iterable, Iterator
|
from collections.abc import Callable, Iterable, Iterator
|
||||||
from typing import Generic, Protocol, Self, TypeVar
|
from typing import Generic, Protocol, Self, TypeVar
|
||||||
@@ -55,3 +56,13 @@ def batched(iterable: Iterable[T], n: int) -> Iterator[tuple[T, ...]]:
|
|||||||
iterator = iter(iterable)
|
iterator = iter(iterable)
|
||||||
while batch := tuple(itertools.islice(iterator, n)):
|
while batch := tuple(itertools.islice(iterator, n)):
|
||||||
yield batch
|
yield batch
|
||||||
|
|
||||||
|
|
||||||
|
ResultT = TypeVar("ResultT")
|
||||||
|
ErrorT = TypeVar("ErrorT")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Result(Generic[ResultT, ErrorT]):
|
||||||
|
result: ResultT | None = None
|
||||||
|
error: ErrorT | None = None
|
||||||
|
|||||||
Reference in New Issue
Block a user