Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NotePilot</title> | |
| <link rel="stylesheet" href="/style.css"> | |
| </head> | |
| <body> | |
| <!-- ββ Score list ββ --> | |
| <div id="list-screen" class="screen active"> | |
| <header id="app-header"> | |
| <button class="brand-home" type="button" onclick="goHome()" aria-label="Return to home page"> | |
| <svg class="brand-mark" viewBox="0 0 64 64" aria-hidden="true" focusable="false"> | |
| <circle class="brand-mark-ring" cx="25" cy="38" r="18"></circle> | |
| <path class="brand-mark-note" d="M30 38.5a11 11 0 1 1-6.5-10V10.5c0-2 2.3-3.1 3.8-1.8l17 13.7c5 4 5.7 11.3 1.4 16.2l-2.5 2.8c1.2-5.6-.7-9.4-5.6-12.4l-8.1-5v14.5z"></path> | |
| <path class="brand-mark-arrow" d="M13.5 39.3l20-8.3-8.3 20-3.9-8-7.8-3.7z"></path> | |
| </svg> | |
| <span class="brand-word">Note<span>Pilot</span></span> | |
| </button> | |
| <div class="top-nav-actions"> | |
| <div id="nav-auth-user" class="nav-auth-user" style="display:none"> | |
| <span id="nav-auth-username"></span> | |
| <button class="btn btn-ghost nav-logout-btn" type="button" onclick="signOut()">Log out</button> | |
| </div> | |
| <button id="theme-toggle" class="theme-toggle" data-theme-toggle onclick="toggleTheme()" aria-label="Toggle light and dark mode">Light</button> | |
| </div> | |
| </header> | |
| <div id="auth-panel" class="auth-panel"> | |
| <div id="auth-logged-out" class="auth-card"> | |
| <h3>Sign in</h3> | |
| <p class="auth-copy">Create an account or sign in to keep your own piece library.</p> | |
| <div class="auth-form"> | |
| <button id="google-signin-btn" class="btn google-signin-btn" type="button" onclick="signInWithGoogle()"> | |
| <span aria-hidden="true">G</span> | |
| Continue with Google | |
| </button> | |
| <div id="google-auth-divider" class="auth-divider"><span>or</span></div> | |
| <input id="auth-email" type="text" placeholder="Username"> | |
| <input id="auth-password" type="password" placeholder="Password"> | |
| <div class="auth-actions"> | |
| <button class="btn btn-primary" type="button" onclick="signIn()">Log in</button> | |
| <button class="btn btn-ghost" type="button" onclick="signUp()">Sign up</button> | |
| <button class="btn btn-ghost guest-signin-btn" type="button" onclick="continueAsGuest()">Continue as Guest</button> | |
| </div> | |
| <div id="auth-status" class="auth-status">Loading sign-inβ¦</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="guest-banner" class="guest-banner" style="display:none"> | |
| <div> | |
| <strong>Guest Mode</strong> | |
| <span>Changes are saved only on this device.</span> | |
| </div> | |
| <div class="guest-banner-actions"> | |
| <button class="btn btn-ghost" type="button" onclick="resetGuestDemo()">Reset Demo</button> | |
| <button class="btn btn-primary" type="button" onclick="showCreateAccountPrompt('Create a free account to save your own pieces and progress.')">Create Account</button> | |
| </div> | |
| </div> | |
| <div class="library-header"> | |
| <div> | |
| <h2 id="library-title">Your Library</h2> | |
| <p id="library-subtitle">Choose a piece, set your part, and let NotePilot follow your tempo.</p> | |
| </div> | |
| <button id="add-piece-btn" class="btn btn-primary" onclick="openAddModal()">+ Add piece</button> | |
| </div> | |
| <div class="score-grid-toolbar"> | |
| <label for="score-grid-columns">Preview columns</label> | |
| <select id="score-grid-columns" class="instr-select" onchange="setScoreGridColumns(this.value)"> | |
| <option value="2">2</option> | |
| <option value="3" selected>3</option> | |
| <option value="4">4</option> | |
| <option value="5">5</option> | |
| <option value="6">6</option> | |
| </select> | |
| </div> | |
| <div id="score-grid" class="score-grid"></div> | |
| </div> | |
| <!-- ββ Add piece modal ββ --> | |
| <div id="add-modal" class="modal-backdrop" style="display:none" onclick="closeAddModal(event)"> | |
| <div class="modal"> | |
| <div class="modal-header"> | |
| <h3>Add a piece</h3> | |
| <button class="modal-close" onclick="closeAddModal()">β</button> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="modal-section"> | |
| <h4>Built-in library</h4> | |
| <p class="modal-copy">Search the music21 corpus and add a score directly.</p> | |
| </div> | |
| <div class="search-row"> | |
| <input id="corpus-search" type="text" placeholder="Search by composer or pieceβ¦" oninput="onSearchInput()" autocomplete="off"> | |
| <span id="search-spinner" style="display:none">β³</span> | |
| </div> | |
| <div id="search-results" class="search-results"> | |
| <p class="search-hint">Type to search the built-in music library (535 pieces).</p> | |
| </div> | |
| <div class="upload-divider"></div> | |
| <div class="modal-section"> | |
| <h4>Import PDF, images, or MusicXML</h4> | |
| <p class="modal-copy">Upload one PDF, a set of page screenshots, or a MusicXML/MuseScore file. PDF/images go through Audiveris first; MusicXML and MuseScore files import directly.</p> | |
| </div> | |
| <div class="upload-form"> | |
| <input id="import-name" type="text" placeholder="Piece name (optional)"> | |
| <input id="import-files" class="file-input-hidden" type="file" accept=".pdf,.png,.jpg,.jpeg,.xml,.mxl,.musicxml,.mscx,.mscz,image/png,image/jpeg,application/pdf,text/xml,application/xml" multiple> | |
| <div id="import-dropzone" class="file-dropzone" role="button" tabindex="0" aria-label="Choose files or drag files here"> | |
| <div class="file-dropzone-title">Choose files</div> | |
| <div class="file-dropzone-copy">or drag PDF, image, MusicXML, or MuseScore files here</div> | |
| <div id="import-file-label" class="file-dropzone-selection">No files selected</div> | |
| </div> | |
| <div class="upload-actions"> | |
| <button id="import-btn" type="button" class="btn btn-primary" onclick="importScoreFiles()">Import</button> | |
| <span id="import-status" class="import-status">No files selected.</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ Confirm dialog ββ --> | |
| <div id="confirm-modal" class="modal-backdrop" style="display:none"> | |
| <div class="modal confirm-modal"> | |
| <div class="modal-header"> | |
| <h3 id="confirm-modal-title">Are you sure?</h3> | |
| </div> | |
| <div class="modal-body"> | |
| <p id="confirm-modal-message" class="confirm-modal-message"></p> | |
| <div class="confirm-modal-actions"> | |
| <button id="confirm-modal-cancel" class="btn btn-ghost" type="button">Cancel</button> | |
| <button id="confirm-modal-ok" class="btn btn-danger" type="button">Delete</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ Play screen ββ --> | |
| <div id="play-screen" class="screen"> | |
| <div class="play-shell"> | |
| <aside class="play-sidebar"> | |
| <div class="play-sidebar-header"> | |
| <div class="play-sidebar-copy"> | |
| <h3>Pieces</h3> | |
| <p>Switch pieces without returning to the gallery.</p> | |
| </div> | |
| <button | |
| id="play-sidebar-toggle" | |
| class="play-sidebar-toggle" | |
| type="button" | |
| onclick="togglePlaySidebar()" | |
| aria-label="Collapse piece sidebar">β°</button> | |
| </div> | |
| <div id="play-piece-list" class="play-piece-list"></div> | |
| <div id="play-guest-banner" class="play-guest-banner" style="display:none"> | |
| <strong>Guest Mode</strong> | |
| <span>Changes are saved only on this device.</span> | |
| <div> | |
| <button class="btn btn-ghost" type="button" onclick="resetGuestDemo()">Reset Demo</button> | |
| <button class="btn btn-primary" type="button" onclick="showCreateAccountPrompt('Create a free account to save your own pieces and progress.')">Create Account</button> | |
| </div> | |
| </div> | |
| <div class="play-sidebar-footer"> | |
| <button class="sidebar-brand-home" type="button" onclick="goHome()" aria-label="Return to home page"> | |
| <span class="sidebar-brand-dot" aria-hidden="true">N</span> | |
| <span class="sidebar-brand-word">Note<span>Pilot</span></span> | |
| </button> | |
| <div class="sidebar-footer-actions"> | |
| <button class="theme-toggle sidebar-theme-toggle" data-theme-toggle onclick="toggleTheme()" aria-label="Toggle light and dark mode">Light</button> | |
| <div class="sidebar-account-menu"> | |
| <button | |
| id="sidebar-account-btn" | |
| class="sidebar-account-btn" | |
| type="button" | |
| onclick="toggleSidebarAccountMenu()" | |
| aria-label="Open account menu" | |
| aria-expanded="false">?</button> | |
| <div id="sidebar-account-popover" class="sidebar-account-popover" style="display:none"> | |
| <span class="sidebar-account-eyebrow">Profile</span> | |
| <strong id="sidebar-account-name">Not signed in</strong> | |
| <span id="sidebar-account-detail">Sign in to save your library.</span> | |
| <button id="sidebar-account-action" class="btn btn-ghost" type="button" onclick="sidebarAccountAction()">Sign in</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <div | |
| class="layout-splitter shell-splitter" | |
| id="shell-splitter" | |
| role="separator" | |
| aria-label="Resize pieces sidebar" | |
| aria-orientation="vertical" | |
| aria-valuemin="180" | |
| aria-valuemax="420" | |
| aria-valuenow="260" | |
| tabindex="0"> | |
| <span></span> | |
| </div> | |
| <div class="play-content"> | |
| <div class="progress-bar"><div id="progress-fill" class="progress-fill" style="width:0%"></div></div> | |
| <div class="practice-area"> | |
| <div class="practice-workspace" id="practice-workspace"> | |
| <div class="sheet-section"> | |
| <div class="sheet-header"> | |
| <div class="sheet-title-row"> | |
| <h4>Sheet music</h4> | |
| <span id="sheet-fingering-status" class="sheet-status-badge" style="display:none"></span> | |
| </div> | |
| <div class="sheet-toolbar"> | |
| <button id="sheet-generate-fingering-btn" class="sheet-action-btn" onclick="generateSheetFingering()" style="display:none">Generate fingering</button> | |
| <button id="sheet-toggle-fingering-btn" class="sheet-action-btn" onclick="toggleSheetFingering()" style="display:none">Show fingering</button> | |
| <button class="sheet-tool-btn" onclick="sheetZoom(-0.1)" title="Zoom out" aria-label="Zoom out">β</button> | |
| <span id="sheet-zoom-label" class="sheet-zoom-label">100%</span> | |
| <button class="sheet-tool-btn" onclick="sheetZoom(0.1)" title="Zoom in" aria-label="Zoom in">+</button> | |
| <button class="sheet-tool-btn" onclick="sheetResetView()" title="Reset view" aria-label="Reset view">β²</button> | |
| <button class="sheet-tool-btn" onclick="sheetRotate()" title="Rotate 90Β°" aria-label="Rotate">β»</button> | |
| <button class="sheet-tool-btn" onclick="sheetDownload()" title="Download source" aria-label="Download">β¬</button> | |
| </div> | |
| </div> | |
| <div id="sheet-fingering-progress" class="sheet-fingering-progress" style="display:none"> | |
| <div class="sheet-fingering-progress-meta"> | |
| <span id="sheet-fingering-progress-message">Generating fingering</span> | |
| <span id="sheet-fingering-progress-value">0%</span> | |
| </div> | |
| <div class="sheet-fingering-progress-bar" aria-hidden="true"> | |
| <div id="sheet-fingering-progress-fill" class="sheet-fingering-progress-fill"></div> | |
| </div> | |
| </div> | |
| <div class="sheet-viewer" id="sheet-viewer"> | |
| <div class="sheet-content-wrap" id="sheet-content-wrap"> | |
| <iframe id="sheet-frame" style="display:none"></iframe> | |
| <div id="sheet-musicxml-fallback" class="sheet-musicxml-fallback" style="display:none"></div> | |
| </div> | |
| <div id="sheet-placeholder" class="sheet-placeholder"> | |
| No sheet music available for this score.<br> | |
| Re-run <code>convert_score.py</code> with verovio installed to generate it. | |
| </div> | |
| </div> | |
| </div> | |
| <div | |
| class="practice-splitter" | |
| id="practice-splitter" | |
| role="separator" | |
| aria-label="Resize sheet music and keyboard visualizer" | |
| aria-orientation="horizontal" | |
| aria-valuemin="0" | |
| aria-valuemax="100" | |
| aria-valuenow="50" | |
| tabindex="0"> | |
| <span></span> | |
| </div> | |
| <div class="keyboard-section" id="keyboard-section"> | |
| <div class="keyboard-heading"> | |
| <div class="keyboard-heading-main"> | |
| <h4>Keyboard visualizer</h4> | |
| <div class="help-tip" tabindex="0" aria-label="Keyboard layout help"> | |
| ? | |
| <div class="help-tip-popup">A S D J K L ; play C D E F G A B. W E I O P play the black keys.</div> | |
| </div> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:.6rem"> | |
| <span id="midi-status" class="midi-badge midi-badge-disconnected" title="MIDI piano status"> | |
| <span class="midi-badge-dot"></span> | |
| <span id="midi-status-text">No piano detected</span> | |
| </span> | |
| <div class="keyboard-view-toggle"> | |
| <button class="keyboard-view-btn" id="keyboard-view-full" onclick="setKeyboardLayoutMode('full')">Full</button> | |
| <button class="keyboard-view-btn" id="keyboard-view-mini" onclick="setKeyboardLayoutMode('mini')">Mini</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="piano-stage"> | |
| <div class="mini-keyboard-panel" id="mini-keyboard-panel"> | |
| <div id="note-highway" class="note-highway"></div> | |
| <div class="kb-layout" id="mini-keyboard-layout"> | |
| <div id="kb-row-main" class="kb-row"></div> | |
| </div> | |
| </div> | |
| <div class="reference-keyboard-panel" id="full-keyboard-panel"> | |
| <div id="full-note-highway" class="full-note-highway"> | |
| <div class="full-note-hit-line"></div> | |
| </div> | |
| <div class="reference-keyboard-shell" id="reference-keyboard-shell"> | |
| <div class="reference-keyboard" id="reference-keyboard"> | |
| <div id="full-kb-whites" class="reference-whites"></div> | |
| <div id="full-kb-blacks" class="reference-blacks"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div | |
| class="layout-splitter panel-splitter" | |
| id="panel-splitter" | |
| role="separator" | |
| aria-label="Resize practice panel" | |
| aria-orientation="vertical" | |
| aria-valuemin="260" | |
| aria-valuemax="520" | |
| aria-valuenow="320" | |
| tabindex="0"> | |
| <span></span> | |
| </div> | |
| <aside class="practice-panel"> | |
| <section class="practice-card setup-card"> | |
| <h4>Practice Setup</h4> | |
| <div class="setup-control-block"> | |
| <label for="bpm-input">Starting tempo</label> | |
| <div class="bpm-row"> | |
| <input id="bpm-input" type="number" value="60" min="20" max="300"> | |
| <span>BPM</span> | |
| </div> | |
| </div> | |
| <div class="tempo-actions setup-actions"> | |
| <button id="start-btn" class="btn btn-primary" onclick="handleStartPause()">βΆ Start</button> | |
| <button id="stop-btn" class="btn btn-danger" onclick="stopPlaying()" disabled>β End</button> | |
| </div> | |
| <div class="latency-row"> | |
| <label for="latency-slider">Latency</label> | |
| <input id="latency-slider" type="range" min="0" max="500" step="10" value="0" | |
| oninput="setLatencyCompensation(this.value)"> | |
| <span id="latency-value">0 ms</span> | |
| </div> | |
| <div id="part-picker" class="part-picker" style="display:none"> | |
| <h5>Your part</h5> | |
| <div id="part-buttons" class="part-buttons"></div> | |
| </div> | |
| <div class="accompaniment-row"> | |
| <span>Accompaniment</span> | |
| <strong>Other parts</strong> | |
| </div> | |
| </section> | |
| <section class="practice-card performance-card"> | |
| <div class="practice-card-header"> | |
| <h4>Practice</h4> | |
| <span id="practice-input-status" class="practice-pill">Keyboard</span> | |
| </div> | |
| <div class="next-note-card"> | |
| <span>Next note</span> | |
| <div id="next-note-display">β</div> | |
| </div> | |
| <div class="status-panel"> | |
| <div class="status-row"> | |
| <span>Current beat</span> | |
| <span class="status-val" id="beat-val">β</span> | |
| </div> | |
| <div class="status-row"> | |
| <span>Detected BPM</span> | |
| <span class="status-val" id="tempo-val">β</span> | |
| </div> | |
| <div class="status-row"> | |
| <span>Following</span> | |
| <span class="status-val" id="practice-following-status">Ready</span> | |
| </div> | |
| </div> | |
| </section> | |
| <section id="guest-tips-card" class="practice-card guest-tips-card" style="display:none"> | |
| <h4>Demo Tips</h4> | |
| <ul id="guest-tips-list"></ul> | |
| <div class="guest-annotation-actions"> | |
| <button class="btn btn-ghost" type="button" onclick="addGuestAnnotation('Practice this measure slowly')">Mark slow spot</button> | |
| <button class="btn btn-ghost" type="button" onclick="addGuestAnnotation('Good run')">Mark good run</button> | |
| </div> | |
| <div id="guest-annotation-list" class="guest-annotation-list"></div> | |
| </section> | |
| <section class="practice-card input-mode-section"> | |
| <h4>Input method</h4> | |
| <div class="input-mode-tabs"> | |
| <button class="mode-tab active" id="tab-keyboard" onclick="setInputMode('keyboard')">β¨ Keyboard</button> | |
| <button class="mode-tab" id="tab-mic" onclick="setInputMode('mic')">π Microphone <span class="beta-badge">Beta</span></button> | |
| </div> | |
| <div id="mic-controls" style="display:none"> | |
| <div class="mic-row"> | |
| <div class="mic-status-dot" id="mic-dot"></div> | |
| <span id="mic-status-text" style="font-size:.85rem;color:#aaa">Mic off</span> | |
| <span id="mic-note-display" style="font-size:.85rem;font-weight:700;color:#7c6af7;margin-left:auto"></span> | |
| <span id="mic-debug" style="font-size:.75rem;color:#555;margin-left:.75rem;font-family:monospace"></span> | |
| </div> | |
| <div class="mic-row" style="margin-top:.6rem"> | |
| <label style="font-size:.8rem;color:#aaa;width:100px">Microphone</label> | |
| <select id="mic-select" class="instr-select" style="flex:1" onchange="onMicChange()"> | |
| <option value="">β select β</option> | |
| </select> | |
| </div> | |
| <div class="mic-row" style="margin-top:.6rem"> | |
| <label style="font-size:.8rem;color:#aaa;width:100px">Speaker</label> | |
| <select id="speaker-select" class="instr-select" style="flex:1" onchange="onSpeakerChange()"> | |
| <option value="">β default β</option> | |
| </select> | |
| </div> | |
| <div class="level-meter-wrap" style="margin-top:.75rem"> | |
| <div class="level-meter-track" id="level-track"> | |
| <div class="level-meter-fill" id="level-fill"></div> | |
| <div class="level-meter-peak" id="level-peak"></div> | |
| <div class="level-meter-gate" id="level-gate"></div> | |
| <span class="level-gate-label" id="level-gate-label">gate</span> | |
| </div> | |
| </div> | |
| <div class="mic-row" style="margin-top:.6rem"> | |
| <label style="font-size:.8rem;color:#aaa;width:100px">Noise gate</label> | |
| <input id="noise-gate" type="range" min="1" max="80" value="1" | |
| oninput="onNoiseGateChange(this.value)" style="flex:1"> | |
| <span id="noise-gate-val" style="font-size:.8rem;color:#555;width:36px;text-align:right">1</span> | |
| </div> | |
| <p style="font-size:.75rem;color:#555;margin-top:.5rem;line-height:1.5"> | |
| The bar shows input level. The red line is the noise gate β sounds below it are ignored.<br> | |
| Raise the gate if false notes fire; lower it if your playing isn't registering. | |
| </p> | |
| </div> | |
| </section> | |
| </aside> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="upgrade-toast" class="upgrade-toast" style="display:none"> | |
| <button class="upgrade-toast-close" type="button" onclick="hideUpgradeToast()" aria-label="Dismiss">β</button> | |
| <strong>Create a free account</strong> | |
| <p id="upgrade-toast-message">Save your own pieces and progress across devices.</p> | |
| <div class="upgrade-toast-actions"> | |
| <button class="btn btn-primary" type="button" onclick="showCreateAccountPrompt('Create a free account to save your own pieces and progress.')">Create Account</button> | |
| <button class="btn btn-ghost" type="button" onclick="hideUpgradeToast()">Keep exploring</button> | |
| </div> | |
| </div> | |
| <script src="https://unpkg.com/tone@15.1.22/build/Tone.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/opensheetmusicdisplay@1.9.2/build/opensheetmusicdisplay.min.js"></script> | |
| <script src="/pitch.js"></script> | |
| <script src="/app.js"></script> | |
| </body> | |
| </html> | |