"""LilyScript — a symbolic-music AIGC app built on the LilyletNotaGen model. Left column: (1) generation parameter panel (2) streaming run log (3) .lyl file list (session outputs + built-in examples) (4) editable lyl editor Right column: sheet-music panel (placeholder for now; later a Lilylet music score renderer reusing the lilylet-live-editor pipeline). Generation streams patch-by-patch: raw decoded text (with `[r:x/y]` stream markers) goes to the run log, while the measure-segmented postprocessed text fills the editor. The backend is the int8 + two-level KV-cache ONNX generator (see lilyscript/generator.py); models load from a local dir for now. """ import os import re import time import json import gradio as gr from lilyscript.generator import StreamingLilyletGenerator HERE = os.path.dirname(os.path.abspath(__file__)) # TODO: swap for huggingface_hub.snapshot_download(repo_id=...) to pull the int8 # ONNX weights from the hub instead of a local dir. MODEL_DIR = os.environ.get('LILYSCRIPT_MODEL_DIR', os.path.join(HERE, 'models')) ASSET_DIR = os.path.join(HERE, 'assets') EXAMPLES_DIR = os.path.join(HERE, 'examples') OUTPUT_DIR = os.path.join(HERE, 'outputs') EXAMPLE_PREFIX = '\U0001F4C4 ' # 📄 examples OUTPUT_PREFIX = '✨ ' # ✨ session outputs # Suggested metadata values (editable — the dropdowns allow custom input), loaded # from assets/styles.json. Drawn from the NotaGenX period/instrumentation # vocabulary + values seen in examples. _STYLES = json.load(open(os.path.join(ASSET_DIR, 'styles.json'), encoding='utf-8')) COMPOSERS = _STYLES['composers'] GENRES = _STYLES['genres'] INSTRUMENTS = _STYLES['instruments'] _GEN = None def get_generator (): '''Lazily build the (heavy) ONNX generator on first use.''' global _GEN if _GEN is None: _GEN = StreamingLilyletGenerator(MODEL_DIR, ASSET_DIR) return _GEN def load_examples (): '''Read built-in example .lyl files into a {label: text} dict.''' store = {} if os.path.isdir(EXAMPLES_DIR): for name in sorted(os.listdir(EXAMPLES_DIR)): if name.endswith('.lyl'): with open(os.path.join(EXAMPLES_DIR, name), encoding='utf-8') as f: store[EXAMPLE_PREFIX + name] = f.read() return store def load_outputs (): '''Read previously-generated .lyl files from the outputs dir into a {label: text} dict, so past session outputs survive a server restart.''' store = {} if os.path.isdir(OUTPUT_DIR): for name in sorted(os.listdir(OUTPUT_DIR)): if name.endswith('.lyl'): with open(os.path.join(OUTPUT_DIR, name), encoding='utf-8') as f: store[OUTPUT_PREFIX + name[:-4]] = f.read() return store def load_library (): '''Initial file list: built-in examples + any persisted session outputs.''' return {**load_examples(), **load_outputs()} _STYLE_LINE_RE = re.compile(r'^\[(composer|genre|instrument)\s+".*"\]\s*$') def sync_prompt (composer, genre, instrument, current): '''Rewrite the metadata-prompt text from the three style dropdowns. The `[composer/genre/instrument "..."]` lines are regenerated from the dropdowns and placed at the top; any other lines the user typed (e.g. `[key "..."]`) are preserved below in their original order. ''' lines = [] for field, value in (('composer', composer), ('genre', genre), ('instrument', instrument)): value = (value or '').strip() if value: lines.append(f'[{field} "{value}"]') # keep every line that isn't one of the three managed style lines for ln in (current or '').splitlines(): if not _STYLE_LINE_RE.match(ln.strip()): if ln.strip(): lines.append(ln) return '\n'.join(lines) def run_generation (prompt, measures, temperature, max_patches, seed, store, top_k=0, top_p=0.9): '''Streaming generate callback. Yields updates for (log, editor, file_list, store). store: {label: lyl_text} dict held in gr.State; the produced document is added to it under a timestamped label once generation finishes. top_k / top_p have fixed defaults (no UI controls); pass them explicitly to override. ''' gen = get_generator() meas = int(measures) if measures and int(measures) > 0 else None store = dict(store or {}) raw = pretty = '' for raw, pretty, done in gen.generate_stream( prompt_text=prompt or '', max_patches=int(max_patches), temperature=float(temperature), top_k=int(top_k), top_p=float(top_p), measures=meas, seed=int(seed)): # stream: update log + editor, keep file list / store unchanged yield raw, pretty, gr.update(), store # finished: persist the document, refresh the file list, select the new entry label = OUTPUT_PREFIX + time.strftime('%H%M%S') + ('_m%d' % meas if meas else '') + '_s%d' % int(seed) store[label] = pretty os.makedirs(OUTPUT_DIR, exist_ok=True) with open(os.path.join(OUTPUT_DIR, label.replace(OUTPUT_PREFIX, '') + '.lyl'), 'w', encoding='utf-8') as f: f.write(pretty) yield raw, pretty, gr.update(choices=list(store.keys()), value=label), store def load_file (label, store): '''File-list selection -> load that document into the editor.''' return (store or {}).get(label, '') SHEET_PLACEHOLDER = '''