| $(document).ready(function() { |
| |
| const AppState = { |
| evtSource: null, |
| isCancelled: false, |
| inferenceErrorOccurred: false, |
| accumulatedErrorMessages: [], |
| errorLogFilePath: null, |
| animationSpeed: 300, |
| |
| logs: [], |
| autoscroll: true, |
| startTime: null, |
| stepStartTimes: {}, |
| stepStatus: { timing: 'Pending', kiai: 'Pending', map: 'Pending', diffusion: 'Pending', export: 'Pending' }, |
|
|
| modelCapabilities: { |
| "v28": {}, |
| "v29": {}, |
| "v30": { |
| supportedGamemodes: ['0'], |
| supportsYear: false, |
| supportedInContextOptions: ['TIMING'], |
| hideHitsoundsOption: true, |
| supportsDescriptors: false, |
| }, |
| } |
| }; |
|
|
| |
| const Utils = { |
| showFlashMessage(message, type = 'success') { |
| const flashContainer = $('#flash-container'); |
| const alertClass = type === 'success' ? 'alert success' : |
| type === 'cancel-success' ? 'alert alert-cancel-success' : |
| 'alert error'; |
| const messageDiv = $(`<div class="${alertClass}">${message}</div>`); |
| flashContainer.append(messageDiv); |
| setTimeout(() => messageDiv.remove(), 5000); |
| }, |
|
|
| smoothScroll(target, offset = 0) { |
| $('html, body').animate({ |
| scrollTop: $(target).offset().top + offset |
| }, 500); |
| }, |
|
|
| resetFormToDefaults() { |
| $('#inferenceForm')[0].reset(); |
|
|
| |
| const defaults = { |
| model: 'v30', gamemode: '0', difficulty: '5', hp_drain_rate: '5', |
| circle_size: '4', keycount: '4', overall_difficulty: '8', |
| approach_rate: '9', slider_multiplier: '1.4', slider_tick_rate: '1', |
| year: '2023', cfg_scale: '1.0', temperature: '0.9', top_p: '0.9' |
| }; |
|
|
| Object.entries(defaults).forEach(([id, value]) => { |
| $(`#${id}`).val(value); |
| }); |
|
|
| |
| $('#hitsounded').prop('checked', true); |
| $('#export_osz, #add_to_beatmap, #super_timing').prop('checked', false); |
|
|
| |
| $('input[name="descriptors"], input[name="in_context_options"]') |
| .removeClass('positive-check negative-check').prop('checked', false); |
|
|
| |
| $('#audio_path, #output_path, #beatmap_path, #mapper_id, #seed, #start_time, #end_time, #hold_note_ratio, #scroll_speed_ratio').val(''); |
| PathManager.clearPlaceholders(); |
| PathManager.validateAndAutofillPaths(false); |
| } |
| }; |
|
|
| |
| const UIManager = { |
| updateConditionalFields() { |
| const selectedGamemode = $("#gamemode").val(); |
| const selectedModel = $("#model").val(); |
| const beatmapPath = $('#beatmap_path').val().trim(); |
|
|
| |
| $('.conditional-field[data-show-for-gamemode]').each(function() { |
| const $field = $(this); |
| const supportedModes = $field.data('show-for-gamemode').toString().split(','); |
| const shouldShow = supportedModes.includes(selectedGamemode); |
|
|
| if (shouldShow && !$field.is(':visible')) { |
| $field.slideDown(AppState.animationSpeed); |
| } else if (!shouldShow && $field.is(':visible')) { |
| $field.slideUp(AppState.animationSpeed); |
| } |
| }); |
|
|
| |
| $('.conditional-field[data-hide-for-model]').each(function() { |
| const $field = $(this); |
| const hiddenModels = $field.data('hide-for-model').toString().split(','); |
| const shouldHide = hiddenModels.includes(selectedModel); |
|
|
| if (shouldHide && $field.is(':visible')) { |
| $field.slideUp(AppState.animationSpeed); |
| } else if (!shouldHide && !$field.is(':visible')) { |
| $field.slideDown(AppState.animationSpeed); |
| } |
| }); |
|
|
| |
| const shouldShowBeatmapFields = beatmapPath !== ''; |
| ['#in-context-options-box', '#add-to-beatmap-option'].forEach(selector => { |
| const $element = $(selector); |
| if (shouldShowBeatmapFields && !$element.is(':visible')) { |
| $element.fadeIn(AppState.animationSpeed); |
| } else if (!shouldShowBeatmapFields && $element.is(':visible')) { |
| $element.fadeOut(AppState.animationSpeed); |
| if (selector === '#add-to-beatmap-option') { |
| $('#add_to_beatmap').prop('checked', false); |
| } |
| } |
| }); |
| }, |
|
|
| updateModelSettings() { |
| const selectedModel = $("#model").val(); |
| const capabilities = AppState.modelCapabilities[selectedModel] || {}; |
|
|
| |
| const $gamemodeSelect = $("#gamemode"); |
| if (selectedModel === "v30") { |
| $gamemodeSelect.val('0').prop('disabled', true); |
| $gamemodeSelect.find("option").each(function() { |
| $(this).prop('disabled', $(this).val() !== '0'); |
| }); |
| } else { |
| $gamemodeSelect.prop('disabled', false); |
| $gamemodeSelect.find("option").prop('disabled', false); |
| } |
|
|
| |
| const supportedContext = capabilities.supportedInContextOptions || |
| ['NONE', 'TIMING', 'KIAI', 'MAP', 'GD', 'NO_HS']; |
|
|
| $('input[name="in_context_options"]').each(function() { |
| const $checkbox = $(this); |
| const value = $checkbox.val(); |
| const $item = $checkbox.closest('.context-option-item'); |
| const isSupported = supportedContext.includes(value); |
|
|
| $item.data('model-allowed', isSupported); |
| $checkbox.prop('disabled', !isSupported); |
|
|
| if (isSupported) { |
| $item.slideDown(AppState.animationSpeed); |
| } else { |
| $item.slideUp(AppState.animationSpeed); |
| } |
| }); |
|
|
| |
| if (capabilities.hideHitsoundsOption) { |
| $('#hitsounded').prop('checked', true); |
| } |
|
|
| this.updateConditionalFields(); |
| } |
| }; |
|
|
| |
| const FileBrowser = { |
| init() { |
| this.attachBrowseHandlers(); |
| }, |
|
|
| attachBrowseHandlers() { |
| $('.browse-button[data-browse-type]').click(async function() { |
| const browseType = $(this).data('browse-type'); |
| const targetId = $(this).data('target'); |
|
|
| try { |
| let path; |
|
|
| if (browseType === 'folder') { |
| path = await window.pywebview.api.browse_folder(); |
| } else { |
| let fileTypes = null; |
|
|
| if (targetId === 'beatmap_path') { |
| fileTypes = [ |
| 'Beatmap Files (*.osu)', |
| 'All files (*.*)' |
| ]; |
| } else if (targetId === 'audio_path') { |
| fileTypes = [ |
| |
| 'Audio Files (*.mp3;*.wav;*.ogg;*.m4a;*.flac)', |
| 'All files (*.*)' |
| ]; |
| } |
|
|
| path = await window.pywebview.api.browse_file(fileTypes); |
| } |
|
|
| if (path) { |
| if (targetId === 'beatmap_path' && !path.toLowerCase().endsWith('.osu')) { |
| Utils.showFlashMessage('Please select a valid .osu file.', 'error'); |
| |
| } |
|
|
| const $targetInput = $(`#${targetId}`); |
| $targetInput.val(path); |
| console.log(`Selected ${browseType}:`, path); |
|
|
| |
| $targetInput.trigger('input'); |
| $targetInput.trigger('blur'); |
| } |
| } catch (error) { |
| console.error(`Error browsing for ${browseType}:`, error); |
| alert(`Could not browse for ${browseType}. Ensure the backend API is running.`); |
| } |
| }); |
| } |
| }; |
|
|
| |
| const PathManager = { |
| init() { |
| this.attachPathChangeHandlers(); |
| this.attachClearButtonHandlers(); |
| $('#audio_path, #beatmap_path, #output_path').trigger('blur'); |
| }, |
|
|
| attachPathChangeHandlers() { |
| |
| $('#audio_path, #beatmap_path, #output_path').on('input', (e) => { |
| this.updateClearButtonVisibility(e.target); |
| }); |
|
|
| |
| $('#audio_path, #beatmap_path, #output_path').on('blur', (e) => { |
| this.updateClearButtonVisibility(e.target); |
| this.validateAndAutofillPaths(false); |
| }); |
| }, |
|
|
| attachClearButtonHandlers() { |
| |
| $('.clear-input-btn').on('click', (e) => { |
| const targetId = $(e.target).data('target'); |
| const $targetInput = $(`#${targetId}`); |
|
|
| $targetInput.val(''); |
| this.updateClearButtonVisibility($targetInput[0]); |
|
|
| this.validateAndAutofillPaths(false); |
| }); |
|
|
| |
| $('#audio_path, #beatmap_path, #output_path').each((index, element) => { |
| this.updateClearButtonVisibility(element); |
| }); |
| }, |
|
|
| updateClearButtonVisibility(inputElement) { |
| const $input = $(inputElement); |
| const $clearBtn = $input.siblings('.clear-input-btn'); |
| const hasValue = $input.val().trim() !== ''; |
|
|
| if (hasValue) { |
| $clearBtn.show(); |
| } else { |
| $clearBtn.hide(); |
| } |
| }, |
|
|
| validateAndAutofillPaths(showFlashMessages = false) { |
| const audioPath = $('#audio_path').val().trim(); |
| const beatmapPath = $('#beatmap_path').val().trim(); |
| const outputPath = $('#output_path').val().trim(); |
|
|
| |
| if (!audioPath && !beatmapPath && !outputPath) { |
| this.clearPlaceholders(); |
| UIManager.updateConditionalFields(); |
| return Promise.resolve(true); |
| } |
|
|
| |
| return new Promise((resolve) => { |
| $.ajax({ |
| url: '/validate_paths', |
| method: 'POST', |
| data: { |
| audio_path: audioPath, |
| beatmap_path: beatmapPath, |
| output_path: outputPath |
| }, |
| success: (response) => { |
| this.handleValidationResponse(response, showFlashMessages); |
| resolve(response.success); |
| }, |
| error: (xhr, status, error) => { |
| console.error('Path validation failed:', error); |
| if (showFlashMessages) { |
| Utils.showFlashMessage('Error validating paths. Check console for details.', 'error'); |
| } |
| resolve(false); |
| } |
| }); |
| }); |
| }, |
|
|
| handleValidationResponse(response, showFlashMessages = false) { |
| this.clearValidationErrors(); |
| const $audioPathInput = $('#audio_path'); |
| const $outputPathInput = $('#output_path'); |
|
|
| |
| if (response.autofilled_audio_path && !$audioPathInput.val().trim()) { |
| $audioPathInput.attr('placeholder', response.autofilled_audio_path); |
| } else if (!$audioPathInput.val().trim()) { |
| $audioPathInput.attr('placeholder', ''); |
| } |
|
|
| if (response.autofilled_output_path && !$outputPathInput.val().trim()) { |
| $outputPathInput.attr('placeholder', response.autofilled_output_path); |
| } else if (!$outputPathInput.val().trim()) { |
| $outputPathInput.attr('placeholder', ''); |
| } |
|
|
| if (showFlashMessages) { |
| |
| response.errors.forEach(error => { |
| Utils.showFlashMessage(error, 'error'); |
| }); |
| } |
|
|
| |
| response.errors.forEach(error => { |
| this.showInlineErrorForMessage(error); |
| }); |
|
|
| |
| UIManager.updateConditionalFields(); |
| }, |
|
|
| showInlineErrorForMessage(error) { |
| const audioPathVal = $('#audio_path').val().trim(); |
| const beatmapPathVal = $('#beatmap_path').val().trim(); |
|
|
| if (error.includes('Audio file not found') && (audioPathVal || beatmapPathVal)) { |
| this.showInlineError('#audio_path', 'Audio file not found'); |
| } else if (error.includes('Beatmap file not found') && beatmapPathVal) { |
| this.showInlineError('#beatmap_path', 'Beatmap file not found'); |
| } else if (error.includes('Beatmap file must have .osu extension') && beatmapPathVal) { |
| this.showInlineError('#beatmap_path', 'Must be .osu file'); |
| } |
| }, |
|
|
| showInlineError(inputSelector, message) { |
| const $input = $(inputSelector); |
| const $inputContainer = $input.closest('.input-with-clear'); |
| |
| if ($input.siblings('.path-validation-error').length > 0) { |
| $input.siblings('.path-validation-error').text(message); |
| } else { |
| const $errorDiv = $(`<div class="path-validation-error" style="color: #ff4444; font-size: 12px; margin-top: 2px;">${message}</div>`); |
| $inputContainer.after($errorDiv); |
| } |
| }, |
|
|
| clearValidationErrors() { |
| $('.path-validation-error').remove(); |
| }, |
|
|
| clearPlaceholders() { |
| $('#audio_path, #output_path').attr('placeholder', ''); |
| this.clearValidationErrors(); |
| }, |
|
|
| |
| applyPlaceholderValues() { |
| const $audioPath = $('#audio_path'); |
| const $outputPath = $('#output_path'); |
|
|
| if (!$audioPath.val().trim() && $audioPath.attr('placeholder')) { |
| $audioPath.val($audioPath.attr('placeholder')); |
| } |
|
|
| if (!$outputPath.val().trim() && $outputPath.attr('placeholder')) { |
| $outputPath.val($outputPath.attr('placeholder')); |
| } |
| } |
| }; |
|
|
| |
| const DescriptorManager = { |
| init() { |
| this.attachDropdownHandler(); |
| this.attachDescriptorClickHandlers(); |
| }, |
|
|
| attachDropdownHandler() { |
| $('.custom-dropdown-descriptors .dropdown-header').click(function() { |
| const $dropdown = $(this).parent(); |
| $dropdown.toggleClass('open'); |
| if ($dropdown.hasClass('open')) { |
| Utils.smoothScroll(this); |
| } |
| }); |
| }, |
|
|
| attachDescriptorClickHandlers() { |
| $('.descriptors-container').on('click', 'input[name="descriptors"]', function(e) { |
| e.preventDefault(); |
| const $checkbox = $(this); |
|
|
| if (!$checkbox.prop('disabled')) { |
| if ($checkbox.hasClass('positive-check')) { |
| $checkbox.removeClass('positive-check').addClass('negative-check'); |
| } else if ($checkbox.hasClass('negative-check')) { |
| $checkbox.removeClass('negative-check'); |
| $checkbox.prop('checked', false); |
| return; |
| } else { |
| $checkbox.addClass('positive-check'); |
| } |
| $checkbox.prop('checked', true); |
| } |
| }); |
| } |
| }; |
|
|
| |
| const ConfigManager = { |
| init() { |
| $('#export-config-btn').click(() => this.exportConfiguration()); |
| $('#import-config-btn').click(() => $('#import-config-input').click()); |
| $('#reset-config-btn').click(() => this.resetToDefaults()); |
| $('#import-config-input').change((e) => this.handleFileImport(e)); |
| }, |
|
|
| exportConfiguration() { |
| const config = this.buildConfigObject(); |
|
|
| if (window.pywebview?.api?.save_file) { |
| this.exportToFile(config); |
| } else { |
| this.fallbackDownload(config); |
| } |
| }, |
|
|
| buildConfigObject() { |
| const config = { |
| version: "1.0", |
| timestamp: new Date().toISOString(), |
| settings: {}, |
| descriptors: { positive: [], negative: [] }, |
| inContextOptions: [] |
| }; |
|
|
| |
| $('#inferenceForm').find('input, select, textarea').each(function() { |
| const $field = $(this); |
| const name = $field.attr('name'); |
| const type = $field.attr('type'); |
|
|
| if (name && type !== 'file') { |
| config.settings[name] = type === 'checkbox' ? $field.prop('checked') : $field.val(); |
| } |
| }); |
|
|
| |
| $('input[name="descriptors"]').each(function() { |
| const $checkbox = $(this); |
| const value = $checkbox.val(); |
| if ($checkbox.hasClass('positive-check')) { |
| config.descriptors.positive.push(value); |
| } else if ($checkbox.hasClass('negative-check')) { |
| config.descriptors.negative.push(value); |
| } |
| }); |
|
|
| |
| $('input[name="in_context_options"]:checked').each(function() { |
| config.inContextOptions.push($(this).val()); |
| }); |
|
|
| return config; |
| }, |
|
|
| async exportToFile(config) { |
| try { |
| const filename = `mapperatorinator-config-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; |
|
|
| const filePath = await window.pywebview.api.save_file(filename); |
| if (!filePath) { |
| this.showConfigStatus("Export cancelled by user", "error"); |
| return; |
| } |
|
|
| $.ajax({ |
| url: "/save_config", |
| method: "POST", |
| data: { |
| file_path: filePath, |
| config_data: JSON.stringify(config, null, 2) |
| }, |
| success: (response) => { |
| if (response.success) { |
| this.showConfigStatus(`Configuration exported successfully to: ${response.file_path}`, "success"); |
| } else { |
| this.showConfigStatus(`Error saving config: ${response.error}`, "error"); |
| } |
| }, |
| error: () => { |
| this.showConfigStatus("Failed to save config to server. Using browser download instead.", "error"); |
| this.fallbackDownload(config); |
| } |
| }); |
| } catch (error) { |
| console.error("Error selecting folder:", error); |
| this.fallbackDownload(config); |
| } |
| }, |
|
|
| fallbackDownload(config) { |
| const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `mapperatorinator-config-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| this.showConfigStatus("Configuration exported successfully (browser download)", "success"); |
| }, |
|
|
| resetToDefaults() { |
| if (confirm("Are you sure you want to reset all settings to default values? This cannot be undone.")) { |
| Utils.resetFormToDefaults(); |
| $("#model, #gamemode, #beatmap_path").trigger('change'); |
| $('#audio_path, #output_path, #beatmap_path').trigger('blur'); |
| this.showConfigStatus("All settings reset to default values", "success"); |
| } |
| }, |
|
|
| handleFileImport(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
|
|
| if (file.type !== 'application/json' && !file.name.endsWith('.json')) { |
| this.showConfigStatus("Please select a valid JSON configuration file.", "error"); |
| return; |
| } |
|
|
| const reader = new FileReader(); |
| reader.onload = (e) => this.importConfiguration(e.target.result); |
| reader.readAsText(file); |
| $(e.target).val(''); |
| }, |
|
|
| importConfiguration(content) { |
| try { |
| const config = JSON.parse(content); |
| if (!config.version) { |
| throw new Error("Invalid configuration file format"); |
| } |
|
|
| |
| if (config.settings) { |
| Object.entries(config.settings).forEach(([name, value]) => { |
| const $field = $(`[name="${name}"]`); |
| if ($field.length) { |
| if ($field.attr('type') === 'checkbox') { |
| $field.prop('checked', value); |
| } else { |
| $field.val(value); |
| } |
| } |
| }); |
| } |
|
|
| |
| $('input[name="descriptors"]').removeClass('positive-check negative-check').prop('checked', false); |
| if (config.descriptors) { |
| config.descriptors.positive?.forEach(value => { |
| $(`input[name="descriptors"][value="${value}"]`) |
| .addClass('positive-check').prop('checked', true); |
| }); |
| config.descriptors.negative?.forEach(value => { |
| $(`input[name="descriptors"][value="${value}"]`) |
| .addClass('negative-check').prop('checked', true); |
| }); |
| } |
|
|
| |
| $('input[name="in_context_options"]').prop('checked', false); |
| config.inContextOptions?.forEach(value => { |
| $(`input[name="in_context_options"][value="${value}"]`).prop('checked', true); |
| }); |
|
|
| |
| $("#model, #gamemode").trigger('change'); |
| $('#audio_path, #output_path, #beatmap_path').trigger('blur'); |
| $('#audio_path, #output_path, #beatmap_path').trigger('input'); |
|
|
| this.showConfigStatus(`Configuration imported successfully! (${config.timestamp || 'Unknown date'})`, "success"); |
|
|
| } catch (error) { |
| console.error("Error importing configuration:", error); |
| this.showConfigStatus(`Error importing configuration: ${error.message}`, "error"); |
| } |
| }, |
|
|
| showConfigStatus(message, type) { |
| const $status = $("#config-status"); |
| $status.text(message) |
| .css('color', type === 'success' ? '#28a745' : '#dc3545') |
| .fadeIn(); |
| setTimeout(() => $status.fadeOut(), 5000); |
| } |
| }; |
|
|
| |
| const InferenceManager = { |
| init() { |
| $('#inferenceForm').submit((e) => this.handleSubmit(e)); |
| $('#cancel-button').click(() => this.cancelInference()); |
| }, |
|
|
| async handleSubmit(e) { |
| e.preventDefault(); |
|
|
| |
| if (!await this.validateForm()) return; |
|
|
| this.resetProgress(); |
| this.startInference(); |
| }, |
|
|
| async validateForm() { |
| PathManager.applyPlaceholderValues(); |
|
|
| const audioPath = $('#audio_path').val().trim(); |
| const beatmapPath = $('#beatmap_path').val().trim(); |
| const outputPath = $('#output_path').val().trim(); |
|
|
| if (!audioPath && !beatmapPath) { |
| Utils.smoothScroll(0); |
| Utils.showFlashMessage("Either 'Beatmap Path' or 'Audio Path' are required for running inference", 'error'); |
| return false; |
| } |
|
|
| if (!outputPath && !beatmapPath) { |
| Utils.smoothScroll(0); |
| Utils.showFlashMessage("Either 'Output Path' or 'Beatmap Path' are required for running inference", 'error'); |
| return false; |
| } |
|
|
| |
| if (beatmapPath && !beatmapPath.toLowerCase().endsWith('.osu')) { |
| Utils.smoothScroll('#beatmap_path'); |
| Utils.showFlashMessage("Beatmap file must have .osu extension", 'error'); |
| PathManager.showInlineError('#beatmap_path', 'Must be .osu file'); |
| return false; |
| } |
|
|
| const pathsAreValid = await PathManager.validateAndAutofillPaths(true); |
| if (!pathsAreValid) { |
| Utils.smoothScroll(0); |
| return false; |
| } |
|
|
| return true; |
| }, |
|
|
| resetProgress() { |
| $('#flash-container').empty(); |
| $("#progress_output").show(); |
| Utils.smoothScroll('#progress_output'); |
|
|
| $("#progressBarContainer, #progressTitle").show(); |
| $("#progressBar").css("width", "0%").removeClass('cancelled error'); |
| $("#beatmapLink, #errorLogLink").hide(); |
| $("#init_message").text("Initializing process... This may take a moment.").show(); |
| $("#progressTitle").text("").css('color', ''); |
| $("#cancel-button").hide().prop('disabled', false).text('Cancel'); |
| $("button[type='submit']").prop("disabled", true); |
|
|
| AppState.inferenceErrorOccurred = false; |
| AppState.accumulatedErrorMessages = []; |
| AppState.isCancelled = false; |
|
|
| |
| AppState.logs = []; |
| AppState.startTime = new Date(); |
| AppState.stepStartTimes = {}; |
| AppState.stepStatus = { timing: 'Pending', kiai: 'Pending', map: 'Pending', diffusion: 'Pending', export: 'Pending' }; |
| $("#logContainer").show(); |
| $("#progressTableContainer").show(); |
| $("#logContent").text(""); |
| $("#progressTable tbody tr").each(function() { |
| $(this).find('td.status').text('Pending').removeClass('status-running status-done status-error').addClass('status-pending'); |
| $(this).find('td.time').text('-'); |
| }); |
| if (document.getElementById('autoscrollToggle')) { |
| $('#autoscrollToggle').prop('checked', true); |
| AppState.autoscroll = true; |
| } |
|
|
| if (AppState.evtSource) { |
| AppState.evtSource.close(); |
| AppState.evtSource = null; |
| } |
| }, |
|
|
| buildFormData() { |
| const formData = new FormData($("#inferenceForm")[0]); |
|
|
| |
| formData.delete('descriptors'); |
| const positiveDescriptors = []; |
| const negativeDescriptors = []; |
|
|
| $('input[name="descriptors"]').each(function() { |
| const $cb = $(this); |
| if ($cb.hasClass('positive-check')) { |
| positiveDescriptors.push($cb.val()); |
| } else if ($cb.hasClass('negative-check')) { |
| negativeDescriptors.push($cb.val()); |
| } |
| }); |
|
|
| positiveDescriptors.forEach(val => formData.append('descriptors', val)); |
| negativeDescriptors.forEach(val => formData.append('negative_descriptors', val)); |
|
|
| |
| if ($("#model").val() === "v30" && !$("#option-item-hitsounded").is(':visible')) { |
| formData.set('hitsounded', 'true'); |
| } |
|
|
| return formData; |
| }, |
|
|
| startInference() { |
| $.ajax({ |
| url: "/start_inference", |
| method: "POST", |
| data: this.buildFormData(), |
| processData: false, |
| contentType: false, |
| success: () => { |
| $("#cancel-button").show(); |
| this.connectToSSE(); |
| }, |
| error: (jqXHR, textStatus, errorThrown) => { |
| console.error("Failed to start inference:", textStatus, errorThrown); |
| let errorMsg = "Failed to start inference process. Check backend console."; |
| if (jqXHR.responseJSON && jqXHR.responseJSON.message) { |
| errorMsg = jqXHR.responseJSON.message; |
| } else if (jqXHR.responseText) { |
| try { |
| const parsed = JSON.parse(jqXHR.responseText); |
| if(parsed && parsed.message) errorMsg = parsed.message; |
| } catch(e) { } |
| } |
| Utils.showFlashMessage(errorMsg, 'error'); |
| $("button[type='submit']").prop("disabled", false); |
| $("#cancel-button").hide(); |
| $("#progress_output").hide(); |
| } |
| }); |
| }, |
|
|
| connectToSSE() { |
| console.log("Connecting to SSE stream..."); |
| AppState.evtSource = new EventSource("/stream_output"); |
| AppState.errorLogFilePath = null; |
|
|
| AppState.evtSource.onmessage = (e) => this.handleSSEMessage(e); |
| AppState.evtSource.onerror = (err) => this.handleSSEError(err); |
| AppState.evtSource.addEventListener("error_log", (e) => { |
| AppState.errorLogFilePath = e.data; |
| }); |
| AppState.evtSource.addEventListener("end", (e) => this.handleSSEEnd(e)); |
| }, |
|
|
| handleSSEMessage(e) { |
| if ($("#init_message").is(":visible")) $("#init_message").hide(); |
| if (AppState.isCancelled) return; |
|
|
| const messageData = e.data; |
| |
| this.appendLog(messageData); |
| |
| this.updateStepsFromMessage(messageData); |
| const errorIndicators = [ |
| "Traceback (most recent call last):", "Error executing job with overrides:", |
| "FileNotFoundError:", "Exception:", "Set the environment variable HYDRA_FULL_ERROR=1" |
| ]; |
|
|
| const isErrorMessage = errorIndicators.some(indicator => messageData.includes(indicator)); |
|
|
| if (isErrorMessage && !AppState.inferenceErrorOccurred) { |
| AppState.inferenceErrorOccurred = true; |
| AppState.accumulatedErrorMessages.push(messageData); |
| $("#progressTitle").text("Error Detected").css('color', 'var(--accent-color)'); |
| $("#progressBar").addClass('error'); |
| } else if (AppState.inferenceErrorOccurred) { |
| AppState.accumulatedErrorMessages.push(messageData); |
| } else { |
| this.updateProgress(messageData); |
| } |
| }, |
|
|
| updateProgress(messageData) { |
| |
| const lowerCaseMessage = messageData.toLowerCase(); |
| const progressTitles = { |
| "generating timing": "Generating Timing", |
| "generating kiai": "Generating Kiai", |
| "generating map": "Generating Map", |
| "seq len": "Refining Positions" |
| }; |
|
|
| Object.entries(progressTitles).forEach(([keyword, title]) => { |
| if (lowerCaseMessage.includes(keyword)) { |
| $("#progressTitle").text(title); |
| } |
| }); |
|
|
| |
| const progressMatch = messageData.match(/^\s*(\d+)%\|/); |
| if (progressMatch) { |
| const currentPercent = parseInt(progressMatch[1].trim(), 10); |
| if (!isNaN(currentPercent)) { |
| $("#progressBar").css("width", currentPercent + "%"); |
| } |
| } |
|
|
| |
| if (messageData.includes("Generated beatmap saved to")) { |
| const parts = messageData.split("Generated beatmap saved to"); |
| if (parts.length > 1) { |
| const fullPath = parts[1].trim().replace(/\\/g, "/"); |
| const folderPath = fullPath.substring(0, fullPath.lastIndexOf("/")); |
|
|
| $("#beatmapLinkAnchor") |
| .attr("href", "#") |
| .text("Click here to open the folder containing your map.") |
| .off("click") |
| .on("click", (e) => { |
| e.preventDefault(); |
| $.get("/open_folder", { folder: folderPath }) |
| .done(response => console.log("Open folder response:", response)) |
| .fail(() => alert("Failed to open folder via backend.")); |
| }); |
| $("#beatmapLink").show(); |
| |
| this.markStepDone('export'); |
| } |
| } |
| }, |
|
|
| |
| appendLog(message) { |
| AppState.logs.push(message); |
| const content = AppState.logs.join('\n'); |
| const $el = $('#logContent'); |
| $el.text(content); |
| if (AppState.autoscroll) { |
| const el = $el[0]; |
| if (el) el.scrollTop = el.scrollHeight; |
| } |
| }, |
|
|
| updateStepsFromMessage(message) { |
| const msg = message.toLowerCase(); |
| const order = ['timing','kiai','map','diffusion']; |
| const detector = [ |
| {key:'timing', kw:['generating timing']}, |
| {key:'kiai', kw:['generating kiai']}, |
| {key:'map', kw:['generating map']}, |
| {key:'diffusion', kw:['seq len','refining positions']}, |
| ]; |
| for (const d of detector) { |
| if (d.kw.some(k => msg.includes(k))) { |
| this.markStepRunning(d.key); |
| |
| const idx = order.indexOf(d.key); |
| if (idx > 0) { |
| const prev = order[idx - 1]; |
| if (AppState.stepStatus[prev] === 'Running') this.markStepDone(prev); |
| } |
| } |
| } |
| }, |
|
|
| markStepRunning(key) { |
| if (AppState.stepStatus[key] !== 'Running' && AppState.stepStatus[key] !== 'Done') { |
| AppState.stepStatus[key] = 'Running'; |
| AppState.stepStartTimes[key] = new Date(); |
| const $row = $(`#progressTable tbody tr[data-step="${key}"]`); |
| $row.find('td.status').text('Running').removeClass('status-pending status-done status-error').addClass('status-running'); |
| } |
| }, |
|
|
| markStepDone(key) { |
| const prev = AppState.stepStatus[key]; |
| if (prev !== 'Done') { |
| AppState.stepStatus[key] = 'Done'; |
| const start = AppState.stepStartTimes[key] || AppState.startTime || new Date(); |
| const durMs = Math.max(0, new Date() - start); |
| const sec = (durMs / 1000).toFixed(1); |
| const $row = $(`#progressTable tbody tr[data-step="${key}"]`); |
| $row.find('td.status').text('Done').removeClass('status-pending status-running status-error').addClass('status-done'); |
| $row.find('td.time').text(`${sec}s`); |
| } |
| }, |
|
|
| markAnyRunningDone() { |
| Object.entries(AppState.stepStatus).forEach(([k, v]) => { |
| if (v === 'Running') this.markStepDone(k); |
| }); |
| }, |
|
|
| markAnyRunningError() { |
| Object.entries(AppState.stepStatus).forEach(([k, v]) => { |
| if (v === 'Running') { |
| AppState.stepStatus[k] = 'Error'; |
| const $row = $(`#progressTable tbody tr[data-step="${k}"]`); |
| $row.find('td.status').text('Error').removeClass('status-pending status-running status-done').addClass('status-error'); |
| } |
| }); |
| }, |
|
|
| handleSSEError(err) { |
| console.error("EventSource failed:", err); |
| if (AppState.evtSource) { |
| AppState.evtSource.close(); |
| AppState.evtSource = null; |
| } |
|
|
| if (!AppState.isCancelled && !AppState.inferenceErrorOccurred) { |
| AppState.inferenceErrorOccurred = true; |
| AppState.accumulatedErrorMessages.push("Error: Connection to process stream lost."); |
| $("#progressTitle").text("Connection Error").css('color', 'var(--accent-color)'); |
| $("#progressBar").addClass('error'); |
| Utils.showFlashMessage("Error: Connection to process stream lost.", "error"); |
| } |
|
|
| if (!AppState.isCancelled) { |
| $("button[type='submit']").prop("disabled", false); |
| } |
| $("#cancel-button").hide(); |
| }, |
|
|
| handleSSEEnd(e) { |
| console.log("Received end event from server.", e.data); |
| if (AppState.evtSource) { |
| AppState.evtSource.close(); |
| AppState.evtSource = null; |
| } |
|
|
| if (AppState.isCancelled) { |
| $("#progressTitle, #progressBarContainer, #beatmapLink, #errorLogLink").hide(); |
| $("#progress_output").hide(); |
| } else if (AppState.inferenceErrorOccurred) { |
| this.handleInferenceError(); |
| } else { |
| $("#progressTitle").show().text("Processing Complete").css('color', ''); |
| $("#progressBarContainer").show(); |
| $("#progressBar").css("width", "100%").removeClass('error'); |
| this.markAnyRunningDone(); |
| if (AppState.stepStatus['export'] !== 'Done') this.markStepDone('export'); |
| } |
|
|
| $("button[type='submit']").prop("disabled", false); |
| $("#cancel-button").hide(); |
| AppState.isCancelled = false; |
| }, |
|
|
| handleInferenceError() { |
| const fullErrorText = AppState.accumulatedErrorMessages.join("\\n"); |
| let specificError = "An error occurred during processing. Check console/logs."; |
|
|
| if (fullErrorText.includes("FileNotFoundError:")) { |
| const fileNotFoundMatch = fullErrorText.match(/FileNotFoundError:.*? file (.*?) not found/); |
| specificError = fileNotFoundMatch?.[1] ? |
| `Error: File not found - ${fileNotFoundMatch[1].replace(/\\\\/g, '\\\\')}` : |
| "Error: A required file was not found."; |
| } else if (fullErrorText.includes("HYDRA_FULL_ERROR=1")) { |
| specificError = "There was an error while creating the beatmap. Check console/logs for details."; |
| } else if (fullErrorText.includes("Error executing job")) { |
| specificError = "There was an error starting or executing the generation task."; |
| } else if (fullErrorText.includes("Connection to process stream lost")) { |
| specificError = "Error: Connection to the generation process was lost."; |
| } |
|
|
| Utils.showFlashMessage(specificError, "error"); |
| $("#progressTitle").text("Processing Failed").css('color', 'var(--accent-color)').show(); |
| $("#progressBar").css("width", "100%").addClass('error'); |
| $("#progressBarContainer").show(); |
| $("#beatmapLink").hide(); |
|
|
| if (AppState.errorLogFilePath) { |
| $("#errorLogLinkAnchor").off("click").on("click", (e) => { |
| e.preventDefault(); |
| $.get("/open_log_file", { path: AppState.errorLogFilePath }) |
| .done(response => console.log("Open log response:", response)) |
| .fail(() => alert("Failed to open log file via backend.")); |
| }); |
| $("#errorLogLink").show(); |
| } |
|
|
| |
| this.markAnyRunningError(); |
| }, |
|
|
| cancelInference() { |
| const $cancelBtn = $("#cancel-button"); |
| $cancelBtn.prop('disabled', true).text('Cancelling...'); |
|
|
| $.ajax({ |
| url: "/cancel_inference", |
| method: "POST", |
| success: (response) => { |
| AppState.isCancelled = true; |
| Utils.showFlashMessage(response.message || "Inference cancelled successfully.", "cancel-success"); |
| }, |
| error: (jqXHR) => { |
| const errorMsg = jqXHR.responseJSON?.message || "Failed to send cancel request. Unknown error."; |
| Utils.showFlashMessage(errorMsg, "error"); |
| $cancelBtn.prop('disabled', false).text('Cancel'); |
| } |
| }); |
| } |
| }; |
|
|
| |
| function initializeApp() { |
| |
| $('.select2').select2({ |
| placeholder: "Select options", |
| allowClear: true, |
| dropdownCssClass: "select2-dropdown-dark", |
| containerCssClass: "select2-container-dark" |
| }); |
|
|
| |
| if (!$("#progressTitle").length) { |
| $("#progress_output h3").after("<div id='progressTitle' style='font-weight:bold; padding-bottom:5px;'></div>"); |
| } |
|
|
| |
| FileBrowser.init(); |
| PathManager.init(); |
| DescriptorManager.init(); |
| ConfigManager.init(); |
| InferenceManager.init(); |
|
|
| |
| if (document.getElementById('autoscrollToggle')) { |
| $('#autoscrollToggle').on('change', function() { |
| AppState.autoscroll = !!this.checked; |
| }); |
| } |
| if (document.getElementById('copyLogsBtn')) { |
| $('#copyLogsBtn').on('click', function() { |
| const text = AppState.logs.join('\n'); |
| if (navigator.clipboard && navigator.clipboard.writeText) { |
| navigator.clipboard.writeText(text).then(() => Utils.showFlashMessage('Logs copied to clipboard.')).catch(() => alert('Copy failed')); |
| } else { |
| |
| const ta = document.createElement('textarea'); |
| ta.value = text; |
| document.body.appendChild(ta); |
| ta.select(); |
| try { document.execCommand('copy'); Utils.showFlashMessage('Logs copied to clipboard.'); } catch (e) { alert('Copy failed'); } |
| document.body.removeChild(ta); |
| } |
| }); |
| } |
| if (document.getElementById('downloadLogsBtn')) { |
| $('#downloadLogsBtn').on('click', function() { |
| const blob = new Blob([AppState.logs.join('\n')], { type: 'text/plain;charset=utf-8' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| const ts = new Date().toISOString().replace(/[:.]/g, '-'); |
| a.download = `beatheritage_logs_${ts}.txt`; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| }); |
| } |
| if (document.getElementById('clearLogsBtn')) { |
| $('#clearLogsBtn').on('click', function() { |
| AppState.logs = []; |
| $('#logContent').text(''); |
| }); |
| } |
|
|
| |
| $("#model").on('change', () => UIManager.updateModelSettings()); |
| $("#gamemode").on('change', () => UIManager.updateConditionalFields()); |
|
|
| |
| UIManager.updateModelSettings(); |
| } |
|
|
| |
| initializeApp(); |
| }); |
|
|