"""app.py – ProGress Music Generation Demo""" from __future__ import annotations # ZeroGPU: import `spaces` before torch so its CUDA-emulation hooks install # first. No-op when not on ZeroGPU; absent (and skipped) in local dev. try: import spaces # noqa: F401 except Exception: pass import base64 import json import os # Disable Gradio's SSR (Node-proxy) mode everywhere, including Hugging Face which # enables it by default. Under SSR the frontend can't build file URLs, so audio # sources collapse to bare relative paths and the survey samples fail to play. # Set before gradio reads it; reinforced by ssr_mode=False on launch() below. os.environ["GRADIO_SSR_MODE"] = "False" import random import sys import tempfile from pathlib import Path import gradio as gr import pandas as pd sys.path.insert(0, str(Path(__file__).parent)) import backend # ── CSS ─────────────────────────────────────────────────────────────────────── CSS = """ /* Fonts (Fraunces + Inter) are loaded via a in the document head (see FONTS_HEAD) — NOT an @import here. Gradio injects this CSS through a constructed stylesheet, where @import is disallowed and silently dropped ("@import rules are not allowed here"), so the web fonts never loaded. */ :root { /* Warm "manuscript" palette — ink on paper with a restrained claret accent. */ --pg-ink: #221a10; /* headings / primary text */ --pg-body: #463a2c; /* body text */ --pg-muted: #8a7b66; /* secondary / captions */ --pg-line: #e7dcc6; /* hairline borders */ --pg-line-soft: #f1ead9; /* very light dividers */ --pg-surface: rgba(255, 253, 247, .66); /* frosted panel (translucent) */ --pg-surface-solid: #fdfaf3; /* opaque variant — menus/popovers */ --pg-tint: #faf5ea; /* page / subtle paper fill */ --pg-code: #fcf8f0; /* code panels — soft warm, a hair lighter than page */ --pg-accent: #8a2b2b; /* claret */ --pg-accent-dk: #6e2020; --pg-field-border: #ddd1ba; /* inputs / secondary buttons */ --pg-hover: #f3ecda; /* secondary button hover */ --pg-row-hover: #f5ecdd; /* table row hover */ --pg-shadow: 0 1px 2px rgba(45,30,10,.04), 0 1px 3px rgba(45,30,10,.05); --pg-shadow-md: 0 6px 20px rgba(45,30,10,.10); --pg-serif: 'Fraunces', Georgia, 'Times New Roman', serif; } /* Dark theme = "candlelit manuscript": warm dark walnut, cream ink, a terracotta accent. Gradio adds `.dark` to the app root, so redefining the palette here flips every --pg-* surface/text; Gradio's own components follow their built-in dark theme, so the two stay coherent. */ .dark { --pg-ink: #f4ecdb; --pg-body: #ddd0bb; --pg-muted: #a8997f; --pg-line: #3a3024; --pg-line-soft: #2a2219; --pg-surface: rgba(34, 27, 19, .55); --pg-surface-solid: #241d15; --pg-tint: #17120b; --pg-code: #211a12; --pg-accent: #d98b6a; /* warm terracotta pops on dark */ --pg-accent-dk: #c4734f; --pg-field-border: #4a3d2d; --pg-hover: #2c241a; --pg-row-hover: #2e2519; --pg-shadow: 0 1px 2px rgba(0,0,0,.35), 0 1px 3px rgba(0,0,0,.45); --pg-shadow-md: 0 6px 20px rgba(0,0,0,.55); } /* ── Recolor Gradio's own blue accents to claret ─────────────────────────── The radios, checkboxes, sliders, the active workflow tab, links and the pale label chips are driven by the theme's *primary* hue (blue) plus a few component vars — NOT by our --pg-* tokens — so they stayed blue. Redefining the primary ramp + those vars recolors them all in one place. (The theme itself can't be .set()-customized — this Gradio build crashes on that — so the override lives here in CSS, which Gradio injects after its own theme rules.) */ :root { --primary-50: #fbf3f3; --primary-100: #f6e1e1; --primary-200: #ecc6c6; --primary-300: #d99e9e; --primary-400: #c16c6c; --primary-500: #a23c3c; --primary-600: #8a2b2b; --primary-700: #6e2020; --primary-800: #5a1c1c; --primary-900: #4a1919; --primary-950: #2c0e0e; --color-accent: #8a2b2b; --color-accent-soft: #f6e1e1; --slider-color: #8a2b2b; --checkbox-background-color-selected: #8a2b2b; --checkbox-border-color-selected: #8a2b2b; --block-label-text-color: #8a2b2b; --block-title-text-color: #8a2b2b; --block-label-background-fill: #f6e1e1; --block-title-background-fill: #f6e1e1; --link-text-color: #8a2b2b; --link-text-color-hover: #6e2020; --link-text-color-active: #6e2020; --link-text-color-visited: #6e2020; } /* Dark mode: Gradio injects its (blue) dark palette AFTER this CSS, so these need !important to win. The label chips also use --block-(label|title)-background- fill, a hardcoded blue in the dark theme — override those too. */ .dark { --primary-50: #2c1812 !important; --primary-100: #3a201a !important; --primary-200: #4e2c22 !important; --primary-300: #6e3e30 !important; --primary-400: #9a5a45 !important; --primary-500: #c4734f !important; --primary-600: #d98b6a !important; --primary-700: #e3a486 !important; --primary-800: #ecbda6 !important; --primary-900: #f3d3c4 !important; --primary-950: #f9e8df !important; --color-accent: #d98b6a !important; --color-accent-soft: #3a201a !important; --slider-color: #d98b6a !important; --checkbox-background-color-selected: #d98b6a !important; --checkbox-border-color-selected: #d98b6a !important; --block-label-background-fill: #3a201a !important; --block-title-background-fill: #3a201a !important; --block-label-text-color: #e0a184 !important; --block-title-text-color: #e0a184 !important; --link-text-color: #e0a184 !important; --link-text-color-hover: #ecbda6 !important; --link-text-color-active: #ecbda6 !important; --link-text-color-visited: #ecbda6 !important; } /* Element-level fallbacks for accents whose variable plumbing varies by build: the selected workflow tab (text + underline) and inline links. */ .tab-nav button.selected, .tab-container button.selected, button[role="tab"].selected, button[role="tab"][aria-selected="true"] { color: var(--pg-accent) !important; border-bottom-color: var(--pg-accent) !important; } .gradio-container a:not(.button) { color: var(--pg-accent) !important; } .gradio-container a:not(.button):hover { color: var(--pg-accent-dk) !important; } /* ── Warm, translucent panels for Gradio's own surfaces ───────────────────── Drive every block / input / radio group / accordion / slider / table through Gradio's surface + border vars so none stay stark white or Gradio grey. A frosted (translucent) fill lets the warm page show through; borders are warm hairlines. Nested wrappers are flattened separately (see .pg-accordion) so the translucency never doubles up into uneven patches. */ :root, .gradio-container { --block-background-fill: rgba(255, 253, 247, .66); --panel-background-fill: rgba(255, 253, 247, .66); --background-fill-primary: rgba(255, 253, 247, .66); --background-fill-secondary: rgba(255, 252, 246, .5); --input-background-fill: rgba(255, 254, 250, .72); --input-background-fill-focus: rgba(255, 255, 255, .92); --checkbox-label-background-fill: transparent; --code-background-fill: var(--pg-code); --block-border-color: var(--pg-line); --border-color-primary: var(--pg-line); --input-border-color: var(--pg-field-border); } .dark, .dark .gradio-container { --block-background-fill: rgba(34, 27, 19, .55); --panel-background-fill: rgba(34, 27, 19, .55); --background-fill-primary: rgba(34, 27, 19, .55); --background-fill-secondary: rgba(24, 19, 12, .5); --input-background-fill: rgba(46, 37, 26, .6); --input-background-fill-focus: rgba(56, 45, 31, .85); --checkbox-label-background-fill: transparent; --code-background-fill: var(--pg-code); --block-border-color: var(--pg-line); --border-color-primary: var(--pg-line); --input-border-color: var(--pg-field-border); } /* Center the whole app in a constrained column instead of edge-to-edge. */ .gradio-container, .gradio-container input, .gradio-container button, .gradio-container textarea, .gradio-container select { font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif !important; } .gradio-container { font-size: 16px !important; width: 100% !important; /* fill up to max-width; don't shrink to content */ max-width: 940px !important; margin: 0 auto !important; padding: 8px 28px 56px !important; color: var(--pg-body); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } /* Keep the page a stable width regardless of content: the container and its inner wrappers fill the available width, and wide content (e.g. the BibTeX block) scrolls instead of stretching the layout. Without this, opening the citation accordion widened the page (and the full-width button), and the Compose tab — with narrower content — made it shrink. */ .gradio-container > div, .gradio-container .main, .gradio-container .wrap { width: 100% !important; } .gradio-container [class*="column"], .gradio-container [class*="row"] { min-width: 0 !important; } .cm-editor, .cm-scroller, [data-testid="code"] { max-width: 100% !important; } [data-testid="code"] { overflow-x: auto !important; } /* Typographic scale — restrained, consistent. */ .gr-markdown p, .gr-markdown li, .prose p, .prose li { font-size: 1rem; line-height: 1.6; } .gr-markdown ol li, .prose ol li { margin-bottom: 6px; } .gr-markdown h2, .prose h2 { font-family: var(--pg-serif) !important; font-size: 1.72rem !important; font-weight: 600; color: var(--pg-ink); letter-spacing: -.005em; margin: 6px 0 16px; padding-bottom: 11px; border-bottom: 1px solid var(--pg-line); } .gr-markdown h3, .prose h3 { font-family: var(--pg-serif) !important; font-size: 1.22rem !important; font-weight: 600; color: var(--pg-ink); letter-spacing: -.005em; } label, .gr-input-label { font-size: .95rem !important; color: var(--pg-body); } /* Page background (handled in CSS because this Gradio build crashes on a .set()-customized theme — see app.py launch() note). IMPORTANT: do NOT blanket-style .block / .form here. In Gradio 6 those classes sit on nearly every wrapper (markdown, the header, each slider…), so adding borders/shadows there boxes the whole page into nested cards with uneven margins. Surfaces come from the theme defaults plus the explicit .stat-box / .section-card classes below. */ body, gradio-app, .gradio-container { background: var(--pg-tint) !important; } /* Real panels (inputs, dataframe, accordions, code) sit on white; plain text/markdown/button rows stay transparent on the page tint. */ .gr-input, .gr-box, .gr-panel, .cm-editor, [data-testid="textbox"], [data-testid="dataframe"] { border-radius: 10px !important; } input, textarea, select { border-radius: 9px !important; border-color: var(--pg-field-border) !important; } /* Harmonic-structure dropdown: let the warm paper show through instead of a hard white block. The block + closed field go translucent (a hairline border keeps it reading as a control); the open options menu is forced opaque so it stays readable over whatever is behind it. */ /* Dropdown: a subtle frosted panel like the other surfaces. The frosted fill lives on the dropdown block (.pg-dropdown); the wrapper Gradio frames it in (its .form / parent .block) is flattened so the panel never doubles up into a darker box. The field uses the shared input fill; the menu stays opaque. */ .pg-dropdown { background: var(--block-background-fill) !important; border: 1px solid var(--pg-line) !important; border-radius: 10px !important; box-shadow: var(--pg-shadow) !important; } .form:has(.pg-dropdown), .gradio-container [class*="form"]:has(.pg-dropdown), .gradio-container .block:has(> .pg-dropdown) { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; } .pg-dropdown .wrap, .pg-dropdown .wrap-inner, .pg-dropdown .secondary-wrap, .pg-dropdown input { background: var(--input-background-fill) !important; } .pg-dropdown .wrap { border: 1px solid var(--pg-field-border) !important; border-radius: 9px !important; } .pg-dropdown ul, .pg-dropdown ul.options, .gradio-container ul.options { background: var(--pg-surface-solid) !important; } /* Reusable frosted panel (e.g. the Sort-by row): same look as the dropdown — one frosted fill with a hairline frame and soft lift, with the inner component blocks flattened so the fill stays a single even layer (no darker doubling). */ .pg-panel { background: var(--block-background-fill) !important; border: 1px solid var(--pg-line) !important; border-radius: 10px !important; box-shadow: var(--pg-shadow) !important; padding: 10px 14px !important; } .pg-panel .block, .pg-panel .form { background: transparent !important; border: none !important; box-shadow: none !important; } /* Code blocks (BibTeX) & accordions read as a soft inset on the paper rather than bright white slabs: a warm parchment / transparent fill with a hairline frame. Syntax tokens and open content stay fully legible. */ .pg-code { background: transparent !important; border: none !important; box-shadow: none !important; } .pg-code [data-testid="code"] { border: 1px solid var(--pg-line) !important; border-radius: 10px !important; box-shadow: none !important; } .pg-code [data-testid="code"], .pg-code .cm-editor, .pg-code .cm-scroller, .pg-code .cm-gutters { background: var(--pg-code) !important; } .pg-code .cm-gutters { border-right: 1px solid var(--pg-line) !important; } .pg-accordion { background: var(--block-background-fill) !important; border: 1px solid var(--pg-line) !important; border-radius: 10px !important; box-shadow: none !important; } .pg-accordion .label-wrap { background: transparent !important; border: none !important; } /* Flatten the accordion's inner wrappers so nested blocks/forms (e.g. the slider row) don't stack a second frosted layer on top of the accordion's panel. */ .pg-accordion [data-testid="accordion-content"], .pg-accordion [data-testid="accordion-content"] .block, .pg-accordion [data-testid="accordion-content"] .form { background: transparent !important; } /* Primary / secondary button colours */ button.primary { background: var(--pg-accent) !important; color: #fff !important; border: none !important; } button.primary:hover { background: var(--pg-accent-dk) !important; } button.secondary { background: var(--pg-surface) !important; color: var(--pg-body) !important; border: 1px solid var(--pg-field-border) !important; } button.secondary:hover { background: var(--pg-hover) !important; } /* Action buttons: medium weight, calm radius, subtle motion. Scoped to primary/secondary so Gradio's internal buttons keep their layout. */ button.primary, button.secondary { flex-grow: 0 !important; font-size: .98rem !important; font-weight: 600 !important; min-height: 42px; border-radius: 9px !important; letter-spacing: .005em; transition: transform .08s ease, box-shadow .15s ease, background .15s ease; } button.primary { box-shadow: var(--pg-shadow); } button.primary:hover { box-shadow: var(--pg-shadow-md); transform: translateY(-1px); } button.primary:active { transform: translateY(0); } button.secondary:hover { transform: translateY(-1px); } button.lg { font-size: 1.02rem !important; padding: 12px 26px !important; min-height: 50px; width: auto !important; min-width: 230px; flex: 0 0 auto !important; } button.sm { min-height: 34px; font-size: .9rem !important; width: fit-content !important; border-radius: 8px !important; } /* Evenly space the three workflow tabs across the full width. gradio 6 decides tab overflow (the "…" menu) by summing the button widths in a HIDDEN ".tab-container.visually-hidden" measuring copy and comparing to the wrapper width. So we must spread ONLY the visible nav and leave the hidden measurer at its natural width — restyling that copy makes it over-measure and push the last tab into the overflow menu. */ .tab-container:not(.visually-hidden) { width: 100% !important; } .tab-container:not(.visually-hidden) > button { flex: 1 1 0 !important; min-width: 0 !important; justify-content: center !important; } /* Older gradio builds use .tab-nav (no hidden measuring copy). */ .tab-nav { display: flex !important; } .tab-nav button { flex: 1 1 0 !important; justify-content: center !important; } .tab-nav button, .tab-container button { font-size: 1rem !important; font-weight: 550 !important; } /* Accordion headers (How it works, Generation parameters, Sections) */ .label-wrap span { font-size: 1.02rem !important; font-weight: 600; color: var(--pg-ink); } /* Header — title-page treatment: tracked eyebrow, serif wordmark, short rule. */ #header { text-align:center; padding: 40px 0 26px; margin-bottom: 8px; overflow-x: clip; } /* the overflowing "fancy" logo text can't cause page scroll */ #header .eyebrow { margin: 0 0 12px; font-size: .74rem; font-weight: 600; color: var(--pg-muted); text-transform: uppercase; letter-spacing: .24em; } #header h1 { margin: 0; font-family: var(--pg-serif) !important; font-size: 3.1rem; font-weight: 600; color: var(--pg-ink); letter-spacing: -.012em; cursor: pointer; user-select: none; -webkit-user-select: none; /* JS (sizeLogo) floors the hit area at min-width ≈ the width of "ProGress" so the hover zone is stable while the text backspaces; width stays auto so the longer "fancy" text grows the (centered) box symmetrically rather than overflowing it to one side. white-space:nowrap keeps it on one line; line-height + min-height stop the box collapsing (and the page jumping) when the text is fully deleted. */ display: inline-block; text-align: center; white-space: nowrap; line-height: 1.18; min-height: 1.18em; } #header .rule { width: 52px; margin: 16px auto 0; border-top: 2px solid var(--pg-accent); } #header .tagline { margin: 14px 0 0; font-family: var(--pg-serif) !important; font-style: italic; font-size: 1.14rem; color: var(--pg-body); font-weight: 400; } #header .device-badge { margin: 16px 0 0; font-size: .74rem; font-weight: 600; color: var(--pg-muted); display: inline-block; padding: 4px 13px; border: 1px solid var(--pg-line); border-radius: 999px; background: var(--pg-surface); letter-spacing: .08em; text-transform: uppercase; } .step-sub { color: var(--pg-muted); font-size: 1rem; margin: 0 0 16px; } /* Stat cards: clean white surface, soft shadow, gentle hover lift. */ .stat-row { display: flex; gap: 16px; margin: 4px 0; } .stat-row .stat-box { flex: 1 1 0; } .stat-box { background: var(--pg-surface); border: 1px solid var(--pg-line); border-radius: 12px; padding: 20px 18px; text-align: center; box-shadow: var(--pg-shadow); transition: box-shadow .15s ease, transform .1s ease; } .stat-box:hover { box-shadow: var(--pg-shadow-md); transform: translateY(-2px); } .stat-box .stat-num { font-family: var(--pg-serif) !important; font-size: 2.7rem; font-weight: 600; color: var(--pg-accent); line-height: 1; letter-spacing: -.01em; } .stat-box .stat-lbl { font-size: .82rem; color: var(--pg-muted); margin-top: 8px; text-transform: uppercase; letter-spacing: .06em; font-weight: 600; } /* Section cards */ .section-card { border: 1px solid var(--pg-line); border-radius: 12px; padding: 16px 18px; margin-bottom: 14px; background: var(--pg-surface); box-shadow: var(--pg-shadow); } .section-label { font-family: var(--pg-serif) !important; font-size: 1.18rem; font-weight: 600; color: var(--pg-ink); margin-bottom: 6px; } .section-meta { font-size: .92rem; color: var(--pg-muted); margin-bottom: 10px; } /* Data table: lighter grid, readable rows, clear hover affordance. */ table th { font-size: .82rem !important; text-transform: uppercase; letter-spacing: .05em; color: var(--pg-muted) !important; font-weight: 600 !important; background: var(--pg-tint) !important; padding: 12px 14px !important; } table td { font-size: .98rem !important; padding: 11px 14px !important; color: var(--pg-body); } table tbody tr { transition: background .1s ease; } table tbody tr:hover td { background: var(--pg-row-hover) !important; cursor: pointer; } /* Selected radio option: white text for contrast on the claret chip (only radios — checkboxes sit on the page, so white would make them vanish). */ label:has(> input[type="radio"]:checked), label:has(> input[type="radio"]:checked) span { color: #fff !important; } /* Dataframe — make a click read as ROW selection, not a spreadsheet cell. Gradio 6 uses a virtualized table: body cells are
(NOT ), the selected one is .cell-selected (it draws the accent "box" via a box-shadow), and two floating ' f'' f'0:00' f'
' ) def home_samples_html() -> str: survey = "".join( _sample_card(f"Example {i}", f"example_{i}.mp3") for i in range(1, 11) ) tango = _sample_card("A Little Tango", "tango.mp3", tango=True) return ( '
' '

ProGress survey examples

' '

Pieces generated by ProGress, used in our listening study.

' f'
{survey}
' '

A Little Tango

' '

Our Bach-trained model generating tango rhythms — ' 'just changing the timbre made some nice results.

' f'
{tango}
' '
' ) AUTHORS_HTML = """

Stephen Ni-Hahn*, Chao Péter Yang*

Mingchen Ma, Cynthia Rudin, Simon Mak, Yue Jiang

Duke University

stephen.hahn@duke.edu · peter.yang@duke.edu

*Equal contribution

""" # Abstract, verbatim from the ProGress demo site. ABSTRACT_HTML = """

Abstract

Artificial Intelligence (AI) for music generation is undergoing rapid developments, with recent symbolic models leveraging sophisticated deep learning and diffusion-model algorithms. One drawback of existing models is that they lack structural cohesion, particularly with respect to harmonic–melodic structure. Furthermore, such existing models are largely “black-box” in nature and are not musically interpretable. This paper addresses these limitations via a novel generative music framework that incorporates concepts of Schenkerian analysis (SchA) in concert with a diffusion modeling framework. This framework, which we call ProGress (Prolongation-enhanced DiGress), adapts state-of-the-art deep models for discrete diffusion (in particular, the DiGress model of Vignac et al., 2023) for interpretable and structured music generation.

Concretely, our contributions include:

  1. Novel adaptations of the DiGress model for music generation;
  2. A novel SchA-inspired phrase fusion methodology; and
  3. A framework allowing users to control various aspects of the generation process to create coherent musical compositions.

Results from human experiments suggest superior performance to existing state-of-the-art methods.

""" # ── App ─────────────────────────────────────────────────────────────────────── def create_app() -> gr.Blocks: # NOTE: theme + css are passed to launch(), NOT here. Gradio 6 moved these # off the Blocks constructor; on Hugging Face, passing them to Blocks is # ignored (with a UserWarning) and the Space renders unstyled — so they must # live on demo.launch() (see __main__). with gr.Blocks(title="ProGress") as demo: # ── State ────────────────────────────────────────────────────────────── pool_state = gr.State(None) # phrases_data dict starting_id = gr.State(None) # locked beginning phrase id preview_pid = gr.State(None) # currently previewed phrase id sorted_df_state = gr.State(None) # last Python-sorted df (for click lookup) # ── Header ───────────────────────────────────────────────────────────── gr.HTML( '', head=FONTS_HEAD + AUDIO_BLOB_HEAD + backend.MIDI_PLAYER_HEAD + backend.OSMD_HEAD + CAT_EGGS_HEAD, ) # ── Home / landing (samples + call-to-action) ────────────────────────── with gr.Column(visible=True) as home_view: gr.HTML(AUTHORS_HTML) home_gen_btn = gr.Button("Generate New Music", variant="primary", size="lg") gr.HTML(ABSTRACT_HTML) with gr.Accordion("Cite this work", open=False, elem_classes="pg-accordion"): gr.Code(BIBTEX, language=None, label=None, interactive=False, elem_classes="pg-code") # Survey samples as plain