Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ $(document).ready(function() {
DescriptorManager.init();
ConfigManager.init();
InferenceManager.init();
CustomizationManager.init();

// Attach event handlers
$("#model").on('change', () => UIManager.updateModelSettings());
Expand All @@ -973,6 +974,188 @@ $(document).ready(function() {
UIManager.updateModelSettings();
}

// Customization Manager for preview time and background
const CustomizationManager = {
audio: null,
lastPickerPosition: 0,

init() {
this.attachEventHandlers();
this.updatePreviewDisplay();
},

attachEventHandlers() {
$('#pick-preview-btn').on('click', () => this.openPreviewPicker());
$('#preview_time').on('input', () => this.updatePreviewDisplay());
$('#background_path').on('input blur', () => this.updateBackgroundPreview());
},

updatePreviewDisplay() {
const ms = parseInt($('#preview_time').val());
const $display = $('#preview-time-display');

if (!isNaN(ms) && ms >= 0) {
const seconds = ms / 1000;
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
$display.text(`(${minutes}:${secs.toString().padStart(2, '0')})`).addClass('has-value');
} else {
$display.text('').removeClass('has-value');
}
},

updateBackgroundPreview() {
const bgPath = $('#background_path').val().trim();
const $preview = $('#background-preview');
const $img = $('#background-preview-img');

if (!bgPath) {
$preview.hide();
return;
}

$.ajax({
url: '/get_image_preview',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ path: bgPath }),
success: (response) => {
if (response.success && response.data) {
$img.attr('src', 'data:image/' + response.type + ';base64,' + response.data);
$preview.show();
} else {
$preview.hide();
}
},
error: () => $preview.hide()
});
},

openPreviewPicker() {
const audioPath = $('#audio_path').val().trim();

if (!audioPath) {
Utils.showFlashMessage('Please select an audio file first.', 'error');
return;
}

this.createPreviewModal(audioPath);
},

createPreviewModal(audioPath) {
$('#preview-picker-modal').remove();

const modalHtml = `
<div id="preview-picker-modal" class="preview-modal-overlay">
<div class="preview-modal">
<div class="preview-modal-header">
<h3>Pick Preview Point</h3>
<button type="button" class="preview-modal-close">×</button>
</div>
<div class="preview-modal-body">
<audio id="preview-audio" controls style="width: 100%; margin-bottom: 15px;"></audio>
<div class="preview-time-labels">
<span id="preview-current-time">0:00</span>
</div>
<div class="preview-input-row">
<label>Milliseconds:</label>
<input type="number" id="preview-ms-input" min="0" value="${this.lastPickerPosition}" />
</div>
<div class="preview-buttons-row">
<button type="button" id="preview-goto-btn" class="browse-button">Go to Time</button>
<button type="button" id="preview-setcurrent-btn" class="browse-button">Use Current</button>
<button type="button" id="preview-set-btn" class="browse-button accent">Use This Point</button>
</div>
</div>
</div>
</div>
`;

$('body').append(modalHtml);
this.setupPreviewAudio(audioPath);
},

setupPreviewAudio(audioPath) {
$.ajax({
url: '/get_audio_info',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ path: audioPath }),
success: (response) => {
if (response.success) {
this.initializeAudioPlayer(response.url);
} else {
Utils.showFlashMessage('Failed to load audio: ' + (response.message || 'Unknown error'), 'error');
$('#preview-picker-modal').remove();
}
},
error: () => {
Utils.showFlashMessage('Failed to load audio file', 'error');
$('#preview-picker-modal').remove();
}
});

$('.preview-modal-close, .preview-modal-overlay').on('click', (e) => {
if (e.target === e.currentTarget) {
this.closePreviewModal();
}
});
Comment on lines +1098 to +1102
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event handlers for modal close are attached multiple times if the modal is opened repeatedly. The click handler on '.preview-modal-overlay' is attached inside setupPreviewAudio which is called every time the modal opens, leading to multiple handlers being bound. Move this event handler attachment to createPreviewModal or use event delegation to avoid duplicate handlers.

Copilot uses AI. Check for mistakes.
},

initializeAudioPlayer(audioUrl) {
const $audio = $('#preview-audio');
$audio.attr('src', audioUrl);

const audio = $audio[0];
this.audio = audio;

// Seek to last position
audio.addEventListener('loadedmetadata', () => {
if (this.lastPickerPosition > 0) {
audio.currentTime = this.lastPickerPosition / 1000;
}
});

// Update time display
audio.addEventListener('timeupdate', () => {
const ms = Math.floor(audio.currentTime * 1000);
const minutes = Math.floor(audio.currentTime / 60);
const secs = Math.floor(audio.currentTime % 60);
$('#preview-current-time').text(`${minutes}:${secs.toString().padStart(2, '0')} (${ms}ms)`);
});
Comment on lines +1113 to +1125
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio event listeners are not cleaned up when the modal is closed. The 'loadedmetadata' and 'timeupdate' event listeners remain attached to the audio element even after closePreviewModal removes it from the DOM. Consider using addEventListener with { once: true } for loadedmetadata or explicitly removing listeners in closePreviewModal.

Copilot uses AI. Check for mistakes.

// Go to time button
$('#preview-goto-btn').on('click', () => {
const ms = parseInt($('#preview-ms-input').val()) || 0;
audio.currentTime = ms / 1000;
});

// Use current button
$('#preview-setcurrent-btn').on('click', () => {
const ms = Math.floor(audio.currentTime * 1000);
$('#preview-ms-input').val(ms);
});

// Set button
$('#preview-set-btn').on('click', () => {
const ms = parseInt($('#preview-ms-input').val()) || 0;
$('#preview_time').val(ms);
this.lastPickerPosition = ms;
this.updatePreviewDisplay();
this.closePreviewModal();
Utils.showFlashMessage(`Preview time set to ${ms}ms`, 'success');
});
},

closePreviewModal() {
if (this.audio) {
this.audio.pause();
this.audio = null;
}
$('#preview-picker-modal').remove();
}
};

// Start the application
initializeApp();
});
110 changes: 110 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -737,3 +737,113 @@ input[type="number"]:hover::-webkit-outer-spin-button {
.clear-input-btn:active {
transform: translateY(-50%) scale(0.95);
}

/* Preview Time Picker */
.preview-input-group {
display: flex;
gap: 8px;
align-items: center;
}

.preview-input-group input {
flex: 1;
}

.preview-time-display {
font-size: 0.85em;
color: var(--text-secondary);
margin-left: 8px;
}

.preview-time-display.has-value {
color: var(--accent-color);
}

/* Preview Modal */
.preview-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}

.preview-modal {
background: var(--card-bg);
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow: auto;
}

.preview-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}

.preview-modal-header h3 {
margin: 0;
}

.preview-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-color);
padding: 0;
line-height: 1;
}

.preview-modal-body {
padding: 20px;
}

.preview-time-labels {
margin-bottom: 15px;
font-size: 14px;
}

.preview-input-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}

.preview-input-row input {
width: 150px;
}

.preview-buttons-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
}

.preview-buttons-row .accent {
background-color: var(--accent-color);
}

/* Background Preview */
.background-preview {
margin-top: 10px;
max-height: 150px;
overflow: hidden;
border-radius: 4px;
}

.background-preview img {
width: 100%;
height: auto;
object-fit: cover;
}
27 changes: 27 additions & 0 deletions template/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,33 @@ <h4 title="List of additional context to provide to the model">In-Context Option
<input type="text" id="seed" name="seed" />
</fieldset>

<!-- Beatmap Customization -->
<fieldset>
<legend>Beatmap Customization</legend>
<div class="form-group">
<label for="preview_time" title="Preview time in milliseconds (the point in the song that plays during beatmap selection)">Preview Time (ms):</label>
<div class="preview-input-group">
<input type="number" id="preview_time" name="preview_time" min="0" placeholder="Auto" />
<button type="button" id="pick-preview-btn" class="browse-button" title="Pick preview point from audio">Pick...</button>
</div>
<span id="preview-time-display" class="preview-time-display"></span>
</div>

<div class="form-group">
<label for="background_path" title="Background image for the beatmap">Background Image:</label>
<div class="path-input-group" style="margin-bottom: 0;">
<div class="input-with-clear">
<input type="text" id="background_path" name="background_path" placeholder="None" />
<button type="button" class="clear-input-btn" data-target="background_path" style="display: none;">×</button>
</div>
<button type="button" class="browse-button" data-browse-type="file" data-target="background_path">Browse...</button>
Comment on lines +169 to +172
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background image input field and button lack proper accessibility attributes. Add aria-label or aria-describedby attributes to the input field for screen reader users, and ensure the button has descriptive text or aria-label since 'Browse...' may not clearly indicate it's for selecting a background image.

Suggested change
<input type="text" id="background_path" name="background_path" placeholder="None" />
<button type="button" class="clear-input-btn" data-target="background_path" style="display: none;">×</button>
</div>
<button type="button" class="browse-button" data-browse-type="file" data-target="background_path">Browse...</button>
<input type="text" id="background_path" name="background_path" placeholder="None" aria-describedby="background_path_desc" />
<button type="button" class="clear-input-btn" data-target="background_path" style="display: none;">×</button>
<span id="background_path_desc" class="visually-hidden">Enter the path to the background image for the beatmap, or use the browse button to select a file.</span>
</div>
<button type="button" class="browse-button" data-browse-type="file" data-target="background_path" aria-label="Browse for background image">Browse...</button>

Copilot uses AI. Check for mistakes.
</div>
</div>
<div id="background-preview" class="background-preview" style="display: none;">
<img id="background-preview-img" src="" alt="Background preview" />
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background preview image lacks appropriate alt text. While 'Background preview' is provided, it would be more helpful to provide more descriptive alt text or use an empty alt attribute (alt="") if the image is purely decorative, with the context provided by the surrounding elements.

Suggested change
<img id="background-preview-img" src="" alt="Background preview" />
<img id="background-preview-img" src="" alt="" />

Copilot uses AI. Check for mistakes.
</div>
</fieldset>

<!-- Generation Interval -->
<fieldset>
<legend>Generation Interval</legend>
Expand Down
Loading