Spaces:
Running
Running
Commit ·
9659b5a
1
Parent(s): 256d374
refined UI.
Browse files- app.py +114 -11
- web/layout-fit.js +65 -0
app.py
CHANGED
|
@@ -350,6 +350,9 @@ def build_head ():
|
|
| 350 |
# the mount/bridge script that wires it to the hidden #ls-editor-state textbox.
|
| 351 |
os.path.join(vendor, 'lyl-editor.bundle.js'),
|
| 352 |
os.path.join(WEB_DIR, 'lyl-editor-mount.js'),
|
|
|
|
|
|
|
|
|
|
| 353 |
]
|
| 354 |
tags = ['<script>window.__LILYSCRIPT_SOUNDFONT_URL=%r;window.__LILYSCRIPT_FLUID_URL=%r;</script>'
|
| 355 |
% (_file_url(os.path.join(WEB_DIR, 'soundfont')) + '/',
|
|
@@ -439,10 +442,9 @@ CUSTOM_CSS = '''
|
|
| 439 |
white-space: normal;
|
| 440 |
word-break: break-all;
|
| 441 |
}
|
| 442 |
-
/* Score List
|
| 443 |
-
|
| 444 |
.score-list {
|
| 445 |
-
max-height: 324px;
|
| 446 |
overflow-y: auto;
|
| 447 |
}
|
| 448 |
/* Generate button turns yellow while a generation is running (the .ls-generating
|
|
@@ -464,6 +466,107 @@ CUSTOM_CSS = '''
|
|
| 464 |
#stop-btn.ls-generating:hover {
|
| 465 |
background: #cf2e2e !important;
|
| 466 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
'''
|
| 468 |
|
| 469 |
|
|
@@ -474,11 +577,11 @@ def build_ui ():
|
|
| 474 |
gr.Markdown('## 🎼 LilyScript — symbolic music generation with Lilylet')
|
| 475 |
store = gr.State(examples)
|
| 476 |
|
| 477 |
-
with gr.Row(
|
| 478 |
# ---------------- LEFT ----------------
|
| 479 |
-
with gr.Column(scale=5):
|
| 480 |
# (1) compose params, with (2) the collapsible run log stacked below
|
| 481 |
-
with gr.Group():
|
| 482 |
gr.Markdown('## Compose')
|
| 483 |
with gr.Group():
|
| 484 |
gr.Markdown('- Style Options')
|
|
@@ -503,19 +606,19 @@ def build_ui ():
|
|
| 503 |
gen_btn = gr.Button('Generate', variant='primary', elem_id='gen-btn')
|
| 504 |
stop_btn = gr.Button('Stop', variant='stop', elem_id='stop-btn')
|
| 505 |
|
| 506 |
-
with gr.Accordion('Logs', open=True):
|
| 507 |
log = gr.Textbox(show_label=False, lines=10, max_lines=10,
|
| 508 |
autoscroll=True, interactive=False, container=False)
|
| 509 |
|
| 510 |
-
# bottom row: (3) file list | (4) editor
|
| 511 |
-
with gr.Row(equal_height=True):
|
| 512 |
with gr.Column(scale=2, min_width=160):
|
| 513 |
with gr.Group():
|
| 514 |
gr.Markdown('## Score List')
|
| 515 |
file_list = gr.Radio(show_label=False, choices=list(examples.keys()),
|
| 516 |
value=None, interactive=True, container=False,
|
| 517 |
elem_classes=['score-list'])
|
| 518 |
-
with gr.Column(scale=5):
|
| 519 |
with gr.Group():
|
| 520 |
gr.Markdown('## Lilylet editor')
|
| 521 |
# Our own CodeMirror 6 editor (lyl-editor.bundle.js) mounts into
|
|
@@ -532,7 +635,7 @@ def build_ui ():
|
|
| 532 |
elem_classes=['ls-editor-state-hidden'], show_label=False, container=False)
|
| 533 |
|
| 534 |
# ---------------- RIGHT ----------------
|
| 535 |
-
with gr.Column(scale=6):
|
| 536 |
with gr.Group():
|
| 537 |
gr.Markdown('## Sheet music')
|
| 538 |
gr.HTML(SHEET_PLACEHOLDER)
|
|
|
|
| 350 |
# the mount/bridge script that wires it to the hidden #ls-editor-state textbox.
|
| 351 |
os.path.join(vendor, 'lyl-editor.bundle.js'),
|
| 352 |
os.path.join(WEB_DIR, 'lyl-editor-mount.js'),
|
| 353 |
+
# computes --ls-fill-h so the bottom Score List | editor row fills the height
|
| 354 |
+
# left under Compose + Logs (robust against Gradio's flex-nesting quirks).
|
| 355 |
+
os.path.join(WEB_DIR, 'layout-fit.js'),
|
| 356 |
]
|
| 357 |
tags = ['<script>window.__LILYSCRIPT_SOUNDFONT_URL=%r;window.__LILYSCRIPT_FLUID_URL=%r;</script>'
|
| 358 |
% (_file_url(os.path.join(WEB_DIR, 'soundfont')) + '/',
|
|
|
|
| 442 |
white-space: normal;
|
| 443 |
word-break: break-all;
|
| 444 |
}
|
| 445 |
+
/* Score List scrolls within the height its column is given (see the layout model
|
| 446 |
+
at the bottom of this stylesheet). */
|
| 447 |
.score-list {
|
|
|
|
| 448 |
overflow-y: auto;
|
| 449 |
}
|
| 450 |
/* Generate button turns yellow while a generation is running (the .ls-generating
|
|
|
|
| 466 |
#stop-btn.ls-generating:hover {
|
| 467 |
background: #cf2e2e !important;
|
| 468 |
}
|
| 469 |
+
|
| 470 |
+
/* ---- Layout: viewport-locked two-column workspace ----------------------------
|
| 471 |
+
Goal: Compose + Logs keep their natural height; the Score List | editor row fills
|
| 472 |
+
the rest; a long score must NOT stretch the page (it scrolls inside the sheet
|
| 473 |
+
panel instead). Gradio's deep wrapper nesting makes pure-CSS flex height
|
| 474 |
+
propagation unreliable, so the panel heights are driven by a small ResizeObserver
|
| 475 |
+
in web/layout-fit.js (sets --ls-fill-h on :root = viewport − the panels above the
|
| 476 |
+
fill row). These rules consume that variable. */
|
| 477 |
+
#main-row {
|
| 478 |
+
align-items: flex-start; /* don't stretch columns to each other's height */
|
| 479 |
+
}
|
| 480 |
+
/* Right (Sheet music) column: the score preview scrolls inside a viewport-capped
|
| 481 |
+
height instead of growing the row. */
|
| 482 |
+
#sheet-col {
|
| 483 |
+
position: sticky;
|
| 484 |
+
top: 8px;
|
| 485 |
+
}
|
| 486 |
+
#sheet-col .ls-score-root {
|
| 487 |
+
height: calc(100vh - 110px);
|
| 488 |
+
min-height: 320px;
|
| 489 |
+
}
|
| 490 |
+
#sheet-col .ls-preview {
|
| 491 |
+
overflow: auto; /* the SVG scrolls here, not the page */
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
/* Left column stacks naturally: Compose, Logs, then the fill row. */
|
| 495 |
+
#compose-col {
|
| 496 |
+
display: flex;
|
| 497 |
+
flex-direction: column;
|
| 498 |
+
}
|
| 499 |
+
/* The bottom Score List | editor row gets the height left under Compose + Logs,
|
| 500 |
+
computed by layout-fit.js into --ls-fill-h (with a sane fallback + floor). */
|
| 501 |
+
#compose-col > .lp-fill {
|
| 502 |
+
height: var(--ls-fill-h, 420px);
|
| 503 |
+
min-height: 360px;
|
| 504 |
+
}
|
| 505 |
+
#compose-col > .lp-fill > .column,
|
| 506 |
+
#compose-col > .lp-fill > .column > .gr-group,
|
| 507 |
+
#compose-col > .lp-fill > .column > .gr-group > .gr-group {
|
| 508 |
+
height: 100%;
|
| 509 |
+
min-height: 0;
|
| 510 |
+
display: flex;
|
| 511 |
+
flex-direction: column;
|
| 512 |
+
}
|
| 513 |
+
/* Score-list column: the "## Score List" header (a .prose block) stays natural at
|
| 514 |
+
the top; the radio list scrolls in the remaining space. The header block sits in
|
| 515 |
+
the inner group alongside the radio, so pin it 0-shrink and let the list fill. */
|
| 516 |
+
#compose-col > .lp-fill > .column:first-child .block:has(.prose) {
|
| 517 |
+
flex: 0 0 auto;
|
| 518 |
+
}
|
| 519 |
+
#compose-col > .lp-fill > .column:first-child .block:has(.score-list) {
|
| 520 |
+
flex: 1 1 0;
|
| 521 |
+
min-height: 0;
|
| 522 |
+
display: flex;
|
| 523 |
+
flex-direction: column;
|
| 524 |
+
overflow: hidden;
|
| 525 |
+
}
|
| 526 |
+
#compose-col > .lp-fill > .column:first-child .score-list {
|
| 527 |
+
flex: 1 1 auto;
|
| 528 |
+
min-height: 0;
|
| 529 |
+
}
|
| 530 |
+
/* Logs textbox stays compact even on long output. */
|
| 531 |
+
#compose-col > .lp-fixed.gr-accordion textarea {
|
| 532 |
+
max-height: 200px;
|
| 533 |
+
}
|
| 534 |
+
/* Editor column: the embedded CodeMirror mount fills the space under its header.
|
| 535 |
+
DOM (Gradio): #editor-col > .gr-group > .gr-group > .styler > { headerBlock,
|
| 536 |
+
editorBlock(.html-container > .gradio-style > #ls-editor-mount), hiddenStateBlock }.
|
| 537 |
+
We make the chain down to .styler full-height flex columns, let the editor block
|
| 538 |
+
fill (flex:1, basis 0 so a long score can't inflate it), and keep the header
|
| 539 |
+
block natural. CM then scrolls inside the bounded mount. */
|
| 540 |
+
#editor-col,
|
| 541 |
+
#editor-col > .gr-group,
|
| 542 |
+
#editor-col > .gr-group > .gr-group,
|
| 543 |
+
#editor-col .styler {
|
| 544 |
+
display: flex;
|
| 545 |
+
flex-direction: column;
|
| 546 |
+
height: 100%;
|
| 547 |
+
min-height: 0;
|
| 548 |
+
}
|
| 549 |
+
/* the editor block (the one wrapping the gr.HTML mount) fills the styler height */
|
| 550 |
+
#editor-col .styler > .block:has(.html-container) {
|
| 551 |
+
flex: 1 1 0;
|
| 552 |
+
min-height: 0;
|
| 553 |
+
display: flex;
|
| 554 |
+
flex-direction: column;
|
| 555 |
+
overflow: hidden;
|
| 556 |
+
}
|
| 557 |
+
#editor-col .html-container,
|
| 558 |
+
#editor-col .html-container > .gradio-style {
|
| 559 |
+
flex: 1 1 0;
|
| 560 |
+
min-height: 0;
|
| 561 |
+
display: flex;
|
| 562 |
+
flex-direction: column;
|
| 563 |
+
overflow: hidden;
|
| 564 |
+
}
|
| 565 |
+
#editor-col #ls-editor-mount {
|
| 566 |
+
flex: 1 1 0;
|
| 567 |
+
height: auto; /* override lyl-editor.css fixed 470px */
|
| 568 |
+
min-height: 0;
|
| 569 |
+
}
|
| 570 |
'''
|
| 571 |
|
| 572 |
|
|
|
|
| 577 |
gr.Markdown('## 🎼 LilyScript — symbolic music generation with Lilylet')
|
| 578 |
store = gr.State(examples)
|
| 579 |
|
| 580 |
+
with gr.Row(elem_id='main-row'):
|
| 581 |
# ---------------- LEFT ----------------
|
| 582 |
+
with gr.Column(scale=5, elem_id='compose-col'):
|
| 583 |
# (1) compose params, with (2) the collapsible run log stacked below
|
| 584 |
+
with gr.Group(elem_classes=['lp-fixed']):
|
| 585 |
gr.Markdown('## Compose')
|
| 586 |
with gr.Group():
|
| 587 |
gr.Markdown('- Style Options')
|
|
|
|
| 606 |
gen_btn = gr.Button('Generate', variant='primary', elem_id='gen-btn')
|
| 607 |
stop_btn = gr.Button('Stop', variant='stop', elem_id='stop-btn')
|
| 608 |
|
| 609 |
+
with gr.Accordion('Logs', open=True, elem_classes=['lp-fixed']):
|
| 610 |
log = gr.Textbox(show_label=False, lines=10, max_lines=10,
|
| 611 |
autoscroll=True, interactive=False, container=False)
|
| 612 |
|
| 613 |
+
# bottom row: (3) file list | (4) editor — flex-fills the remaining height
|
| 614 |
+
with gr.Row(equal_height=True, elem_classes=['lp-fill']):
|
| 615 |
with gr.Column(scale=2, min_width=160):
|
| 616 |
with gr.Group():
|
| 617 |
gr.Markdown('## Score List')
|
| 618 |
file_list = gr.Radio(show_label=False, choices=list(examples.keys()),
|
| 619 |
value=None, interactive=True, container=False,
|
| 620 |
elem_classes=['score-list'])
|
| 621 |
+
with gr.Column(scale=5, elem_id='editor-col'):
|
| 622 |
with gr.Group():
|
| 623 |
gr.Markdown('## Lilylet editor')
|
| 624 |
# Our own CodeMirror 6 editor (lyl-editor.bundle.js) mounts into
|
|
|
|
| 635 |
elem_classes=['ls-editor-state-hidden'], show_label=False, container=False)
|
| 636 |
|
| 637 |
# ---------------- RIGHT ----------------
|
| 638 |
+
with gr.Column(scale=6, elem_id='sheet-col'):
|
| 639 |
with gr.Group():
|
| 640 |
gr.Markdown('## Sheet music')
|
| 641 |
gr.HTML(SHEET_PLACEHOLDER)
|
web/layout-fit.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* LilyScript layout fitter.
|
| 2 |
+
*
|
| 3 |
+
* Gradio's deep wrapper nesting makes pure-CSS flex height propagation unreliable
|
| 4 |
+
* (a long score in the right panel, or the editor's own content, can blow out the
|
| 5 |
+
* left column). So we compute the height available to the bottom "Score List |
|
| 6 |
+
* editor" row in JS and expose it as the CSS variable --ls-fill-h on :root.
|
| 7 |
+
*
|
| 8 |
+
* --ls-fill-h = viewport_height - fillRow.top - bottom_gap
|
| 9 |
+
*
|
| 10 |
+
* i.e. the fill row gets exactly the space left under Compose + Logs, down to the
|
| 11 |
+
* bottom of the viewport. score-player.css / app.py consume the variable. We
|
| 12 |
+
* recompute on resize and whenever the left column's size changes (Logs expanding,
|
| 13 |
+
* accordion toggle, fonts loading) via a ResizeObserver.
|
| 14 |
+
*/
|
| 15 |
+
(function () {
|
| 16 |
+
'use strict';
|
| 17 |
+
|
| 18 |
+
var BOTTOM_GAP = 16; // breathing room below the fill row
|
| 19 |
+
var MIN_FILL = 360; // never collapse the editor/score-list below this
|
| 20 |
+
|
| 21 |
+
function fillEl () {
|
| 22 |
+
var col = document.getElementById('compose-col');
|
| 23 |
+
return col ? col.querySelector(':scope > .lp-fill') : null;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function recompute () {
|
| 27 |
+
var fill = fillEl();
|
| 28 |
+
if (!fill) return;
|
| 29 |
+
var top = fill.getBoundingClientRect().top; // viewport-relative
|
| 30 |
+
var avail = window.innerHeight - top - BOTTOM_GAP;
|
| 31 |
+
if (avail < MIN_FILL) avail = MIN_FILL;
|
| 32 |
+
document.documentElement.style.setProperty('--ls-fill-h', avail + 'px');
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
var raf = null;
|
| 36 |
+
function schedule () {
|
| 37 |
+
if (raf) return;
|
| 38 |
+
raf = requestAnimationFrame(function () { raf = null; recompute(); });
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function boot () {
|
| 42 |
+
recompute();
|
| 43 |
+
window.addEventListener('resize', schedule);
|
| 44 |
+
// observe the left column so Logs growth / accordion toggles re-fit
|
| 45 |
+
var col = document.getElementById('compose-col');
|
| 46 |
+
if (col && window.ResizeObserver) {
|
| 47 |
+
var ro = new ResizeObserver(schedule);
|
| 48 |
+
ro.observe(col);
|
| 49 |
+
// also observe the two fixed blocks directly (their height drives fill.top)
|
| 50 |
+
var fixed = col.querySelectorAll(':scope > .lp-fixed');
|
| 51 |
+
for (var i = 0; i < fixed.length; i++) ro.observe(fixed[i]);
|
| 52 |
+
}
|
| 53 |
+
// Gradio mounts asynchronously; retry a few times until #compose-col exists.
|
| 54 |
+
if (!col) setTimeout(boot, 300);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if (document.readyState === 'loading') {
|
| 58 |
+
document.addEventListener('DOMContentLoaded', boot);
|
| 59 |
+
} else {
|
| 60 |
+
boot();
|
| 61 |
+
}
|
| 62 |
+
// a late pass after fonts/score player settle
|
| 63 |
+
setTimeout(recompute, 1200);
|
| 64 |
+
console.log('[layout-fit] loaded');
|
| 65 |
+
})();
|