From 5789e8f431722795f0907843b2bbc56e0c242e30 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 27 Feb 2026 08:53:13 -0500 Subject: [PATCH 001/149] Implemented drag to reorder tracks in media clipboard --- src/widgets/TrackAreaWidget.h | 123 ++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 0fcd8c27..b890ed0d 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -27,6 +27,7 @@ class TrackAreaWidget : public Component, TrackAreaWidget(DisplayMode mode = DisplayMode::Input, int trackHeight = 0) : displayMode(mode), fixedTrackHeight(trackHeight) { + addMouseListener(this, true); } ~TrackAreaWidget() { resetState(); } @@ -34,6 +35,24 @@ class TrackAreaWidget : public Component, void paint(Graphics& g) override { g.fillAll(getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); + + // Draws drop line for track reordering + if (isDraggingTrack && dragInsertIndex >= 0) + { + int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); + int lineY = dragInsertIndex * trackSlotHeight; + + g.setColour(Colours::white); + g.fillRect(0, lineY - 1, getWidth(), 3); + } + + // Makes the dragged track look visually distinct + if (isDraggingTrack && draggedTrack != nullptr) + { + Rectangle draggedBounds = draggedTrack->getBounds(); + g.setColour(Colours::black.withAlpha(0.3f)); + g.fillRect(draggedBounds); + } } void resized() override @@ -328,6 +347,25 @@ class TrackAreaWidget : public Component, resized(); } + void reorderTrack(MediaDisplayComponent* draggedDisplay, int newIndex) + { + auto it = + std::find_if(mediaDisplays.begin(), + mediaDisplays.end(), + [draggedDisplay](const auto& ptr) { return ptr.get() == draggedDisplay; }); + + if (it == mediaDisplays.end()) return; + + auto draggedPtr = std::move(*it); + mediaDisplays.erase(it); + + newIndex = jlimit(0, (int)mediaDisplays.size(), newIndex); + + mediaDisplays.insert(mediaDisplays.begin() + newIndex, std::move(draggedPtr)); + + resized(); + } + void filesDropped(const StringArray& files, int /*x*/, int /*y*/) override { for (String f : files) @@ -357,6 +395,86 @@ class TrackAreaWidget : public Component, } } + int getInsertIndexAtY(int y) + { + int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); + int index = y / trackSlotHeight; + return jlimit(0, getNumTracks(), index); + } + + void mouseDown(const MouseEvent& e) override + { + if (!isThumbnailWidget()) return; + + Component* clicked = e.eventComponent; + + MediaDisplayComponent* clickedDisplay = nullptr; + Component* c = clicked; + while (c != nullptr && c != this) + { + if (auto* md = dynamic_cast(c)) + { + clickedDisplay = md; + break; + } + c = c->getParentComponent(); + } + + if (clickedDisplay) + { + draggedTrack = clickedDisplay; + isDraggingTrack = false; + } + } + + void mouseDrag(const MouseEvent& e) override + { + if (!isThumbnailWidget() || draggedTrack == nullptr) return; + + Point posInThis = e.getEventRelativeTo(this).getPosition(); + + if (!isDraggingTrack && e.getDistanceFromDragStart() > 5) + { + isDraggingTrack = true; + } + + if (isDraggingTrack) + { + dragInsertIndex = getInsertIndexAtY(posInThis.y); + // Draws the drop indicator line + repaint(); + } + } + + void mouseUp(const MouseEvent& e) override + { + if (!isThumbnailWidget()) return; + + if (isDraggingTrack && draggedTrack != nullptr) + { + int currentIndex = -1; + for (int i = 0; i < (int)mediaDisplays.size(); i++) + { + if (mediaDisplays[i].get() == draggedTrack) + { + currentIndex = i; + break; + } + } + + if (dragInsertIndex != currentIndex && dragInsertIndex != currentIndex + 1) + { + reorderTrack(draggedTrack, dragInsertIndex); + } + } + + // Reset the drag state + draggedTrack = nullptr; + dragInsertIndex = -1; + isDraggingTrack = false; + repaint(); + } + const DisplayMode displayMode; const int fixedTrackHeight = 0; @@ -364,5 +482,10 @@ class TrackAreaWidget : public Component, int fixedTotalWidth = 0; int minTotalHeight = 0; + // For reordering tracks via dragging + MediaDisplayComponent* draggedTrack = nullptr; + int dragInsertIndex = -1; + bool isDraggingTrack = false; + std::vector> mediaDisplays; }; From 98a9d63a27872902159c522b5c01b7d4a42fbcdd Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Mon, 2 Mar 2026 11:07:53 -0600 Subject: [PATCH 002/149] Gradio error handling and status updates --- src/clients/Client.h | 3 + src/clients/GradioClient.h | 279 +++++++++++++++++++++++++++++++-- src/utils/Errors.h | 20 ++- src/utils/Messages.h | 135 ++++++++++++++++ src/widgets/StatusAreaWidget.h | 114 ++++++++++---- 5 files changed, 501 insertions(+), 50 deletions(-) create mode 100644 src/utils/Messages.h diff --git a/src/clients/Client.h b/src/clients/Client.h index 5596eb01..588b74a8 100644 --- a/src/clients/Client.h +++ b/src/clients/Client.h @@ -14,6 +14,7 @@ #include "../utils/Errors.h" #include "../utils/Labels.h" #include "../utils/Logging.h" +#include "../utils/Messages.h" #include "../utils/Settings.h" using namespace juce; @@ -292,6 +293,8 @@ class Client String getCommonHeaders() const { return getAuthorizationHeader() + acceptHeader; } String getJSONHeaders() const { return getCommonHeaders() + contentTypeJSONHeader; } + SharedResourcePointer statusMessage; + private: String getAuthorizationHeader() const { diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index 9b77390f..5f144ed8 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -18,7 +18,6 @@ class GradioClient : public Client Complete, Heartbeat, Error - //Generating }; GradioClient() @@ -643,7 +642,7 @@ class GradioClient : public Client return OpResult::ok(); } - String extractPayLoad(String response) + String extractPayload(String response) { String payload = response.trim(); @@ -655,6 +654,225 @@ class GradioClient : public Client return payload; } + static bool isEventLine(const String& line, GradioEvents event) + { + return line.trim().equalsIgnoreCase("event: " + enumToString(event)); + } + + static String extractErrorTextFromPayload(const String& payload) + { + String normalizedPayload = payload.trim(); + + if (normalizedPayload.isEmpty() || normalizedPayload.equalsIgnoreCase("null") + || normalizedPayload.equalsIgnoreCase("none")) + { + return ""; + } + + var parsedPayload = JSON::parse(normalizedPayload); + + if (auto* payloadDict = parsedPayload.getDynamicObject()) + { + for (auto key : { "error", "message", "detail" }) + { + if (payloadDict->hasProperty(key)) + { + String value = payloadDict->getProperty(key).toString().trim(); + + if (value.isNotEmpty() && ! value.equalsIgnoreCase("null")) + { + return value; + } + } + } + } + + return normalizedPayload; + } + + static bool isExplicitQuotaError(const String& text) + { + if (text.isEmpty()) + { + return false; + } + + if (text.containsIgnoreCase("exceeded your gpu quota")) + { + return true; + } + + if (text.containsIgnoreCase("gradio_client.exceptions.apperror") + && text.containsIgnoreCase("usage quota")) + { + return true; + } + + if (text.containsIgnoreCase("spaces.zero.gradio.htmlerror") + && text.containsIgnoreCase("gpu quota")) + { + return true; + } + + return false; + } + + static String extractExceptionLabel(const String& text) + { + if (text.isEmpty()) + { + return ""; + } + + for (auto label : { "NameError", "TypeError", "ImportError", "ModuleNotFoundError", + "ValueError", "KeyError", "AttributeError", "RuntimeError", + "AssertionError", "IndexError", "SyntaxError", "OSError" }) + { + if (text.containsIgnoreCase(label)) + { + return label; + } + } + + StringArray tokens = StringArray::fromTokens( + text, " \t\r\n,:;()[]{}<>\"'`\\/|", ""); + + for (auto& token : tokens) + { + String trimmedToken = token.trim(); + + if (trimmedToken.length() > 5 && trimmedToken.endsWithIgnoreCase("Error") + && ! trimmedToken.equalsIgnoreCase("error")) + { + return trimmedToken; + } + } + + return ""; + } + + static String extractShortReason(const String& text) + { + String firstLine = text.upToFirstOccurrenceOf("\n", false, false).trim(); + + if (firstLine.length() > 120) + { + firstLine = firstLine.substring(0, 120).trimEnd() + "..."; + } + + return firstLine; + } + + String collectErrorContextText(std::unique_ptr& stream, int maxLines = 8) + { + String contextText; + + for (int i = 0; i < maxLines && ! stream->isExhausted(); ++i) + { + String contextLine = stream->readNextLine().trim(); + + if (contextLine.isEmpty()) + { + continue; + } + + if (contextLine.startsWithIgnoreCase("event:")) + { + break; + } + + String normalizedContext = extractErrorTextFromPayload(extractPayload(contextLine)); + + if (normalizedContext.isEmpty()) + { + continue; + } + + if (contextText.isNotEmpty()) + { + contextText += "\n"; + } + + contextText += normalizedContext; + } + + return contextText; + } + + static String extractMessageTypeFromPayload(const String& payload) + { + String normalizedPayload = payload.trim(); + + if (normalizedPayload.isEmpty() || normalizedPayload.equalsIgnoreCase("null") + || normalizedPayload.equalsIgnoreCase("none")) + { + return ""; + } + + var parsedPayload = JSON::parse(normalizedPayload); + + if (auto* payloadDict = parsedPayload.getDynamicObject()) + { + if (payloadDict->hasProperty("msg")) + { + String value = payloadDict->getProperty("msg").toString().trim(); + + if (value.isNotEmpty() && ! value.equalsIgnoreCase("null")) + { + return value; + } + } + } + + return ""; + } + + static String formatProcessMessage(const String& messageType) + { + if (messageType.isEmpty()) + { + return ""; + } + + if (messageType.equalsIgnoreCase("process_starts")) + { + return "[process] started"; + } + + if (messageType.equalsIgnoreCase("process_completed")) + { + return "[process] completed"; + } + + if (messageType.startsWithIgnoreCase("process_")) + { + String detail = + messageType.fromFirstOccurrenceOf("process_", false, false).replace("_", " "); + return "[process] " + detail; + } + + return "[status] " + messageType.replace("_", " "); + } + + void appendProcessMessageFromDataLine(const String& dataLine) + { + if (statusMessage == nullptr) + { + return; + } + + String payload = extractPayload(dataLine); + String messageType = extractMessageTypeFromPayload(payload); + String statusText = formatProcessMessage(messageType); + + if (statusText.isEmpty()) + { + return; + } + + statusMessage->setMessage(statusText); + } + // String response version OpResult makeGETRequest(const URL endpoint, String& response, @@ -676,32 +894,69 @@ class GradioClient : public Client DBG_AND_LOG("GradioClient::makeGETRequest: Streamed response \"" << response << "\"."); - if (response.containsIgnoreCase(enumToString(GradioEvents::Complete))) + String eventLine = response.trim(); + + if (eventLine.isEmpty()) { - response = extractPayLoad(stream->readNextLine()); + continue; + } + + if (isEventLine(eventLine, GradioEvents::Complete)) + { + response = extractPayload(stream->readNextLine()); DBG_AND_LOG("GradioClient::makeGETRequest: Final response \"" << response << "\"."); break; } - else if (response.containsIgnoreCase(enumToString(GradioEvents::Error))) + else if (isEventLine(eventLine, GradioEvents::Error)) { - response = stream->readNextLine(); + String errorDataLine = stream->readNextLine(); + + DBG_AND_LOG( + "GradioClient::makeGETRequest: Error response \"" << errorDataLine << "\"."); + + String payload = extractPayload(errorDataLine); + String diagnosticText = extractErrorTextFromPayload(payload); + String contextText = collectErrorContextText(stream); + + String classificationText = diagnosticText; + + if (contextText.isNotEmpty()) + { + if (classificationText.isNotEmpty()) + { + classificationText += "\n"; + } + + classificationText += contextText; + } - DBG_AND_LOG("GradioClient::makeGETRequest: Error response \"" << response << "\"."); + String reasonText = diagnosticText.isNotEmpty() ? diagnosticText : contextText; - // TODO - could potentially identify other errors (e.g., too many requests) + String reason = extractExceptionLabel(reasonText); - return OpResult::fail(GradioError { GradioError::Type::RuntimeError, errorPath }); + if (reason.isEmpty() && reasonText.isNotEmpty()) + { + reason = extractShortReason(reasonText); + } + + GradioError::Type errorType = + isExplicitQuotaError(classificationText) ? GradioError::Type::QuotaExceeded + : GradioError::Type::RuntimeError; + + return OpResult::fail(GradioError { errorType, errorPath, reason }); + } + else if (eventLine.startsWithIgnoreCase("data:")) + { + appendProcessMessageFromDataLine(eventLine); } else { // TODO - what other information is available? - // Informational or progress events + // Unhandled SSE events (e.g., heartbeat) // Examples: // - event: heartbeat - // - event: log - // - event: progress } } diff --git a/src/utils/Errors.h b/src/utils/Errors.h index fd9c8318..6a905457 100644 --- a/src/utils/Errors.h +++ b/src/utils/Errors.h @@ -221,12 +221,14 @@ struct GradioError { enum class Type { - RuntimeError + RuntimeError, + QuotaExceeded }; Type type; String endpointPath; + String reason; }; inline String toUserMessage(const GradioError& e) @@ -237,15 +239,21 @@ inline String toUserMessage(const GradioError& e) { case GradioError::Type::RuntimeError: - userMessage = "A runtime error occurred at endpoint"; + userMessage = "The Hugging Face Space reported a runtime error"; - if (e.endpointPath.isNotEmpty()) + if (e.reason.isNotEmpty()) { - userMessage += " \"" + e.endpointPath + "\""; + userMessage += " (" + e.reason + ")"; } - userMessage += ". If this is a Hugging Face space running on ZeroGPU, this " - "can also indicate a user has exceeded their daily ZeroGPU quota."; + userMessage += ". Please open the Space logs for details."; + + return userMessage; + + case GradioError::Type::QuotaExceeded: + + userMessage = "ZeroGPU quota appears to be exceeded for this Space. " + "Please try again later or use an account with available quota."; return userMessage; } diff --git a/src/utils/Messages.h b/src/utils/Messages.h new file mode 100644 index 00000000..6c980748 --- /dev/null +++ b/src/utils/Messages.h @@ -0,0 +1,135 @@ +/** + * @file Messages.h + * @brief Shared message resources used across UI and clients. + * @author saumya-pailwan + */ + +#pragma once + +#include + +using namespace juce; + +struct SharedMessage : public ChangeBroadcaster +{ + virtual ~SharedMessage() = default; + + virtual void setMessage(const String& m) + { + { + const ScopedLock lock(messageLock); + message = m; + } + + sendChangeMessage(); + } + + virtual void clearMessage() + { + { + const ScopedLock lock(messageLock); + message.clear(); + } + + sendChangeMessage(); + } + + String getMessage() const + { + const ScopedLock lock(messageLock); + return message; + } + +protected: + mutable CriticalSection messageLock; + String message; +}; + +struct StatusHistorySnapshot +{ + uint64 revision = 0; + String lastEntry; + uint64 trimRevision = 0; + uint64 clearRevision = 0; +}; + +struct StatusMessage : SharedMessage +{ + static constexpr int maxHistoryEntries = 250; + + void setMessage(const String& m) override + { + { + const ScopedLock lock(messageLock); + message = m; + appendHistoryEntryUnsafe(m); + } + + sendChangeMessage(); + } + + void clearMessage() override + { + { + const ScopedLock lock(messageLock); + message.clear(); + history.clear(); + ++revision; + ++clearRevision; + lastEntry.clear(); + } + + sendChangeMessage(); + } + + String getHistoryText() const + { + const ScopedLock lock(messageLock); + return history.joinIntoString("\n"); + } + + StatusHistorySnapshot getHistorySnapshot() + { + const ScopedLock lock(messageLock); + return StatusHistorySnapshot { revision, lastEntry, trimRevision, clearRevision }; + } + +private: + void appendHistoryEntryUnsafe(const String& entryText) + { + if (entryText.isEmpty()) + { + return; + } + + String timestamp = Time::getCurrentTime().formatted("%H:%M:%S"); + + lastEntry = "[" + timestamp + "] " + entryText; + history.add(lastEntry); + + bool didTrim = false; + + while (history.size() > maxHistoryEntries) + { + history.remove(0); + didTrim = true; + } + + ++revision; + + if (didTrim) + { + ++trimRevision; + } + } + + uint64 revision = 0; + uint64 trimRevision = 0; + uint64 clearRevision = 0; + String lastEntry; + StringArray history; +}; + +struct InstructionsMessage : SharedMessage +{ +}; diff --git a/src/widgets/StatusAreaWidget.h b/src/widgets/StatusAreaWidget.h index 3c7189a3..3693e815 100644 --- a/src/widgets/StatusAreaWidget.h +++ b/src/widgets/StatusAreaWidget.h @@ -8,31 +8,9 @@ #include -using namespace juce; - -struct SharedMessage : public ChangeBroadcaster -{ - void setMessage(const String& m) - { - message = m; - sendChangeMessage(); - } - - void clearMessage() - { - message.clear(); - sendChangeMessage(); - } - - String message; -}; +#include "../utils/Messages.h" -struct StatusMessage : SharedMessage -{ -}; -struct InstructionsMessage : SharedMessage -{ -}; +using namespace juce; template class MessageBox : public Component, ChangeListener @@ -51,7 +29,7 @@ class MessageBox : public Component, ChangeListener ~MessageBox() override { sharedMessage->removeChangeListener(this); } - void paint(Graphics& g) + void paint(Graphics& g) override { g.setColour(Colour(0x33, 0x33, 0x33)); g.fillAll(); @@ -60,11 +38,11 @@ class MessageBox : public Component, ChangeListener g.drawRect(getLocalBounds(), 1); } - void resized() { messageLabel.setBounds(getLocalBounds()); } + void resized() override { messageLabel.setBounds(getLocalBounds()); } - void changeListenerCallback(ChangeBroadcaster* /*source*/) + void changeListenerCallback(ChangeBroadcaster* /*source*/) override { - messageLabel.setText(sharedMessage->message, dontSendNotification); + messageLabel.setText(sharedMessage->getMessage(), dontSendNotification); } private: @@ -75,16 +53,88 @@ class MessageBox : public Component, ChangeListener using StatusBox = MessageBox; using InstructionsBox = MessageBox; +class StatusHistoryBox : public Component, ChangeListener +{ +public: + StatusHistoryBox() + { + historyEditor.setMultiLine(true); + historyEditor.setReadOnly(true); + historyEditor.setScrollbarsShown(false); + historyEditor.setCaretVisible(false); + historyEditor.setPopupMenuEnabled(false); + historyEditor.setColour(TextEditor::backgroundColourId, Colour(0x33, 0x33, 0x33)); + historyEditor.setColour(TextEditor::textColourId, Colour(0xE0, 0xE0, 0xE0)); + + addAndMakeVisible(historyEditor); + + StatusHistorySnapshot snapshot = sharedMessage->getHistorySnapshot(); + historyEditor.setText(sharedMessage->getHistoryText(), false); + historyEditor.moveCaretToEnd(); + lastRevisionSeen = snapshot.revision; + lastTrimRevisionSeen = snapshot.trimRevision; + lastClearRevisionSeen = snapshot.clearRevision; + + sharedMessage->addChangeListener(this); + } + + ~StatusHistoryBox() override { sharedMessage->removeChangeListener(this); } + + void paint(Graphics& g) override + { + g.setColour(Colour(0x44, 0x44, 0x44)); + g.drawRect(getLocalBounds(), 1); + } + + void resized() override { historyEditor.setBounds(getLocalBounds()); } + + void changeListenerCallback(ChangeBroadcaster* /*source*/) override + { + StatusHistorySnapshot snapshot = sharedMessage->getHistorySnapshot(); + + bool trimChanged = snapshot.trimRevision != lastTrimRevisionSeen; + bool clearChanged = snapshot.clearRevision != lastClearRevisionSeen; + bool shouldRebuild = (snapshot.revision <= lastRevisionSeen) || trimChanged || clearChanged + || snapshot.revision != (lastRevisionSeen + 1); + + if (shouldRebuild) + { + historyEditor.setText(sharedMessage->getHistoryText(), false); + } + else if (snapshot.lastEntry.isNotEmpty()) + { + if (historyEditor.getText().isNotEmpty()) + { + historyEditor.insertTextAtCaret("\n"); + } + + historyEditor.insertTextAtCaret(snapshot.lastEntry); + } + + lastRevisionSeen = snapshot.revision; + lastTrimRevisionSeen = snapshot.trimRevision; + lastClearRevisionSeen = snapshot.clearRevision; + historyEditor.moveCaretToEnd(); + } + +private: + SharedResourcePointer sharedMessage; + TextEditor historyEditor; + uint64 lastRevisionSeen = 0; + uint64 lastTrimRevisionSeen = 0; + uint64 lastClearRevisionSeen = 0; +}; + class StatusAreaWidget : public Component { public: StatusAreaWidget() { addAndMakeVisible(instructionsBox); - addAndMakeVisible(statusBox); + addAndMakeVisible(statusHistoryBox); } - ~StatusAreaWidget() {} + ~StatusAreaWidget() override {} void resized() override { @@ -92,7 +142,7 @@ class StatusAreaWidget : public Component statusArea.flexDirection = FlexBox::Direction::row; statusArea.items.add(FlexItem(instructionsBox).withFlex(1).withMargin(marginSize)); - statusArea.items.add(FlexItem(statusBox).withFlex(1).withMargin(marginSize)); + statusArea.items.add(FlexItem(statusHistoryBox).withFlex(1).withMargin(marginSize)); statusArea.performLayout(getLocalBounds()); } @@ -101,5 +151,5 @@ class StatusAreaWidget : public Component const float marginSize = 2; InstructionsBox instructionsBox; - StatusBox statusBox; + StatusHistoryBox statusHistoryBox; }; From 216e96c507ab9fea7207df66c38ca17db102135e Mon Sep 17 00:00:00 2001 From: Elek Yuhas Date: Wed, 4 Mar 2026 11:56:03 -0500 Subject: [PATCH 003/149] Initial copy-paste implementation for MacOS. --- CMakeLists.txt | 2 ++ src/media/MediaDisplayComponent.cpp | 51 ++++++++++++++++++++++++++++- src/media/MediaDisplayComponent.h | 6 ++++ src/media/copy.h | 4 +++ src/media/copy.mm | 20 +++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/media/copy.h create mode 100644 src/media/copy.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index c2efdf26..c9f9e63a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,6 +99,8 @@ target_sources(${PROJECT_NAME} src/media/MidiDisplayComponent.cpp src/media/OutputLabelComponent.cpp + src/media/copy.mm + src/media/pianoroll/KeyboardComponent.cpp src/media/pianoroll/NoteGridComponent.cpp src/media/pianoroll/PianoRollComponent.cpp diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 111a311c..7c0c096f 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1,6 +1,7 @@ #include "MediaDisplayComponent.h" #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" +#include "copy.h" MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} @@ -11,6 +12,7 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW deviceManager.initialise(0, 2, nullptr, true, {}, nullptr); deviceManager.addAudioCallback(&sourcePlayer); + sourcePlayer.setSource(&transportSource); @@ -99,6 +101,24 @@ void MediaDisplayComponent::initializeButtons() saveFileButton.addMode(saveFileButtonInactiveInfo); headerComponent.addAndMakeVisible(saveFileButton); + // Mode when an copyable file is loaded + copyFileButtonActiveInfo = MultiButton::Mode { "Copy-Active", + "Click to copy the media file.", + [this] { copyFileCallback(); }, + MultiButton::DrawingMode::IconOnly, + Colours::lightblue, + fontawesome::Save }; + // Mode when there is nothing to copy + copyFileButtonInactiveInfo = + MultiButton::Mode { "Copy-Inactive", "Nothing to copy.", + [this] {}, MultiButton::DrawingMode::IconOnly, + Colours::lightgrey, fontawesome::Save }; + copyFileButton.addMode(copyFileButtonActiveInfo); + copyFileButton.addMode(copyFileButtonInactiveInfo); + headerComponent.addAndMakeVisible(copyFileButton); + + + resetButtonState(); } @@ -244,7 +264,7 @@ void MediaDisplayComponent::resized() if (isOutputTrack()) { buttonsFlexBox.items.add( - FlexItem(saveFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(copyFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); } buttonsFlexBox.performLayout(buttonsComponent.getBounds()); @@ -428,6 +448,7 @@ void MediaDisplayComponent::resetButtonState() playStopButton.setMode(playButtonInactiveInfo.displayLabel); chooseFileButton.setMode(chooseFileButtonInfo.displayLabel); saveFileButton.setMode(saveFileButtonInactiveInfo.displayLabel); + copyFileButton.setMode(copyFileButtonInactiveInfo.displayLabel); } void MediaDisplayComponent::initializeDisplay(const URL& filePath) @@ -460,6 +481,7 @@ void MediaDisplayComponent::updateDisplay(const URL& filePath) playStopButton.setMode(playButtonActiveInfo.displayLabel); saveFileButton.setMode(saveFileButtonActiveInfo.displayLabel); + copyFileButton.setMode(copyFileButtonActiveInfo.displayLabel); } void MediaDisplayComponent::setOriginalFilePath(URL filePath) @@ -740,6 +762,33 @@ void MediaDisplayComponent::saveFileCallback() } } + +void MediaDisplayComponent::copyFileCallback() +{ + if (!isFileLoaded()) + { + if (statusMessage != nullptr) + statusMessage->setMessage("No file loaded."); + return; + } + + // If you're copying the original file path: + juce::File file = getOriginalFilePath().getLocalFile(); + + if (file.exists()) + { + copyFileToClipboard(file); + + if (statusMessage != nullptr) + statusMessage->setMessage("File path copied to clipboard."); + } + else + { + if (statusMessage != nullptr) + statusMessage->setMessage("File does not exist."); + } +} + float MediaDisplayComponent::getPixelsPerSecond() { if (visibleRange.getLength()) diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 312d9410..9cf6a55e 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -110,6 +110,8 @@ class MediaDisplayComponent : public Component, void saveFileCallback(); + void copyFileCallback(); + virtual double getTotalLengthInSecs() = 0; virtual double getTimeAtOrigin() { return visibleRange.getStart(); } virtual float getPixelsPerSecond(); @@ -236,6 +238,7 @@ class MediaDisplayComponent : public Component, // Media + overhead panel (if any) Component mediaAreaContainer; + // Header sub-components Label trackNameLabel; MultiButton playStopButton; @@ -247,6 +250,9 @@ class MediaDisplayComponent : public Component, MultiButton saveFileButton; MultiButton::Mode saveFileButtonActiveInfo; MultiButton::Mode saveFileButtonInactiveInfo; + MultiButton copyFileButton; + MultiButton::Mode copyFileButtonActiveInfo; + MultiButton::Mode copyFileButtonInactiveInfo; // Panel displaying overhead labels ColorablePanel overheadPanel { overheadPanelColor }; diff --git a/src/media/copy.h b/src/media/copy.h new file mode 100644 index 00000000..d23614b4 --- /dev/null +++ b/src/media/copy.h @@ -0,0 +1,4 @@ +#pragma once +#include + +void copyFileToClipboard (const juce::File& file); \ No newline at end of file diff --git a/src/media/copy.mm b/src/media/copy.mm new file mode 100644 index 00000000..d4f5dd68 --- /dev/null +++ b/src/media/copy.mm @@ -0,0 +1,20 @@ +#import + +#include "copy.h" + +void copyFileToClipboard (const juce::File& file) +{ + if (! file.existsAsFile()) + return; + + NSPasteboard* pb = [NSPasteboard generalPasteboard]; + + [pb declareTypes: [NSArray arrayWithObject: NSPasteboardTypeFileURL] + owner: nil]; + + NSString* path = [NSString stringWithUTF8String: file.getFullPathName().toUTF8()]; + NSURL* fileURL = [NSURL fileURLWithPath: path]; + + [pb setString: [fileURL absoluteString] + forType: NSPasteboardTypeFileURL]; +} \ No newline at end of file From ed56787bb71ae36b3655ace548d76b54d2fe3463 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 5 Mar 2026 12:52:46 -0500 Subject: [PATCH 004/149] Upgraded to latest version of JUCE. --- JUCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JUCE b/JUCE index 9f643254..501c0767 160000 --- a/JUCE +++ b/JUCE @@ -1 +1 @@ -Subproject commit 9f64325446ec0baf11f0f5f99c8484a15bbd1ab0 +Subproject commit 501c07674e1ad693085a7e7c398f205c2677f5da From 3cd192f400c025646a5f37f941636150efa11233 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 6 Mar 2026 13:29:07 -0500 Subject: [PATCH 005/149] Fixed downward drag offset and out-of-bounds drag. --- src/widgets/TrackAreaWidget.h | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index b890ed0d..7a60f06a 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -356,9 +356,17 @@ class TrackAreaWidget : public Component, if (it == mediaDisplays.end()) return; + // Track the old index for downward reordering + int oldIndex = std::distance(mediaDisplays.begin(), it); + auto draggedPtr = std::move(*it); mediaDisplays.erase(it); + + // Decrement index if moving downward to account for shifting indicies + if (newIndex > oldIndex) + newIndex--; + newIndex = jlimit(0, (int)mediaDisplays.size(), newIndex); mediaDisplays.insert(mediaDisplays.begin() + newIndex, std::move(draggedPtr)); @@ -440,8 +448,18 @@ class TrackAreaWidget : public Component, if (isDraggingTrack) { - dragInsertIndex = getInsertIndexAtY(posInThis.y); - // Draws the drop indicator line + // Only update the drop index while inside the widget + if (getLocalBounds().contains(posInThis)) + { + dragInsertIndex = getInsertIndexAtY(posInThis.y); + } + else + { + // Hides the indicator line when outside the widget + dragInsertIndex = -1; + } + + // Updates the drop indicator line repaint(); } } @@ -450,7 +468,11 @@ class TrackAreaWidget : public Component, { if (!isThumbnailWidget()) return; - if (isDraggingTrack && draggedTrack != nullptr) + // Tracks the release psoition of the mouse + Point releasePos = e.getEventRelativeTo(this).getPosition(); + + // Only reorders if the mouse was released inside the widget + if (isDraggingTrack && draggedTrack != nullptr && getLocalBounds().contains(releasePos)) { int currentIndex = -1; for (int i = 0; i < (int)mediaDisplays.size(); i++) From 790ca8ea155104799aa018d60e9d4c3b2b573dc2 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Mon, 16 Mar 2026 20:14:28 -0400 Subject: [PATCH 006/149] Initial attempt to implement time axis --- src/media/MediaDisplayComponent.cpp | 124 ++++++++++++++++++++++++++-- src/media/MediaDisplayComponent.h | 5 ++ 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 91178c12..7142ae06 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -2,6 +2,82 @@ #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" +#include + +namespace +{ +class MediaDisplayComponent::TimeAxisStrip : public Component +{ +public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override + { + if (owner == nullptr || ! owner->isFileLoaded()) + return; + + const auto& visibleRange = owner->getVisibleRange(); + const float pps = owner->getPixelsPerSecond(); + const double totalLength = owner->getTotalLengthInSecs(); + + if (pps <= 0.0f || visibleRange.getLength() <= 0.0) + return; + + const double visibleStart = visibleRange.getStart(); + const double visibleEnd = visibleRange.getEnd(); + const int w = getWidth(); + const int h = getHeight(); + + g.setColour(Colours::darkgrey); + g.fillRect(getLocalBounds()); + + g.setColour(Colours::lightgrey.withAlpha(0.8f)); + + // Choose tick interval so we get roughly 5–15 major ticks + double visibleLength = visibleRange.getLength(); + double step = 1.0; + if (visibleLength > 0.0) + { + double logStep = std::ceil(std::log10(visibleLength / 10.0)); + step = std::pow(10.0, logStep); + step = std::max(0.01, step); + } + + const double firstTick = std::ceil(visibleStart / step) * step; + const int labelHeight = jmin(14, h - 2); + g.setFont(static_cast(labelHeight)); + + for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + { + const float x = static_cast((t - visibleStart) * pps); + if (x < -50.0f || x > w + 50.0f) + continue; + + g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); + + String label; + if (t >= 60.0) + label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; + else if (step >= 1.0) + label = String(static_cast(t)) + "s"; + else + label = String(t, 1) + "s"; + + g.drawText(label, + static_cast(x) + 2, + 0, + jmin(80, w - static_cast(x)), + h, + Justification::centredLeft, + true); + } + } + +private: + MediaDisplayComponent* owner = nullptr; +}; +} // namespace + MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) @@ -36,8 +112,11 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW horizontalScrollBar.setAutoHide(false); horizontalScrollBar.addListener(this); + timeAxisStrip = std::make_unique(this); + mediaAreaContainer.addAndMakeVisible(overheadPanel); mediaAreaContainer.addAndMakeVisible(contentComponent); + mediaAreaContainer.addAndMakeVisible(*timeAxisStrip); mediaAreaContainer.addAndMakeVisible(horizontalScrollBar); addAndMakeVisible(mediaAreaContainer); @@ -277,6 +356,20 @@ void MediaDisplayComponent::resized() // Media component takes remaining space mediaAreaFlexBox.items.add(FlexItem(contentComponent).withFlex(1)); + if (timeAxisStrip != nullptr) + { + timeAxisStrip->setVisible(horizontalScrollBar.isVisible()); + if (timeAxisStrip->isVisible()) + { + mediaAreaFlexBox.items.add(FlexItem(*timeAxisStrip) + .withHeight(timeAxisHeight) + .withMargin({ 0, + getVerticalControlsWidth(), + static_cast(controlSpacing), + getMediaXPos() })); + } + } + if (horizontalScrollBar.isVisible()) { // Add horizontal scrollbar with fixed height @@ -823,6 +916,9 @@ void MediaDisplayComponent::updateVisibleRange(Range r) updateCursorPosition(); repositionLabels(); + if (timeAxisStrip != nullptr) + timeAxisStrip->repaint(); + visibleRangeCallback(); } @@ -839,16 +935,30 @@ void MediaDisplayComponent::horizontalMove(double deltaT) void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) { - horizontalZoomFactor = jlimit(1.0, 2.0, horizontalZoomFactor + deltaZoom); + const float mediaWidth = getMediaWidth(); + const double totalLength = getTotalLengthInSecs(); - double newScale = jmax(0.05, getTotalLengthInSecs() * (2.0 - horizontalZoomFactor)); + if (mediaWidth <= 0.0f || totalLength <= 0.0) + return; - double visibleStart = visibleRange.getStart(); - double visibleEnd = visibleRange.getEnd(); - double visibleLength = visibleRange.getLength(); + const float pps = getPixelsPerSecond(); + if (pps <= 0.0f) + return; + + // Fixed time scale: zoom is seconds per pixel. Same scale = same time span for any file. + const float minPps = 20.0f; + const float maxPps = static_cast(mediaWidth / totalLength); + + float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); + newPps = jlimit(minPps, maxPps, newPps); + + double newVisibleLength = static_cast(mediaWidth) / static_cast(newPps); + if (newVisibleLength > totalLength) + newVisibleLength = totalLength; - double newStart = scrollPosT - newScale * (scrollPosT - visibleStart) / visibleLength; - double newEnd = scrollPosT + newScale * (visibleEnd - scrollPosT) / visibleLength; + double newStart = scrollPosT - newVisibleLength * 0.5; + newStart = jlimit(0.0, totalLength - newVisibleLength, newStart); + double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); } diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index a4020658..5a57c0b8 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -115,6 +115,7 @@ class MediaDisplayComponent : public Component, virtual double getTotalLengthInSecs() = 0; virtual double getTimeAtOrigin() { return visibleRange.getStart(); } virtual float getPixelsPerSecond(); + const Range& getVisibleRange() const { return visibleRange; } virtual void setPlaybackPosition(double t) { transportSource.setPosition(t); } virtual double getPlaybackPosition() { return transportSource.getCurrentPosition(); } @@ -151,6 +152,7 @@ class MediaDisplayComponent : public Component, const int controlSpacing = 1; const int scrollBarSize = 8; + const int timeAxisHeight = 20; // Media (audio or MIDI) content area Component contentComponent; @@ -159,6 +161,9 @@ class MediaDisplayComponent : public Component, Range visibleRange; + class TimeAxisStrip; + std::unique_ptr timeAxisStrip; + AudioFormatManager formatManager; AudioDeviceManager deviceManager; From 4441387d732f8981b521adf304e4ba0c8ad8f514 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 17 Mar 2026 10:06:43 -0400 Subject: [PATCH 007/149] Second attempt to add time axis --- src/media/MediaDisplayComponent.cpp | 74 ----------------------------- src/media/MediaDisplayComponent.h | 74 ++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 76 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7142ae06..8b842711 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -4,80 +4,6 @@ #include -namespace -{ -class MediaDisplayComponent::TimeAxisStrip : public Component -{ -public: - explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} - - void paint(Graphics& g) override - { - if (owner == nullptr || ! owner->isFileLoaded()) - return; - - const auto& visibleRange = owner->getVisibleRange(); - const float pps = owner->getPixelsPerSecond(); - const double totalLength = owner->getTotalLengthInSecs(); - - if (pps <= 0.0f || visibleRange.getLength() <= 0.0) - return; - - const double visibleStart = visibleRange.getStart(); - const double visibleEnd = visibleRange.getEnd(); - const int w = getWidth(); - const int h = getHeight(); - - g.setColour(Colours::darkgrey); - g.fillRect(getLocalBounds()); - - g.setColour(Colours::lightgrey.withAlpha(0.8f)); - - // Choose tick interval so we get roughly 5–15 major ticks - double visibleLength = visibleRange.getLength(); - double step = 1.0; - if (visibleLength > 0.0) - { - double logStep = std::ceil(std::log10(visibleLength / 10.0)); - step = std::pow(10.0, logStep); - step = std::max(0.01, step); - } - - const double firstTick = std::ceil(visibleStart / step) * step; - const int labelHeight = jmin(14, h - 2); - g.setFont(static_cast(labelHeight)); - - for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) - { - const float x = static_cast((t - visibleStart) * pps); - if (x < -50.0f || x > w + 50.0f) - continue; - - g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); - - String label; - if (t >= 60.0) - label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; - else if (step >= 1.0) - label = String(static_cast(t)) + "s"; - else - label = String(t, 1) + "s"; - - g.drawText(label, - static_cast(x) + 2, - 0, - jmin(80, w - static_cast(x)), - h, - Justification::centredLeft, - true); - } - } - -private: - MediaDisplayComponent* owner = nullptr; -}; -} // namespace - MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 5a57c0b8..58c4151b 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -161,7 +161,77 @@ class MediaDisplayComponent : public Component, Range visibleRange; - class TimeAxisStrip; + class TimeAxisStrip : public Component + { + public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override + { + if (owner == nullptr || ! owner->isFileLoaded()) + return; + + const auto& visibleRange = owner->getVisibleRange(); + const float pps = owner->getPixelsPerSecond(); + const double totalLength = owner->getTotalLengthInSecs(); + + if (pps <= 0.0f || visibleRange.getLength() <= 0.0) + return; + + const double visibleStart = visibleRange.getStart(); + const double visibleEnd = visibleRange.getEnd(); + const int w = getWidth(); + const int h = getHeight(); + + g.setColour(Colours::darkgrey); + g.fillRect(getLocalBounds()); + + g.setColour(Colours::lightgrey.withAlpha(0.8f)); + + // Choose tick interval so we get roughly 5–15 major ticks + double visibleLength = visibleRange.getLength(); + double step = 1.0; + if (visibleLength > 0.0) + { + double logStep = std::ceil(std::log10(visibleLength / 10.0)); + step = std::pow(10.0, logStep); + step = std::max(0.01, step); + } + + const double firstTick = std::ceil(visibleStart / step) * step; + const int labelHeight = jmin(14, h - 2); + g.setFont(static_cast(labelHeight)); + + for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + { + const float x = static_cast((t - visibleStart) * pps); + if (x < -50.0f || x > w + 50.0f) + continue; + + g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); + + String label; + if (t >= 60.0) + label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; + else if (step >= 1.0) + label = String(static_cast(t)) + "s"; + else + label = String(t, 1) + "s"; + + g.drawText(label, + static_cast(x) + 2, + 0, + jmin(80, w - static_cast(x)), + h, + Justification::centredLeft, + true); + } + } + + private: + MediaDisplayComponent* owner = nullptr; + }; + std::unique_ptr timeAxisStrip; AudioFormatManager formatManager; @@ -296,4 +366,4 @@ class MediaDisplayComponent : public Component, SharedResourcePointer instructionsMessage; SharedResourcePointer statusMessage; -}; +}; \ No newline at end of file From 89b5c38a193d5db6f4a76149bce2fbdf6a610b18 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Thu, 19 Mar 2026 11:47:21 -0500 Subject: [PATCH 008/149] gradio info error handling and non-modal error popup --- src/ModelTab.h | 177 +++++++++++++++++++++++++-------- src/clients/GradioClient.h | 129 +++++++++++++++++++++++- src/utils/Errors.h | 8 +- src/widgets/StatusAreaWidget.h | 2 +- 4 files changed, 267 insertions(+), 49 deletions(-) diff --git a/src/ModelTab.h b/src/ModelTab.h index e6e8e1d0..081e3921 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -17,6 +17,7 @@ #include "utils/Errors.h" #include "utils/Logging.h" +#include "utils/Settings.h" #include "utils/Tutorial.h" using namespace juce; @@ -327,79 +328,168 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas void openErrorPopup(const Error error, std::function onExit = {}) { - MessageBoxOptions errorPopup = - MessageBoxOptions() - .withIconType(AlertWindow::WarningIcon) - .withTitle("Error") // TODO - Name of error family would be nice here - // error ? toUserMessage(*error) : "An unknown error occurred." - .withMessage(toUserMessage(error)); - std::optional openablePath = getOpenablePath(error); + String errorMessage = toUserMessage(error); - if (openablePath.has_value()) + constexpr int buttonOpenURL = 1; + constexpr int buttonOpenLogs = 2; + constexpr int buttonSendReport = 3; + constexpr int buttonOk = 0; + + String popupMessage = errorMessage + + "\n\nOptional: add context below before sending the report."; + + if (errorPopupWindow != nullptr) { - errorPopup = errorPopup.withButton("Open URL"); + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); } - errorPopup = errorPopup.withButton("Open Logs").withButton("Ok"); + errorPopupWindow = std::make_unique("Error", popupMessage, AlertWindow::WarningIcon); + AlertWindow* alertWindow = errorPopupWindow.get(); - auto alertCallback = [this, error, openablePath, onExit, errorPopup](int choice) + auto bindButtonCallback = [alertWindow](const String& buttonText, std::function callback) { - DBG_AND_LOG("ModelTab::loadModelCallback::alertCallback: Chose button index: " << choice - << "."); - - enum Choice + for (int i = 0; i < alertWindow->getNumChildComponents(); ++i) { - OpenURL, - OpenLogs, - OK - }; - - /* - TODO - The button indices assigned by MessageBoxOptions do not follow the order in which - they were added. This should be fixed in JUCE v8. The following is a temporary workaround. - - See https://forum.juce.com/t/wrong-callback-value-for-alertwindow-showokcancelbox/55671/2 + if (auto* button = dynamic_cast(alertWindow->getChildComponent(i))) + { + if (button->getButtonText() == buttonText) + { + button->onClick = std::move(callback); + return; + } + } + } + }; - When this is fixed, errorPopup can be removed from the argument list. - */ + if (openablePath.has_value()) + { + alertWindow->addButton("Open URL", buttonOpenURL); + bindButtonCallback("Open URL", [this, openablePath, onExit] { - std::map observedButtonIndicesMap = {}; + URL(*openablePath).launchInDefaultBrowser(); - if (errorPopup.getNumButtons() == 3) + if (onExit) { - observedButtonIndicesMap.insert({ 1, Choice::OpenURL }); + onExit(); } - observedButtonIndicesMap.insert( - { errorPopup.getNumButtons() - 1, Choice::OpenLogs }); + if (errorPopupWindow != nullptr) + { + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); + } + }); + } - observedButtonIndicesMap.insert({ 0, Choice::OK }); + alertWindow->addButton("Open Logs", buttonOpenLogs); + bindButtonCallback("Open Logs", [this, onExit] + { + HARPLogger::getInstance()->getLogFile().revealToUser(); - choice = observedButtonIndicesMap[choice]; + if (onExit) + { + onExit(); } - if (choice == Choice::OpenURL) + if (errorPopupWindow != nullptr) { - URL(*openablePath).launchInDefaultBrowser(); + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); } - else if (choice == Choice::OpenLogs) + }); + + alertWindow->addButton("Send Error Log to TeamUP", buttonSendReport); + bindButtonCallback("Send Error Log to TeamUP", [this, error, errorMessage, onExit] + { + if (errorPopupWindow != nullptr) { - HARPLogger::getInstance()->getLogFile().revealToUser(); + sendErrorReportEmail( + error, errorMessage, errorPopupWindow->getTextEditorContents("errorReportNotes")); } - else + + if (onExit) { - // Nothing to do + onExit(); + } + + if (errorPopupWindow != nullptr) + { + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); } + }); + alertWindow->addButton("Ok", buttonOk); + bindButtonCallback("Ok", [this, onExit] + { if (onExit) { - // Perform optional state cleanup onExit(); } - }; - AlertWindow::showAsync(errorPopup, alertCallback); + if (errorPopupWindow != nullptr) + { + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); + } + }); + + alertWindow->addTextEditor( + "errorReportNotes", + "", + "What were you doing when this happened? (optional)"); + + addAndMakeVisible(*errorPopupWindow); + errorPopupWindow->setAlwaysOnTop(true); + + int popupWidth = jmax(560, jmin(getWidth() - 24, 1100)); + int popupHeight = 280; + errorPopupWindow->setBounds(getLocalBounds().withSizeKeepingCentre(popupWidth, popupHeight)); + errorPopupWindow->toFront(true); + } + + void sendErrorReportEmail(const Error& error, const String& errorMessage, const String& notes) + { + String supportEmail = Settings::getString("supportEmail", "support@teamup.tech").trim(); + + if (supportEmail.isEmpty()) + { + AlertWindow::showMessageBoxAsync( + AlertWindow::WarningIcon, + "Missing Support Email", + "Unable to send report because support email is not configured.", + "Ok"); + return; + } + + File logFile = HARPLogger::getInstance()->getLogFile(); + + String body; + body << "Please investigate this HARP error.\n\n"; + body << "Error:\n" << errorMessage << "\n\n"; + + if (const auto* gradioError = std::get_if(&error)) + { + if (gradioError->endpointPath.isNotEmpty()) + { + body << "Endpoint:\n" << gradioError->endpointPath << "\n\n"; + } + } + + body << "User notes:\n" << (notes.isNotEmpty() ? notes : "(none)") << "\n\n"; + body << "Log file:\n" << logFile.getFullPathName() << "\n\n"; + body << "Please attach the log file from the path above when sending."; + + String subject = "HARP Error Report"; + + String escapedSubject = URL::addEscapeChars(subject, true); + String escapedBody = URL::addEscapeChars(body, true); + String escapedRecipient = URL::addEscapeChars(supportEmail, true); + + URL("mailto:" + escapedRecipient + "?subject=" + escapedSubject + "&body=" + escapedBody) + .launchInDefaultBrowser(); } void loadModelCallback() @@ -603,4 +693,5 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas ThreadPool processingThreadPool { 10 }; std::atomic currentProcessID { 0 }; + std::unique_ptr errorPopupWindow; }; diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index 5f144ed8..ff4ed03b 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -586,6 +586,29 @@ class GradioClient : public Client if (statusCode != 200) { + String diagnosticText = extractErrorTextFromPayload(response); + + if (diagnosticText.isNotEmpty()) + { + String reason = extractExceptionLabel(diagnosticText); + + if (reason.isEmpty()) + { + reason = extractShortReason(diagnosticText); + } + + if (statusMessage != nullptr && reason.isNotEmpty()) + { + statusMessage->setMessage("[error] " + reason); + } + + GradioError::Type errorType = + isExplicitQuotaError(diagnosticText) ? GradioError::Type::QuotaExceeded + : GradioError::Type::RuntimeError; + + return OpResult::fail(GradioError { errorType, errorPath, reason }); + } + return OpResult::fail(HttpError { HttpError::Type::BadStatusCode, HttpError::Request::POST, errorPath, statusCode }); } @@ -827,6 +850,90 @@ class GradioClient : public Client return ""; } + static String extractFirstNonEmptyField( + const DynamicObject* object, std::initializer_list keys) + { + if (object == nullptr) + { + return ""; + } + + for (auto key : keys) + { + Identifier id(key); + + if (! object->hasProperty(id)) + { + continue; + } + + String value = object->getProperty(id).toString().trim(); + + if (value.isNotEmpty() && ! value.equalsIgnoreCase("null") + && ! value.equalsIgnoreCase("undefined")) + { + return value; + } + } + + return ""; + } + + static String extractGradioAlertStatusFromPayload(const String& payload) + { + String normalizedPayload = payload.trim(); + + if (normalizedPayload.isEmpty() || normalizedPayload.equalsIgnoreCase("null") + || normalizedPayload.equalsIgnoreCase("none")) + { + return ""; + } + + var parsedPayload = JSON::parse(normalizedPayload); + auto* payloadDict = parsedPayload.getDynamicObject(); + + if (payloadDict == nullptr) + { + return ""; + } + + String severity = + extractFirstNonEmptyField(payloadDict, { "type", "level", "severity", "status" }) + .toLowerCase(); + String message = + extractFirstNonEmptyField(payloadDict, { "message", "detail", "error", "reason" }); + + if (message.isEmpty()) + { + var outputVar = payloadDict->getProperty("output"); + + if (auto* outputDict = outputVar.getDynamicObject()) + { + message = + extractFirstNonEmptyField(outputDict, { "message", "detail", "error", "reason" }); + } + } + + if (message.isEmpty()) + { + return ""; + } + + if (severity.isEmpty() || severity == "success") + { + if (message.containsIgnoreCase("error")) + { + severity = "error"; + } + else + { + severity = "info"; + } + } + + return "[" + severity + "] " + message; + } + static String formatProcessMessage(const String& messageType) { if (messageType.isEmpty()) @@ -863,7 +970,17 @@ class GradioClient : public Client String payload = extractPayload(dataLine); String messageType = extractMessageTypeFromPayload(payload); - String statusText = formatProcessMessage(messageType); + String statusText; + + if (messageType.isNotEmpty()) + { + statusText = formatProcessMessage(messageType); + } + + if (statusText.isEmpty()) + { + statusText = extractGradioAlertStatusFromPayload(payload); + } if (statusText.isEmpty()) { @@ -945,6 +1062,16 @@ class GradioClient : public Client isExplicitQuotaError(classificationText) ? GradioError::Type::QuotaExceeded : GradioError::Type::RuntimeError; + if (statusMessage != nullptr) + { + String statusErrorText = reasonText.isNotEmpty() ? reasonText : classificationText; + + if (statusErrorText.isNotEmpty()) + { + statusMessage->setMessage("[error] " + extractShortReason(statusErrorText)); + } + } + return OpResult::fail(GradioError { errorType, errorPath, reason }); } else if (eventLine.startsWithIgnoreCase("data:")) diff --git a/src/utils/Errors.h b/src/utils/Errors.h index 6a905457..1b2795dc 100644 --- a/src/utils/Errors.h +++ b/src/utils/Errors.h @@ -168,9 +168,9 @@ inline String toUserMessage(const HttpError& e) if (e.request == HttpError::Request::POST) { - userMessage += " If this is a valid Hugging Face space, this " - "could indicate the space is sleeping or restarting. " - "Please try again in a few minutes."; + userMessage += " This may be a temporary connection issue. " + "Click on Open URL to get the space restarted. " + "If it persists, open the Space and check its logs/status."; } return userMessage; @@ -210,7 +210,7 @@ inline String toUserMessage(const HttpError& e) if (e.statusCode == 503) { userMessage += " If this is a valid Hugging Face space, this could indicate " - "the space is paused or down due to a build or runtime error."; + "the space is sleeping, paused, or down due to a build/runtime error."; } } diff --git a/src/widgets/StatusAreaWidget.h b/src/widgets/StatusAreaWidget.h index 3693e815..bea6891c 100644 --- a/src/widgets/StatusAreaWidget.h +++ b/src/widgets/StatusAreaWidget.h @@ -60,7 +60,7 @@ class StatusHistoryBox : public Component, ChangeListener { historyEditor.setMultiLine(true); historyEditor.setReadOnly(true); - historyEditor.setScrollbarsShown(false); + historyEditor.setScrollbarsShown(true); historyEditor.setCaretVisible(false); historyEditor.setPopupMenuEnabled(false); historyEditor.setColour(TextEditor::backgroundColourId, Colour(0x33, 0x33, 0x33)); From 0aea58f8126c02699d96146f3daf26afaf42c990 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Thu, 19 Mar 2026 18:58:30 -0400 Subject: [PATCH 009/149] Third attempt to add time axis --- src/media/MediaDisplayComponent.cpp | 73 +++++++++++++++++++++++-- src/media/MediaDisplayComponent.h | 84 +++++------------------------ 2 files changed, 83 insertions(+), 74 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 8b842711..a46c3ba9 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -4,6 +4,68 @@ #include +void TimeAxisStrip::paint(Graphics& g) +{ + if (owner == nullptr || ! owner->isFileLoaded()) + return; + + const auto& visibleRange = owner->getVisibleRange(); + const float pps = owner->getPixelsPerSecond(); + const double totalLength = owner->getTotalLengthInSecs(); + + if (pps <= 0.0f || visibleRange.getLength() <= 0.0) + return; + + const double visibleStart = visibleRange.getStart(); + const double visibleEnd = visibleRange.getEnd(); + const int w = getWidth(); + const int h = getHeight(); + + g.setColour(Colours::darkgrey); + g.fillRect(getLocalBounds()); + + g.setColour(Colours::lightgrey.withAlpha(0.8f)); + + // Choose tick interval so we get roughly 5–15 major ticks + double visibleLength = visibleRange.getLength(); + double step = 1.0; + if (visibleLength > 0.0) + { + double logStep = std::ceil(std::log10(visibleLength / 10.0)); + step = std::pow(10.0, logStep); + step = std::max(0.01, step); + } + + const double firstTick = std::ceil(visibleStart / step) * step; + const int labelHeight = jmin(14, h - 2); + g.setFont(static_cast(labelHeight)); + + for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + { + const float x = static_cast((t - visibleStart) * pps); + if (x < -50.0f || x > w + 50.0f) + continue; + + g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); + + String label; + if (t >= 60.0) + label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; + else if (step >= 1.0) + label = String(static_cast(t)) + "s"; + else + label = String(t, 1) + "s"; + + g.drawText(label, + static_cast(x) + 2, + 0, + jmin(80, w - static_cast(x)), + h, + Justification::centredLeft, + true); + } +} + MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) @@ -853,8 +915,10 @@ void MediaDisplayComponent::horizontalMove(double deltaT) double visibleStart = visibleRange.getStart(); double visibleLength = visibleRange.getLength(); + const double totalLength = getTotalLengthInSecs(); + const double maxStart = jmax(0.0, totalLength - visibleLength); double newStart = visibleStart - deltaT * visibleLength / 10.0; - newStart = jlimit(0.0, jmax(0.0, getTotalLengthInSecs() - visibleLength), newStart); + newStart = jlimit(0.0, maxStart, newStart); updateVisibleRange({ newStart, newStart + visibleLength }); } @@ -873,7 +937,9 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) // Fixed time scale: zoom is seconds per pixel. Same scale = same time span for any file. const float minPps = 20.0f; - const float maxPps = static_cast(mediaWidth / totalLength); + float maxPps = static_cast(mediaWidth / totalLength); + if (maxPps < minPps) + maxPps = minPps; float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); @@ -882,8 +948,9 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) if (newVisibleLength > totalLength) newVisibleLength = totalLength; + const double maxStart = jmax(0.0, totalLength - newVisibleLength); double newStart = scrollPosT - newVisibleLength * 0.5; - newStart = jlimit(0.0, totalLength - newVisibleLength, newStart); + newStart = jlimit(0.0, maxStart, newStart); double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 58c4151b..225c9698 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -17,6 +17,8 @@ using namespace juce; +class MediaDisplayComponent; + enum class DisplayMode { Input, @@ -46,6 +48,17 @@ class ColorablePanel : public Component Colour backgroundColor; }; +class TimeAxisStrip : public Component +{ +public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override; + +private: + MediaDisplayComponent* owner = nullptr; +}; + class MediaDisplayComponent : public Component, public ChangeListener, public ChangeBroadcaster, @@ -161,77 +174,6 @@ class MediaDisplayComponent : public Component, Range visibleRange; - class TimeAxisStrip : public Component - { - public: - explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} - - void paint(Graphics& g) override - { - if (owner == nullptr || ! owner->isFileLoaded()) - return; - - const auto& visibleRange = owner->getVisibleRange(); - const float pps = owner->getPixelsPerSecond(); - const double totalLength = owner->getTotalLengthInSecs(); - - if (pps <= 0.0f || visibleRange.getLength() <= 0.0) - return; - - const double visibleStart = visibleRange.getStart(); - const double visibleEnd = visibleRange.getEnd(); - const int w = getWidth(); - const int h = getHeight(); - - g.setColour(Colours::darkgrey); - g.fillRect(getLocalBounds()); - - g.setColour(Colours::lightgrey.withAlpha(0.8f)); - - // Choose tick interval so we get roughly 5–15 major ticks - double visibleLength = visibleRange.getLength(); - double step = 1.0; - if (visibleLength > 0.0) - { - double logStep = std::ceil(std::log10(visibleLength / 10.0)); - step = std::pow(10.0, logStep); - step = std::max(0.01, step); - } - - const double firstTick = std::ceil(visibleStart / step) * step; - const int labelHeight = jmin(14, h - 2); - g.setFont(static_cast(labelHeight)); - - for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) - { - const float x = static_cast((t - visibleStart) * pps); - if (x < -50.0f || x > w + 50.0f) - continue; - - g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); - - String label; - if (t >= 60.0) - label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; - else if (step >= 1.0) - label = String(static_cast(t)) + "s"; - else - label = String(t, 1) + "s"; - - g.drawText(label, - static_cast(x) + 2, - 0, - jmin(80, w - static_cast(x)), - h, - Justification::centredLeft, - true); - } - } - - private: - MediaDisplayComponent* owner = nullptr; - }; - std::unique_ptr timeAxisStrip; AudioFormatManager formatManager; From d33ab53a672c3949cd2d4e2addb3b74c9d3a6c0c Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Thu, 19 Mar 2026 23:42:02 -0400 Subject: [PATCH 010/149] Add cross-platform copy to clipboard implementation --- CMakeLists.txt | 10 +++++-- src/media/MediaDisplayComponent.cpp | 2 +- src/{media => utils/Copy}/copy.h | 0 src/utils/Copy/copyLinux.cpp | 17 +++++++++++ src/{media/copy.mm => utils/Copy/copyMac.mm} | 2 +- src/utils/Copy/copyWindows.cpp | 31 ++++++++++++++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) rename src/{media => utils/Copy}/copy.h (100%) create mode 100644 src/utils/Copy/copyLinux.cpp rename src/{media/copy.mm => utils/Copy/copyMac.mm} (99%) create mode 100644 src/utils/Copy/copyWindows.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c9f9e63a..b778a4eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,8 @@ juce_generate_juce_header(${PROJECT_NAME}) # Finally, we supply a list of source files that will be built into the target. This is a standard # CMake command. + + target_sources(${PROJECT_NAME} PRIVATE src/Main.cpp @@ -99,8 +101,6 @@ target_sources(${PROJECT_NAME} src/media/MidiDisplayComponent.cpp src/media/OutputLabelComponent.cpp - src/media/copy.mm - src/media/pianoroll/KeyboardComponent.cpp src/media/pianoroll/NoteGridComponent.cpp src/media/pianoroll/PianoRollComponent.cpp @@ -132,6 +132,12 @@ target_sources(${PROJECT_NAME} src/external/fontaudio/data/FontAudioIcons.h ) +if(APPLE) +target_sources(HARP PRIVATE src/utils/Copy/copyMac.mm) +elseif(WIN32) +target_sources(HARP PRIVATE src/utils/Copy/copyWindows.cpp) +endif() + # `target_compile_definitions` adds some preprocessor definitions to our target. In a Projucer # project, these might be passed in the 'Preprocessor Definitions' field. JUCE modules also make use # of compile definitions to switch certain features on/off, so if there's a particular feature you diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7c0c096f..89b78c52 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1,7 +1,7 @@ #include "MediaDisplayComponent.h" #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" -#include "copy.h" +#include "../utils/Copy/copy.h" MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} diff --git a/src/media/copy.h b/src/utils/Copy/copy.h similarity index 100% rename from src/media/copy.h rename to src/utils/Copy/copy.h diff --git a/src/utils/Copy/copyLinux.cpp b/src/utils/Copy/copyLinux.cpp new file mode 100644 index 00000000..685436b4 --- /dev/null +++ b/src/utils/Copy/copyLinux.cpp @@ -0,0 +1,17 @@ +#include +#include "copy.h" + +void copyFileToClipboard (const juce::File& file) +{ + if (! file.existsAsFile()) + return; + + const juce::String uri = "file://" + file.getFullPathName(); + + GtkClipboard* clipboard = + gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + + gtk_clipboard_set_text(clipboard, + uri.toRawUTF8(), + -1); +} \ No newline at end of file diff --git a/src/media/copy.mm b/src/utils/Copy/copyMac.mm similarity index 99% rename from src/media/copy.mm rename to src/utils/Copy/copyMac.mm index d4f5dd68..63058284 100644 --- a/src/media/copy.mm +++ b/src/utils/Copy/copyMac.mm @@ -17,4 +17,4 @@ void copyFileToClipboard (const juce::File& file) [pb setString: [fileURL absoluteString] forType: NSPasteboardTypeFileURL]; -} \ No newline at end of file +} diff --git a/src/utils/Copy/copyWindows.cpp b/src/utils/Copy/copyWindows.cpp new file mode 100644 index 00000000..2537fc82 --- /dev/null +++ b/src/utils/Copy/copyWindows.cpp @@ -0,0 +1,31 @@ +#include +#include "copy.h" + +void copyFileToClipboard (const juce::File& file) +{ + if (! file.existsAsFile()) + return; + + if (!OpenClipboard(nullptr)) + return; + + EmptyClipboard(); + + std::wstring path = file.getFullPathName().toWideCharPointer(); + + size_t size = sizeof(DROPFILES) + (path.size() + 2) * sizeof(wchar_t); + + HGLOBAL hMem = GlobalAlloc(GHND, size); + DROPFILES* df = (DROPFILES*)GlobalLock(hMem); + + df->pFiles = sizeof(DROPFILES); + df->fWide = TRUE; + + wchar_t* data = (wchar_t*)((BYTE*)df + sizeof(DROPFILES)); + wcscpy(data, path.c_str()); + + GlobalUnlock(hMem); + + SetClipboardData(CF_HDROP, hMem); + CloseClipboard(); +} \ No newline at end of file From b7f91e776a3de96632a47c18330a3807bce92def Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 20 Mar 2026 09:37:23 -0500 Subject: [PATCH 011/149] Replace email error reporting with gitHub template --- .../ISSUE_TEMPLATE/runtime_error_report.md | 29 ++++++++ src/ModelTab.h | 66 +++++++++++-------- 2 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/runtime_error_report.md diff --git a/.github/ISSUE_TEMPLATE/runtime_error_report.md b/.github/ISSUE_TEMPLATE/runtime_error_report.md new file mode 100644 index 00000000..b1ac4e5c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/runtime_error_report.md @@ -0,0 +1,29 @@ +--- +name: Runtime error report +about: Report a runtime/model/space error from HARP +title: "HARP runtime error report" +labels: bug +assignees: "" +--- + +## Summary + + +## Endpoint + + +## User Notes + + +## Environment + + +## Reproduction +1. +2. +3. + +## Expected Behavior + +## Actual Behavior + diff --git a/src/ModelTab.h b/src/ModelTab.h index 081e3921..d26fb141 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -337,7 +337,7 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas constexpr int buttonOk = 0; String popupMessage = errorMessage - + "\n\nOptional: add context below before sending the report."; + + "\n\nOptional: add context below before opening the issue."; if (errorPopupWindow != nullptr) { @@ -400,12 +400,12 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas } }); - alertWindow->addButton("Send Error Log to TeamUP", buttonSendReport); - bindButtonCallback("Send Error Log to TeamUP", [this, error, errorMessage, onExit] + alertWindow->addButton("Open GitHub Issue", buttonSendReport); + bindButtonCallback("Open GitHub Issue", [this, error, errorMessage, onExit] { if (errorPopupWindow != nullptr) { - sendErrorReportEmail( + openGitHubIssue( error, errorMessage, errorPopupWindow->getTextEditorContents("errorReportNotes")); } @@ -450,45 +450,55 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas errorPopupWindow->toFront(true); } - void sendErrorReportEmail(const Error& error, const String& errorMessage, const String& notes) + void openGitHubIssue(const Error& error, const String& errorMessage, const String& notes) { - String supportEmail = Settings::getString("supportEmail", "support@teamup.tech").trim(); + static const String issueBaseUrl = "https://github.com/TEAMuP-dev/HARP/issues/new"; + static const String issueTemplate = "runtime_error_report.md"; - if (supportEmail.isEmpty()) + File logFile = HARPLogger::getInstance()->getLogFile(); + + String issueTitle = "HARP runtime error report"; + if (const auto* gradioError = std::get_if(&error)) { - AlertWindow::showMessageBoxAsync( - AlertWindow::WarningIcon, - "Missing Support Email", - "Unable to send report because support email is not configured.", - "Ok"); - return; + if (gradioError->reason.isNotEmpty()) + { + issueTitle = "HARP: " + gradioError->reason; + } + else if (gradioError->type == GradioError::Type::QuotaExceeded) + { + issueTitle = "HARP: Hugging Face quota exceeded"; + } } - File logFile = HARPLogger::getInstance()->getLogFile(); - String body; - body << "Please investigate this HARP error.\n\n"; - body << "Error:\n" << errorMessage << "\n\n"; + body << "## Summary\n"; + body << errorMessage << "\n\n"; if (const auto* gradioError = std::get_if(&error)) { if (gradioError->endpointPath.isNotEmpty()) { - body << "Endpoint:\n" << gradioError->endpointPath << "\n\n"; + body << "## Endpoint\n"; + body << gradioError->endpointPath << "\n\n"; } } - body << "User notes:\n" << (notes.isNotEmpty() ? notes : "(none)") << "\n\n"; - body << "Log file:\n" << logFile.getFullPathName() << "\n\n"; - body << "Please attach the log file from the path above when sending."; - - String subject = "HARP Error Report"; - - String escapedSubject = URL::addEscapeChars(subject, true); + body << "## User Notes\n"; + body << (notes.isNotEmpty() ? notes : "(none)") << "\n\n"; + body << "## Environment\n"; + body << "- HARP version: " << JUCE_APPLICATION_VERSION_STRING << "\n"; + body << "- Time (local): " << Time::getCurrentTime().toString(true, true) << "\n"; + body << "- Log file: " << logFile.getFullPathName() << "\n\n"; + body << "## Reproduction\n"; + body << "1. \n"; + body << "2. \n"; + body << "3. \n"; + + String escapedTitle = URL::addEscapeChars(issueTitle, true); String escapedBody = URL::addEscapeChars(body, true); - String escapedRecipient = URL::addEscapeChars(supportEmail, true); - - URL("mailto:" + escapedRecipient + "?subject=" + escapedSubject + "&body=" + escapedBody) + String escapedTemplate = URL::addEscapeChars(issueTemplate, true); + URL(issueBaseUrl + "?template=" + escapedTemplate + "&title=" + escapedTitle + "&body=" + + escapedBody) .launchInDefaultBrowser(); } From 2f6196304023572cbf1f7b7c7d563b0b6c0cfff4 Mon Sep 17 00:00:00 2001 From: ella-granger <44934397+ella-granger@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:31:10 -0400 Subject: [PATCH 012/149] Update host.md --- website/content/pyharp_docs/host.md | 116 +++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/website/content/pyharp_docs/host.md b/website/content/pyharp_docs/host.md index 54ccb031..270ed0c7 100644 --- a/website/content/pyharp_docs/host.md +++ b/website/content/pyharp_docs/host.md @@ -1,3 +1,117 @@ # Hosting PyHARP Apps in the Cloud -(Coming soon) \ No newline at end of file +Automatically generated Gradio endpoints are only available for 72 hours. If you'd like to keep the endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely: + +## Gradio Endpoints +Gradio endpoints are the most convenient solution for a PyHARP application. It will allow you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, which is free from additional charges. + +1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Gradio as the Space SDK. +2. Choose the Blank template. +3. You can choose ZeroGPU hardware if you are a PRO subscriber and your application requires GPU computation. You can also customize the hardware. +4. Clone the initialized repository locally: +```bash +git clone https://huggingface.co/spaces// +``` +5. Add your files to the repository, commit, then push to the `main` branch: +```bash +git add . +git commit -m "initial commit" +git push -u origin main +``` +6. Here are the suggested configurations in the related files. + - `README.md` + + Set __sdk_version__ to __5.28.0__. This is the recommended version, as HARP may not work with the very latest or earlier versions of Gradio. + + - `requirements.txt` + + This is the place where you put all your required **pip** packages. You should put in the latest PyHARP: + ``` + git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 + ``` + If you are using Gradio endpoints, you don't have to include the gradio package here. + + - `packages.txt` + + This is the place to put **apt-get install** debian packages, which are required by some models. + +## Docker Endpoints +PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or prior version, which may cause issues when upgrading python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. + +Using Docker endpoints can help you fix this issue. The Docker will allow you to customize the deployment, which will make room for potential patches and fixes. However, the ZeroGPU hardware will not be available for Docker spaces and you need to pay for the GPU usage. + +1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Docker as the Space SDK. +2. Choose the Blank template. +3. Clone the initialized repository locally: +```bash +git clone https://huggingface.co/spaces// +``` +4. Add your files to the repository, commit, then push to the `main` branch: +```bash +git add . +git commit -m "initial commit" +git push -u origin main +``` +5. Configurations + - `README.md` + + Set **app_port** to ``. + + - `requirements.txt` and `packages.txt` + + Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile`, which will be introduced in the next step. + + For `requirements.txt`, you should include: + ``` + gradio==5.28.0 + git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 + ``` + + - `Dockerfile` + + Here is an example `Dockerfile` configuration, which will install the general packages and fix the `madmom` package. + + ```Docker + FROM python:3.10-slim # Set python version + + WORKDIR /app + COPY packages.txt /app/packages.txt + + # System deps for building packages from source + RUN apt-get update + RUN xargs apt-get install -y --no-install-recommends < /app/packages.txt + RUN rm -rf /var/lib/apt/lists/* + + COPY requirements.txt /app/requirements.txt + # disable build isolation so Cython installed in the environment is visible at build time + ENV PIP_NO_BUILD_ISOLATION=1 + RUN pip install --no-cache-dir -U pip wheel Cython + RUN pip install --no-cache-dir setuptools==80.9.0 + RUN pip install --no-cache-dir -r /app/requirements.txt + RUN pip install --no-cache-dir --no-build-isolation madmom + + # madmom patch + COPY patch_madmom.py /app/scripts/patch_madmom.py # A patch fix in the repo + RUN python /app/scripts/patch_madmom.py + RUN python -c "import madmom; print('madmom import OK')" + + # Copy the rest of the repo + COPY . /app + + # HF Spaces routes traffic to $PORT. Gradio should listen on it. + ENV PORT= # in README.md + EXPOSE + + # Run the app + CMD ["python", "app.py"] + ``` + +--- +Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed by the two methods will have the same UI and functionality. + +Here are a few tips and best-practices when dealing with HuggingFace Spaces: +- Spaces operate based off of the files in the `main` branch +- An [access token](https://huggingface.co/docs/hub/security-tokens) may be required to push commits to HuggingFace Spaces +- A `.gitignore` file should be added to maintain repository orderliness (_e.g._, to ignore `src/_outputs`) + +For more information, please refer to the offical document from Hugging Face about [Spaces](https://huggingface.co/docs/hub/spaces). From 5cba27a20fcb66df54f10569d4c8702a4ed2013d Mon Sep 17 00:00:00 2001 From: Derek LLanes Date: Wed, 25 Mar 2026 01:00:05 -0400 Subject: [PATCH 013/149] Fix Windows runtime DLL path for VS BuildTools installs --- CMakeLists.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2efdf26..a21ebd92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,14 +212,16 @@ if (WIN32) ) # Set the base path for Visual Studio 2022 redistributable DLLs - set(VS_CRT_BASE_PATH "C:/Program Files/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT") + set(VS_CRT_BASE_PATH + "C:/Program Files/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" + "C:/Program Files (x86)/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" + ) # Search for the DLLs in the specified base path file(GLOB_RECURSE all_runtime_dlls - LIST_DIRECTORIES false - "${VS_CRT_BASE_PATH}/*.dll" + LIST_DIRECTORIES false + ${VS_CRT_BASE_PATH}/*.dll ) - set(runtime_dlls) foreach(dll ${all_runtime_dlls}) From 02330b7d8729e39f2f14a9e5f88f004b505ecd14 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 27 Mar 2026 00:09:50 -0400 Subject: [PATCH 014/149] Implemented visual drag and drop with gap logic using an overlay that spans the entire application. --- src/MainComponent.cpp | 5 + src/MainComponent.h | 3 +- src/widgets/MediaClipboardWidget.h | 6 +- src/widgets/TrackAreaWidget.h | 219 +++++++++++++++++++++++------ 4 files changed, 189 insertions(+), 44 deletions(-) diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index b7402618..08c52816 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -15,9 +15,12 @@ MainComponent::MainComponent() addAndMakeVisible(mainModelTab); addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); + addAndMakeVisible(dragOverlay); showStatusArea = Settings::getBoolValue("view.showStatusArea", true); showMediaClipboard = Settings::getBoolValue("view.showMediaClipboard", false); + dragOverlay.setVisible(false); + dragOverlay.toFront(false); requiredWindowWidth = minimumWindowWidth; requiredWindowHeight = minimumWindowHeight; @@ -126,6 +129,8 @@ void MainComponent::resized() { welcomeWindow->refreshHighlightForCurrentStep(); } + + dragOverlay.setBounds(getLocalBounds()); } void MainComponent::updateWindowConstraints() diff --git a/src/MainComponent.h b/src/MainComponent.h index 2acb5841..d7ebde43 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -152,7 +152,8 @@ class MainComponent : public Component, ModelTab mainModelTab; StatusAreaWidget statusAreaWidget; - MediaClipboardWidget mediaClipboardWidget; + DragOverlayComponent dragOverlay; + MediaClipboardWidget mediaClipboardWidget { &dragOverlay }; bool isTutorialActive = false; Rectangle tutorialHighlightRect; diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 3753cbfd..5a2b551b 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -19,7 +19,9 @@ using namespace juce; class MediaClipboardWidget : public Component, public ChangeListener { public: - MediaClipboardWidget() + MediaClipboardWidget(DragOverlayComponent* overlay) + : dragOverlay(overlay), + trackAreaWidget(DisplayMode::Thumbnail, 75, overlay) { selectionTextBox.onReturnKey = [this] { renameSelectionCallback(); }; controlsComponent.addAndMakeVisible(selectionTextBox); @@ -625,6 +627,8 @@ class MediaClipboardWidget : public Component, public ChangeListener MultiButton::Mode sendToDAWButtonInactiveInfo1; MultiButton::Mode sendToDAWButtonInactiveInfo2; + DragOverlayComponent* dragOverlay = nullptr; + Viewport trackArea; TrackAreaWidget trackAreaWidget { DisplayMode::Thumbnail, 75 }; diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 7a60f06a..c6b59e5e 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -18,14 +18,85 @@ using namespace juce; +class DragOverlayComponent : public Component +{ +public: + DragOverlayComponent() + { + setInterceptsMouseClicks(false, false); + } + + void startDrag(Image snapshot, Point offset) + { + if (isActive) return; + dragImage = snapshot; + dragOffset = offset; + isActive = true; + setVisible(true); + repaint(); + } + + // Called every mouseDrag event with the current mouse position + void updatePosition(Point newPos) + { + if (newPos == currentPos) return; + + // Clear old position + repaint(currentPos.x - dragOffset.x, + currentPos.y - dragOffset.y, + dragImage.getWidth(), + dragImage.getHeight()); + + currentPos = newPos; + + // Repaint only new position + repaint(currentPos.x - dragOffset.x, + currentPos.y - dragOffset.y, + dragImage.getWidth(), + dragImage.getHeight()); + } + + void stopDrag() + { + isActive = false; + dragImage = Image(); // Releases the image data + setVisible(false); + repaint(); + } + + void paint(Graphics& g) override + { + if (!isActive || dragImage.isNull()) + return; + + g.drawImage(dragImage, + currentPos.x - dragOffset.x, + currentPos.y - dragOffset.y, + dragImage.getWidth(), + dragImage.getHeight(), + 0, + 0, + dragImage.getWidth(), + dragImage.getHeight()); + } + +private: + Image dragImage; + Point currentPos; + Point dragOffset; + bool isActive = false; +}; + class TrackAreaWidget : public Component, public ChangeListener, public ChangeBroadcaster, public FileDragAndDropTarget { public: - TrackAreaWidget(DisplayMode mode = DisplayMode::Input, int trackHeight = 0) - : displayMode(mode), fixedTrackHeight(trackHeight) + TrackAreaWidget(DisplayMode mode = DisplayMode::Input, + int trackHeight = 0, + DragOverlayComponent* overlay = nullptr) + : displayMode(mode), fixedTrackHeight(trackHeight), dragOverlay(overlay) { addMouseListener(this, true); } @@ -35,24 +106,6 @@ class TrackAreaWidget : public Component, void paint(Graphics& g) override { g.fillAll(getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); - - // Draws drop line for track reordering - if (isDraggingTrack && dragInsertIndex >= 0) - { - int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); - int lineY = dragInsertIndex * trackSlotHeight; - - g.setColour(Colours::white); - g.fillRect(0, lineY - 1, getWidth(), 3); - } - - // Makes the dragged track look visually distinct - if (isDraggingTrack && draggedTrack != nullptr) - { - Rectangle draggedBounds = draggedTrack->getBounds(); - g.setColour(Colours::black.withAlpha(0.3f)); - g.fillRect(draggedBounds); - } } void resized() override @@ -66,8 +119,32 @@ class TrackAreaWidget : public Component, if (getNumTracks() > 0) { + int draggedIndex = isDraggingTrack ? getDraggedTrackIndex() : -1; + + int visualGapIndex = dragInsertIndex; + if (isDraggingTrack && draggedIndex >= 0 && dragInsertIndex > draggedIndex) + visualGapIndex = dragInsertIndex - 1; + + int layoutIndex = 0; + for (auto& m : mediaDisplays) { + // Don't draw dragged track + if (m.get() == draggedTrack && isDraggingTrack) + continue; + + if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) + { + FlexItem gap; + + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + + mainBox.items.add(gap); + } + FlexItem i = FlexItem(*m); if (fixedTrackHeight) @@ -80,6 +157,21 @@ class TrackAreaWidget : public Component, } mainBox.items.add(i.withMargin(marginSize)); + + layoutIndex++; + } + + // If the drag gap is at the end of the list + if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) + { + FlexItem gap; + + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + + mainBox.items.add(gap); } if (fixedTrackHeight) @@ -362,7 +454,6 @@ class TrackAreaWidget : public Component, auto draggedPtr = std::move(*it); mediaDisplays.erase(it); - // Decrement index if moving downward to account for shifting indicies if (newIndex > oldIndex) newIndex--; @@ -406,14 +497,42 @@ class TrackAreaWidget : public Component, int getInsertIndexAtY(int y) { int trackSlotHeight = fixedTrackHeight + static_cast(2 * marginSize); + + if (isDraggingTrack && draggedTrack != nullptr) + { + int draggedIndex = getDraggedTrackIndex(); + + int draggedSlotTop = draggedIndex * trackSlotHeight; + + if (draggedIndex >= 0 && y > draggedSlotTop) + y += trackSlotHeight; + } + int index = y / trackSlotHeight; return jlimit(0, getNumTracks(), index); } + int getDraggedTrackIndex() const + { + if (draggedTrack == nullptr) return -1; + + for (int i = 0; i < (int)mediaDisplays.size(); i++) + { + if (mediaDisplays[i].get() == draggedTrack) + return i; + } + + return -1; + } + void mouseDown(const MouseEvent& e) override { if (!isThumbnailWidget()) return; + // Reset drag state at the start of every click + draggedTrack = nullptr; + isDraggingTrack = false; + Component* clicked = e.eventComponent; MediaDisplayComponent* clickedDisplay = nullptr; @@ -431,7 +550,10 @@ class TrackAreaWidget : public Component, if (clickedDisplay) { draggedTrack = clickedDisplay; - isDraggingTrack = false; + + Point mouseInThis = e.getEventRelativeTo(this).getPosition(); + Point trackTopLeft = draggedTrack->getBounds().getTopLeft(); + dragClickOffset = mouseInThis - trackTopLeft; } } @@ -444,23 +566,35 @@ class TrackAreaWidget : public Component, if (!isDraggingTrack && e.getDistanceFromDragStart() > 5) { isDraggingTrack = true; + + // Takes a snapshot of the track at the moment dragging starts + if (dragOverlay != nullptr) + { + Image snapshot = draggedTrack->createComponentSnapshot(draggedTrack->getLocalBounds()); + dragOverlay->startDrag(snapshot, dragClickOffset); + draggedTrack->setVisible(false); + } } if (isDraggingTrack) { - // Only update the drop index while inside the widget + int newInsertIndex = -1; + if (getLocalBounds().contains(posInThis)) + newInsertIndex = getInsertIndexAtY(posInThis.y); + + if (newInsertIndex != dragInsertIndex) { - dragInsertIndex = getInsertIndexAtY(posInThis.y); + dragInsertIndex = newInsertIndex; + resized(); } - else + + // Converts the position to the overlay's coordinate space + if (dragOverlay != nullptr) { - // Hides the indicator line when outside the widget - dragInsertIndex = -1; + Point posInOverlay = dragOverlay->getLocalPoint(this, posInThis); + dragOverlay->updatePosition(posInOverlay); } - - // Updates the drop indicator line - repaint(); } } @@ -468,33 +602,32 @@ class TrackAreaWidget : public Component, { if (!isThumbnailWidget()) return; - // Tracks the release psoition of the mouse + // Tracks the release position of the mouse Point releasePos = e.getEventRelativeTo(this).getPosition(); // Only reorders if the mouse was released inside the widget if (isDraggingTrack && draggedTrack != nullptr && getLocalBounds().contains(releasePos)) { - int currentIndex = -1; - for (int i = 0; i < (int)mediaDisplays.size(); i++) - { - if (mediaDisplays[i].get() == draggedTrack) - { - currentIndex = i; - break; - } - } + int currentIndex = getDraggedTrackIndex(); - if (dragInsertIndex != currentIndex && dragInsertIndex != currentIndex + 1) + if (dragInsertIndex != currentIndex) { reorderTrack(draggedTrack, dragInsertIndex); } } + // Restore the track's appearance and stop the overlay + if (draggedTrack != nullptr) + draggedTrack->setVisible(true); + if (dragOverlay != nullptr) + dragOverlay->stopDrag(); + // Reset the drag state draggedTrack = nullptr; dragInsertIndex = -1; isDraggingTrack = false; - repaint(); + + resized(); } const DisplayMode displayMode; @@ -506,8 +639,10 @@ class TrackAreaWidget : public Component, // For reordering tracks via dragging MediaDisplayComponent* draggedTrack = nullptr; + DragOverlayComponent* dragOverlay = nullptr; int dragInsertIndex = -1; bool isDraggingTrack = false; + Point dragClickOffset { 0, 0 }; std::vector> mediaDisplays; }; From 58987dcb3eebc71d3eac1a49d150fa3f1efc1666 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Mon, 30 Mar 2026 11:04:58 -0400 Subject: [PATCH 015/149] Fourth attempt to add time axis --- src/media/MediaDisplayComponent.cpp | 129 ++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index a46c3ba9..7380f0b4 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -4,6 +4,67 @@ #include +namespace +{ +struct TickScheme +{ + double majorStep; + double minorStep; + int minorCount; +}; + +TickScheme chooseTickScheme(double visibleLength) +{ + static const TickScheme schemes[] = { + { 0.1, 0.02, 5 }, + { 0.5, 0.1, 5 }, + { 1.0, 0.25, 4 }, + { 2.0, 0.5, 4 }, + { 5.0, 1.0, 5 }, + { 15.0, 5.0, 3 }, + { 30.0, 10.0, 3 }, + { 60.0, 15.0, 4 }, + { 120.0, 30.0, 4 }, + { 300.0, 60.0, 5 }, + { 600.0, 120.0, 5 }, + }; + + // Pick the scheme that gives roughly 4–12 major ticks across the visible range + for (const auto& s : schemes) + { + double numMajor = visibleLength / s.majorStep; + if (numMajor >= 2.0 && numMajor <= 15.0) + return s; + } + + // Fallback for very long files + double majorStep = std::pow(10.0, std::floor(std::log10(visibleLength / 5.0))); + majorStep = std::max(0.01, majorStep); + return { majorStep, majorStep / 5.0, 5 }; +} + +String formatTime(double t, double step) +{ + if (t >= 3600.0) + { + int hrs = static_cast(t / 3600.0); + int mins = static_cast(std::fmod(t, 3600.0) / 60.0); + int secs = static_cast(std::fmod(t, 60.0)); + return String(hrs) + "h " + String(mins) + "m " + String(secs) + "s"; + } + if (t >= 60.0) + { + int mins = static_cast(t / 60.0); + int secs = static_cast(std::fmod(t, 60.0)); + return String(mins) + "m " + String(secs) + "s"; + } + if (step >= 1.0) + return String(static_cast(t)) + "s"; + + return String(t, 2) + "s"; +} +} // namespace + void TimeAxisStrip::paint(Graphics& g) { if (owner == nullptr || ! owner->isFileLoaded()) @@ -24,45 +85,57 @@ void TimeAxisStrip::paint(Graphics& g) g.setColour(Colours::darkgrey); g.fillRect(getLocalBounds()); - g.setColour(Colours::lightgrey.withAlpha(0.8f)); + const double visibleLength = visibleRange.getLength(); + const auto scheme = chooseTickScheme(visibleLength); + const double majorStep = scheme.majorStep; + const double minorStep = scheme.minorStep; - // Choose tick interval so we get roughly 5–15 major ticks - double visibleLength = visibleRange.getLength(); - double step = 1.0; - if (visibleLength > 0.0) + const float majorTickTop = 0.0f; + const float majorTickBot = static_cast(h); + const float minorTickTop = static_cast(h) * 0.55f; + const float minorTickBot = static_cast(h); + + // --- Minor ticks --- + g.setColour(Colours::grey.withAlpha(0.5f)); + + const double firstMinor = std::ceil(visibleStart / minorStep) * minorStep; + for (double t = firstMinor; t <= visibleEnd && t <= totalLength; t += minorStep) { - double logStep = std::ceil(std::log10(visibleLength / 10.0)); - step = std::pow(10.0, logStep); - step = std::max(0.01, step); + // Skip positions that coincide with major ticks + double remainder = std::fmod(t, majorStep); + if (remainder < minorStep * 0.1 || (majorStep - remainder) < minorStep * 0.1) + continue; + + const float x = static_cast((t - visibleStart) * pps); + if (x < 0.0f || x > static_cast(w)) + continue; + + g.drawVerticalLine(static_cast(x), minorTickTop, minorTickBot); } - const double firstTick = std::ceil(visibleStart / step) * step; - const int labelHeight = jmin(14, h - 2); - g.setFont(static_cast(labelHeight)); + // --- Major ticks and labels --- + g.setColour(Colours::lightgrey.withAlpha(0.9f)); - for (double t = firstTick; t < visibleEnd && t <= totalLength; t += step) + const int labelH = jmin(13, h - 2); + g.setFont(static_cast(labelH)); + + const double firstMajor = std::ceil(visibleStart / majorStep) * majorStep; + for (double t = firstMajor; t <= visibleEnd && t <= totalLength; t += majorStep) { const float x = static_cast((t - visibleStart) * pps); - if (x < -50.0f || x > w + 50.0f) + if (x < -60.0f || x > static_cast(w) + 60.0f) continue; - g.drawVerticalLine(static_cast(x), 0.0f, static_cast(h)); - - String label; - if (t >= 60.0) - label = String(static_cast(t / 60)) + "m " + String(static_cast(std::fmod(t, 60))) + "s"; - else if (step >= 1.0) - label = String(static_cast(t)) + "s"; - else - label = String(t, 1) + "s"; + g.drawVerticalLine(static_cast(x), majorTickTop, majorTickBot); + String label = formatTime(t, majorStep); g.drawText(label, - static_cast(x) + 2, - 0, - jmin(80, w - static_cast(x)), - h, - Justification::centredLeft, - true); + static_cast(x) + 3, + 0, + jmin(90, w - static_cast(x)), + h, + Justification::centredLeft, + true); } } From 4e5fd2e8780c813973bcf369b858d1ae9f12a97c Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Mon, 30 Mar 2026 11:47:12 -0400 Subject: [PATCH 016/149] Fifth attempt to fix time axis --- src/media/MediaDisplayComponent.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7380f0b4..e4508b41 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1008,9 +1008,9 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) if (pps <= 0.0f) return; - // Fixed time scale: zoom is seconds per pixel. Same scale = same time span for any file. - const float minPps = 20.0f; - float maxPps = static_cast(mediaWidth / totalLength); + const double minVisibleSeconds = 5.0; + const float minPps = static_cast(mediaWidth / totalLength); + float maxPps = static_cast(mediaWidth / minVisibleSeconds); if (maxPps < minPps) maxPps = minPps; From 557ecd8f02dfc7f482d8f3805849d61aef6ad332 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Wed, 1 Apr 2026 11:29:10 -0400 Subject: [PATCH 017/149] Sixth attempt to fix time axis --- src/media/MediaDisplayComponent.cpp | 16 ++++++++++++---- src/media/MediaDisplayComponent.h | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index e4508b41..7b351529 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -996,18 +996,19 @@ void MediaDisplayComponent::horizontalMove(double deltaT) updateVisibleRange({ newStart, newStart + visibleLength }); } -void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) +bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) { const float mediaWidth = getMediaWidth(); const double totalLength = getTotalLengthInSecs(); if (mediaWidth <= 0.0f || totalLength <= 0.0) - return; + return false; const float pps = getPixelsPerSecond(); if (pps <= 0.0f) - return; + return false; + // minPps = zoomed all the way out; maxPps = zoomed all the way in (~5 seconds visible). const double minVisibleSeconds = 5.0; const float minPps = static_cast(mediaWidth / totalLength); float maxPps = static_cast(mediaWidth / minVisibleSeconds); @@ -1017,6 +1018,10 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); + // If pps didn't change, zoom has hit a limit + if (std::abs(newPps - pps) < 0.01f) + return false; + double newVisibleLength = static_cast(mediaWidth) / static_cast(newPps); if (newVisibleLength > totalLength) newVisibleLength = totalLength; @@ -1027,6 +1032,7 @@ void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); + return true; } void MediaDisplayComponent::scrollBarMoved(ScrollBar* scrollBarThatHasMoved, @@ -1070,7 +1076,9 @@ void MediaDisplayComponent::mouseWheelMove(const MouseEvent& evt, const MouseWhe } else { - horizontalZoom(static_cast(wheel.deltaY), scrollTime); + bool zoomed = horizontalZoom(static_cast(wheel.deltaY), scrollTime); + if (! zoomed) + horizontalMove(static_cast(wheel.deltaY)); } } else diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 225c9698..154b698f 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -216,7 +216,7 @@ class MediaDisplayComponent : public Component, int correctMediaXBounds(float mX, float width); void horizontalMove(double deltaT); - void horizontalZoom(double deltaZoom, double scrollPosT); + bool horizontalZoom(double deltaZoom, double scrollPosT); void scrollBarMoved(ScrollBar* scrollBarThatHasMoved, double scrollBarRangeStart) override; From 12ece4036eeddf8eb239b38be5fe808bcc82fe61 Mon Sep 17 00:00:00 2001 From: ella-granger Date: Fri, 3 Apr 2026 09:50:25 -0400 Subject: [PATCH 018/149] Update TIGER document and the hosting document. --- website/content/pyharp_docs/example.md | 130 ++++++++++++++++++------- website/content/pyharp_docs/host.md | 22 ++--- 2 files changed, 104 insertions(+), 48 deletions(-) diff --git a/website/content/pyharp_docs/example.md b/website/content/pyharp_docs/example.md index 97b14144..a1c212e3 100644 --- a/website/content/pyharp_docs/example.md +++ b/website/content/pyharp_docs/example.md @@ -10,7 +10,13 @@ To get started, we'll install [the code](https://github.com/JusperLee/TIGER) nee ``` git clone https://github.com/JusperLee/TIGER.git -cd TIGER && pip install -r requirements.txt +cd TIGER +``` + +In the `requirements.txt`, remove `triton==3.1.0` to avoid dependency issues. Then pip install: + +``` +pip install -r requirements.txt ``` Next, we'll look at how TIGER processes audio files to separate speech. From [`TIGER/inference_speech.py`](https://github.com/JusperLee/TIGER/blob/main/inference_speech.py), we can see that TIGER requires audio files to be resampled to 16kHz and formatted as a PyTorch tensor of shape `[1, C, T]` where `C` is the number of channels and `T` is the number of audio samples; then, the model can be applied in one line of code: @@ -21,11 +27,12 @@ Next, we'll look at how TIGER processes audio files to separate speech. From [`T ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] ``` -Here, `num_spk` is the estimated number of speakers in the recording. For our example application, we'll only return audio of the first speaker. +Here, `num_spk` is the estimated number of speakers in the recording. The TIGER model sets `num_spk` to two. We are going to take both outputs from the model: ``` # Select audio of first speaker -output = ests_speech[:, 0, :] # Expected output shape: [1, T] +output_1 = ests_speech[:, 0, :] # Expected output shape: [1, T] +output_2 = ests_speech[:, 1, :] # Expected output shape: [1, T] ``` And that's it -- to deploy TIGER in HARP, we need to write a Gradio application for processing audio files like this, sprinkle in some PyHARP functions, and then run our application in the background to handle any audio sent by HARP. @@ -42,17 +49,28 @@ Now that we have a grip on how to run TIGER, let's revisit the [elements](/conte name="TIGER", description="The TIGER speech separation model of Xu et al. (https://arxiv.org/abs/2410.01469)", author="Your name", - tags=["example", "speech separation"], - midi_in=False, - midi_out=False + tags=["example", "speech separation"] ) ``` -* We need to define a list of __Gradio interactive components__ specifying the interface. In our case, because we only require audio input/output, we'll leave this list empty: +* We need to define a list of __Gradio interactive components__ specifying the interface. In our case, we need one audio input and two audio outputs: ``` # Define Gradio Components - components = [] - ``` -* We need to define a __processing function__ for handling file input and output. This function will load an audio file from a given path, format it for TIGER and run separation as discussed above, save the output to a new file, and return the path of this output file. + # Input + input_audio = gr.Audio( + label="Input Audio", + type="filepath", + sources=["upload", "microphone"] + ) + # Outputs + output_audio_1 = gr.Audio( + type="filepath", + label="Output Audio 1" + ) + output_audio_2 = gr.Audio( + type="filepath", + label="Output Audio 2" + ) +* Then we define a __processing function__ for handling file input and output. This function will load an audio file from a given path, format it for TIGER and run separation as discussed above, save the outputs to new files, and return the paths of the output files. Note that the parameters of the function correspond to the input components and the return values correspond to the output components. ``` # Define the processing function @torch.inference_mode() @@ -61,31 +79,49 @@ Now that we have a grip on how to run TIGER, let's revisit the [elements](/conte # By default, load audio as a Descript-AudioTools `AudioSignal` object sig = load_audio(input_audio_path) # Wraps a tensor of shape [1, C, T] - audio_input = sig.resample(16_000).audio_data # Tensor of shape [1, C, T] + audio_input = sig.resample(16_000).audio_data.to(device) # Tensor of shape [1, C, T] # Apply TIGER ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] - output = ests_speech[:, 0, :] # Expected output shape: [1, T] + output_1 = ests_speech[:, 0, :] # Expected output shape: [1, T] + output_2 = ests_speech[:, 1, :] - sig.audio_data = output - output_audio_path = save_audio(sig) + # Create two new audio + sig_1 = AudioSignal(output_1.cpu().numpy().astype("float32"), sample_rate=16000) + sig_2 = AudioSignal(output_2.cpu().numpy().astype("float32"), sample_rate=16000) - # Because this application does not apply labels to audio, return an empty - # label list - output_labels = LabelList() + # save to files + output_dir = Path("_outputs").resolve() + output_dir.mkdir(exist_ok=True, parents=True) - return output_audio_path, output_labels + output_audio_path_1 = output_dir / "sig1.wav" + output_audio_path_2 = output_dir / "sig2.wav" + + save_audio(sig_1, output_audio_path_1) + save_audio(sig_2, output_audio_path_2) + + return output_audio_path_1, output_audio_path_2 + ``` +* After we define the __model card__, __I/O components__ and the __processing function__, we aggregate them into an endpoint: + ``` + # Build Endpoint + app = build_endpoint( + model_card=model_card, + input_components = [input_audio], + output_components = [output_audio_1, output_audio_2], + process_fn=process_fn + ) ``` -Adding imports and some model-loading code, our final `app.py` should look like this: +Adding imports and model-loading code, our final `app.py` should look like this: ``` from pyharp import * +from audiotools import AudioSignal -import yaml import os +from pathlib import Path import gradio as gr -import torchaudio import torch import look2hear.models @@ -107,9 +143,7 @@ model_card = ModelCard( name="TIGER", description="The TIGER speech separation model of Xu et al. (https://arxiv.org/abs/2410.01469)", author="Your name", - tags=["example", "speech separation"], - midi_in=False, - midi_out=False + tags=["example", "speech separation"] ) # Define the processing function @@ -123,25 +157,47 @@ def process_fn(input_audio_path): # Apply TIGER ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] - output = ests_speech[:, :1, :] # Expected output shape: [1, 1, T] - - sig.audio_data = output - output_audio_path = save_audio(sig.cpu()) # Ensure our output signal is on CPU + output_1 = ests_speech[:, 0, :] # Expected output shape: [1, 1, T] + output_2 = ests_speech[:, 1, :] + + sig_1 = AudioSignal(output_1.cpu().numpy().astype("float32"), sample_rate=16000) + sig_2 = AudioSignal(output_2.cpu().numpy().astype("float32"), sample_rate=16000) + + output_dir = Path("_outputs").resolve() + output_dir.mkdir(exist_ok=True, parents=True) + + output_audio_path_1 = output_dir / "sig1.wav" + output_audio_path_2 = output_dir / "sig2.wav" - # Because this application does not apply labels to audio, return an empty - # label list - output_labels = LabelList() + save_audio(sig_1, output_audio_path_1) + save_audio(sig_2, output_audio_path_2) - return output_audio_path, output_labels + return output_audio_path_1, output_audio_path_2 # Build Gradio endpoint with gr.Blocks() as demo: # Define Gradio Components - components = [] - - app = build_endpoint(model_card=model_card, - components=components, - process_fn=process_fn) + input_audio = gr.Audio( + label="Input Audio", + type="filepath", + sources=["upload", "microphone"] + ) + output_audio_1 = gr.Audio( + type="filepath", + label="Output Audio 1" + ) + output_audio_2 = gr.Audio( + type="filepath", + label="Output Audio 2" + ) + # output_labels = gr.JSON(label="Separation") + + app = build_endpoint( + model_card=model_card, + input_components = [input_audio], + output_components = [output_audio_1, output_audio_2], # , output_labels], + process_fn=process_fn + ) demo.queue() demo.launch(share=True, show_error=True) diff --git a/website/content/pyharp_docs/host.md b/website/content/pyharp_docs/host.md index 270ed0c7..26a970d7 100644 --- a/website/content/pyharp_docs/host.md +++ b/website/content/pyharp_docs/host.md @@ -3,7 +3,7 @@ Automatically generated Gradio endpoints are only available for 72 hours. If you'd like to keep the endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely: ## Gradio Endpoints -Gradio endpoints are the most convenient solution for a PyHARP application. It will allow you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, which is free from additional charges. +Gradio endpoints are the most convenient solution for a PyHARP application. It will give you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, at no additional charge. 1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Gradio as the Space SDK. 2. Choose the Blank template. @@ -25,7 +25,7 @@ git push -u origin main - `requirements.txt` - This is the place where you put all your required **pip** packages. You should put in the latest PyHARP: + This is the place where you put all the **pip** packages. You should also put in the latest PyHARP: ``` git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 ``` @@ -33,12 +33,12 @@ git push -u origin main - `packages.txt` - This is the place to put **apt-get install** debian packages, which are required by some models. + This is there to put **apt-get install** Debian package list, which are required by some models. ## Docker Endpoints -PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or prior version, which may cause issues when upgrading python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. +PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or a prior version, which may cause issues when upgrading Python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. -Using Docker endpoints can help you fix this issue. The Docker will allow you to customize the deployment, which will make room for potential patches and fixes. However, the ZeroGPU hardware will not be available for Docker spaces and you need to pay for the GPU usage. +Using Docker endpoints can help you fix this issue. Docker will allow you to customize deployments, making room for potential patches and fixes. However, the ZeroGPU hardware will not be available in Docker spaces, and you will need to pay for GPU usage. 1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Docker as the Space SDK. 2. Choose the Blank template. @@ -59,7 +59,7 @@ git push -u origin main - `requirements.txt` and `packages.txt` - Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile`, which will be introduced in the next step. + Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile` introduced in the next step. For `requirements.txt`, you should include: ``` @@ -69,7 +69,7 @@ git push -u origin main - `Dockerfile` - Here is an example `Dockerfile` configuration, which will install the general packages and fix the `madmom` package. + Here is an example `Dockerfile` configuration that installs general packages and fixes the `madmom` package. ```Docker FROM python:3.10-slim # Set python version @@ -107,11 +107,11 @@ git push -u origin main ``` --- -Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed by the two methods will have the same UI and functionality. +Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed using two methods will have the same UI and functionality. -Here are a few tips and best-practices when dealing with HuggingFace Spaces: -- Spaces operate based off of the files in the `main` branch +Here are a few tips and best practices when dealing with HuggingFace Spaces: +- Spaces operate based on the files in the `main` branch - An [access token](https://huggingface.co/docs/hub/security-tokens) may be required to push commits to HuggingFace Spaces - A `.gitignore` file should be added to maintain repository orderliness (_e.g._, to ignore `src/_outputs`) -For more information, please refer to the offical document from Hugging Face about [Spaces](https://huggingface.co/docs/hub/spaces). +For more information, please refer to the offical Hugging Face document on [Spaces](https://huggingface.co/docs/hub/spaces). From 0dc94eb01cc27cea08bb0539988526d10ec0c4be Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 3 Apr 2026 13:29:14 -0500 Subject: [PATCH 019/149] Stability error handling --- src/utils/Errors.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/utils/Errors.h b/src/utils/Errors.h index 1b2795dc..41c8cee4 100644 --- a/src/utils/Errors.h +++ b/src/utils/Errors.h @@ -177,6 +177,24 @@ inline String toUserMessage(const HttpError& e) case HttpError::Type::BadStatusCode: + if (e.statusCode == 402) + { + if (e.endpointPath.containsIgnoreCase("stability.ai")) + { + userMessage = "Stability AI could not complete this request because your usage " + "credits or quota may be exhausted. Please check your Stability " + "account usage/billing and try again."; + } + else + { + userMessage = "This request could not be completed because the service " + "reported payment/quota limits. Please check your account " + "usage/billing and try again."; + } + + return userMessage; + } + userMessage.clear(); if (e.request == HttpError::Request::POST) From 194565ff171acbf113e83c24f2265016cb98cdef Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Fri, 3 Apr 2026 14:45:58 -0400 Subject: [PATCH 020/149] Add header to copyWindows --- src/utils/Copy/copyWindows.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/Copy/copyWindows.cpp b/src/utils/Copy/copyWindows.cpp index 2537fc82..2ab58b61 100644 --- a/src/utils/Copy/copyWindows.cpp +++ b/src/utils/Copy/copyWindows.cpp @@ -1,4 +1,5 @@ #include +#include #include "copy.h" void copyFileToClipboard (const juce::File& file) @@ -28,4 +29,4 @@ void copyFileToClipboard (const juce::File& file) SetClipboardData(CF_HDROP, hMem); CloseClipboard(); -} \ No newline at end of file +} From 81d1606a8dc0e52e937dc15eef36d9345aaf7016 Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Fri, 3 Apr 2026 14:48:01 -0400 Subject: [PATCH 021/149] Add save AND copy buttons to GUI Also Increased Size of Buttons --- src/media/MediaDisplayComponent.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 89b78c52..c8a0e539 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -255,16 +255,18 @@ void MediaDisplayComponent::resized() // Add buttons to flex with equal height buttonsFlexBox.items.add( - FlexItem(playStopButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(playStopButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); if (isInputTrack()) { buttonsFlexBox.items.add( - FlexItem(chooseFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(chooseFileButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); } if (isOutputTrack()) { buttonsFlexBox.items.add( - FlexItem(copyFileButton).withHeight(22).withWidth(22).withMargin({ 2, 0, 2, 0 })); + FlexItem(saveFileButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); + buttonsFlexBox.items.add( + FlexItem(copyFileButton).withHeight(25).withWidth(25).withMargin({ 2, 0, 2, 0 })); } buttonsFlexBox.performLayout(buttonsComponent.getBounds()); From ba45d20f68b0aa09168f0bb76f9211e1b72fb7a8 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 3 Apr 2026 17:41:41 -0500 Subject: [PATCH 022/149] zerogpu error handling fixes --- src/clients/GradioClient.h | 185 ++++++++++++++++++++----------------- 1 file changed, 100 insertions(+), 85 deletions(-) diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index ff4ed03b..b1515c33 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -523,6 +523,45 @@ class GradioClient : public Client return array.size() == 2; } + bool isZeroGPUSpace(const String& modelPath) + { + if (! isValidHuggingFacePath(modelPath)) + return false; + + URL runtimeEndpoint = + URL("https://huggingface.co/api/spaces/" + inferHostSlashModel(modelPath) + "/runtime"); + + std::unique_ptr stream; + + if (makeGETRequestStream(runtimeEndpoint, stream, "", 5000).failed() || stream == nullptr) + return false; + + String responseJSON = stream->readEntireStreamAsString(); + + DynamicObject::Ptr responseDict; + + if (stringJSONToDict(responseJSON, responseDict).failed()) + return false; + + // Navigate: hardware -> current + static const Identifier hardwareKey { "hardware" }; + static const Identifier currentKey { "current" }; + + if (! responseDict->hasProperty(hardwareKey)) + return false; + + var hardwareVar = responseDict->getProperty(hardwareKey); + auto* hardwareDict = hardwareVar.getDynamicObject(); + + if (hardwareDict == nullptr || ! hardwareDict->hasProperty(currentKey)) + return false; + + String hardwareCurrent = hardwareDict->getProperty(currentKey).toString(); + + // ZeroGPU hardware names start with "zero-" (e.g. "zero-a10g", "zero-a100") + return hardwareCurrent.startsWithIgnoreCase("zero-"); + } + OpResult makePOSTRequest(URL endpoint, const String headers, const String body, @@ -590,21 +629,16 @@ class GradioClient : public Client if (diagnosticText.isNotEmpty()) { - String reason = extractExceptionLabel(diagnosticText); - - if (reason.isEmpty()) - { - reason = extractShortReason(diagnosticText); - } + String reason = extractShortReason(diagnosticText); if (statusMessage != nullptr && reason.isNotEmpty()) { statusMessage->setMessage("[error] " + reason); } - GradioError::Type errorType = - isExplicitQuotaError(diagnosticText) ? GradioError::Type::QuotaExceeded - : GradioError::Type::RuntimeError; + GradioError::Type errorType = isExplicitQuotaError(diagnosticText) + ? GradioError::Type::QuotaExceeded + : GradioError::Type::RuntimeError; return OpResult::fail(GradioError { errorType, errorPath, reason }); } @@ -720,7 +754,8 @@ class GradioClient : public Client return false; } - if (text.containsIgnoreCase("exceeded your gpu quota")) + if ((text.containsIgnoreCase("exceeded your") && text.containsIgnoreCase("gpu quota")) + || text.containsIgnoreCase("zerogpu quota exceeded")) { return true; } @@ -747,9 +782,18 @@ class GradioClient : public Client return ""; } - for (auto label : { "NameError", "TypeError", "ImportError", "ModuleNotFoundError", - "ValueError", "KeyError", "AttributeError", "RuntimeError", - "AssertionError", "IndexError", "SyntaxError", "OSError" }) + for (auto label : { "NameError", + "TypeError", + "ImportError", + "ModuleNotFoundError", + "ValueError", + "KeyError", + "AttributeError", + "RuntimeError", + "AssertionError", + "IndexError", + "SyntaxError", + "OSError" }) { if (text.containsIgnoreCase(label)) { @@ -757,8 +801,7 @@ class GradioClient : public Client } } - StringArray tokens = StringArray::fromTokens( - text, " \t\r\n,:;()[]{}<>\"'`\\/|", ""); + StringArray tokens = StringArray::fromTokens(text, " \t\r\n,:;()[]{}<>\"'`\\/|", ""); for (auto& token : tokens) { @@ -786,42 +829,6 @@ class GradioClient : public Client return firstLine; } - String collectErrorContextText(std::unique_ptr& stream, int maxLines = 8) - { - String contextText; - - for (int i = 0; i < maxLines && ! stream->isExhausted(); ++i) - { - String contextLine = stream->readNextLine().trim(); - - if (contextLine.isEmpty()) - { - continue; - } - - if (contextLine.startsWithIgnoreCase("event:")) - { - break; - } - - String normalizedContext = extractErrorTextFromPayload(extractPayload(contextLine)); - - if (normalizedContext.isEmpty()) - { - continue; - } - - if (contextText.isNotEmpty()) - { - contextText += "\n"; - } - - contextText += normalizedContext; - } - - return contextText; - } - static String extractMessageTypeFromPayload(const String& payload) { String normalizedPayload = payload.trim(); @@ -850,8 +857,8 @@ class GradioClient : public Client return ""; } - static String extractFirstNonEmptyField( - const DynamicObject* object, std::initializer_list keys) + static String extractFirstNonEmptyField(const DynamicObject* object, + std::initializer_list keys) { if (object == nullptr) { @@ -909,8 +916,8 @@ class GradioClient : public Client if (auto* outputDict = outputVar.getDynamicObject()) { - message = - extractFirstNonEmptyField(outputDict, { "message", "detail", "error", "reason" }); + message = extractFirstNonEmptyField(outputDict, + { "message", "detail", "error", "reason" }); } } @@ -1005,6 +1012,11 @@ class GradioClient : public Client return result; } + // Track whether ZeroGPU allocated the GPU and started the process. + // If an error arrives before process_starts, it was likely rejected at + // the quota-check stage rather than failing inside the function body. + bool seenProcessStarts = false; + while (! stream->isExhausted()) { response = stream->readNextLine(); @@ -1030,52 +1042,44 @@ class GradioClient : public Client { String errorDataLine = stream->readNextLine(); - DBG_AND_LOG( - "GradioClient::makeGETRequest: Error response \"" << errorDataLine << "\"."); + DBG_AND_LOG("GradioClient::makeGETRequest: Error response \"" << errorDataLine + << "\"."); String payload = extractPayload(errorDataLine); String diagnosticText = extractErrorTextFromPayload(payload); - String contextText = collectErrorContextText(stream); - String classificationText = diagnosticText; + String reason = extractShortReason(diagnosticText); - if (contextText.isNotEmpty()) - { - if (classificationText.isNotEmpty()) - { - classificationText += "\n"; - } + GradioError::Type errorType; - classificationText += contextText; + if (isExplicitQuotaError(diagnosticText)) + { + errorType = GradioError::Type::QuotaExceeded; } - - String reasonText = diagnosticText.isNotEmpty() ? diagnosticText : contextText; - - String reason = extractExceptionLabel(reasonText); - - if (reason.isEmpty() && reasonText.isNotEmpty()) + else if (diagnosticText.isEmpty() && ! seenProcessStarts) { - reason = extractShortReason(reasonText); + errorType = GradioError::Type::QuotaExceeded; } - - GradioError::Type errorType = - isExplicitQuotaError(classificationText) ? GradioError::Type::QuotaExceeded - : GradioError::Type::RuntimeError; - - if (statusMessage != nullptr) + else { - String statusErrorText = reasonText.isNotEmpty() ? reasonText : classificationText; + errorType = GradioError::Type::RuntimeError; + } - if (statusErrorText.isNotEmpty()) - { - statusMessage->setMessage("[error] " + extractShortReason(statusErrorText)); - } + if (statusMessage != nullptr && diagnosticText.isNotEmpty()) + { + statusMessage->setMessage("[error] " + extractShortReason(diagnosticText)); } return OpResult::fail(GradioError { errorType, errorPath, reason }); } else if (eventLine.startsWithIgnoreCase("data:")) { + String dataPayload = extractPayload(eventLine); + if (extractMessageTypeFromPayload(dataPayload).equalsIgnoreCase("process_starts")) + { + seenProcessStarts = true; + } + appendProcessMessageFromDataLine(eventLine); } else @@ -1191,12 +1195,23 @@ class GradioClient : public Client response.clear(); - /* WARNING: it's very important to give Gradio enough time to yield a response + /* Note: it's very important to give Gradio enough time to yield a response (10 seconds was too little for ZeroGPU spaces and led to stream == nullptr) */ result = makeGETRequest(endpoint, response, inferDocumentationPath(modelPath), 120000); if (result.failed()) { + if (const auto* gradioErr = std::get_if(&result.getError())) + { + if (gradioErr->type == GradioError::Type::QuotaExceeded + && gradioErr->reason.isEmpty() && ! isZeroGPUSpace(modelPath)) + { + return OpResult::fail(GradioError { GradioError::Type::RuntimeError, + gradioErr->endpointPath, + gradioErr->reason }); + } + } + return result; } From 47316274cc1e56adb453ab48c640ff21b926add0 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 3 Apr 2026 18:05:20 -0500 Subject: [PATCH 023/149] info and warning logs added --- src/clients/GradioClient.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index b1515c33..25f6cc27 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -907,8 +907,8 @@ class GradioClient : public Client String severity = extractFirstNonEmptyField(payloadDict, { "type", "level", "severity", "status" }) .toLowerCase(); - String message = - extractFirstNonEmptyField(payloadDict, { "message", "detail", "error", "reason" }); + String message = extractFirstNonEmptyField( + payloadDict, { "message", "detail", "error", "reason", "log" }); if (message.isEmpty()) { @@ -948,6 +948,12 @@ class GradioClient : public Client return ""; } + // "log" messages carry info/warning text + if (messageType.equalsIgnoreCase("log")) + { + return ""; + } + if (messageType.equalsIgnoreCase("process_starts")) { return "[process] started"; From 7ee2d9514fa111a799fe701ba0b09afbff4b0895 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Wed, 8 Apr 2026 09:59:12 -0400 Subject: [PATCH 024/149] Working copy/paste on linux using xclip and reorganization. --- CMakeLists.txt | 6 ++++-- src/media/MediaDisplayComponent.cpp | 3 ++- src/utils/Copy/copy.h | 4 ---- src/utils/Copy/copyLinux.cpp | 17 ---------------- src/utils/Interface.h | 4 +++- src/utils/copy/CopyLinux.cpp | 20 +++++++++++++++++++ .../{Copy/copyMac.mm => copy/CopyMacOS.mm} | 8 +++++++- .../copyWindows.cpp => copy/CopyWindows.cpp} | 19 ++++++++++++------ 8 files changed, 49 insertions(+), 32 deletions(-) delete mode 100644 src/utils/Copy/copy.h delete mode 100644 src/utils/Copy/copyLinux.cpp create mode 100644 src/utils/copy/CopyLinux.cpp rename src/utils/{Copy/copyMac.mm => copy/CopyMacOS.mm} (80%) rename src/utils/{Copy/copyWindows.cpp => copy/CopyWindows.cpp} (60%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b778a4eb..390be5fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,9 +133,11 @@ target_sources(${PROJECT_NAME} ) if(APPLE) -target_sources(HARP PRIVATE src/utils/Copy/copyMac.mm) + target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyMacOS.mm) elseif(WIN32) -target_sources(HARP PRIVATE src/utils/Copy/copyWindows.cpp) + target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyWindows.cpp) +else() + target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyLinux.cpp) endif() # `target_compile_definitions` adds some preprocessor definitions to our target. In a Projucer diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index c8a0e539..95b7ce5b 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1,7 +1,8 @@ #include "MediaDisplayComponent.h" #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" -#include "../utils/Copy/copy.h" + +#include "../utils/Interface.h" MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} diff --git a/src/utils/Copy/copy.h b/src/utils/Copy/copy.h deleted file mode 100644 index d23614b4..00000000 --- a/src/utils/Copy/copy.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once -#include - -void copyFileToClipboard (const juce::File& file); \ No newline at end of file diff --git a/src/utils/Copy/copyLinux.cpp b/src/utils/Copy/copyLinux.cpp deleted file mode 100644 index 685436b4..00000000 --- a/src/utils/Copy/copyLinux.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include "copy.h" - -void copyFileToClipboard (const juce::File& file) -{ - if (! file.existsAsFile()) - return; - - const juce::String uri = "file://" + file.getFullPathName(); - - GtkClipboard* clipboard = - gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); - - gtk_clipboard_set_text(clipboard, - uri.toRawUTF8(), - -1); -} \ No newline at end of file diff --git a/src/utils/Interface.h b/src/utils/Interface.h index a44effa1..620c9f25 100644 --- a/src/utils/Interface.h +++ b/src/utils/Interface.h @@ -1,7 +1,7 @@ /** * @file Interface.h * @brief Simple helper functions for interface. - * @author hugofloresgarcia + * @author hugofloresgarcia, JEYuhas */ #pragma once @@ -20,3 +20,5 @@ inline Colour getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour uiCo return fallback; } + +void copyFileToClipboard(const File& file); diff --git a/src/utils/copy/CopyLinux.cpp b/src/utils/copy/CopyLinux.cpp new file mode 100644 index 00000000..aab8d157 --- /dev/null +++ b/src/utils/copy/CopyLinux.cpp @@ -0,0 +1,20 @@ +/** + * @file CopyLinux.cpp + * @brief Copy file path to clipboard on Linux. + * @author JEYuhas, cwitkowitz + */ + +#include "../Interface.h" + +void copyFileToClipboard(const File& file) +{ + if (! file.existsAsFile()) + return; + + String cmd; + + cmd << "printf \"file://" << file.getFullPathName() + << "\\n\" | xclip -selection clipboard -t text/uri-list"; + + std::system(cmd.toRawUTF8()); +} diff --git a/src/utils/Copy/copyMac.mm b/src/utils/copy/CopyMacOS.mm similarity index 80% rename from src/utils/Copy/copyMac.mm rename to src/utils/copy/CopyMacOS.mm index 63058284..d501d6c6 100644 --- a/src/utils/Copy/copyMac.mm +++ b/src/utils/copy/CopyMacOS.mm @@ -1,6 +1,12 @@ +/** + * @file CopyMacOS.mm + * @brief Copy file path to clipboard on MacOS. + * @author JEYuhas + */ + #import -#include "copy.h" +#include "../Interface.h" void copyFileToClipboard (const juce::File& file) { diff --git a/src/utils/Copy/copyWindows.cpp b/src/utils/copy/CopyWindows.cpp similarity index 60% rename from src/utils/Copy/copyWindows.cpp rename to src/utils/copy/CopyWindows.cpp index 2ab58b61..973def2b 100644 --- a/src/utils/Copy/copyWindows.cpp +++ b/src/utils/copy/CopyWindows.cpp @@ -1,13 +1,20 @@ -#include +/** + * @file CopyWindows.cpp + * @brief Copy file path to clipboard on Windows. + * @author JEYuhas + */ + #include -#include "copy.h" +#include + +#include "../Interface.h" -void copyFileToClipboard (const juce::File& file) +void copyFileToClipboard(const juce::File& file) { if (! file.existsAsFile()) return; - if (!OpenClipboard(nullptr)) + if (! OpenClipboard(nullptr)) return; EmptyClipboard(); @@ -17,12 +24,12 @@ void copyFileToClipboard (const juce::File& file) size_t size = sizeof(DROPFILES) + (path.size() + 2) * sizeof(wchar_t); HGLOBAL hMem = GlobalAlloc(GHND, size); - DROPFILES* df = (DROPFILES*)GlobalLock(hMem); + DROPFILES* df = (DROPFILES*) GlobalLock(hMem); df->pFiles = sizeof(DROPFILES); df->fWide = TRUE; - wchar_t* data = (wchar_t*)((BYTE*)df + sizeof(DROPFILES)); + wchar_t* data = (wchar_t*) ((BYTE*) df + sizeof(DROPFILES)); wcscpy(data, path.c_str()); GlobalUnlock(hMem); From 998c804b968729b455017a2496312acdd82ca6c7 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Wed, 8 Apr 2026 10:08:15 -0400 Subject: [PATCH 025/149] Copy icon, removed non-loaded file status message, and formatting. --- CMakeLists.txt | 2 -- src/media/MediaDisplayComponent.cpp | 43 +++++++++++++---------------- src/media/MediaDisplayComponent.h | 2 -- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 390be5fa..09de67b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,8 +64,6 @@ juce_generate_juce_header(${PROJECT_NAME}) # Finally, we supply a list of source files that will be built into the target. This is a standard # CMake command. - - target_sources(${PROJECT_NAME} PRIVATE src/Main.cpp diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 95b7ce5b..f386fef8 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -13,7 +13,6 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW deviceManager.initialise(0, 2, nullptr, true, {}, nullptr); deviceManager.addAudioCallback(&sourcePlayer); - sourcePlayer.setSource(&transportSource); @@ -108,18 +107,16 @@ void MediaDisplayComponent::initializeButtons() [this] { copyFileCallback(); }, MultiButton::DrawingMode::IconOnly, Colours::lightblue, - fontawesome::Save }; + fontawesome::Copy }; // Mode when there is nothing to copy copyFileButtonInactiveInfo = MultiButton::Mode { "Copy-Inactive", "Nothing to copy.", [this] {}, MultiButton::DrawingMode::IconOnly, - Colours::lightgrey, fontawesome::Save }; + Colours::lightgrey, fontawesome::Copy }; copyFileButton.addMode(copyFileButtonActiveInfo); copyFileButton.addMode(copyFileButtonInactiveInfo); headerComponent.addAndMakeVisible(copyFileButton); - - resetButtonState(); } @@ -765,30 +762,28 @@ void MediaDisplayComponent::saveFileCallback() } } - void MediaDisplayComponent::copyFileCallback() { - if (!isFileLoaded()) + if (isFileLoaded()) { - if (statusMessage != nullptr) - statusMessage->setMessage("No file loaded."); - return; - } - - // If you're copying the original file path: - juce::File file = getOriginalFilePath().getLocalFile(); + File file = getOriginalFilePath().getLocalFile(); - if (file.exists()) - { - copyFileToClipboard(file); + if (file.exists()) + { + copyFileToClipboard(file); - if (statusMessage != nullptr) - statusMessage->setMessage("File path copied to clipboard."); - } - else - { - if (statusMessage != nullptr) - statusMessage->setMessage("File does not exist."); + if (statusMessage != nullptr) + { + statusMessage->setMessage("File copied to clipboard."); + } + } + else + { + if (statusMessage != nullptr) + { + statusMessage->setMessage("Failed to copy file to clipboard."); + } + } } } diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 9cf6a55e..6e68ba47 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -109,7 +109,6 @@ class MediaDisplayComponent : public Component, bool isDuplicateFile(const URL& fileParth); void saveFileCallback(); - void copyFileCallback(); virtual double getTotalLengthInSecs() = 0; @@ -238,7 +237,6 @@ class MediaDisplayComponent : public Component, // Media + overhead panel (if any) Component mediaAreaContainer; - // Header sub-components Label trackNameLabel; MultiButton playStopButton; From 600e3a7f9d792ae3c8493bf2e211f0a0992b2b18 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Wed, 8 Apr 2026 10:56:06 -0400 Subject: [PATCH 026/149] More general X11 copy implementation for linux. --- CMakeLists.txt | 7 +- src/utils/copy/CopyLinux.cpp | 163 ++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 09de67b4..a2669bef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,7 +134,7 @@ if(APPLE) target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyMacOS.mm) elseif(WIN32) target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyWindows.cpp) -else() +elseif(LINUX) target_sources(${PROJECT_NAME} PRIVATE src/utils/copy/CopyLinux.cpp) endif() @@ -204,6 +204,11 @@ target_link_libraries(${PROJECT_NAME} juce::juce_recommended_warning_flags ) +if(LINUX) + find_package(X11 REQUIRED) + target_link_libraries(${PROJECT_NAME} PRIVATE X11::X11) +endif() + # C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\msvcp140.dll # C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140_1.dll # C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140.dll diff --git a/src/utils/copy/CopyLinux.cpp b/src/utils/copy/CopyLinux.cpp index aab8d157..ba0d1542 100644 --- a/src/utils/copy/CopyLinux.cpp +++ b/src/utils/copy/CopyLinux.cpp @@ -4,17 +4,172 @@ * @author JEYuhas, cwitkowitz */ +extern "C" +{ +#include +#include +} + +#include +#include + #include "../Interface.h" +namespace +{ +struct Clipboard +{ + Display* display = nullptr; + Window window = 0; + + Atom clipboard; + Atom targets; + Atom uriList; + Atom textPlain; + Atom utf8; + Atom gnome; + + std::string dataUri; + std::string dataPlain; + std::string dataGnome; + + std::atomic running { true }; +}; + +Clipboard* clipboardState = nullptr; + +void handleRequest(XEvent& e) +{ + auto* req = &e.xselectionrequest; + + XEvent res {}; + res.xselection.type = SelectionNotify; + res.xselection.display = req->display; + res.xselection.requestor = req->requestor; + res.xselection.selection = req->selection; + res.xselection.target = req->target; + res.xselection.time = req->time; + res.xselection.property = None; + + if (req->target == clipboardState->targets) + { + Atom list[] = { clipboardState->targets, + clipboardState->uriList, + clipboardState->textPlain, + clipboardState->utf8, + clipboardState->gnome }; + + XChangeProperty(req->display, + req->requestor, + req->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char*) list, + 5); + + res.xselection.property = req->property; + } + else if (req->target == clipboardState->uriList) + { + XChangeProperty(req->display, + req->requestor, + req->property, + clipboardState->uriList, + 8, + PropModeReplace, + (unsigned char*) clipboardState->dataUri.c_str(), + (int) clipboardState->dataUri.size()); + + res.xselection.property = req->property; + } + else if (req->target == clipboardState->textPlain || req->target == clipboardState->utf8) + { + XChangeProperty(req->display, + req->requestor, + req->property, + req->target, + 8, + PropModeReplace, + (unsigned char*) clipboardState->dataPlain.c_str(), + (int) clipboardState->dataPlain.size()); + + res.xselection.property = req->property; + } + else if (req->target == clipboardState->gnome) + { + XChangeProperty(req->display, + req->requestor, + req->property, + clipboardState->gnome, + 8, + PropModeReplace, + (unsigned char*) clipboardState->dataGnome.c_str(), + (int) clipboardState->dataGnome.size()); + + res.xselection.property = req->property; + } + + XSendEvent(req->display, req->requestor, False, 0, &res); + XFlush(req->display); +} + +void runLoop() +{ + while (clipboardState->running) + { + while (XPending(clipboardState->display)) + { + XEvent e; + XNextEvent(clipboardState->display, &e); + + if (e.type == SelectionRequest) + handleRequest(e); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} +} // namespace + void copyFileToClipboard(const File& file) { if (! file.existsAsFile()) return; - String cmd; + if (clipboardState) + { + clipboardState->running = false; + delete clipboardState; + clipboardState = nullptr; + } + + clipboardState = new Clipboard(); + + clipboardState->display = XOpenDisplay(nullptr); + if (! clipboardState->display) + return; + + clipboardState->window = XCreateSimpleWindow( + clipboardState->display, DefaultRootWindow(clipboardState->display), 0, 0, 1, 1, 0, 0, 0); + + clipboardState->clipboard = XInternAtom(clipboardState->display, "CLIPBOARD", False); + clipboardState->targets = XInternAtom(clipboardState->display, "TARGETS", False); + clipboardState->uriList = XInternAtom(clipboardState->display, "text/uri-list", False); + clipboardState->textPlain = XInternAtom(clipboardState->display, "text/plain", False); + clipboardState->utf8 = XInternAtom(clipboardState->display, "UTF8_STRING", False); + clipboardState->gnome = + XInternAtom(clipboardState->display, "x-special/gnome-copied-files", False); + + std::string path = file.getFullPathName().toStdString(); + std::string uri = "file://" + path; + + clipboardState->dataUri = uri + "\r\n"; + clipboardState->dataPlain = path; + clipboardState->dataGnome = "copy\n" + uri + "\n"; - cmd << "printf \"file://" << file.getFullPathName() - << "\\n\" | xclip -selection clipboard -t text/uri-list"; + XSetSelectionOwner( + clipboardState->display, clipboardState->clipboard, clipboardState->window, CurrentTime); - std::system(cmd.toRawUTF8()); + std::thread(runLoop).detach(); } From aa80e334843b03825b9651c58d860c15fa954e1f Mon Sep 17 00:00:00 2001 From: Derek LLanes Date: Wed, 8 Apr 2026 12:42:50 -0400 Subject: [PATCH 027/149] Implement Ticket 377 drum MIDI visualization --- src/media/MidiDisplayComponent.cpp | 10 ++--- src/media/pianoroll/NoteGridComponent.cpp | 46 ++++++++++++++++------- src/media/pianoroll/NoteGridComponent.hpp | 6 ++- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/media/MidiDisplayComponent.cpp b/src/media/MidiDisplayComponent.cpp index 2c13d614..8c8a7cb4 100644 --- a/src/media/MidiDisplayComponent.cpp +++ b/src/media/MidiDisplayComponent.cpp @@ -96,10 +96,9 @@ void MidiDisplayComponent::loadMediaFile(const URL& filePath) int channel = midiMessage.getChannel(); - if (channel == 10) - { - // TODO - handle drums channel - } + // Handle drums channel + bool isDrum = (channel == 10); + if (midiMessage.isProgramChange()) { @@ -137,7 +136,8 @@ void MidiDisplayComponent::loadMediaFile(const URL& filePath) startTime, duration, static_cast(velocity), - static_cast(instrument)); + static_cast(instrument), + isDrum); pianoRoll.insertNote(n); } } diff --git a/src/media/pianoroll/NoteGridComponent.cpp b/src/media/pianoroll/NoteGridComponent.cpp index 3fc574c7..c7e15637 100644 --- a/src/media/pianoroll/NoteGridComponent.cpp +++ b/src/media/pianoroll/NoteGridComponent.cpp @@ -27,26 +27,46 @@ void NoteGridComponent::paint(Graphics& g) Rectangle bounds(noteXPos, noteYPos, jmax(3.0f, noteWidth), noteHeight - 1.0f); + float maxDrumWidth = 0.01f * getParentComponent()->getWidth(); + float hue = static_cast(n->instrument) / 127.0f; Colour color = Colour::fromHSV(hue, 1.0f, 1.0f, 1.0f).brighter(); // Note fill - g.setColour(color.withAlpha(0.75f)); - g.fillRect(bounds.toNearestInt()); + + if (n->isDrum) + { + g.setColour(color); + + const float thickness = jmax(2.0f, noteHeight * 0.18f); + const float pad = jmax(1.0f, thickness * 0.5f); - if ((noteWidth >= 5) & (noteHeight >= 8)) + const float x1 = noteXPos - maxDrumWidth/2; + const float y1 = bounds.getY() + pad; + const float x2 = noteXPos + maxDrumWidth/2; + const float y2 = bounds.getBottom() - pad; + + g.drawLine(x1, y1, x2, y2, thickness); + g.drawLine(x2, y1, x1, y2, thickness); + } + else { - const float maxVelocityWidth = static_cast(noteWidth - 4); - const float verticalOffset = static_cast(noteHeight) * 0.5f - 2.0f; - const float velocityHeight = 4.0f; - - // Velocity fill - g.setColour(color.brighter()); - g.fillRect(bounds.translated(2, verticalOffset) - .withWidth(maxVelocityWidth * n->velocity / 127.0f) - .withHeight(velocityHeight) - .toNearestInt()); + g.setColour(color.withAlpha(0.75f)); + g.fillRect(bounds.toNearestInt()); + + if ((noteWidth >= 5) && (noteHeight >= 8)) + { + const float maxVelocityWidth = static_cast(noteWidth - 4); + const float verticalOffset = static_cast(noteHeight) * 0.5f - 2.0f; + const float velocityHeight = 4.0f; + + g.setColour(color.brighter()); + g.fillRect(bounds.translated(2, verticalOffset) + .withWidth(maxVelocityWidth * n->velocity / 127.0f) + .withHeight(velocityHeight) + .toNearestInt()); + } } } } diff --git a/src/media/pianoroll/NoteGridComponent.hpp b/src/media/pianoroll/NoteGridComponent.hpp index 7a3eb19a..d4f8e13e 100644 --- a/src/media/pianoroll/NoteGridComponent.hpp +++ b/src/media/pianoroll/NoteGridComponent.hpp @@ -15,8 +15,8 @@ using namespace juce; struct MidiNote { public: - MidiNote(unsigned char n, double s, double d, unsigned char v, unsigned char i = 0) - : noteNumber(n), startTime(s), duration(d), velocity(v), instrument(i) + MidiNote(unsigned char n, double s, double d, unsigned char v, unsigned char i = 0, bool drum = false) + : noteNumber(n), startTime(s), duration(d), velocity(v), instrument(i), isDrum(drum) { } @@ -27,6 +27,7 @@ struct MidiNote duration = other.duration; velocity = other.velocity; instrument = other.instrument; + isDrum = other.isDrum; } unsigned char noteNumber; @@ -34,6 +35,7 @@ struct MidiNote double duration; unsigned char velocity; unsigned char instrument; + bool isDrum; }; class NoteGridComponent : public KeyboardComponent From 53fa7ef01e69d189ef1a9ccc35e0053ec7bd0e7e Mon Sep 17 00:00:00 2001 From: Richard Zhu Date: Wed, 8 Apr 2026 23:11:16 -0400 Subject: [PATCH 028/149] Add audio format conversion when sending to DAW When the selected file's format doesn't match the DAW-linked file's format, attempt conversion using JUCE's AudioFormatManager rather than showing an error. Supports WAV, AIFF (.aif/.aiff), and FLAC. --- src/widgets/MediaClipboardWidget.h | 128 ++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 11 deletions(-) diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 3753cbfd..0004af60 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -133,7 +133,10 @@ class MediaClipboardWidget : public Component, public ChangeListener Rectangle getClipboardTrackAreaBounds() const { return trackArea.getBounds().expanded(2); } - Rectangle getClipboardControlsBounds() const { return controlsComponent.getBounds().expanded(2); } + Rectangle getClipboardControlsBounds() const + { + return controlsComponent.getBounds().expanded(2); + } Rectangle getClipboardNameBoxBounds() const { @@ -273,20 +276,49 @@ class MediaClipboardWidget : public Component, public ChangeListener + validExtensions.joinIntoString(", ") + ".", "OK"); }*/ - + // RZ if (originalFile.getFileExtension() != selectedFile.getFileExtension()) { - AlertWindow::showMessageBoxAsync( - AlertWindow::WarningIcon, - "File Type Mismatch", - "Cannot overwrite file of type \"" - + originalFile.getFileExtension() - + "\" with file of type \"" - + selectedFile.getFileExtension() + "\".", - "OK"); + File tempFile = selectedFile.getSiblingFile( + selectedFile.getFileNameWithoutExtension() + + "_converted" + originalFile.getFileExtension()); - // TODO - perform conversion if possible + if (convertAudioFile(selectedFile, tempFile)) + { + if (tempFile.copyFileTo(originalFile)) + { + DBG_AND_LOG( + "MediaClipboardWidget::sendToDAWCallback: Converted and overwrote " + << originalFile.getFullPathName() << " with " + << selectedFile.getFullPathName() << "."); + + tempFile.deleteFile(); + + linkedDisplays[selectedIndex]->initializeDisplay( + URL(originalFile)); + removeSelectionCallback(); + linkedDisplays[selectedIndex]->selectTrack(); + } + else + { + tempFile.deleteFile(); + DBG_AND_LOG( + "MediaClipboardWidget::sendToDAWCallback: Conversion succeeded " + "but failed to copy to " + << originalFile.getFullPathName()); + } + } + else + { + AlertWindow::showMessageBoxAsync( + AlertWindow::WarningIcon, + "File Type Mismatch", + "Cannot convert file of type \"" + + selectedFile.getFileExtension() + "\" to \"" + + originalFile.getFileExtension() + "\".", + "OK"); + } } else { @@ -544,6 +576,80 @@ class MediaClipboardWidget : public Component, public ChangeListener } } + // RZ edit + bool convertAudioFile(const File& source, const File& target) + { + AudioFormatManager formatManager; + formatManager.registerBasicFormats(); + + std::unique_ptr reader(formatManager.createReaderFor(source)); + + if (! reader) + { + DBG_AND_LOG( + "convertAudioFile: Could not read source file: " << source.getFullPathName()); + return false; + } + + String ext = target.getFileExtension().toLowerCase(); + AudioFormat* format = nullptr; + + if (ext == ".wav") + format = formatManager.findFormatForFileExtension("wav"); + else if (ext == ".aiff" || ext == ".aif") + format = formatManager.findFormatForFileExtension("aiff"); + else if (ext == ".flac") + format = formatManager.findFormatForFileExtension("flac"); + else + { + DBG_AND_LOG("convertAudioFile: Unsupported target format: " << ext); + return false; + } + + if (! format) + { + DBG_AND_LOG("convertAudioFile: Format not available: " << ext); + return false; + } + + target.deleteFile(); + auto outputStream = target.createOutputStream(); + if (! outputStream) + { + DBG_AND_LOG( + "convertAudioFile: Could not create output file: " << target.getFullPathName()); + return false; + } + + std::unique_ptr writer(format->createWriterFor( + outputStream.release(), reader->sampleRate, reader->numChannels, 16, {}, 0)); + + if (! writer) + { + DBG_AND_LOG( + "convertAudioFile: Could not create writer for: " << target.getFullPathName()); + return false; + } + + const int blockSize = 4096; + AudioBuffer buffer((int) reader->numChannels, blockSize); + int64 totalFrames = (int64) reader->lengthInSamples; + int64 position = 0; + + while (position < totalFrames) + { + int64 framesToRead = jmin((int64) blockSize, totalFrames - position); + reader->read(&buffer, 0, (int) framesToRead, position, true, true); + writer->writeFromAudioSampleBuffer(buffer, 0, (int) framesToRead); + position += framesToRead; + } + + DBG_AND_LOG("convertAudioFile: Converted " << source.getFullPathName() << " -> " + << target.getFullPathName()); + + return true; + } + void resetState() { selectionTextBox.clear(); From 103cebc1df059fbb88df753af17e122e9f3956cf Mon Sep 17 00:00:00 2001 From: Richard Zhu Date: Wed, 8 Apr 2026 23:20:10 -0400 Subject: [PATCH 029/149] Add audio format conversion when sending to DAW When the selected file's format doesn't match the DAW-linked file's format, attempt conversion using JUCE's AudioFormatManager rather than showing an error. Supports WAV, AIFF (.aif/.aiff), and FLAC. --- src/widgets/MediaClipboardWidget.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 0004af60..29541ceb 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -133,10 +133,7 @@ class MediaClipboardWidget : public Component, public ChangeListener Rectangle getClipboardTrackAreaBounds() const { return trackArea.getBounds().expanded(2); } - Rectangle getClipboardControlsBounds() const - { - return controlsComponent.getBounds().expanded(2); - } + Rectangle getClipboardControlsBounds() const { return controlsComponent.getBounds().expanded(2); } Rectangle getClipboardNameBoxBounds() const { From 2b5622f9965f3cdb2fe9922532d7256dd065f0d0 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 9 Apr 2026 12:35:48 -0400 Subject: [PATCH 030/149] Added initial check for attempt to convert audio to midi and some minor cleanup. --- src/widgets/MediaClipboardWidget.h | 112 ++++++++++++++++------------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 29541ceb..4dd5d940 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -133,7 +133,10 @@ class MediaClipboardWidget : public Component, public ChangeListener Rectangle getClipboardTrackAreaBounds() const { return trackArea.getBounds().expanded(2); } - Rectangle getClipboardControlsBounds() const { return controlsComponent.getBounds().expanded(2); } + Rectangle getClipboardControlsBounds() const + { + return controlsComponent.getBounds().expanded(2); + } Rectangle getClipboardNameBoxBounds() const { @@ -261,7 +264,7 @@ class MediaClipboardWidget : public Component, public ChangeListener File selectedFile = selectedTrack->getOriginalFilePath().getLocalFile(); - /*StringArray validExtensions = + StringArray validExtensions = originalTrack->getInstanceExtensions(); if (! validExtensions.contains(selectedFile.getFileExtension())) @@ -269,13 +272,19 @@ class MediaClipboardWidget : public Component, public ChangeListener AlertWindow::showMessageBoxAsync( AlertWindow::WarningIcon, "Invalid File", - "This track can only be overwritten with data of the following file types: " - + validExtensions.joinIntoString(", ") + ".", + "This track cannot be overwritten with the selected file.", "OK"); - }*/ - // RZ - if (originalFile.getFileExtension() - != selectedFile.getFileExtension()) + + return; + } + + bool successfulOverwrite = false; + + String ext = originalFile.getFileExtension().toLowerCase(); + + if (AudioDisplayComponent::getSupportedExtensions().contains( + ext) + && ext != selectedFile.getFileExtension().toLowerCase()) { File tempFile = selectedFile.getSiblingFile( selectedFile.getFileNameWithoutExtension() @@ -286,25 +295,22 @@ class MediaClipboardWidget : public Component, public ChangeListener if (tempFile.copyFileTo(originalFile)) { DBG_AND_LOG( - "MediaClipboardWidget::sendToDAWCallback: Converted and overwrote " - << originalFile.getFullPathName() << " with " - << selectedFile.getFullPathName() << "."); - - tempFile.deleteFile(); + "MediaClipboardWidget::sendToDAWCallback: Converted and overwrote \"" + << originalFile.getFullPathName() + << "\" with \"" + << selectedFile.getFullPathName() << "\"."); - linkedDisplays[selectedIndex]->initializeDisplay( - URL(originalFile)); - removeSelectionCallback(); - linkedDisplays[selectedIndex]->selectTrack(); + successfulOverwrite = true; } else { - tempFile.deleteFile(); DBG_AND_LOG( "MediaClipboardWidget::sendToDAWCallback: Conversion succeeded " - "but failed to copy to " - << originalFile.getFullPathName()); + "but failed to copy to \"" + << originalFile.getFullPathName() << "\"."); } + + tempFile.deleteFile(); } else { @@ -322,28 +328,33 @@ class MediaClipboardWidget : public Component, public ChangeListener if (selectedFile.copyFileTo(originalFile)) { DBG_AND_LOG( - "MediaClipboardWidget::sendToDAWCallback: Overwriting file " - << originalFile.getFullPathName() << " with " - << selectedFile.getFullPathName() << "."); + "MediaClipboardWidget::sendToDAWCallback: Overwriting file \"" + << originalFile.getFullPathName() << "\" with \"" + << selectedFile.getFullPathName() << "\"."); - // Update display with overwritten media - linkedDisplays[selectedIndex]->initializeDisplay( - URL(originalFile)); - - // Remove selected track - removeSelectionCallback(); - - // Select overwritten track - linkedDisplays[selectedIndex]->selectTrack(); + successfulOverwrite = true; } else { DBG_AND_LOG( - "MediaClipboardWidget::sendToDAWCallback: Failed to overwrite file " - << originalFile.getFullPathName() << " with " - << selectedFile.getFullPathName() << "."); + "MediaClipboardWidget::sendToDAWCallback: Failed to overwrite file \"" + << originalFile.getFullPathName() << "\" with \"" + << selectedFile.getFullPathName() << "\"."); } } + + if (successfulOverwrite) + { + // Update display with overwritten media + linkedDisplays[selectedIndex]->initializeDisplay( + URL(originalFile)); + + // Remove selected track + removeSelectionCallback(); + + // Select overwritten track + linkedDisplays[selectedIndex]->selectTrack(); + } } } }), @@ -573,7 +584,7 @@ class MediaClipboardWidget : public Component, public ChangeListener } } - // RZ edit + // TODO - move to utils/? bool convertAudioFile(const File& source, const File& target) { AudioFormatManager formatManager; @@ -583,8 +594,9 @@ class MediaClipboardWidget : public Component, public ChangeListener if (! reader) { - DBG_AND_LOG( - "convertAudioFile: Could not read source file: " << source.getFullPathName()); + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Could not read source file \"" + << source.getFullPathName() << "\"."); + return false; } @@ -599,22 +611,21 @@ class MediaClipboardWidget : public Component, public ChangeListener format = formatManager.findFormatForFileExtension("flac"); else { - DBG_AND_LOG("convertAudioFile: Unsupported target format: " << ext); - return false; - } + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Unsupported target format \"" + << ext << "\"."); - if (! format) - { - DBG_AND_LOG("convertAudioFile: Format not available: " << ext); return false; } target.deleteFile(); + auto outputStream = target.createOutputStream(); + if (! outputStream) { - DBG_AND_LOG( - "convertAudioFile: Could not create output file: " << target.getFullPathName()); + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Could not create output file \"" + << target.getFullPathName() << "\"."); + return false; } @@ -623,8 +634,9 @@ class MediaClipboardWidget : public Component, public ChangeListener if (! writer) { - DBG_AND_LOG( - "convertAudioFile: Could not create writer for: " << target.getFullPathName()); + DBG_AND_LOG("convertAudioFile: Could not create writer for \"" + << target.getFullPathName() << "\"."); + return false; } @@ -641,8 +653,8 @@ class MediaClipboardWidget : public Component, public ChangeListener position += framesToRead; } - DBG_AND_LOG("convertAudioFile: Converted " << source.getFullPathName() << " -> " - << target.getFullPathName()); + DBG_AND_LOG("MediaClipboardWidget::convertAudioFile: Converted and saved \"" + << source.getFullPathName() << "\" to \"" << target.getFullPathName() << "\"."); return true; } From c7d277e89b12064c05d0b1d9e1ef5324cf7869be Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 9 Apr 2026 13:15:29 -0400 Subject: [PATCH 031/149] Attempt to generalize Windows-specific build steps. --- CMakeLists.txt | 65 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a21ebd92..bf7ee352 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,61 +196,74 @@ target_link_libraries(${PROJECT_NAME} juce::juce_recommended_warning_flags ) -# C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\msvcp140.dll -# C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140_1.dll -# C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.36.32532\x64\Microsoft.VC143.CRT\vcruntime140.dll if (WIN32) # Function to find the specific runtime DLLs function(find_vc_runtime_dlls out_var) - message(STATUS "Searching for specific runtime DLLs...") + message(STATUS "Locating MSVC runtime DLLs...") # Define the target DLLs set(target_dlls - "msvcp140.dll" - "vcruntime140_1.dll" - "vcruntime140.dll" + msvcp140.dll + vcruntime140.dll + vcruntime140_1.dll ) - # Set the base path for Visual Studio 2022 redistributable DLLs - set(VS_CRT_BASE_PATH - "C:/Program Files/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" - "C:/Program Files (x86)/Microsoft Visual Studio/2022/*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT" - ) + # Get the MSVC toolchain root from the compiler path + get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) - # Search for the DLLs in the specified base path - file(GLOB_RECURSE all_runtime_dlls - LIST_DIRECTORIES false - ${VS_CRT_BASE_PATH}/*.dll + # Walk up to: .../VC/Tools/MSVC/ + get_filename_component(_msvc_root "${_compiler_dir}/../../.." ABSOLUTE) + + # Construct Redist path dynamically + file(GLOB _redist_dirs + "${_msvc_root}/Redist/MSVC/*" ) + set(runtime_dlls) - foreach(dll ${all_runtime_dlls}) - get_filename_component(dll_name ${dll} NAME) - if(dll_name IN_LIST target_dlls) - list(APPEND runtime_dlls ${dll}) - endif() + foreach(redist_dir ${_redist_dirs}) + # Handle both x64 and x86 automatically + foreach(arch x64 x86) + set(crt_dir "${redist_dir}/${arch}/Microsoft.VC*.CRT") + + file(GLOB dlls + "${crt_dir}/*.dll" + ) + + foreach(dll ${dlls}) + get_filename_component(name "${dll}" NAME) + if(name IN_LIST target_dlls) + list(APPEND runtime_dlls "${dll}") + endif() + endforeach() + endforeach() endforeach() + list(REMOVE_DUPLICATES runtime_dlls) + if(runtime_dlls) - message(STATUS "Found specific runtime DLLs:") + message(STATUS "Found MSVC runtime DLLs:") foreach(dll ${runtime_dlls}) - message(STATUS "${dll}") + message(STATUS " ${dll}") endforeach() set(${out_var} ${runtime_dlls} PARENT_SCOPE) else() - message(FATAL_ERROR "Required specific runtime DLLs not found") + message(FATAL_ERROR "MSVC runtime DLLs not found") endif() endfunction() # Call the function to find the specific DLLs find_vc_runtime_dlls(RUNTIME_DLLS) - # Add a post-build step to copy the specific DLLs to the output directory + # Add a post-build step to copy the DLLs to the output directory add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${RUNTIME_DLLS} - $) + $ + ) + endif() + # copy the pyinstaller tools to the bundle # if (APPLE) # add_custom_command(TARGET ${PROJECT_NAME} From bb4d68c77fa4893c894e634cbcc684c360e4fe56 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 9 Apr 2026 13:23:17 -0400 Subject: [PATCH 032/149] Attempt to simplify Windows-specific build steps. --- CMakeLists.txt | 67 +++----------------------------------------------- 1 file changed, 4 insertions(+), 63 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf7ee352..b14b3ff8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,71 +197,12 @@ target_link_libraries(${PROJECT_NAME} ) if (WIN32) - # Function to find the specific runtime DLLs - function(find_vc_runtime_dlls out_var) - message(STATUS "Locating MSVC runtime DLLs...") - - # Define the target DLLs - set(target_dlls - msvcp140.dll - vcruntime140.dll - vcruntime140_1.dll - ) - - # Get the MSVC toolchain root from the compiler path - get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) - - # Walk up to: .../VC/Tools/MSVC/ - get_filename_component(_msvc_root "${_compiler_dir}/../../.." ABSOLUTE) - - # Construct Redist path dynamically - file(GLOB _redist_dirs - "${_msvc_root}/Redist/MSVC/*" - ) - - set(runtime_dlls) - - foreach(redist_dir ${_redist_dirs}) - # Handle both x64 and x86 automatically - foreach(arch x64 x86) - set(crt_dir "${redist_dir}/${arch}/Microsoft.VC*.CRT") - - file(GLOB dlls - "${crt_dir}/*.dll" - ) - - foreach(dll ${dlls}) - get_filename_component(name "${dll}" NAME) - if(name IN_LIST target_dlls) - list(APPEND runtime_dlls "${dll}") - endif() - endforeach() - endforeach() - endforeach() - - list(REMOVE_DUPLICATES runtime_dlls) - - if(runtime_dlls) - message(STATUS "Found MSVC runtime DLLs:") - foreach(dll ${runtime_dlls}) - message(STATUS " ${dll}") - endforeach() - set(${out_var} ${runtime_dlls} PARENT_SCOPE) - else() - message(FATAL_ERROR "MSVC runtime DLLs not found") - endif() - endfunction() - - # Call the function to find the specific DLLs - find_vc_runtime_dlls(RUNTIME_DLLS) - - # Add a post-build step to copy the DLLs to the output directory add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${RUNTIME_DLLS} - $ + COMMAND ${CMAKE_COMMAND} -E copy + $ + $ + COMMAND_EXPAND_LISTS ) - endif() # copy the pyinstaller tools to the bundle From e42e81d042657f8a29fa33e1bbe6cfa1846b86ed Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Thu, 9 Apr 2026 19:56:03 -0400 Subject: [PATCH 033/149] Seventh attempt to fix time axis --- src/media/MediaDisplayComponent.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7b351529..e7d57142 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -29,7 +29,6 @@ TickScheme chooseTickScheme(double visibleLength) { 600.0, 120.0, 5 }, }; - // Pick the scheme that gives roughly 4–12 major ticks across the visible range for (const auto& s : schemes) { double numMajor = visibleLength / s.majorStep; @@ -37,7 +36,6 @@ TickScheme chooseTickScheme(double visibleLength) return s; } - // Fallback for very long files double majorStep = std::pow(10.0, std::floor(std::log10(visibleLength / 5.0))); majorStep = std::max(0.01, majorStep); return { majorStep, majorStep / 5.0, 5 }; @@ -95,13 +93,12 @@ void TimeAxisStrip::paint(Graphics& g) const float minorTickTop = static_cast(h) * 0.55f; const float minorTickBot = static_cast(h); - // --- Minor ticks --- + // Minor ticks g.setColour(Colours::grey.withAlpha(0.5f)); const double firstMinor = std::ceil(visibleStart / minorStep) * minorStep; for (double t = firstMinor; t <= visibleEnd && t <= totalLength; t += minorStep) { - // Skip positions that coincide with major ticks double remainder = std::fmod(t, majorStep); if (remainder < minorStep * 0.1 || (majorStep - remainder) < minorStep * 0.1) continue; @@ -113,7 +110,7 @@ void TimeAxisStrip::paint(Graphics& g) g.drawVerticalLine(static_cast(x), minorTickTop, minorTickBot); } - // --- Major ticks and labels --- + // Major ticks and labels g.setColour(Colours::lightgrey.withAlpha(0.9f)); const int labelH = jmin(13, h - 2); @@ -1008,7 +1005,6 @@ bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) if (pps <= 0.0f) return false; - // minPps = zoomed all the way out; maxPps = zoomed all the way in (~5 seconds visible). const double minVisibleSeconds = 5.0; const float minPps = static_cast(mediaWidth / totalLength); float maxPps = static_cast(mediaWidth / minVisibleSeconds); @@ -1018,7 +1014,6 @@ bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); - // If pps didn't change, zoom has hit a limit if (std::abs(newPps - pps) < 0.01f) return false; @@ -1076,9 +1071,7 @@ void MediaDisplayComponent::mouseWheelMove(const MouseEvent& evt, const MouseWhe } else { - bool zoomed = horizontalZoom(static_cast(wheel.deltaY), scrollTime); - if (! zoomed) - horizontalMove(static_cast(wheel.deltaY)); + horizontalZoom(static_cast(wheel.deltaY), scrollTime); } } else From cfa52948a7e851ad946c28f59706ddafd45452c5 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 16 Apr 2026 13:14:14 -0400 Subject: [PATCH 034/149] Fixed merge conflict, improved feel of zoom anchoring, and some minor cleanup. --- src/media/MediaDisplayComponent.cpp | 48 +++++++++++++---------------- src/media/MediaDisplayComponent.h | 2 +- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 2703c1be..7d9c9023 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -2,7 +2,8 @@ #include "AudioDisplayComponent.h" #include "MidiDisplayComponent.h" -<<<<<<< feature/375-time-axis-horizontal-zoom +#include "../utils/Interface.h" + #include namespace @@ -17,17 +18,9 @@ struct TickScheme TickScheme chooseTickScheme(double visibleLength) { static const TickScheme schemes[] = { - { 0.1, 0.02, 5 }, - { 0.5, 0.1, 5 }, - { 1.0, 0.25, 4 }, - { 2.0, 0.5, 4 }, - { 5.0, 1.0, 5 }, - { 15.0, 5.0, 3 }, - { 30.0, 10.0, 3 }, - { 60.0, 15.0, 4 }, - { 120.0, 30.0, 4 }, - { 300.0, 60.0, 5 }, - { 600.0, 120.0, 5 }, + { 0.1, 0.02, 5 }, { 0.5, 0.1, 5 }, { 1.0, 0.25, 4 }, { 2.0, 0.5, 4 }, + { 5.0, 1.0, 5 }, { 15.0, 5.0, 3 }, { 30.0, 10.0, 3 }, { 60.0, 15.0, 4 }, + { 120.0, 30.0, 4 }, { 300.0, 60.0, 5 }, { 600.0, 120.0, 5 }, }; for (const auto& s : schemes) @@ -136,9 +129,6 @@ void TimeAxisStrip::paint(Graphics& g) true); } } -======= -#include "../utils/Interface.h" ->>>>>>> develop MediaDisplayComponent::MediaDisplayComponent() : MediaDisplayComponent("Media Track") {} @@ -1042,41 +1032,47 @@ void MediaDisplayComponent::horizontalMove(double deltaT) updateVisibleRange({ newStart, newStart + visibleLength }); } -bool MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) +void MediaDisplayComponent::horizontalZoom(double deltaZoom, double scrollPosT) { const float mediaWidth = getMediaWidth(); const double totalLength = getTotalLengthInSecs(); if (mediaWidth <= 0.0f || totalLength <= 0.0) - return false; + return; const float pps = getPixelsPerSecond(); if (pps <= 0.0f) - return false; + return; const double minVisibleSeconds = 5.0; const float minPps = static_cast(mediaWidth / totalLength); float maxPps = static_cast(mediaWidth / minVisibleSeconds); - if (maxPps < minPps) - maxPps = minPps; + maxPps = jmax(maxPps, minPps); float newPps = pps * (1.0f + 0.5f * static_cast(deltaZoom)); newPps = jlimit(minPps, maxPps, newPps); if (std::abs(newPps - pps) < 0.01f) - return false; + return; double newVisibleLength = static_cast(mediaWidth) / static_cast(newPps); - if (newVisibleLength > totalLength) - newVisibleLength = totalLength; + newVisibleLength = jmin(newVisibleLength, totalLength); + + double anchorRatio = 0.5; + double visibleStart = visibleRange.getStart(); + double visibleLength = visibleRange.getLength(); + + if (visibleLength > 0.0) + anchorRatio = (scrollPosT - visibleStart) / visibleLength; + + anchorRatio = jlimit(0.0, 1.0, anchorRatio); const double maxStart = jmax(0.0, totalLength - newVisibleLength); - double newStart = scrollPosT - newVisibleLength * 0.5; + double newStart = scrollPosT - anchorRatio * newVisibleLength; newStart = jlimit(0.0, maxStart, newStart); double newEnd = newStart + newVisibleLength; updateVisibleRange({ newStart, newEnd }); - return true; } void MediaDisplayComponent::scrollBarMoved(ScrollBar* scrollBarThatHasMoved, @@ -1105,7 +1101,7 @@ void MediaDisplayComponent::mouseWheelMove(const MouseEvent& evt, const MouseWhe double scrollTime = mediaXToTime(evt.position.getX()); - if (! commandMod) + if (! commandMod && evt.eventComponent == getMediaComponent()) { if (std::abs(wheel.deltaX) > 2 * std::abs(wheel.deltaY)) { diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 9ca18b66..39b91e58 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -217,7 +217,7 @@ class MediaDisplayComponent : public Component, int correctMediaXBounds(float mX, float width); void horizontalMove(double deltaT); - bool horizontalZoom(double deltaZoom, double scrollPosT); + void horizontalZoom(double deltaZoom, double scrollPosT); void scrollBarMoved(ScrollBar* scrollBarThatHasMoved, double scrollBarRangeStart) override; From 1e9b57f697b9b9957ee51f8a42a288b151822f95 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 16 Apr 2026 13:36:51 -0400 Subject: [PATCH 035/149] Protect against $ returning empty. --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b14b3ff8..6e5c13df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,10 +198,12 @@ target_link_libraries(${PROJECT_NAME} if (WIN32) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy + COMMAND ${CMAKE_COMMAND} -E echo "Copying runtime DLLs (if any)" + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ COMMAND_EXPAND_LISTS + VERBATIM ) endif() From 9f6df123c71ec4b3408fd8cdc9fc1b855fe690e2 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 16 Apr 2026 13:49:12 -0400 Subject: [PATCH 036/149] Switch to install-time packaging. --- .github/workflows/cmake_ctest.yml | 7 +++++-- CMakeLists.txt | 12 +++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cmake_ctest.yml b/.github/workflows/cmake_ctest.yml index 25a7d9e9..a28a97f9 100644 --- a/.github/workflows/cmake_ctest.yml +++ b/.github/workflows/cmake_ctest.yml @@ -149,6 +149,9 @@ jobs: run: cmake --build ${{ env.BUILD_DIR }} --config ${{ env.BUILD_TYPE }} --parallel 4 # dummy build # run: touch dummy + - name: Install + shell: bash + run: cmake --install ${{ env.BUILD_DIR }} --config ${{ env.BUILD_TYPE }} --prefix install # why the python3 -m pip install packaging? see this issue: https://github.com/nodejs/node-gyp/issues/2869 - name: Install Python dependencies for MACOS dmg @@ -167,14 +170,14 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ env.PRODUCT_NAME }} - path: ${{ env.ARTIFACTS_PATH }} + path: ${{ env.BUILD_DIR }}/install - name: Upload Zip (Linux) if: ${{ matrix.name == 'Linux' }} uses: actions/upload-artifact@v4 with: name: ${{ env.PRODUCT_NAME }} - path: ${{ env.ARTIFACTS_PATH }} + path: ${{ env.BUILD_DIR }}/install - name: Upload DMG (macOS) if: ${{ matrix.name == 'macOS' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e5c13df..2f0744c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,13 +197,11 @@ target_link_libraries(${PROJECT_NAME} ) if (WIN32) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E echo "Copying runtime DLLs (if any)" - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND_EXPAND_LISTS - VERBATIM + install(TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION . + RUNTIME_DEPENDENCIES + PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" + POST_EXCLUDE_REGEXES "system32" ) endif() From 615886dffbbea3d5089b863293a4c8099596fa7b Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Thu, 16 Apr 2026 19:43:36 -0500 Subject: [PATCH 037/149] restore settings default --- src/MainComponent.cpp | 16 +++++++- src/MainComponent.h | 1 + src/utils/Logging.h | 8 ++++ src/windows/settings/GeneralSettingsTab.cpp | 41 ++++++++++++++++++++- src/windows/settings/GeneralSettingsTab.h | 8 +++- src/windows/settings/SettingsWindow.h | 6 ++- 6 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index a31a32df..5787487f 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -207,7 +207,7 @@ void MainComponent::openSettingsWindow() DialogWindow::LaunchOptions options; options.dialogTitle = "Settings"; options.dialogBackgroundColour = Colours::darkgrey; - options.content.setOwned(new SettingsWindow()); + options.content.setOwned(new SettingsWindow([this] { restoreViewDefaults(); })); options.useNativeTitleBar = true; options.resizable = true; @@ -216,6 +216,20 @@ void MainComponent::openSettingsWindow() options.launchAsync(); } +void MainComponent::restoreViewDefaults() +{ + // Default: status area shown — toggle if currently hidden + if (! showStatusArea) + viewStatusAreaCallback(); + + // Default: media clipboard shown — toggle if currently hidden + if (! showMediaClipboard) + viewMediaClipboardCallback(); + + // showWelcomePopup default (true) is already restored by clearing settings; + // it will show on the next launch automatically. +} + /* --View-- */ void MainComponent::viewStatusAreaCallback() diff --git a/src/MainComponent.h b/src/MainComponent.h index b229d7a4..0cfc1ac8 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -67,6 +67,7 @@ class MainComponent : public Component, // View void viewStatusAreaCallback(); void viewMediaClipboardCallback(); + void restoreViewDefaults(); // Help void openAboutWindow(); diff --git a/src/utils/Logging.h b/src/utils/Logging.h index e603efb4..9478fd2f 100644 --- a/src/utils/Logging.h +++ b/src/utils/Logging.h @@ -48,6 +48,14 @@ class HARPLogger : private DeletedAtShutdown File getLogFile() const { return logger->getLogFile(); } + void clearLog() + { + File logFile = logger->getLogFile(); + logger.reset(); // release file handle before truncating + logFile.replaceWithText(""); // truncate + initializeLogger(); // reopen + } + private: HARPLogger() = default; // Prevents instantiation from outside diff --git a/src/windows/settings/GeneralSettingsTab.cpp b/src/windows/settings/GeneralSettingsTab.cpp index ff7c2c05..54270a07 100644 --- a/src/windows/settings/GeneralSettingsTab.cpp +++ b/src/windows/settings/GeneralSettingsTab.cpp @@ -1,6 +1,7 @@ #include "GeneralSettingsTab.h" -GeneralSettingsTab::GeneralSettingsTab() +GeneralSettingsTab::GeneralSettingsTab(std::function onRestoreDefaults) + : onRestoreDefaults(std::move(onRestoreDefaults)) { // Set up button to open log folder openLogFolderButton.setButtonText("Open Log Folder"); @@ -11,6 +12,16 @@ GeneralSettingsTab::GeneralSettingsTab() openSettingsButton.setButtonText("Open Settings File"); openSettingsButton.onClick = [this] { handleOpenSettings(); }; addAndMakeVisible(openSettingsButton); + + // Set up button to clear logs + clearLogsButton.setButtonText("Clear Logs"); + clearLogsButton.onClick = [this] { handleClearLogs(); }; + addAndMakeVisible(clearLogsButton); + + // Set up button to restore default settings + restoreDefaultsButton.setButtonText("Restore Default Settings"); + restoreDefaultsButton.onClick = [this] { handleRestoreDefaults(); }; + addAndMakeVisible(restoreDefaultsButton); } void GeneralSettingsTab::resized() @@ -18,10 +29,15 @@ void GeneralSettingsTab::resized() Rectangle area = getLocalBounds().reduced(10); openLogFolderButton.setBounds(area.removeFromTop(30)); + area.removeFromTop(10); - area.removeFromTop(10); // Filler space + clearLogsButton.setBounds(area.removeFromTop(30)); + area.removeFromTop(10); openSettingsButton.setBounds(area.removeFromTop(30)); + area.removeFromTop(10); + + restoreDefaultsButton.setBounds(area.removeFromTop(30)); } void GeneralSettingsTab::handleOpenLogFolder() @@ -40,3 +56,24 @@ void GeneralSettingsTab::handleOpenSettings() // TODO - handler error case } } + +void GeneralSettingsTab::handleClearLogs() +{ + HARPLogger::getInstance()->clearLog(); +} + +void GeneralSettingsTab::handleRestoreDefaults() +{ + if (auto* settings = Settings::getUserSettings()) + { + StringArray allKeys = settings->getAllProperties().getAllKeys(); + + for (const auto& key : allKeys) + settings->removeValue(key); + + settings->saveIfNeeded(); + } + + if (onRestoreDefaults) + onRestoreDefaults(); +} diff --git a/src/windows/settings/GeneralSettingsTab.h b/src/windows/settings/GeneralSettingsTab.h index 2462303b..78287db5 100644 --- a/src/windows/settings/GeneralSettingsTab.h +++ b/src/windows/settings/GeneralSettingsTab.h @@ -16,7 +16,7 @@ using namespace juce; class GeneralSettingsTab : public Component { public: - GeneralSettingsTab(); + GeneralSettingsTab(std::function onRestoreDefaults = {}); ~GeneralSettingsTab() override = default; void resized() override; @@ -24,9 +24,15 @@ class GeneralSettingsTab : public Component private: void handleOpenLogFolder(); void handleOpenSettings(); + void handleClearLogs(); + void handleRestoreDefaults(); + + std::function onRestoreDefaults; TextButton openLogFolderButton; + TextButton clearLogsButton; TextButton openSettingsButton; + TextButton restoreDefaultsButton; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GeneralSettingsTab) }; diff --git a/src/windows/settings/SettingsWindow.h b/src/windows/settings/SettingsWindow.h index 3bdc9dd2..40de2761 100644 --- a/src/windows/settings/SettingsWindow.h +++ b/src/windows/settings/SettingsWindow.h @@ -17,9 +17,11 @@ using namespace juce; class SettingsWindow : public Component { public: - SettingsWindow() : tabComponent(TabbedButtonBar::TabsAtTop) + SettingsWindow(std::function onRestoreDefaults = {}) + : tabComponent(TabbedButtonBar::TabsAtTop) { - tabComponent.addTab("General", Colours::darkgrey, new GeneralSettingsTab(), true); + tabComponent.addTab( + "General", Colours::darkgrey, new GeneralSettingsTab(onRestoreDefaults), true); tabComponent.addTab("API Keys", Colours::darkgrey, new LoginTab(), true); //tabComponent.addTab("Audio", Colours::darkgrey, new AudioSettingsTab(), true); addAndMakeVisible(tabComponent); From d3827a65870841b225f1f2caa8c8157896396ca8 Mon Sep 17 00:00:00 2001 From: Huiran Yu Date: Fri, 17 Apr 2026 10:50:50 -0400 Subject: [PATCH 038/149] CMakeLists.txt update. #288 --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f0744c1..c45547b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,10 +198,11 @@ target_link_libraries(${PROJECT_NAME} if (WIN32) install(TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION . RUNTIME_DEPENDENCIES PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" POST_EXCLUDE_REGEXES "system32" + RUNTIME DESTINATION . + RESOURCE DESTINATION . ) endif() From 4a47f179ad1d904c3eba232734d1a93f5c2e757a Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 17 Apr 2026 14:05:41 -0400 Subject: [PATCH 039/149] Track gap remains and resets to origin position if track dragged outside media clipboard --- src/widgets/TrackAreaWidget.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index c6b59e5e..51f3e2b8 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -550,6 +550,7 @@ class TrackAreaWidget : public Component, if (clickedDisplay) { draggedTrack = clickedDisplay; + dragOriginIndex = getDraggedTrackIndex(); Point mouseInThis = e.getEventRelativeTo(this).getPosition(); Point trackTopLeft = draggedTrack->getBounds().getTopLeft(); @@ -581,7 +582,13 @@ class TrackAreaWidget : public Component, int newInsertIndex = -1; if (getLocalBounds().contains(posInThis)) + { newInsertIndex = getInsertIndexAtY(posInThis.y); + } + else + { + newInsertIndex = dragOriginIndex; + } if (newInsertIndex != dragInsertIndex) { @@ -625,6 +632,7 @@ class TrackAreaWidget : public Component, // Reset the drag state draggedTrack = nullptr; dragInsertIndex = -1; + dragOriginIndex = -1; isDraggingTrack = false; resized(); @@ -641,6 +649,7 @@ class TrackAreaWidget : public Component, MediaDisplayComponent* draggedTrack = nullptr; DragOverlayComponent* dragOverlay = nullptr; int dragInsertIndex = -1; + int dragOriginIndex = -1; bool isDraggingTrack = false; Point dragClickOffset { 0, 0 }; From 2c9ef2659a09a2cff54685fa39e5cb91804b7d99 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 17 Apr 2026 14:21:56 -0400 Subject: [PATCH 040/149] ghost track when dragging outside of media clipboard implemented --- src/widgets/TrackAreaWidget.h | 76 +++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 51f3e2b8..43a7d148 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -87,6 +87,29 @@ class DragOverlayComponent : public Component bool isActive = false; }; +class GhostTrackComponent : public Component +{ +public: + void setImage(const Image& img) + { + ghostImage = img; + repaint(); + } + + void paint(Graphics& g) override + { + if (ghostImage.isNull()) return; + + g.setOpacity(0.4f); + g.drawImage(ghostImage, + getLocalBounds().toFloat(), + RectanglePlacement::stretchToFit); + } + +private: + Image ghostImage; +}; + class TrackAreaWidget : public Component, public ChangeListener, public ChangeBroadcaster, @@ -99,6 +122,7 @@ class TrackAreaWidget : public Component, : displayMode(mode), fixedTrackHeight(trackHeight), dragOverlay(overlay) { addMouseListener(this, true); + addChildComponent(ghostTrack); } ~TrackAreaWidget() { resetState(); } @@ -136,12 +160,22 @@ class TrackAreaWidget : public Component, if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) { FlexItem gap; - - if (fixedTrackHeight) - gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); - else - gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); - + if (isDraggingOutside) + { + ghostTrack.setVisible(true); + if (fixedTrackHeight) + gap = FlexItem(ghostTrack).withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem(ghostTrack).withFlex(1).withMinHeight(50).withMargin(marginSize); + } + else + { + ghostTrack.setVisible(false); + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + } mainBox.items.add(gap); } @@ -165,12 +199,22 @@ class TrackAreaWidget : public Component, if (isDraggingTrack && dragInsertIndex >= 0 && layoutIndex == visualGapIndex) { FlexItem gap; - - if (fixedTrackHeight) - gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + if (isDraggingOutside) + { + ghostTrack.setVisible(true); + if (fixedTrackHeight) + gap = FlexItem(ghostTrack).withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem(ghostTrack).withFlex(1).withMinHeight(50).withMargin(marginSize); + } else - gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); - + { + ghostTrack.setVisible(false); + if (fixedTrackHeight) + gap = FlexItem().withHeight(fixedTrackHeight).withMargin(marginSize); + else + gap = FlexItem().withFlex(1).withMinHeight(50).withMargin(marginSize); + } mainBox.items.add(gap); } @@ -572,6 +616,7 @@ class TrackAreaWidget : public Component, if (dragOverlay != nullptr) { Image snapshot = draggedTrack->createComponentSnapshot(draggedTrack->getLocalBounds()); + ghostTrack.setImage(snapshot); dragOverlay->startDrag(snapshot, dragClickOffset); draggedTrack->setVisible(false); } @@ -580,17 +625,20 @@ class TrackAreaWidget : public Component, if (isDraggingTrack) { int newInsertIndex = -1; + bool wasOutside = isDraggingOutside; if (getLocalBounds().contains(posInThis)) { newInsertIndex = getInsertIndexAtY(posInThis.y); + isDraggingOutside = false; } else { newInsertIndex = dragOriginIndex; + isDraggingOutside = true; } - if (newInsertIndex != dragInsertIndex) + if (newInsertIndex != dragInsertIndex || isDraggingOutside != wasOutside) { dragInsertIndex = newInsertIndex; resized(); @@ -634,6 +682,8 @@ class TrackAreaWidget : public Component, dragInsertIndex = -1; dragOriginIndex = -1; isDraggingTrack = false; + isDraggingOutside = false; + ghostTrack.setVisible(false); resized(); } @@ -648,9 +698,11 @@ class TrackAreaWidget : public Component, // For reordering tracks via dragging MediaDisplayComponent* draggedTrack = nullptr; DragOverlayComponent* dragOverlay = nullptr; + GhostTrackComponent ghostTrack; int dragInsertIndex = -1; int dragOriginIndex = -1; bool isDraggingTrack = false; + bool isDraggingOutside = false; Point dragClickOffset { 0, 0 }; std::vector> mediaDisplays; From a59c70948f14a16db320a30d0350fb1a7e3b6f74 Mon Sep 17 00:00:00 2001 From: derekllanes Date: Mon, 27 Apr 2026 23:51:21 -0400 Subject: [PATCH 041/149] Fix Ticket 378: Add banner for optional input tracks --- src/media/MediaDisplayComponent.cpp | 39 +++++++++++++++++++++++++++++ src/media/MediaDisplayComponent.h | 1 + 2 files changed, 40 insertions(+) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7d9c9023..b42b38ff 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -315,10 +315,49 @@ void MediaDisplayComponent::paint(Graphics& g) } } +void MediaDisplayComponent::paintOverChildren(Graphics& g) +{ + // Detect optional AND input track AND not a thumbnail + if (!isRequired() && isInputTrack() && !isThumbnailTrack()) + { + // Grab that 24-pixel vertical slice on the far left that we reserved in resized() + auto bannerArea = getLocalBounds().removeFromLeft(24); + + // Draw the solid orange background + g.setColour(Colours::darkorange); + g.fillRect(bannerArea); + + // Setup text formatting + g.setColour(Colours::white); + g.setFont(12.0f); + + // Save the graphics state before rotating + Graphics::ScopedSaveState state(g); + + // Rotate the graphics context -90 degrees around the center of our banner + float cx = static_cast(bannerArea.getCentreX()); + float cy = static_cast(bannerArea.getCentreY()); + g.addTransform(AffineTransform::rotation(-MathConstants::halfPi, cx, cy)); + + // Create a rotated bounding box for the text + Rectangle textBounds(0, 0, static_cast(bannerArea.getHeight()), static_cast(bannerArea.getWidth())); + textBounds.setCentre(cx, cy); + + // Draw the text inside our rotated box + g.drawText("OPTIONAL INPUT TRACK", textBounds, Justification::centred, false); + } +} + void MediaDisplayComponent::resized() { Rectangle totalBounds = getLocalBounds(); + // Reserve 24 pixels on the left edge for the vertical "Optional Input" banner. + if (!isRequired() && isInputTrack() && !isThumbnailTrack()) + { + totalBounds.removeFromLeft(24); + } + // Remove existing items in main flex mainFlexBox.items.clear(); diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 39b91e58..7e4c9a84 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -79,6 +79,7 @@ class MediaDisplayComponent : public Component, virtual StringArray getInstanceExtensions() = 0; void paint(Graphics& g) override; + void paintOverChildren(Graphics& g) override; virtual void resized() override; void repositionLabels(); From 6e93d7140351d3baa61c5c9ea6425cf9136724f4 Mon Sep 17 00:00:00 2001 From: derekllanes Date: Wed, 29 Apr 2026 03:00:31 -0400 Subject: [PATCH 042/149] Add support for file_track inputs and integrate pyharp updates --- CMakeLists.txt | 1 + pyharp | 2 +- src/Model.h | 23 +++++++++++++ src/media/FileDisplayComponent.cpp | 55 ++++++++++++++++++++++++++++++ src/media/FileDisplayComponent.h | 25 ++++++++++++++ src/utils/Controls.h | 6 ++++ src/widgets/TrackAreaWidget.h | 15 ++++++++ 7 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/media/FileDisplayComponent.cpp create mode 100644 src/media/FileDisplayComponent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b05d238..0d0d0f71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ target_sources(${PROJECT_NAME} src/media/MediaDisplayComponent.cpp src/media/AudioDisplayComponent.cpp src/media/MidiDisplayComponent.cpp + src/media/FileDisplayComponent.cpp src/media/OutputLabelComponent.cpp src/media/pianoroll/KeyboardComponent.cpp diff --git a/pyharp b/pyharp index be3e7540..d8ef2c0e 160000 --- a/pyharp +++ b/pyharp @@ -1 +1 @@ -Subproject commit be3e7540442e3c85fc1c5d2daf4675f8ffa88916 +Subproject commit d8ef2c0ef42bbef6cfa8362dc786e672324e6fb0 diff --git a/src/Model.h b/src/Model.h index eb205086..bb0a2d56 100644 --- a/src/Model.h +++ b/src/Model.h @@ -440,6 +440,18 @@ class Model DBG_AND_LOG("Model::extractInputs: MIDI track input \"" + midiTrack->label + "\" extracted."); } + // Generic File + else if (type == "file_track") + { + std::shared_ptr fileTrack = + std::make_shared(controlsDict); + + newInputs.push_back(fileTrack); + tempComponentIDs.push_back(fileTrack->id); + + DBG_AND_LOG("Model::extractInputs: File track input \"" + fileTrack->label + + "\" extracted."); + } else if (type == "text_box") { std::shared_ptr textControl = @@ -563,6 +575,17 @@ class Model DBG_AND_LOG("Model::extractOutputs: MIDI track output \"" + midiTrack->label + "\" extracted."); } + // Generic File + else if (type == "file_track") + { + std::shared_ptr fileTrack = + std::make_shared(controlsDict); + + newOutputs.push_back(fileTrack); + + DBG_AND_LOG("Model::extractOutputs: File track output \"" + fileTrack->label + + "\" extracted."); + } else if (type == "json") { // Labels are handled separately diff --git a/src/media/FileDisplayComponent.cpp b/src/media/FileDisplayComponent.cpp new file mode 100644 index 00000000..cd87714f --- /dev/null +++ b/src/media/FileDisplayComponent.cpp @@ -0,0 +1,55 @@ +#include "FileDisplayComponent.h" + +FileDisplayComponent::FileDisplayComponent() + : FileDisplayComponent("File Track") +{ +} + +FileDisplayComponent::FileDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) + : MediaDisplayComponent(name, req, fromDAW, mode) +{ +} + +FileDisplayComponent::~FileDisplayComponent() +{ +} + +StringArray FileDisplayComponent::getSupportedExtensions() +{ + return { + ".nam", + ".txt", + ".csv", + ".json", + ".pth", + ".pt", + ".onnx" + }; +} + +StringArray FileDisplayComponent::getInstanceExtensions() +{ + return FileDisplayComponent::getSupportedExtensions(); +} + +double FileDisplayComponent::getTotalLengthInSecs() +{ + return 0.0; +} + +void FileDisplayComponent::loadMediaFile(const URL& filePath) +{ + setTrackName(filePath.getFileName()); + postLoadActions(filePath); + repaint(); +} + +void FileDisplayComponent::resetMedia() +{ + repaint(); +} + +void FileDisplayComponent::postLoadActions(const URL& /*filePath*/) +{ + // No extra action needed for generic files. +} \ No newline at end of file diff --git a/src/media/FileDisplayComponent.h b/src/media/FileDisplayComponent.h new file mode 100644 index 00000000..5bd907fc --- /dev/null +++ b/src/media/FileDisplayComponent.h @@ -0,0 +1,25 @@ +#pragma once + +#include "MediaDisplayComponent.h" + +class FileDisplayComponent : public MediaDisplayComponent +{ +public: + FileDisplayComponent(); + FileDisplayComponent(String name, + bool req = true, + bool fromDAW = false, + DisplayMode mode = DisplayMode::Input); + ~FileDisplayComponent() override; + + static StringArray getSupportedExtensions(); + + StringArray getInstanceExtensions() override; + + double getTotalLengthInSecs() override; + + void loadMediaFile(const URL& filePath) override; + void resetMedia() override; + + void postLoadActions(const URL& filePath) override; +}; \ No newline at end of file diff --git a/src/utils/Controls.h b/src/utils/Controls.h index 051cb9c0..2aa5a665 100644 --- a/src/utils/Controls.h +++ b/src/utils/Controls.h @@ -83,6 +83,12 @@ struct MidiTrackComponentInfo : public TrackComponentInfo using TrackComponentInfo::TrackComponentInfo; }; +// New struct for generic file type +struct FileTrackComponentInfo : public TrackComponentInfo +{ + using TrackComponentInfo::TrackComponentInfo; +}; + struct TextBoxComponentInfo : public ModelComponentInfo, public TextEditor::Listener { std::string value { "" }; diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index f81f7765..4d678a1c 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -11,6 +11,7 @@ #include "../media/AudioDisplayComponent.h" #include "../media/MediaDisplayComponent.h" #include "../media/MidiDisplayComponent.h" +#include "../media/FileDisplayComponent.h" #include "../utils/Controls.h" #include "../utils/Interface.h" @@ -214,6 +215,11 @@ class TrackAreaWidget : public Component, m = std::make_unique( label, midiTrackInfo->required, fromDAW, displayMode); } + else if (auto fileTrackInfo = dynamic_cast(trackInfo)) + { + m = std::make_unique( + label, fileTrackInfo->required, fromDAW, displayMode); + } else { DBG_AND_LOG( @@ -306,6 +312,15 @@ class TrackAreaWidget : public Component, trackInfo = std::move(midiTrackInfo); } + else if (FileDisplayComponent::getSupportedExtensions().contains(ext)) + { + auto fileTrackInfo = std::make_unique(); + + fileTrackInfo->required = false; + fileTrackInfo->label = label.toStdString(); + + trackInfo = std::move(fileTrackInfo); + } else { DBG_AND_LOG("TrackAreaWidget::addTrackFromFilePath: Tried to add file " From 9d7028eebd8de9fc29aa4d98a4f025fb7ccb7323 Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Wed, 29 Apr 2026 11:48:58 -0400 Subject: [PATCH 043/149] First Draft for MultiTabs in HARP --- CMakeLists.txt | 1 + src/MainComponent.cpp | 132 ++++++++++++++++++++++------- src/MainComponent.h | 10 ++- src/ModelTab.h | 2 +- src/widgets/ModelSelectionWidget.h | 2 +- src/windows/WelcomeWindow.h | 14 +-- 6 files changed, 119 insertions(+), 42 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b05d238..3f01d933 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ target_sources(${PROJECT_NAME} src/Main.cpp src/Application.cpp src/MainComponent.cpp + src/ModelTabContainer.h src/ModelTab.h src/Model.h diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index a31a32df..cdd17b63 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -10,12 +10,19 @@ MainComponent::MainComponent() initializeMenuBar(); - mainModelTab.addChangeListener(this); + modelTabs.addChangeListener(this); - addAndMakeVisible(mainModelTab); + addAndMakeVisible(modelTabs); addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); + addAndMakeVisible(addTabButton); + + addTabButton.onClick = [this] + { + modelTabs.createNewTab(); + }; + showStatusArea = Settings::getBoolValue("view.showStatusArea", true); showMediaClipboard = Settings::getBoolValue("view.showMediaClipboard", false); @@ -31,7 +38,7 @@ MainComponent::MainComponent() MainComponent::~MainComponent() { deinitializeMenuBar(); - mainModelTab.removeChangeListener(this); + modelTabs.removeChangeListener(this); } void MainComponent::paint(Graphics& g) @@ -83,6 +90,11 @@ void MainComponent::paintOverChildren(Graphics& g) } } +ModelTab* MainComponent::getCurrentModelTab() const +{ + return modelTabs.getCurrentModelTab(); +} + void MainComponent::resized() { Rectangle fullArea = getLocalBounds(); @@ -92,13 +104,31 @@ void MainComponent::resized() fullArea.removeFromTop(LookAndFeel::getDefaultLookAndFeel().getDefaultMenuBarHeight())); #endif + + FlexBox fullWindow; fullWindow.flexDirection = FlexBox::Direction::row; FlexBox mainPanel; mainPanel.flexDirection = FlexBox::Direction::column; - mainPanel.items.add(FlexItem(mainModelTab).withFlex(1.0)); + mainPanel.items.add(FlexItem(modelTabs).withFlex(1.0)); + + auto bounds = getLocalBounds(); + + // Give full area to tabs + modelTabs.setBounds(bounds); + + // Get tab bar height + int tabBarHeight = modelTabs.getTabBarDepth(); + + // Position "+" button inside tab bar + addTabButton.setBounds( + bounds.getRight() - 35, // right edge + bounds.getY() + 2, // small padding from top + 30, + tabBarHeight - 4 // match tab height nicely + ); if (showStatusArea) { @@ -128,8 +158,13 @@ void MainComponent::resized() } } + + void MainComponent::updateWindowConstraints() { + auto* tab = getCurrentModelTab(); + if (!tab) return; + if (auto* window = findParentComponentOfClass()) { // Compute percentage of total window width given to main panel @@ -138,12 +173,13 @@ void MainComponent::updateWindowConstraints() // Determine minimum width needed to display controls plus padding const int requiredMainPanelWidth = jmax(minimumMainPanelWidth, - mainModelTab.getMinimumRequiredControlWidth() + minimumMainPanelHorPadding); - // Determine current width of main panel - const int mainPanelWidth = jmax(requiredMainPanelWidth, mainModelTab.getWidth()); - // Determine minimum height needed to display all model contents plus status widget + tab->getMinimumRequiredControlWidth() + minimumMainPanelHorPadding); + + const int mainPanelWidth = + jmax(requiredMainPanelWidth, tab->getWidth()); + const int requiredMainPanelHeight = - mainModelTab.getMinimumRequiredHeightForWidth(mainPanelWidth) + tab->getMinimumRequiredHeightForWidth(mainPanelWidth) + (showStatusArea ? statusAreaHeight : 0); // Determine effective minimum width of entire window @@ -407,61 +443,97 @@ void MainComponent::setTutorialExtraHighlights(std::vector> bound void MainComponent::ensureTutorialModelLoaded() { - if (! mainModelTab.isModelLoaded()) - mainModelTab.loadDefaultModel(); +if (auto* tab = getCurrentModelTab()) +{ + if (!tab->isModelLoaded()) + tab->loadDefaultModel(); +} } void MainComponent::resetTutorialAutoLoadedModel() { - if (! mainModelTab.isModelLoaded()) + if (auto* tab = getCurrentModelTab()) +{ + if (!tab->isModelLoaded()) return; - - if (mainModelTab.getLoadedPath() == TutorialConstants::fallbackModelPath) +} + if (auto* tab = getCurrentModelTab()) +{ + if (tab->getLoadedPath() == TutorialConstants::fallbackModelPath) { - mainModelTab.resetState(); + tab->resetState(); } } +} Rectangle MainComponent::getModelSelectBounds() { - auto bounds = mainModelTab.getModelSelectBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getModelSelectBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getControlsBounds() { - auto bounds = mainModelTab.getControlsBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getControlsBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputTrackBounds() { - auto bounds = mainModelTab.getInputTrackBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputTrackBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputFolderBounds() { - auto bounds = mainModelTab.getInputFolderBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputFolderBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputPlayBounds() { - auto bounds = mainModelTab.getInputPlayBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputPlayBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getProcessButtonBounds() { - auto bounds = mainModelTab.getProcessButtonBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getProcessButtonBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getTracksBounds() { - auto bounds = mainModelTab.getTracksBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getTracksBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getClipboardBounds() @@ -607,7 +679,7 @@ void MainComponent::focusCallback() void MainComponent::changeListenerCallback(ChangeBroadcaster* source) { - if (source == &mainModelTab) + if (source == &modelTabs) { updateWindowConstraints(); } diff --git a/src/MainComponent.h b/src/MainComponent.h index b229d7a4..f9a8caf9 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -9,6 +9,7 @@ #include #include "ModelTab.h" +#include "ModelTabContainer.h" #include "clients/Client.h" @@ -74,14 +75,15 @@ class MainComponent : public Component, /* Tutorial */ - ModelTab* getModelTab() { return &mainModelTab; } - void setTutorialActive(bool active); void setTutorialHighlight(Rectangle bounds); void setTutorialExtraHighlights(std::vector> bounds); void ensureTutorialModelLoaded(); void resetTutorialAutoLoadedModel(); + ModelTab* getCurrentModelTab() const; + + // Bounds accessors for tutorial steps (public for WelcomeWindow) Rectangle getModelSelectBounds(); Rectangle getControlsBounds(); @@ -150,7 +152,8 @@ class MainComponent : public Component, bool showStatusArea; bool showMediaClipboard; - ModelTab mainModelTab; + + ModelTabContainer modelTabs; StatusAreaWidget statusAreaWidget; MediaClipboardWidget mediaClipboardWidget; @@ -158,6 +161,7 @@ class MainComponent : public Component, Rectangle tutorialHighlightRect; std::vector> tutorialExtraHighlights; std::unique_ptr welcomeWindow; + juce::TextButton addTabButton { "+" }; SharedResourcePointer sharedTokens; SharedResourcePointer statusMessage; diff --git a/src/ModelTab.h b/src/ModelTab.h index 3c172ede..e4bf0713 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -21,7 +21,7 @@ using namespace juce; -class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster +class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster { public: ModelTab() diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index bba39217..b5ffe518 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -480,7 +480,7 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public } /** - * Create callbacks for and launch the custom path popup. + * Create caollbacks for and launch the custom path popup. */ void openCustomPathPopup(const String& prefillText = "") { diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index ad3d7188..ed905f90 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -54,7 +54,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - mainComponent->getModelTab()->addChangeListener(this); + mainComponent->getCurrentModelTab()->addChangeListener(this); mainComponent->setTutorialActive(true); } @@ -66,7 +66,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent) { - mainComponent->getModelTab()->removeChangeListener(this); + mainComponent->getCurrentModelTab()->removeChangeListener(this); mainComponent->setTutorialActive(false); } } @@ -78,7 +78,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); auto loadedPath = model ? model->getLoadedPath() : String(); autoLoadedByTutorialFallback = (loadedPath == TutorialConstants::fallbackModelPath); @@ -87,7 +87,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } else if (autoLoadedByTutorialFallback && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); auto loadedPath = model ? model->getLoadedPath() : String(); if (loadedPath != TutorialConstants::fallbackModelPath) autoLoadedByTutorialFallback = false; @@ -134,7 +134,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); if (model && model->isLoaded()) { modelName = model->getMetadata().name; @@ -178,7 +178,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // 4. Configure Parameters (Dynamic) if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); if (model && model->isLoaded()) { String controlsStepTitle = "Configure Parameters (Optional)"; @@ -714,7 +714,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (content->currentStep == 1 && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); if (! model || ! model->isLoaded()) { pendingTutorialFallbackLoad = true; From cea85ba370ef9ca07e748860b3b277b80b0ca031 Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Wed, 29 Apr 2026 12:00:38 -0400 Subject: [PATCH 044/149] Add ModelTabContainer class for multi-tab support --- src/ModelTabContainer.h | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/ModelTabContainer.h diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h new file mode 100644 index 00000000..97b7275b --- /dev/null +++ b/src/ModelTabContainer.h @@ -0,0 +1,62 @@ +/** + * @brief Adds tab container to HARP for MultiTabs + * @author JEYuhas + */ +#pragma once + +#include + +#include "Model.h" +#include "ModelTab.h" + +#include "widgets/ControlAreaWidget.h" +#include "widgets/ModelInfoWidget.h" +#include "widgets/ModelSelectionWidget.h" +#include "widgets/TrackAreaWidget.h" + +#include "utils/Errors.h" +#include "utils/Logging.h" +#include "utils/Tutorial.h" + +using namespace juce; + +class ModelTabContainer : public TabbedComponent, + private ChangeListener, + public ChangeBroadcaster +{ +public: + ModelTabContainer() + : TabbedComponent(TabbedButtonBar::TabsAtTop) + { + createNewTab(); // start with one + } + + void createNewTab() + { + int index = getNumTabs() + 1; + + auto* tab = new ModelTab(); + tab->addChangeListener(this); + + addTab("Model " + String(index), + Colours::lightgrey, + tab, + true); + + setCurrentTabIndex(index - 1); + } + + ModelTab* getCurrentModelTab() const + { + return dynamic_cast(getCurrentContentComponent()); + } + +private: + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (dynamic_cast(source)) + { + sendChangeMessage(); // bubble up to MainComponent + } + } +}; From 61de9c815f9a9aeff51b4fdd09c81cc0f1981790 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 30 Apr 2026 12:03:54 -0400 Subject: [PATCH 045/149] Minor updates to pyharp web documentation. --- website/content/pyharp_docs/example.md | 4 +- website/content/pyharp_docs/host.md | 90 +++++++++++++------------ website/content/pyharp_docs/install.md | 2 + website/content/pyharp_docs/overview.md | 2 +- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/website/content/pyharp_docs/example.md b/website/content/pyharp_docs/example.md index a1c212e3..a0246d63 100644 --- a/website/content/pyharp_docs/example.md +++ b/website/content/pyharp_docs/example.md @@ -30,7 +30,7 @@ ests_speech = model(audio_input) # Expected output shape: [1, num_spk, T] Here, `num_spk` is the estimated number of speakers in the recording. The TIGER model sets `num_spk` to two. We are going to take both outputs from the model: ``` -# Select audio of first speaker +# Select audio of each speaker independently output_1 = ests_speech[:, 0, :] # Expected output shape: [1, T] output_2 = ests_speech[:, 1, :] # Expected output shape: [1, T] ``` @@ -205,7 +205,7 @@ demo.launch(share=True, show_error=True) ### Deploying Our App -For now, we'll put `app.py` inside the `TIGER/` directory to avoid further installation steps. Note that while PyHARP's utilities use [Descript-AudioTools](https://github.com/descriptinc/audiotools) under the hood to handle audio loading and saving, you're free to use whichever libraries you want as long as they can read and produce valid audio files. +For now, we'll put `app.py` inside the cloned `TIGER/` directory to avoid further installation steps. Note that while PyHARP's utilities use [Descript-AudioTools](https://github.com/descriptinc/audiotools) under the hood to handle audio loading and saving, you're free to use whichever libraries you want as long as they can read and produce valid audio files. With our application up and running, it's time to link it to HARP. Run: diff --git a/website/content/pyharp_docs/host.md b/website/content/pyharp_docs/host.md index 26a970d7..24e18bee 100644 --- a/website/content/pyharp_docs/host.md +++ b/website/content/pyharp_docs/host.md @@ -1,14 +1,13 @@ -# Hosting PyHARP Apps in the Cloud +# Hosting PyHARP Apps in the Cloud (HuggingFace Spaces) +Automatically generated Gradio endpoints are only available for a maximum of 72 hours. If you'd like to keep an endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely. -Automatically generated Gradio endpoints are only available for 72 hours. If you'd like to keep the endpoint active and share it with other users, you can use [HuggingFace Spaces](https://huggingface.co/docs/hub/spaces-overview) (similar hosting services are also available) to host your PyHARP app indefinitely: +### Gradio Endpoints +This is the most convenient solution for hosting a PyHARP app. If you are a Hugging Face PRO subscriber, you can use [ZeroGPU](https://huggingface.co/docs/hub/spaces-zerogpu) to dynamically allocate GPU resources according to user requests without any additional charges. Non-PRO users can select from CPU environments or paid GPU options. -## Gradio Endpoints -Gradio endpoints are the most convenient solution for a PyHARP application. It will give you access to GPU computation with ZeroGPU hardware if you are a PRO subscriber, at no additional charge. - -1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Gradio as the Space SDK. -2. Choose the Blank template. -3. You can choose ZeroGPU hardware if you are a PRO subscriber and your application requires GPU computation. You can also customize the hardware. -4. Clone the initialized repository locally: +1. Create a new [HuggingFace Space](https://huggingface.co/new-space). +2. Choose Gradio as the SDK along with the blank template. +3. Select the desired hardware option. +4. Create the space and clone the initialized repository locally: ```bash git clone https://huggingface.co/spaces// ``` @@ -18,87 +17,90 @@ git add . git commit -m "initial commit" git push -u origin main ``` -6. Here are the suggested configurations in the related files. +6. Configure the following repository files: - `README.md` - Set __sdk_version__ to __5.28.0__. This is the recommended version, as HARP may not work with the very latest or earlier versions of Gradio. + Set __sdk_version__ to __5.28.0__, the recommended version of `gradio`. HARP may not work with the very latest or earlier versions. - `requirements.txt` - This is the place where you put all the **pip** packages. You should also put in the latest PyHARP: + Place all of the required **pip** packages in this file. It should also include the latest version of `pyharp`: ``` git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 ``` - If you are using Gradio endpoints, you don't have to include the gradio package here. + Note that you do not have to include the `gradio` package in this file. - `packages.txt` - This is there to put **apt-get install** Debian package list, which are required by some models. + Place any necessary **apt-get install** debian packages in this file. Some models may require these. -## Docker Endpoints -PyHARP requires `gradio==5.28.0`, which requires `python>=3.10`. Some previous models are developed with `python=3.9` or a prior version, which may cause issues when upgrading Python or other packages. For example, the `numpy.float` and `numpy.int` deprecation in numpy version `1.24` breaks some old packages. Therefore, we may need to run patch fixes during deployment to modify the affected files in the package. However, this is not supported by the highly-modularized Gradio spaces. +### Docker Endpoints +Some models may have been developed with older versions of Python, and attempting to deploy them would lead to dependency issues. For example, the `numpy.float` and `numpy.int` deprecation in `numpy==1.24` breaks older packages such as `madmom`. Therefore, we may need to patch any corresponding source files during the deployment process. However, this is not supported by the highly-modularized Gradio SDK. -Using Docker endpoints can help you fix this issue. Docker will allow you to customize deployments, making room for potential patches and fixes. However, the ZeroGPU hardware will not be available in Docker spaces, and you will need to pay for GPU usage. +Using Docker endpoints can help circumvent these issues. Docker will allow you to customize the deployment, which makes room for any necessary patches. Note however that ZeroGPU is not available for Docker spaces, meaning you must pay to use GPU resources with this option. -1. Create a new [HuggingFace Space](https://huggingface.co/new-space), and choose Docker as the Space SDK. -2. Choose the Blank template. -3. Clone the initialized repository locally: +1. Create a new [HuggingFace Space](https://huggingface.co/new-space). +2. Choose Docker as the SDK along with the blank template. +3. Select the desired hardware option. +4. Create the space and clone the initialized repository locally: ```bash git clone https://huggingface.co/spaces// ``` -4. Add your files to the repository, commit, then push to the `main` branch: +5. Add your files to the repository, commit, then push to the `main` branch: ```bash git add . git commit -m "initial commit" git push -u origin main ``` -5. Configurations +6. Configure the following repository files: - `README.md` - Set **app_port** to ``. + Set **app_port** to any valid ``. - - `requirements.txt` and `packages.txt` + - `requirements.txt` - Similar in the Gradio endpoint setting, they are for **pip** and **apt-get** packages. You are going to install them through the `Dockerfile` introduced in the next step. - - For `requirements.txt`, you should include: - ``` - gradio==5.28.0 - git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 - ``` + Place all of the required **pip** packages in this file. It should also include the recommended version of `gradio` and the latest version of `pyharp`: + ``` + gradio==5.28.0 + git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0 + ``` - - `Dockerfile` + - `packages.txt` + + Place any necessary **apt-get install** debian packages in this file. Some models may require these. - Here is an example `Dockerfile` configuration that installs general packages and fixes the `madmom` package. + - `Dockerfile` + Installs the required **pip** and **apt-get** packages, and supports manual patching (_e.g._ of `madmom` in the following example): ```Docker FROM python:3.10-slim # Set python version WORKDIR /app COPY packages.txt /app/packages.txt - # System deps for building packages from source + # System dependencies for building packages from source RUN apt-get update RUN xargs apt-get install -y --no-install-recommends < /app/packages.txt RUN rm -rf /var/lib/apt/lists/* COPY requirements.txt /app/requirements.txt - # disable build isolation so Cython installed in the environment is visible at build time + # Disable build isolation so Cython installed in the environment is visible at build time ENV PIP_NO_BUILD_ISOLATION=1 RUN pip install --no-cache-dir -U pip wheel Cython RUN pip install --no-cache-dir setuptools==80.9.0 RUN pip install --no-cache-dir -r /app/requirements.txt RUN pip install --no-cache-dir --no-build-isolation madmom - # madmom patch - COPY patch_madmom.py /app/scripts/patch_madmom.py # A patch fix in the repo + # Patch madmom package + COPY patch_madmom.py /app/scripts/patch_madmom.py # Script to patch madmom source files RUN python /app/scripts/patch_madmom.py RUN python -c "import madmom; print('madmom import OK')" - # Copy the rest of the repo + # Copy remainder of the repo COPY . /app - # HF Spaces routes traffic to $PORT. Gradio should listen on it. + # HF Spaces route traffic to + # Gradio should listen accordingly ENV PORT= # in README.md EXPOSE @@ -107,11 +109,13 @@ git push -u origin main ``` --- -Your PyHARP app will then begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The app deployed using two methods will have the same UI and functionality. - Here are a few tips and best practices when dealing with HuggingFace Spaces: -- Spaces operate based on the files in the `main` branch +- Spaces operate based off of the files in the `main` branch - An [access token](https://huggingface.co/docs/hub/security-tokens) may be required to push commits to HuggingFace Spaces - A `.gitignore` file should be added to maintain repository orderliness (_e.g._, to ignore `src/_outputs`) +- Pin versions for `numpy` (_e.g._, `<2`), `torch` (_e.g._, `==2.2.2`), and `torchaudio` (_e.g._, `==2.2.2`) to avoid unexpected build issues caused by the latest versions of these packages + +For more information, please refer to the offical document from Hugging Face about [Spaces](https://huggingface.co/docs/hub/spaces). -For more information, please refer to the offical Hugging Face document on [Spaces](https://huggingface.co/docs/hub/spaces). +## Accessing Within HARP +PyHARP apps deployed to HuggingFace will begin running at `https://huggingface.co/spaces//`. The shorthand `/` can also be used within HARP to reference the endpoint. The two deployment methods above produce identical UIs and functionality. diff --git a/website/content/pyharp_docs/install.md b/website/content/pyharp_docs/install.md index b888406f..4ff988ed 100644 --- a/website/content/pyharp_docs/install.md +++ b/website/content/pyharp_docs/install.md @@ -4,3 +4,5 @@ PyHARP can be installed using `pip`: - **Clone the PyHarp repository:** `git clone https://github.com/TEAMuP-dev/pyharp` - **Install:** `pip install -e pyharp && cd pyharp` + +Note that PyHARP depends on [Gradio](https://www.gradio.app/). We recommend installing `gradio==5.28.0`, which requires `python>=3.10`. diff --git a/website/content/pyharp_docs/overview.md b/website/content/pyharp_docs/overview.md index 76ee1b03..df900688 100644 --- a/website/content/pyharp_docs/overview.md +++ b/website/content/pyharp_docs/overview.md @@ -6,7 +6,7 @@ We can use a variety of [state-of-the-art models within HARP](/content/usage/mod Under the hood, HARP simply routes data (audio, MIDI, etc.) to Python applications for processing and renders the results in the editor. To make this work, we require (1) that these applications are built using [Gradio](https://www.gradio.app/), and (2) that these applications follow a simple set of specifications defined in the [PyHARP](https://github.com/TEAMuP-dev/pyharp) library. Crucially, PyHARP wraps a number of interactive components from Gradio (e.g. knobs, sliders, text boxes) to allow for rendering application-defined interfaces within the HARP editor. -Within these constraints, developers can build a wide variety of audio and MIDI-processing applications. For instance, HARP is compatible with any Python deep learning framework because it only requires that applications consume and produce audio files, MIDI files, or structrued labels -- it places no limits on how these objects are created or modified. +Within these constraints, developers can build a wide variety of audio and MIDI-processing applications. For instance, HARP is compatible with any Python deep learning framework because it only requires that applications consume and produce audio files, MIDI files, or structured labels -- it places no limits on how these objects are created or modified. ### Host Your Endpoint Locally or in the Cloud From 2e3e81fd942fded7ad9a713be5a4be899d82fb1a Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 1 May 2026 12:07:52 -0500 Subject: [PATCH 046/149] review changes final --- src/Model.h | 59 ++-- src/ModelTab.h | 342 +++++++++++++++----- src/clients/GradioClient.h | 73 ++--- src/utils/Errors.h | 26 +- src/widgets/StatusAreaWidget.h | 10 +- src/windows/settings/GeneralSettingsTab.cpp | 6 + 6 files changed, 348 insertions(+), 168 deletions(-) diff --git a/src/Model.h b/src/Model.h index eb205086..e5362bbd 100644 --- a/src/Model.h +++ b/src/Model.h @@ -1,7 +1,7 @@ /** * @file Model.h * @brief Model state and interface for loading and processing. - * @author hugofloresgarcia, aldo-aguilar, xribene, cwitkowitz + * @author hugofloresgarcia, aldo-aguilar, xribene, cwitkowitz, saumya-pailwan */ #pragma once @@ -85,19 +85,25 @@ struct ModelMetadata class Model { public: - Model() { resetState(); } - ~Model() { resetState(); } + Model() { resetState(/*suppressStatus=*/true); } + ~Model() { resetState(/*suppressStatus=*/true); } bool isEmpty() { return loadedPath.isEmpty() || client == nullptr; } bool isLoaded() { return ! isEmpty(); } - void setStatus(ModelStatus newStatus) + void setStatus(ModelStatus newStatus, const String& pathContext = "") { status = newStatus; if (statusMessage != nullptr) { - statusMessage->setMessage("Model Status: " + enumToString(status)); + String msg = enumToString(status); + + String shortPath = pathContext.fromLastOccurrenceOf("/", false, false).trim(); + if (shortPath.isNotEmpty()) + msg += " | " + shortPath; + + statusMessage->setMessage(msg); } } @@ -109,7 +115,7 @@ class Model ModelComponentInfoList getInputTracks() { return inputTrackComponents; } ModelComponentInfoList getOutputTracks() { return outputTrackComponents; } - void resetState() + void resetState(bool suppressStatus = false) { metadata = ModelMetadata {}; @@ -124,7 +130,8 @@ class Model loadedPath.clear(); openablePath.clear(); - setStatus(ModelStatus::EMPTY); + if (! suppressStatus) + setStatus(ModelStatus::EMPTY); } OpResult load(String pathToLoad) @@ -137,12 +144,12 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, pathToLoad); return result; } - setStatus(ModelStatus::QUERYING_CONTROLS); + setStatus(ModelStatus::QUERYING_CONTROLS, pathToLoad); // Initialize empty dictionary to hold query response DynamicObject::Ptr controls; @@ -152,7 +159,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, pathToLoad); return result; } @@ -164,7 +171,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, pathToLoad); return result; } @@ -177,7 +184,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, pathToLoad); return result; } @@ -189,28 +196,26 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, pathToLoad); return result; } - resetState(); + resetState(/*suppressStatus=*/true); - // Update model information if all loading operations are successful metadata = newMetadata; controlComponents = newControls; inputTrackComponents = newInputs; outputTrackComponents = newOutputs; - // Register extracted component IDs orderedInputComponentIDs = std::move(tempComponentIDs); - // Replace existing client + client = std::move(tempClient); - // Keep track of successfully loaded path + loadedPath = pathToLoad; openablePath = client->inferDocumentationPath(loadedPath); - setStatus(ModelStatus::READY); + setStatus(ModelStatus::READY, loadedPath); return OpResult::ok(); } @@ -222,7 +227,7 @@ class Model OpResult result = OpResult::ok(); - setStatus(ModelStatus::PREPARING_REQUEST); + setStatus(ModelStatus::PREPARING_REQUEST, loadedPath); for (auto& fileEntry : inputFiles) { @@ -235,7 +240,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, loadedPath); return result; } @@ -331,36 +336,36 @@ class Model DBG_AND_LOG("Model::process: Payload \"" + payloadJSON + "\" prepared for processing."); - setStatus(ModelStatus::PROCESSING); + setStatus(ModelStatus::PROCESSING, loadedPath); result = client->process(loadedPath, payloadJSON, outputFiles, labels); if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, loadedPath); return result; } - setStatus(ModelStatus::READY); + setStatus(ModelStatus::READY, loadedPath); return result; } OpResult cancel() { - setStatus(ModelStatus::CANCELING); + setStatus(ModelStatus::CANCELING, loadedPath); OpResult result = client->cancel(loadedPath); if (result.failed()) { - setStatus(ModelStatus::FAILURE); + setStatus(ModelStatus::FAILURE, loadedPath); return result; } - setStatus(ModelStatus::READY); + setStatus(ModelStatus::READY, loadedPath); return OpResult::ok(); } diff --git a/src/ModelTab.h b/src/ModelTab.h index 3331270b..7bb31855 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -1,7 +1,7 @@ /** * @file ModelTab.h * @brief Reusable component containing HARP GUI elements and state for a single model. - * @author hugofloresgarcia, xribene, cwitkowitz + * @author hugofloresgarcia, xribene, cwitkowitz, saumya-pailwan */ #pragma once @@ -191,6 +191,23 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas totalTracks); tabArea.performLayout(getLocalBounds()); + + // Re-centre the error popup when the parent is resized so it never + // drifts off-screen or clips against the new bounds. + if (errorPopupWindow != nullptr) + { + Component* topLevel = getTopLevelComponent(); + int windowWidth = (topLevel != nullptr) ? topLevel->getWidth() : getWidth(); + int popupWidth = jmin(520, windowWidth - 24); + int popupHeight = errorPopupWindow->getHeight(); + Point windowCentreScreen = + (topLevel != nullptr) + ? topLevel->localPointToGlobal(topLevel->getLocalBounds().getCentre()) + : localPointToGlobal(getLocalBounds().getCentre()); + Point centreInLocal = getLocalPoint(nullptr, windowCentreScreen); + errorPopupWindow->setBounds( + Rectangle(popupWidth, popupHeight).withCentre(centreInLocal)); + } } int getMinimumRequiredControlWidth() { return controlAreaWidget.getMinimumRequiredWidth(); } @@ -333,8 +350,16 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas constexpr int buttonSendReport = 3; constexpr int buttonOk = 0; - String popupMessage = errorMessage - + "\n\nOptional: add context below before opening the issue."; + // Determine whether this error warrants a GitHub bug report. + // Quota errors, invalid paths, and expected HTTP failures are user-actionable + // and don't need a report; only unexpected runtime/parse errors do. + bool isReportableError = false; + if (const auto* gradioErr = std::get_if(&error)) + isReportableError = (gradioErr->type == GradioError::Type::RuntimeError); + else if (std::get_if(&error) || std::get_if(&error)) + isReportableError = true; + + String popupMessage = errorMessage; if (errorPopupWindow != nullptr) { @@ -342,10 +367,14 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas errorPopupWindow.reset(); } - errorPopupWindow = std::make_unique("Error", popupMessage, AlertWindow::WarningIcon); + errorPopupWindow = std::make_unique( + "Error", popupMessage, AlertWindow::WarningIcon); + centredAlertLF.messageText = popupMessage; + errorPopupWindow->setLookAndFeel(¢redAlertLF); AlertWindow* alertWindow = errorPopupWindow.get(); - auto bindButtonCallback = [alertWindow](const String& buttonText, std::function callback) + auto bindButtonCallback = + [alertWindow](const String& buttonText, std::function callback) { for (int i = 0; i < alertWindow->getNumChildComponents(); ++i) { @@ -363,87 +392,100 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas if (openablePath.has_value()) { alertWindow->addButton("Open URL", buttonOpenURL); - bindButtonCallback("Open URL", [this, openablePath, onExit] - { - URL(*openablePath).launchInDefaultBrowser(); - - if (onExit) - { - onExit(); - } - - if (errorPopupWindow != nullptr) - { - removeChildComponent(errorPopupWindow.get()); - errorPopupWindow.reset(); - } - }); + bindButtonCallback("Open URL", + [this, openablePath, onExit] + { + URL(*openablePath).launchInDefaultBrowser(); + + if (onExit) + { + onExit(); + } + + if (errorPopupWindow != nullptr) + { + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); + } + }); } alertWindow->addButton("Open Logs", buttonOpenLogs); - bindButtonCallback("Open Logs", [this, onExit] + bindButtonCallback("Open Logs", + [this, onExit] + { + HARPLogger::getInstance()->getLogFile().revealToUser(); + + if (onExit) + { + onExit(); + } + + if (errorPopupWindow != nullptr) + { + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); + } + }); + + if (isReportableError) { - HARPLogger::getInstance()->getLogFile().revealToUser(); - - if (onExit) - { - onExit(); - } - - if (errorPopupWindow != nullptr) - { - removeChildComponent(errorPopupWindow.get()); - errorPopupWindow.reset(); - } - }); - - alertWindow->addButton("Open GitHub Issue", buttonSendReport); - bindButtonCallback("Open GitHub Issue", [this, error, errorMessage, onExit] - { - if (errorPopupWindow != nullptr) - { - openGitHubIssue( - error, errorMessage, errorPopupWindow->getTextEditorContents("errorReportNotes")); - } - - if (onExit) - { - onExit(); - } - - if (errorPopupWindow != nullptr) - { - removeChildComponent(errorPopupWindow.get()); - errorPopupWindow.reset(); - } - }); + alertWindow->addButton("Report", buttonSendReport); + bindButtonCallback("Report", + [this, error, errorMessage, onExit] + { + // Opens GitHub new-issue page pre-filled with error details. + openGitHubIssue(error, errorMessage, ""); + + if (onExit) + { + onExit(); + } + + if (errorPopupWindow != nullptr) + { + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); + } + }); + } alertWindow->addButton("Ok", buttonOk); - bindButtonCallback("Ok", [this, onExit] - { - if (onExit) - { - onExit(); - } - - if (errorPopupWindow != nullptr) - { - removeChildComponent(errorPopupWindow.get()); - errorPopupWindow.reset(); - } - }); - - alertWindow->addTextEditor( - "errorReportNotes", - "", - "What were you doing when this happened? (optional)"); + bindButtonCallback("Ok", + [this, onExit] + { + if (onExit) + { + onExit(); + } + + if (errorPopupWindow != nullptr) + { + removeChildComponent(errorPopupWindow.get()); + errorPopupWindow.reset(); + } + }); addAndMakeVisible(*errorPopupWindow); errorPopupWindow->setAlwaysOnTop(true); - int popupWidth = jmax(560, jmin(getWidth() - 24, 1100)); - int popupHeight = 280; - errorPopupWindow->setBounds(getLocalBounds().withSizeKeepingCentre(popupWidth, popupHeight)); + // Size based on full window width so the popup is never squashed when + // the media clipboard panel is open and ModelTab is narrow. + Component* topLevel = getTopLevelComponent(); + int windowWidth = (topLevel != nullptr) ? topLevel->getWidth() : getWidth(); + int popupWidth = jmin(520, windowWidth - 24); + int popupHeight = isReportableError ? 260 : 230; + + // Find the window's centre in screen space, then convert to ModelTab's + // local coordinate space so the popup is centred in the full window + // regardless of where ModelTab sits within it. + Point windowCentreScreen = + (topLevel != nullptr) + ? topLevel->localPointToGlobal(topLevel->getLocalBounds().getCentre()) + : localPointToGlobal(getLocalBounds().getCentre()); + Point centreInLocal = getLocalPoint(nullptr, windowCentreScreen); + errorPopupWindow->setBounds( + Rectangle(popupWidth, popupHeight).withCentre(centreInLocal)); errorPopupWindow->toFront(true); } @@ -494,8 +536,8 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas String escapedTitle = URL::addEscapeChars(issueTitle, true); String escapedBody = URL::addEscapeChars(body, true); String escapedTemplate = URL::addEscapeChars(issueTemplate, true); - URL(issueBaseUrl + "?template=" + escapedTemplate + "&title=" + escapedTitle + "&body=" - + escapedBody) + URL(issueBaseUrl + "?template=" + escapedTemplate + "&title=" + escapedTitle + + "&body=" + escapedBody) .launchInDefaultBrowser(); } @@ -680,6 +722,147 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas inputTrackAreaWidget.setLoadTrackEnabled(true); } + static constexpr int popupButtonBottomPadding = 24; + + /* LookAndFeel override that draws the AlertWindow message text centred. + We re-derive both from the ACTUAL window height here so that + text fills the real available space and buttons are accounted for at the + bottom where BottomButtonAlertWindow will move them. */ + + struct CentredAlertLookAndFeel : public LookAndFeel_V4 + { + String messageText; // set before showing the popup + + void drawAlertBox(Graphics& g, + AlertWindow& alert, + const Rectangle& /*textArea*/, + TextLayout& /*unused*/) override + { + // Background + auto cornerSize = 4.0f; + g.setColour(alert.findColour(AlertWindow::outlineColourId)); + g.drawRoundedRectangle(alert.getLocalBounds().toFloat(), cornerSize, 2.0f); + + auto bounds = alert.getLocalBounds().reduced(1); + g.reduceClipRegion(bounds); + + g.setColour(alert.findColour(AlertWindow::backgroundColourId)); + g.fillRoundedRectangle(bounds.toFloat(), cornerSize); + + // Icon + auto iconSpaceUsed = 0; + const auto iconWidth = 80; + auto iconSize = jmin(iconWidth + 50, bounds.getHeight() + 20); + + if (alert.containsAnyExtraComponents() || alert.getNumButtons() > 2) + iconSize = jmin(iconSize, 200); + + Rectangle iconRect(iconSize / -10, iconSize / -10, iconSize, iconSize); + + if (alert.getAlertType() != MessageBoxIconType::NoIcon) + { + Path icon; + char character; + uint32 colour; + + if (alert.getAlertType() == MessageBoxIconType::WarningIcon) + { + character = '!'; + icon.addTriangle((float) iconRect.getX() + (float) iconRect.getWidth() * 0.5f, + (float) iconRect.getY(), + (float) iconRect.getRight(), + (float) iconRect.getBottom(), + (float) iconRect.getX(), + (float) iconRect.getBottom()); + icon = icon.createPathWithRoundedCorners(5.0f); + colour = 0x66ff2a00; + } + else + { + colour = Colour(0xff00b0b9).withAlpha(0.4f).getARGB(); + character = alert.getAlertType() == MessageBoxIconType::InfoIcon ? 'i' : '?'; + icon.addEllipse(iconRect.toFloat()); + } + + GlyphArrangement ga; + ga.addFittedText({ (float) iconRect.getHeight() * 0.9f, Font::bold }, + String::charToString((juce_wchar) (uint8) character), + (float) iconRect.getX(), + (float) iconRect.getY(), + (float) iconRect.getWidth(), + (float) iconRect.getHeight(), + Justification::centred, + false); + ga.createPath(icon); + icon.setUsingNonZeroWinding(false); + g.setColour(Colour(colour)); + g.fillPath(icon); + + iconSpaceUsed = iconWidth; + } + + if (messageText.isNotEmpty()) + { + const int buttonH = getAlertWindowButtonHeight(); + const int titleH = 24; + const int edgeGap = 10; + const int bottomOfText = + alert.getHeight() - buttonH - popupButtonBottomPadding - 10; + + const int rightPadding = edgeGap + iconSpaceUsed; + Rectangle fullTextArea( + (float) (edgeGap + iconSpaceUsed), + (float) (edgeGap + titleH), + (float) (alert.getWidth() - edgeGap - iconSpaceUsed - rightPadding), + (float) (bottomOfText - (edgeGap + titleH))); + + Font msgFont(16.0f); + + AttributedString attrStr; + attrStr.setJustification(Justification::topLeft); + attrStr.append(messageText, msgFont, alert.findColour(AlertWindow::textColourId)); + + TextLayout layout; + layout.createLayout(attrStr, fullTextArea.getWidth()); + layout.draw(g, fullTextArea); + } + } + }; + + class BottomButtonAlertWindow : public AlertWindow + { + public: + BottomButtonAlertWindow(const String& title, + const String& message, + MessageBoxIconType iconType) + : AlertWindow(title, message, iconType) + { + } + + void resized() override + { + const int buttonH = getLookAndFeel().getAlertWindowButtonHeight(); + const int targetY = getHeight() - popupButtonBottomPadding - buttonH; + const int spacer = 16; + + Array btns; + for (int i = 0; i < getNumChildComponents(); ++i) + if (auto* btn = dynamic_cast(getChildComponent(i))) + btns.add(btn); + + int totalWidth = -spacer; + for (auto* btn : btns) + totalWidth += btn->getWidth() + spacer; + + int x = (getWidth() - totalWidth) / 2; + for (auto* btn : btns) + { + btn->setTopLeftPosition(x, targetY); + x += btn->getWidth() + spacer; + } + } + }; + static constexpr float marginSize = 2; static constexpr int modelSelectionRowHeight = 30; @@ -709,5 +892,6 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas ThreadPool processingThreadPool { 10 }; std::atomic currentProcessID { 0 }; - std::unique_ptr errorPopupWindow; + CentredAlertLookAndFeel centredAlertLF; + std::unique_ptr errorPopupWindow; }; diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index 25f6cc27..bf171c31 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -1,7 +1,7 @@ /** * @file GradioClient.h * @brief Client specifics for Gradio and Hugging Face (simple JSON requests). - * @author xribene, huiranyu, cwitkowitz + * @author xribene, huiranyu, cwitkowitz, saumya-pailwan */ #pragma once @@ -623,6 +623,12 @@ class GradioClient : public Client response = stream->readEntireStreamAsString(); + if (isHTMLResponse(response)) + { + return OpResult::fail(HttpError { + HttpError::Type::BadStatusCode, HttpError::Request::POST, errorPath, 503 }); + } + if (statusCode != 200) { String diagnosticText = extractErrorTextFromPayload(response); @@ -747,6 +753,12 @@ class GradioClient : public Client return normalizedPayload; } + static bool isHTMLResponse(const String& text) + { + String trimmed = text.trimStart(); + return trimmed.startsWithIgnoreCase("\"'`\\/|", ""); - - for (auto& token : tokens) - { - String trimmedToken = token.trim(); - - if (trimmedToken.length() > 5 && trimmedToken.endsWithIgnoreCase("Error") - && ! trimmedToken.equalsIgnoreCase("error")) - { - return trimmedToken; - } - } - - return ""; - } - static String extractShortReason(const String& text) { String firstLine = text.upToFirstOccurrenceOf("\n", false, false).trim(); @@ -1022,6 +992,8 @@ class GradioClient : public Client // If an error arrives before process_starts, it was likely rejected at // the quota-check stage rather than failing inside the function body. bool seenProcessStarts = false; + bool seenAnyDataEvent = false; + bool checkedFirstLine = false; while (! stream->isExhausted()) { @@ -1036,6 +1008,17 @@ class GradioClient : public Client continue; } + if (! checkedFirstLine) + { + checkedFirstLine = true; + + if (isHTMLResponse(eventLine)) + { + return OpResult::fail(HttpError { + HttpError::Type::BadStatusCode, HttpError::Request::GET, errorPath, 503 }); + } + } + if (isEventLine(eventLine, GradioEvents::Complete)) { response = extractPayload(stream->readNextLine()); @@ -1062,7 +1045,7 @@ class GradioClient : public Client { errorType = GradioError::Type::QuotaExceeded; } - else if (diagnosticText.isEmpty() && ! seenProcessStarts) + else if (diagnosticText.isEmpty() && ! seenProcessStarts && ! seenAnyDataEvent) { errorType = GradioError::Type::QuotaExceeded; } @@ -1080,6 +1063,8 @@ class GradioClient : public Client } else if (eventLine.startsWithIgnoreCase("data:")) { + seenAnyDataEvent = true; + String dataPayload = extractPayload(eventLine); if (extractMessageTypeFromPayload(dataPayload).equalsIgnoreCase("process_starts")) { diff --git a/src/utils/Errors.h b/src/utils/Errors.h index 41c8cee4..095b94ab 100644 --- a/src/utils/Errors.h +++ b/src/utils/Errors.h @@ -168,9 +168,9 @@ inline String toUserMessage(const HttpError& e) if (e.request == HttpError::Request::POST) { - userMessage += " This may be a temporary connection issue. " - "Click on Open URL to get the space restarted. " - "If it persists, open the Space and check its logs/status."; + userMessage += + "\n\nThe Space may be sleeping or temporarily unavailable." + "\n\nTry loading again in a few seconds or Click 'Open URL' to open and check it on Huggingface."; } return userMessage; @@ -209,14 +209,7 @@ inline String toUserMessage(const HttpError& e) { } - userMessage += " request to endpoint "; - - if (e.endpointPath.isNotEmpty()) - { - userMessage += "\"" + e.endpointPath + "\" "; - } - - userMessage += "failed"; + userMessage += " request failed"; if (e.statusCode != 0) { @@ -227,8 +220,9 @@ inline String toUserMessage(const HttpError& e) if (e.statusCode == 503) { - userMessage += " If this is a valid Hugging Face space, this could indicate " - "the space is sleeping, paused, or down due to a build/runtime error."; + userMessage += + "\n\nThe Hugging Face Space may be sleeping or paused." + "\n\nTry loading again in a few seconds or Click 'Open URL' to open and check it on Huggingface."; } } @@ -257,14 +251,14 @@ inline String toUserMessage(const GradioError& e) { case GradioError::Type::RuntimeError: - userMessage = "The Hugging Face Space reported a runtime error"; + userMessage = "The Hugging Face Space reported a runtime error."; if (e.reason.isNotEmpty()) { - userMessage += " (" + e.reason + ")"; + userMessage += "\n\nDetails: " + e.reason; } - userMessage += ". Please open the Space logs for details."; + userMessage += "\n\nPlease open the Space logs for more information."; return userMessage; diff --git a/src/widgets/StatusAreaWidget.h b/src/widgets/StatusAreaWidget.h index bea6891c..8133a43b 100644 --- a/src/widgets/StatusAreaWidget.h +++ b/src/widgets/StatusAreaWidget.h @@ -63,8 +63,12 @@ class StatusHistoryBox : public Component, ChangeListener historyEditor.setScrollbarsShown(true); historyEditor.setCaretVisible(false); historyEditor.setPopupMenuEnabled(false); - historyEditor.setColour(TextEditor::backgroundColourId, Colour(0x33, 0x33, 0x33)); + // Transparent background so the parent's paint() fill shows through, + // giving the same appearance as the InstructionsBox (which uses a plain Label). + historyEditor.setColour(TextEditor::backgroundColourId, Colours::transparentBlack); historyEditor.setColour(TextEditor::textColourId, Colour(0xE0, 0xE0, 0xE0)); + historyEditor.setColour(TextEditor::outlineColourId, Colours::transparentBlack); + historyEditor.setColour(TextEditor::focusedOutlineColourId, Colours::transparentBlack); addAndMakeVisible(historyEditor); @@ -82,6 +86,8 @@ class StatusHistoryBox : public Component, ChangeListener void paint(Graphics& g) override { + g.setColour(Colour(0x33, 0x33, 0x33)); + g.fillAll(); g.setColour(Colour(0x44, 0x44, 0x44)); g.drawRect(getLocalBounds(), 1); } @@ -141,8 +147,8 @@ class StatusAreaWidget : public Component FlexBox statusArea; statusArea.flexDirection = FlexBox::Direction::row; - statusArea.items.add(FlexItem(instructionsBox).withFlex(1).withMargin(marginSize)); statusArea.items.add(FlexItem(statusHistoryBox).withFlex(1).withMargin(marginSize)); + statusArea.items.add(FlexItem(instructionsBox).withFlex(1).withMargin(marginSize)); statusArea.performLayout(getLocalBounds()); } diff --git a/src/windows/settings/GeneralSettingsTab.cpp b/src/windows/settings/GeneralSettingsTab.cpp index 54270a07..55a53915 100644 --- a/src/windows/settings/GeneralSettingsTab.cpp +++ b/src/windows/settings/GeneralSettingsTab.cpp @@ -76,4 +76,10 @@ void GeneralSettingsTab::handleRestoreDefaults() if (onRestoreDefaults) onRestoreDefaults(); + + AlertWindow::showMessageBoxAsync( + AlertWindow::InfoIcon, + "Settings Restored", + "All settings (except API tokens) have been restored to their defaults.", + "Ok"); } From ae1cc85919d60f382dd4a401f957f65f29c652c6 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 1 May 2026 19:47:15 -0500 Subject: [PATCH 047/149] keep popup open until ok --- src/ModelTab.h | 47 ++++------------------------------------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/src/ModelTab.h b/src/ModelTab.h index 7bb31855..22d14d92 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -393,60 +393,21 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas { alertWindow->addButton("Open URL", buttonOpenURL); bindButtonCallback("Open URL", - [this, openablePath, onExit] - { - URL(*openablePath).launchInDefaultBrowser(); - - if (onExit) - { - onExit(); - } - - if (errorPopupWindow != nullptr) - { - removeChildComponent(errorPopupWindow.get()); - errorPopupWindow.reset(); - } - }); + [openablePath] { URL(*openablePath).launchInDefaultBrowser(); }); } alertWindow->addButton("Open Logs", buttonOpenLogs); bindButtonCallback("Open Logs", - [this, onExit] - { - HARPLogger::getInstance()->getLogFile().revealToUser(); - - if (onExit) - { - onExit(); - } - - if (errorPopupWindow != nullptr) - { - removeChildComponent(errorPopupWindow.get()); - errorPopupWindow.reset(); - } - }); + [] { HARPLogger::getInstance()->getLogFile().revealToUser(); }); if (isReportableError) { alertWindow->addButton("Report", buttonSendReport); bindButtonCallback("Report", - [this, error, errorMessage, onExit] + [this, error, errorMessage] { - // Opens GitHub new-issue page pre-filled with error details. + // Open GitHub issue but keep the popup open openGitHubIssue(error, errorMessage, ""); - - if (onExit) - { - onExit(); - } - - if (errorPopupWindow != nullptr) - { - removeChildComponent(errorPopupWindow.get()); - errorPopupWindow.reset(); - } }); } From 8f6649758d52d3abe3f982e9d3c4e053c5b72206 Mon Sep 17 00:00:00 2001 From: derekllanes Date: Fri, 8 May 2026 13:46:51 -0400 Subject: [PATCH 048/149] Refine optional input banner text and color --- src/media/MediaDisplayComponent.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index b42b38ff..5444deef 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -323,8 +323,8 @@ void MediaDisplayComponent::paintOverChildren(Graphics& g) // Grab that 24-pixel vertical slice on the far left that we reserved in resized() auto bannerArea = getLocalBounds().removeFromLeft(24); - // Draw the solid orange background - g.setColour(Colours::darkorange); + // Draw the background color + g.setColour(Colour::fromRGB(90, 105, 105)); g.fillRect(bannerArea); // Setup text formatting @@ -344,7 +344,7 @@ void MediaDisplayComponent::paintOverChildren(Graphics& g) textBounds.setCentre(cx, cy); // Draw the text inside our rotated box - g.drawText("OPTIONAL INPUT TRACK", textBounds, Justification::centred, false); + g.drawText("OPTIONAL", textBounds, Justification::centred, false); } } @@ -352,7 +352,7 @@ void MediaDisplayComponent::resized() { Rectangle totalBounds = getLocalBounds(); - // Reserve 24 pixels on the left edge for the vertical "Optional Input" banner. + // Reserve 24 pixels on the left edge for the vertical "Optional" banner. if (!isRequired() && isInputTrack() && !isThumbnailTrack()) { totalBounds.removeFromLeft(24); From edf8000f63a0de091583215cee6a178bb94e6afd Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 8 May 2026 15:05:51 -0400 Subject: [PATCH 049/149] Implemented media clipboard drag to resize. Main window doesn't have a minimum width so far, but it's recoverable if made too small and scales relatively well. --- src/MainComponent.cpp | 21 ++++++---- src/MainComponent.h | 3 +- src/widgets/MediaClipboardWidget.h | 65 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index 46bf43b9..df45d20d 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -15,6 +15,11 @@ MainComponent::MainComponent() addAndMakeVisible(mainModelTab); addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); + mediaClipboardWidget.onResize = [this](int newWidth) + { + clipboardWidth = newWidth; + resized(); + }; addAndMakeVisible(dragOverlay); showStatusArea = Settings::getBoolValue("view.showStatusArea", true); @@ -116,7 +121,7 @@ void MainComponent::resized() if (showMediaClipboard) { - fullWindow.items.add(FlexItem(mediaClipboardWidget).withFlex(mediaClipboardFlex)); + fullWindow.items.add(FlexItem(mediaClipboardWidget).withWidth(clipboardWidth)); } else { @@ -138,8 +143,9 @@ void MainComponent::updateWindowConstraints() if (auto* window = findParentComponentOfClass()) { // Compute percentage of total window width given to main panel - const float mainPanelRatio = showMediaClipboard ? (1.0f / mediaClipboardScale) : 1.0f; - + const float mainPanelRatio = showMediaClipboard + ? (float(getWidth() - clipboardWidth) / float(jmax(1, getWidth()))) + : 1.0f; // Determine minimum width needed to display controls plus padding const int requiredMainPanelWidth = jmax(minimumMainPanelWidth, @@ -313,21 +319,20 @@ void MainComponent::viewMediaClipboardCallback() if (showMediaClipboard) { - // Scale bounds to extend window by 40% of main width + clipboardWidth = mediaClipboardWidget.getDefaultWidth(); windowBounds.setWidth( - jmin(currentDisplayWidth, - static_cast(mediaClipboardScale * windowBounds.getWidth()))); + jmin(currentDisplayWidth, windowBounds.getWidth() + clipboardWidth)); } else { if (! window->isFullScreen()) { - // Scale bounds to reduce window to main width windowBounds.setWidth( - static_cast(windowBounds.getWidth() / mediaClipboardScale)); + jmax(minimumWindowWidth, windowBounds.getWidth() - clipboardWidth)); } } + // Set extended or reduced bounds window->setBounds(windowBounds); } diff --git a/src/MainComponent.h b/src/MainComponent.h index 353ec27f..2851dc78 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -131,8 +131,7 @@ class MainComponent : public Component, /* Interface */ const int statusAreaHeight = 100; - const float mediaClipboardFlex = 0.4f; - const float mediaClipboardScale = 1.4f; + int clipboardWidth = 250; // Minimum size to ensure all controls remain visible and functional: // - WelcomeWindow popup is 480x500, needs padding diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 19f3fcf8..1baeea88 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -16,6 +16,34 @@ using namespace juce; +class MediaClipboardWidget; + +class ResizeEdgeComponent : public Component +{ +public: + ResizeEdgeComponent(MediaClipboardWidget* ownerIn) : owner(ownerIn) {} + + void mouseMove(const MouseEvent&) override + { + setMouseCursor(MouseCursor::LeftRightResizeCursor); + } + + void mouseDown(const MouseEvent& e) override; + + void mouseDrag(const MouseEvent& e) override; + + void mouseUp(const MouseEvent&) override + { + dragStartX = -1; + dragStartWidth = -1; + } + +private: + MediaClipboardWidget* owner = nullptr; + int dragStartX = -1; + int dragStartWidth = -1; +}; + class MediaClipboardWidget : public Component, public ChangeListener { public: @@ -34,6 +62,7 @@ class MediaClipboardWidget : public Component, public ChangeListener trackAreaWidget.addChangeListener(this); trackArea.setViewedComponent(&trackAreaWidget, false); addAndMakeVisible(trackArea); + addAndMakeVisible(resizeEdge); } ~MediaClipboardWidget() { trackAreaWidget.removeChangeListener(this); } @@ -42,6 +71,8 @@ class MediaClipboardWidget : public Component, public ChangeListener void resized() override { + resizeEdge.setBounds(0, 0, resizeEdgeWidth, getHeight()); + Rectangle totalBounds = getLocalBounds(); // Flex for whole media clipboard @@ -170,6 +201,10 @@ class MediaClipboardWidget : public Component, public ChangeListener return getLocalArea(&buttonsComponent, sendToDAWButton.getBounds()).expanded(2); } + std::function onResize; + + int getDefaultWidth() const { return defaultWidth; } + void addFileCallback() { StringArray validExtensions = MediaDisplayComponent::getSupportedExtensions(); @@ -705,8 +740,13 @@ class MediaClipboardWidget : public Component, public ChangeListener currentlySelectedDisplay = mediaDisplay; } + friend class ResizeEdgeComponent; + const float marginSize = 2; const float buttonWidth = 26; + const int resizeEdgeWidth = 6; + const int defaultWidth = 250; + const int minimumWidth = 150; // Main controls component Component controlsComponent; @@ -714,6 +754,8 @@ class MediaClipboardWidget : public Component, public ChangeListener TextEditor selectionTextBox; // Buttons area subcomponent Component buttonsComponent; + // Draggable edge component + ResizeEdgeComponent resizeEdge { this }; // Button components /*MultiButton renameSelectionButton; @@ -751,3 +793,26 @@ class MediaClipboardWidget : public Component, public ChangeListener MediaDisplayComponent* currentlySelectedDisplay; }; + +inline void ResizeEdgeComponent::mouseDown(const MouseEvent& e) +{ + dragStartX = e.getScreenX(); + dragStartWidth = owner->getWidth(); +} + +inline void ResizeEdgeComponent::mouseDrag(const MouseEvent& e) +{ + if (dragStartX < 0) + { + return; + } + // Dragging left (negative delta) widens, + // Dragging right (positive delta) narrows. + int delta = e.getScreenX() - dragStartX; + int newWidth = jmax(owner->minimumWidth, dragStartWidth - delta); + + if (owner->onResize) + { + owner->onResize(newWidth); + } +} \ No newline at end of file From cc058b45ab0ea22262259267beaf8746a13c6f0d Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Thu, 14 May 2026 22:43:20 -0400 Subject: [PATCH 050/149] add debug messages to mouse actions --- src/widgets/TrackAreaWidget.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index e15f3c05..28d2b60b 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -615,10 +615,12 @@ class TrackAreaWidget : public Component, if (!isThumbnailWidget() || draggedTrack == nullptr) return; Point posInThis = e.getEventRelativeTo(this).getPosition(); + DBG_AND_LOG("mouseDrag called, pos: " << posInThis.x << ", " << posInThis.y); if (!isDraggingTrack && e.getDistanceFromDragStart() > 5) { isDraggingTrack = true; + DBG_AND_LOG("drag started"); // Takes a snapshot of the track at the moment dragging starts if (dragOverlay != nullptr) @@ -632,6 +634,7 @@ class TrackAreaWidget : public Component, if (isDraggingTrack) { + DBG_AND_LOG("isDraggingTrack true, processing..."); int newInsertIndex = -1; bool wasOutside = isDraggingOutside; @@ -663,6 +666,7 @@ class TrackAreaWidget : public Component, void mouseUp(const MouseEvent& e) override { + DBG_AND_LOG("mouseUp called, isDraggingTrack: " << (int)isDraggingTrack); if (!isThumbnailWidget()) return; // Tracks the release position of the mouse From 2f4ba35cbf24a854ff448bfff356761a57e9041b Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 15 May 2026 00:49:49 -0400 Subject: [PATCH 051/149] Was a huge mess, but fixed all drag logic. Only issue is that there's a ghost when releasing the drag. One more issue: dragging out of app moves the original file, whereas it should copy. --- src/MainComponent.h | 1 + src/media/MediaDisplayComponent.cpp | 28 ++++++++++++++++++++++++---- src/media/MediaDisplayComponent.h | 16 +++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/MainComponent.h b/src/MainComponent.h index 353ec27f..687bf6df 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -31,6 +31,7 @@ class WelcomeWindow; class MainComponent : public Component, public MenuBarModel, public ApplicationCommandTarget, + public DragAndDropContainer, private ChangeListener { diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7d9c9023..98a80c75 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1317,10 +1317,30 @@ void MediaDisplayComponent::mouseDrag(const MouseEvent& e) if (! getLocalBounds().contains(getMouseXYRelative())) { - //performExternalDragDropOfFiles( - // StringArray(getTempFilePath().getLocalFile().getFullPathName()), true, this); - performExternalDragDropOfFiles( - StringArray(getOriginalFilePath().getLocalFile().getFullPathName()), true, this); + Component* topLevel = getTopLevelComponent(); + Rectangle appBounds = + topLevel != nullptr ? topLevel->getScreenBounds() : Rectangle(); + Point screenPos = localPointToGlobal(getMouseXYRelative()); + + // if the cursor goes outside the HARP window + if (auto* container = DragAndDropContainer::findParentDragContainerFor(this)) + { + if (appBounds.contains(screenPos)) // use internal JUCE drag inside window + { + if (!container->isDragAndDropActive()) + { + container->startDragging( + getOriginalFilePath().getLocalFile().getFullPathName(), this, ScaledImage{}, false); + } + } + else // use OS file drag outside window + { + container->performExternalDragDropOfFiles( + StringArray(getOriginalFilePath().getLocalFile().getFullPathName()), + true, + this); + } + } } updateCursorPosition(); diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 39b91e58..e13d1a64 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -62,8 +62,8 @@ class TimeAxisStrip : public Component class MediaDisplayComponent : public Component, public ChangeListener, public ChangeBroadcaster, + public DragAndDropTarget, public FileDragAndDropTarget, - public DragAndDropContainer, private Timer, private ScrollBar::Listener { @@ -121,6 +121,20 @@ class MediaDisplayComponent : public Component, bool isInterestedInFileDrag(const StringArray& /*files*/) override { return isInputTrack(); } + bool isInterestedInDragSource(const DragAndDropTarget::SourceDetails& details) override + { + return isInputTrack() && details.description.isString(); + } + + void itemDropped(const DragAndDropTarget::SourceDetails& details) override + { + String filePath = details.description.toString(); + if (filePath.isNotEmpty()) + { + initializeDisplay(URL(File(filePath))); + } + } + bool isDuplicateFile(const URL& fileParth); void saveFileCallback(); From fa372d01f6407d37c6927c47457ea6a923a8a805 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 15 May 2026 00:52:32 -0400 Subject: [PATCH 052/149] Files now copy when dragged outside the app into a different folder rather than being moved, which could have caused some confusion. --- src/media/MediaDisplayComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 98a80c75..3b697a55 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -1337,7 +1337,7 @@ void MediaDisplayComponent::mouseDrag(const MouseEvent& e) { container->performExternalDragDropOfFiles( StringArray(getOriginalFilePath().getLocalFile().getFullPathName()), - true, + false, this); } } From fe6a1a1bd4eac1a413fec0114cd39bb089f29808 Mon Sep 17 00:00:00 2001 From: Natalie Smith Date: Fri, 15 May 2026 01:09:43 -0400 Subject: [PATCH 053/149] Removed debug log lines. --- src/widgets/TrackAreaWidget.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 28d2b60b..e15f3c05 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -615,12 +615,10 @@ class TrackAreaWidget : public Component, if (!isThumbnailWidget() || draggedTrack == nullptr) return; Point posInThis = e.getEventRelativeTo(this).getPosition(); - DBG_AND_LOG("mouseDrag called, pos: " << posInThis.x << ", " << posInThis.y); if (!isDraggingTrack && e.getDistanceFromDragStart() > 5) { isDraggingTrack = true; - DBG_AND_LOG("drag started"); // Takes a snapshot of the track at the moment dragging starts if (dragOverlay != nullptr) @@ -634,7 +632,6 @@ class TrackAreaWidget : public Component, if (isDraggingTrack) { - DBG_AND_LOG("isDraggingTrack true, processing..."); int newInsertIndex = -1; bool wasOutside = isDraggingOutside; @@ -666,7 +663,6 @@ class TrackAreaWidget : public Component, void mouseUp(const MouseEvent& e) override { - DBG_AND_LOG("mouseUp called, isDraggingTrack: " << (int)isDraggingTrack); if (!isThumbnailWidget()) return; // Tracks the release position of the mouse From 333aa726f4b759b1df718579a60cd4206e48aabb Mon Sep 17 00:00:00 2001 From: ella-granger <44934397+ella-granger@users.noreply.github.com> Date: Fri, 15 May 2026 10:37:19 -0400 Subject: [PATCH 054/149] Update requirements.txt instructions in example.md Fix requirements.txt dependency issue in the tiger repo --- website/content/pyharp_docs/example.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/pyharp_docs/example.md b/website/content/pyharp_docs/example.md index a0246d63..aee47da8 100644 --- a/website/content/pyharp_docs/example.md +++ b/website/content/pyharp_docs/example.md @@ -13,7 +13,7 @@ git clone https://github.com/JusperLee/TIGER.git cd TIGER ``` -In the `requirements.txt`, remove `triton==3.1.0` to avoid dependency issues. Then pip install: +In the `requirements.txt`, remove `triton==3.1.0` and add `typer==0.16.1` to avoid dependency issues. Then pip install: ``` pip install -r requirements.txt @@ -222,4 +222,4 @@ Your terminal should display something like this: This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces) ``` -Copy the public URL and open up HARP. Paste the URL as a `custom path...`, click `Load`, and voila -- you should now be able to run state-of-the-art speech separation in your DAW! \ No newline at end of file +Copy the public URL and open up HARP. Paste the URL as a `custom path...`, click `Load`, and voila -- you should now be able to run state-of-the-art speech separation in your DAW! From 585f1f1ccf50345e154235ff28def617af149744 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Fri, 15 May 2026 14:08:00 -0400 Subject: [PATCH 055/149] Adapted banner to current flexbox layout. --- src/media/MediaDisplayComponent.cpp | 72 +++++++++++++---------------- src/media/MediaDisplayComponent.h | 32 ++++++++----- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 5444deef..2a1765b0 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -6,6 +6,27 @@ #include +void OptionalBannerComponent::paint(Graphics& g) +{ + const float cx = static_cast(getWidth()) / 2.0f; + const float cy = static_cast(getHeight()) / 2.0f; + + g.setColour(Colour::fromRGB(90, 105, 105)); + g.fillAll(); + + g.setColour(Colours::white); + g.setFont(12.0f); + + Graphics::ScopedSaveState state(g); + g.addTransform(AffineTransform::rotation(-MathConstants::halfPi, cx, cy)); + + Rectangle textBounds( + 0, 0, static_cast(getHeight()), static_cast(getWidth())); + textBounds.setCentre(cx, cy); + + g.drawText("OPTIONAL", textBounds, Justification::centred, false); +} + namespace { struct TickScheme @@ -164,6 +185,8 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW horizontalScrollBar.setAutoHide(false); horizontalScrollBar.addListener(this); + addAndMakeVisible(optionalBanner); + timeAxisStrip = std::make_unique(this); mediaAreaContainer.addAndMakeVisible(overheadPanel); @@ -315,49 +338,10 @@ void MediaDisplayComponent::paint(Graphics& g) } } -void MediaDisplayComponent::paintOverChildren(Graphics& g) -{ - // Detect optional AND input track AND not a thumbnail - if (!isRequired() && isInputTrack() && !isThumbnailTrack()) - { - // Grab that 24-pixel vertical slice on the far left that we reserved in resized() - auto bannerArea = getLocalBounds().removeFromLeft(24); - - // Draw the background color - g.setColour(Colour::fromRGB(90, 105, 105)); - g.fillRect(bannerArea); - - // Setup text formatting - g.setColour(Colours::white); - g.setFont(12.0f); - - // Save the graphics state before rotating - Graphics::ScopedSaveState state(g); - - // Rotate the graphics context -90 degrees around the center of our banner - float cx = static_cast(bannerArea.getCentreX()); - float cy = static_cast(bannerArea.getCentreY()); - g.addTransform(AffineTransform::rotation(-MathConstants::halfPi, cx, cy)); - - // Create a rotated bounding box for the text - Rectangle textBounds(0, 0, static_cast(bannerArea.getHeight()), static_cast(bannerArea.getWidth())); - textBounds.setCentre(cx, cy); - - // Draw the text inside our rotated box - g.drawText("OPTIONAL", textBounds, Justification::centred, false); - } -} - void MediaDisplayComponent::resized() { Rectangle totalBounds = getLocalBounds(); - // Reserve 24 pixels on the left edge for the vertical "Optional" banner. - if (!isRequired() && isInputTrack() && !isThumbnailTrack()) - { - totalBounds.removeFromLeft(24); - } - // Remove existing items in main flex mainFlexBox.items.clear(); @@ -374,6 +358,16 @@ void MediaDisplayComponent::resized() { // Place header beside media mainFlexBox.flexDirection = FlexBox::Direction::row; + + if (! isRequired() && isInputTrack() && ! isThumbnailTrack()) + { + mainFlexBox.items.add(FlexItem(optionalBanner).withWidth(24)); + } + else + { + optionalBanner.setBounds(0, 0, 0, 0); + } + // Fixed area for track label and buttons mainFlexBox.items.add(FlexItem(headerComponent).withFlex(1).withMaxWidth(40).withMargin(4)); } diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 7e4c9a84..7769861b 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -27,6 +27,23 @@ enum class DisplayMode Thumbnail // Reduced functionality }; +class OptionalBannerComponent : public Component +{ +public: + void paint(Graphics& g) override; +}; + +class TimeAxisStrip : public Component +{ +public: + explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} + + void paint(Graphics& g) override; + +private: + MediaDisplayComponent* owner = nullptr; +}; + class ColorablePanel : public Component { public: @@ -48,17 +65,6 @@ class ColorablePanel : public Component Colour backgroundColor; }; -class TimeAxisStrip : public Component -{ -public: - explicit TimeAxisStrip(MediaDisplayComponent* ownerIn) : owner(ownerIn) {} - - void paint(Graphics& g) override; - -private: - MediaDisplayComponent* owner = nullptr; -}; - class MediaDisplayComponent : public Component, public ChangeListener, public ChangeBroadcaster, @@ -79,7 +85,6 @@ class MediaDisplayComponent : public Component, virtual StringArray getInstanceExtensions() = 0; void paint(Graphics& g) override; - void paintOverChildren(Graphics& g) override; virtual void resized() override; void repositionLabels(); @@ -273,6 +278,9 @@ class MediaDisplayComponent : public Component, MultiButton::Mode copyFileButtonActiveInfo; MultiButton::Mode copyFileButtonInactiveInfo; + // Banner shown on left edge of optional input tracks + OptionalBannerComponent optionalBanner; + // Panel displaying overhead labels ColorablePanel overheadPanel { overheadPanelColor }; From 623e80b96c7796d075f527867bf4b312a82f17bd Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Fri, 15 May 2026 14:33:00 -0400 Subject: [PATCH 056/149] Updated references to netlify url. --- README.md | 8 ++++---- src/windows/AboutWindow.h | 4 ++-- src/windows/WelcomeWindow.h | 4 ++-- website/.vitepress/config.mjs | 2 +- website/README.md | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 279061a8..8b594a01 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![herofig_revised](https://github.com/TEAMuP-dev/HARP/assets/26678616/c4f5cdbb-aaff-4196-b9d2-3b6f69130856) - + @@ -25,7 +25,7 @@ HARP is a sample editor for **h**osted, **a**synchronous, **r**emote **p**rocess * **[Distribution](#distribution)** * **[Citations](#citations)** -For more information on HARP, please see [our website](https://harp-plugin.netlify.app) or our most recent [NeurIPS workshop paper](https://openreview.net/pdf?id=Ha2z921r51). +For more information on HARP, please see [our website](https://harp3.netlify.app) or our most recent [NeurIPS workshop paper](https://openreview.net/pdf?id=Ha2z921r51). @@ -39,7 +39,7 @@ HARP has been tested on the following operating systems: -Please visit [our website](https://harp-plugin.netlify.app/content/install/macos.html) for instructions on installing HARP and setting it up as an external sample editor for specific operating systems and DAWs. +Please visit [our website](https://harp3.netlify.app/content/install/macos.html) for instructions on installing HARP and setting it up as an external sample editor for specific operating systems and DAWs. # Usage @@ -67,7 +67,7 @@ HARP processes full regions in the DAW. Therefore, to edit a portion of an audio ## Models -Please visit [our website](https://harp-plugin.netlify.app/content/usage/models.html) for a full list of supported models. +Please visit [our website](https://harp3.netlify.app/content/usage/models.html) for a full list of supported models. ## Workflow diff --git a/src/windows/AboutWindow.h b/src/windows/AboutWindow.h index 4e4e4280..2b633457 100644 --- a/src/windows/AboutWindow.h +++ b/src/windows/AboutWindow.h @@ -25,14 +25,14 @@ class AboutWindow : public Component modelGlossaryButton.setButtonText("Model Glossary"); modelGlossaryButton.setURL( - URL("https://harp-plugin.netlify.app/content/usage/models.html")); + URL("https://harp3.netlify.app/content/usage/models.html")); modelGlossaryButton.setSize(380, 24); modelGlossaryButton.setTopLeftPosition(10, 110); modelGlossaryButton.setColour(HyperlinkButton::textColourId, Colours::blue); addAndMakeVisible(modelGlossaryButton); visitWebpageButton.setButtonText("Visit HARP Webpage"); - visitWebpageButton.setURL(URL("https://harp-plugin.netlify.app/")); + visitWebpageButton.setURL(URL("https://harp3.netlify.app/")); visitWebpageButton.setSize(380, 24); visitWebpageButton.setTopLeftPosition(10, 140); visitWebpageButton.setColour(HyperlinkButton::textColourId, Colours::blue); diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index ad3d7188..6bca1ee6 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -526,7 +526,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener addAndMakeVisible(learnMoreLink); learnMoreLink.setButtonText("Learn more"); - learnMoreLink.setURL(URL("https://harp-plugin.netlify.app/content/intro.html")); + learnMoreLink.setURL(URL("https://harp3.netlify.app/content/intro.html")); addAndMakeVisible(copyrightLabel); copyrightLabel.setText("Copyright 2026 TEAMuP. All rights reserved.", @@ -640,7 +640,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener Label pageIndicator; HyperlinkButton learnMoreLink { "Learn more", - URL("https://harp-plugin.netlify.app/content/intro.html") }; + URL("https://harp3.netlify.app/content/intro.html") }; Label copyrightLabel; TextButton showDetailsButton { "Show detailed control descriptions" }; diff --git a/website/.vitepress/config.mjs b/website/.vitepress/config.mjs index 4477dd03..d9e37510 100644 --- a/website/.vitepress/config.mjs +++ b/website/.vitepress/config.mjs @@ -28,7 +28,7 @@ export default defineConfig({ nav: [ { text: 'Home', link: '/' }, { text: 'Guide', link: '/content/intro' }, - // { text: 'API', link: 'https://harp-plugin.netlify.app/doxygen-docs/html/index.html'}, + // { text: 'API', link: 'https://harp3.netlify.app/doxygen-docs/html/index.html'}, ], footer: { diff --git a/website/README.md b/website/README.md index 97e61439..66accb37 100644 --- a/website/README.md +++ b/website/README.md @@ -2,7 +2,7 @@ This website is built using [Vitepress](https://vitepress.dev/). -We use [Netlify](https://www.netlify.com/) to deploy it under the domain [harp-plugin.netlify.app](https://harp-plugin.netlify.app/). +We use [Netlify](https://www.netlify.com/) to deploy it under the domain [harp3.netlify.app](https://harp3.netlify.app/). ## Build locally @@ -63,7 +63,7 @@ The website's content is primarily managed through Markdown files and VitePress ## Deploy to Netlify -* **Production Deployment:** The `main` branch of the `HARP` repository is automatically deployed to our live URL: [harp-plugin.netlify.app](https://harp-plugin.netlify.app/). +* **Production Deployment:** The `main` branch of the `HARP` repository is automatically deployed to our live URL: [harp3.netlify.app](https://harp3.netlify.app/). * **Deployment Previews:** Netlify automatically generates deployment previews for: * All Pull Requests (PRs) made against the `main` branch. * Any branch whose name starts with the prefix `website/*` (e.g., `website/new-feature`, `website/docs-update`). From 15b7814729132999954062779e0a4907bb678767 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Fri, 15 May 2026 14:50:57 -0400 Subject: [PATCH 057/149] Added netlify.toml --- website/netlify.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 website/netlify.toml diff --git a/website/netlify.toml b/website/netlify.toml new file mode 100644 index 00000000..7a0e77a8 --- /dev/null +++ b/website/netlify.toml @@ -0,0 +1,6 @@ +[build] + command = "pnpm run build" + publish = ".vitepress/dist" + +[build.environment] + NODE_VERSION = "18" From 6660b468db4586401968157cd34043afc4f906f8 Mon Sep 17 00:00:00 2001 From: ella-granger <44934397+ella-granger@users.noreply.github.com> Date: Fri, 15 May 2026 16:05:39 -0400 Subject: [PATCH 058/149] Python version constraint Python should be >=3.10 and <=3.12 --- website/content/pyharp_docs/example.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/pyharp_docs/example.md b/website/content/pyharp_docs/example.md index aee47da8..ebd3ff84 100644 --- a/website/content/pyharp_docs/example.md +++ b/website/content/pyharp_docs/example.md @@ -4,9 +4,9 @@ To get the hang of building PyHARP applications, we'll deploy a state-of-the-art ### TIGER: a Powerful and Lightweight Speech Separation Model -DAW users such as video editors and podcasters often work with noisy speech recordings captured in less-than-ideal conditions, and "cleaning up" these recordings can be difficult and time-intensive. Luckily, there are a number of cutting-edge deep learning models capable of automating this clean-up process by separating a speech signal from any background sounds in a recording, including environment noise and even other speakers. In this tutorial, we'll look at [TIGER](https://arxiv.org/abs/2410.01469), a lightweight speech separation model capable of running efficiently on a laptop CPU. This means that you won't need a GPU for this tutorial -- just working HARP and PyHARP installations! +DAW users such as video editors and podcasters often work with noisy speech recordings captured in less-than-ideal conditions, and "cleaning up" these recordings can be difficult and time-intensive. Luckily, there are a number of cutting-edge deep learning models capable of automating this cleanup process by separating a speech signal from any background sounds in a recording, including environment noise and even other speakers. In this tutorial, we'll look at [TIGER](https://arxiv.org/abs/2410.01469), a lightweight speech separation model capable of running efficiently on a laptop CPU. This means that you won't need a GPU for this tutorial -- just working HARP and PyHARP installations! -To get started, we'll install [the code](https://github.com/JusperLee/TIGER) needed to run TIGER: +To get started, please set the Python version to `>=3.10, <=3.12` and install PyHARP. Then, we'll install [the code](https://github.com/JusperLee/TIGER) needed to run TIGER: ``` git clone https://github.com/JusperLee/TIGER.git From cfb59599d0410b88713e615964fc9124e2872c2e Mon Sep 17 00:00:00 2001 From: derekllanes Date: Fri, 15 May 2026 15:32:56 -0500 Subject: [PATCH 059/149] Add generic file picker control for file inputs --- CMakeLists.txt | 1 + pyharp | 2 +- src/Model.h | 39 +++++----- src/gui/FilePickerWithLabel.h | 126 ++++++++++++++++++++++++++++++++ src/utils/Controls.h | 28 ++++++- src/widgets/ControlAreaWidget.h | 39 +++++++++- src/widgets/TrackAreaWidget.h | 15 ---- 7 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 src/gui/FilePickerWithLabel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d0d0f71..b92305b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,7 @@ target_sources(${PROJECT_NAME} src/gui/HoverHandler.h src/gui/ControlComponent.h src/gui/ToggleWithLabel.h + src/gui/FilePickerWithLabel.h src/media/MediaDisplayComponent.cpp src/media/AudioDisplayComponent.cpp diff --git a/pyharp b/pyharp index d8ef2c0e..80b265ea 160000 --- a/pyharp +++ b/pyharp @@ -1 +1 @@ -Subproject commit d8ef2c0ef42bbef6cfa8362dc786e672324e6fb0 +Subproject commit 80b265ea1adf3fded4e2f3963a28062215e21e0d diff --git a/src/Model.h b/src/Model.h index bb0a2d56..4a6b6e71 100644 --- a/src/Model.h +++ b/src/Model.h @@ -314,6 +314,22 @@ class Model { controlValue = var(comboBoxComponentInfo->value); } + else if (auto filePickerComponentInfo = + dynamic_cast(componentInfo.get())) + { + if (filePickerComponentInfo->path.empty()) + { + controlValue = var(); + } + else + { + DynamicObject::Ptr fileObj = new DynamicObject(); + fileObj->setProperty("path", var(filePickerComponentInfo->path)); + controlValue = var(fileObj); + } + + wasFile = true; + } else { // Unsupported control was added @@ -441,15 +457,15 @@ class Model + "\" extracted."); } // Generic File - else if (type == "file_track") + else if (type == "file_picker") { - std::shared_ptr fileTrack = - std::make_shared(controlsDict); + std::shared_ptr filePicker = + std::make_shared(controlsDict); - newInputs.push_back(fileTrack); - tempComponentIDs.push_back(fileTrack->id); + newControls.push_back(filePicker); + tempComponentIDs.push_back(filePicker->id); - DBG_AND_LOG("Model::extractInputs: File track input \"" + fileTrack->label + DBG_AND_LOG("Model::extractInputs: File picker control \"" + filePicker->label + "\" extracted."); } else if (type == "text_box") @@ -575,17 +591,6 @@ class Model DBG_AND_LOG("Model::extractOutputs: MIDI track output \"" + midiTrack->label + "\" extracted."); } - // Generic File - else if (type == "file_track") - { - std::shared_ptr fileTrack = - std::make_shared(controlsDict); - - newOutputs.push_back(fileTrack); - - DBG_AND_LOG("Model::extractOutputs: File track output \"" + fileTrack->label - + "\" extracted."); - } else if (type == "json") { // Labels are handled separately diff --git a/src/gui/FilePickerWithLabel.h b/src/gui/FilePickerWithLabel.h new file mode 100644 index 00000000..7cc498aa --- /dev/null +++ b/src/gui/FilePickerWithLabel.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#include "ControlComponent.h" +#include "../utils/Controls.h" + +using namespace juce; + +class FilePickerWithLabel : public ControlComponent, private Button::Listener +{ +public: + explicit FilePickerWithLabel(FilePickerComponentInfo* infoToUse) + : info(infoToUse), + browseButton("Browse...") + { + if (info != nullptr) + { + label.setText(info->label, dontSendNotification); + } + + label.setJustificationType(Justification::centred); + + pathBox.setReadOnly(true); + pathBox.setText("No file selected", dontSendNotification); + pathBox.setMultiLine(false); + pathBox.setScrollbarsShown(false); + pathBox.setCaretVisible(false); + + browseButton.addListener(this); + + addAndMakeVisible(label); + addAndMakeVisible(pathBox); + addAndMakeVisible(browseButton); + } + + ~FilePickerWithLabel() override + { + browseButton.removeListener(this); + } + + void resized() override + { + auto area = getLocalBounds(); + + auto labelArea = area.removeFromTop(20); + label.setBounds(labelArea); + + area.removeFromTop(4); + + auto buttonArea = area.removeFromLeft(90); + browseButton.setBounds(buttonArea); + + area.removeFromLeft(6); + pathBox.setBounds(area); + } + + int getMinimumRequiredWidth() const override + { + const int labelWidth = getLabelWidth(label); + return jmax(260, labelWidth + defaultPadding); + } + +private: + void buttonClicked(Button* button) override + { + if (button != &browseButton || info == nullptr) + { + return; + } + + String pattern = buildWildcardPattern(); + + fileChooser = std::make_unique( + "Select file", + File(), + pattern + ); + + fileChooser->launchAsync( + FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, + [this](const FileChooser& chooser) + { + File selectedFile = chooser.getResult(); + + if (selectedFile.existsAsFile()) + { + info->path = selectedFile.getFullPathName().toStdString(); + pathBox.setText(selectedFile.getFullPathName(), dontSendNotification); + } + } + ); + } + + String buildWildcardPattern() const + { + if (info == nullptr || info->fileTypes.empty()) + { + return "*"; + } + + StringArray patterns; + + for (const auto& ext : info->fileTypes) + { + String extension(ext); + + if (! extension.startsWithChar('.')) + { + extension = "." + extension; + } + + patterns.add("*" + extension); + } + + return patterns.joinIntoString(";"); + } + + FilePickerComponentInfo* info = nullptr; + + Label label; + TextEditor pathBox; + TextButton browseButton; + + std::unique_ptr fileChooser; +}; \ No newline at end of file diff --git a/src/utils/Controls.h b/src/utils/Controls.h index 2aa5a665..c12d1b9d 100644 --- a/src/utils/Controls.h +++ b/src/utils/Controls.h @@ -83,10 +83,32 @@ struct MidiTrackComponentInfo : public TrackComponentInfo using TrackComponentInfo::TrackComponentInfo; }; -// New struct for generic file type -struct FileTrackComponentInfo : public TrackComponentInfo +struct FilePickerComponentInfo : public ModelComponentInfo { - using TrackComponentInfo::TrackComponentInfo; + bool required = true; + std::string path { "" }; + std::vector fileTypes; + + FilePickerComponentInfo(DynamicObject* input) : ModelComponentInfo(input) + { + if (input->hasProperty("required")) + { + required = stringToBool(input->getProperty("required").toString()); + } + + if (input->hasProperty("file_types")) + { + Array* types = input->getProperty("file_types").getArray(); + + if (types != nullptr) + { + for (int i = 0; i < types->size(); ++i) + { + fileTypes.push_back(types->getReference(i).toString().toStdString()); + } + } + } + } }; struct TextBoxComponentInfo : public ModelComponentInfo, public TextEditor::Listener diff --git a/src/widgets/ControlAreaWidget.h b/src/widgets/ControlAreaWidget.h index 58c8ec98..d02cc133 100644 --- a/src/widgets/ControlAreaWidget.h +++ b/src/widgets/ControlAreaWidget.h @@ -16,6 +16,7 @@ #include "../gui/SliderWithLabel.h" #include "../gui/TextBoxWithLabel.h" #include "../gui/ToggleWithLabel.h" +#include "../gui/FilePickerWithLabel.h" #include "../utils/Controls.h" #include "../utils/Logging.h" @@ -81,7 +82,7 @@ class ControlAreaWidget : public Component int getNumControls() const { return sliderComponents.size() + toggleComponents.size() + dropdownComponents.size() - + textComponents.size(); + + textComponents.size() + filePickerComponents.size(); } int getMinimumRequiredWidth() const @@ -100,6 +101,7 @@ class ControlAreaWidget : public Component checkGroup(toggleComponents); checkGroup(dropdownComponents); checkGroup(textComponents); + checkGroup(filePickerComponents); return requiredWidth + 2 * (marginSize + minEdgeGap); } @@ -156,6 +158,12 @@ class ControlAreaWidget : public Component } dropdownComponents.clear(); + for (auto& c : filePickerComponents) + { + removeChildComponent(c.get()); + } + filePickerComponents.clear(); + handlers.clear(); } @@ -182,6 +190,10 @@ class ControlAreaWidget : public Component { addDropdown(dropdownInfo); } + else if (auto* filePickerInfo = dynamic_cast(info.get())) + { + addFilePicker(filePickerInfo); + } else { // Unsupported control detected @@ -287,6 +299,18 @@ class ControlAreaWidget : public Component dropdownComponents.push_back(std::move(dropdownComponent)); } + void addFilePicker(FilePickerComponentInfo* info) + { + std::unique_ptr filePickerComponent = + std::make_unique(info); + + addHandler(filePickerComponent.get(), info); + + addAndMakeVisible(*filePickerComponent); + + filePickerComponents.push_back(std::move(filePickerComponent)); + } + void addHandler(Component* comp, ModelComponentInfo* info) { std::unique_ptr handler = std::make_unique(*comp); @@ -341,14 +365,16 @@ class ControlAreaWidget : public Component addGroupToRows(rows, toggleComponents, 1, width); addGroupToRows(rows, dropdownComponents, 2, width); addGroupToRows(rows, textComponents, 3, width); + addGroupToRows(rows, filePickerComponents, 4, width); return rows; } + template void addGroupToRows(std::vector>& rows, - const auto& components, - int type, - int availableWidth) const + const ComponentList& components, + int type, + int availableWidth) const { auto spec = getLayoutSpec(type); @@ -403,6 +429,8 @@ class ControlAreaWidget : public Component return { preferredDropdownWidth, minDropdownHeight }; case 3: return { preferredTextBoxWidth, minTextBoxHeight }; + case 4: + return { preferredFilePickerWidth, minFilePickerHeight }; } return { preferredDropdownWidth, minDropdownHeight }; @@ -426,11 +454,13 @@ class ControlAreaWidget : public Component static constexpr int minToggleHeight = 34; static constexpr int minDropdownHeight = 44; static constexpr int minTextBoxHeight = 84; + static constexpr int minFilePickerHeight = 64; static constexpr int preferredSliderWidth = 108; static constexpr int preferredToggleWidth = 112; static constexpr int preferredDropdownWidth = 140; static constexpr int preferredTextBoxWidth = 200; + static constexpr int preferredFilePickerWidth = 260; static constexpr int minInterItemGap = 6; static constexpr int minEdgeGap = 4; @@ -441,6 +471,7 @@ class ControlAreaWidget : public Component std::vector> toggleComponents; std::vector> sliderComponents; std::vector> dropdownComponents; + std::vector> filePickerComponents; std::vector> handlers; diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 4d678a1c..f81f7765 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -11,7 +11,6 @@ #include "../media/AudioDisplayComponent.h" #include "../media/MediaDisplayComponent.h" #include "../media/MidiDisplayComponent.h" -#include "../media/FileDisplayComponent.h" #include "../utils/Controls.h" #include "../utils/Interface.h" @@ -215,11 +214,6 @@ class TrackAreaWidget : public Component, m = std::make_unique( label, midiTrackInfo->required, fromDAW, displayMode); } - else if (auto fileTrackInfo = dynamic_cast(trackInfo)) - { - m = std::make_unique( - label, fileTrackInfo->required, fromDAW, displayMode); - } else { DBG_AND_LOG( @@ -312,15 +306,6 @@ class TrackAreaWidget : public Component, trackInfo = std::move(midiTrackInfo); } - else if (FileDisplayComponent::getSupportedExtensions().contains(ext)) - { - auto fileTrackInfo = std::make_unique(); - - fileTrackInfo->required = false; - fileTrackInfo->label = label.toStdString(); - - trackInfo = std::move(fileTrackInfo); - } else { DBG_AND_LOG("TrackAreaWidget::addTrackFromFilePath: Tried to add file " From 53e2ddb69e50e038211265201e751f1f445bb36f Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Sun, 17 May 2026 23:33:36 -0500 Subject: [PATCH 060/149] review changes: improved on error reporting, SSE log handling, and UI fixes --- src/Model.h | 37 +++++-- src/clients/GradioClient.h | 112 +++++++++++++++++--- src/utils/Errors.h | 21 ++-- src/utils/Logging.h | 9 +- src/widgets/ModelSelectionWidget.h | 27 ++++- src/windows/settings/GeneralSettingsTab.cpp | 13 +++ 6 files changed, 179 insertions(+), 40 deletions(-) diff --git a/src/Model.h b/src/Model.h index e5362bbd..c323c4c7 100644 --- a/src/Model.h +++ b/src/Model.h @@ -107,6 +107,27 @@ class Model } } + void setFailure(const OpResult& result, const String& pathContext = "") + { + status = ModelStatus::FAILURE; + + if (statusMessage != nullptr) + { + String errMsg = toUserMessage(result.getError()); + // Keep only the first line so the status bar stays readable + String shortMsg = errMsg.upToFirstOccurrenceOf("\n", false, false).trim(); + if (shortMsg.isEmpty()) + shortMsg = errMsg.trim(); + + String shortPath = pathContext.fromLastOccurrenceOf("/", false, false).trim(); + String msg = "[error] " + shortMsg; + if (shortPath.isNotEmpty()) + msg += " | " + shortPath; + + statusMessage->setMessage(msg); + } + } + String getLoadedPath() { return loadedPath; } String getOpenablePath() { return openablePath; } @@ -144,7 +165,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, pathToLoad); + setFailure(result, pathToLoad); return result; } @@ -159,7 +180,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, pathToLoad); + setFailure(result, pathToLoad); return result; } @@ -171,7 +192,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, pathToLoad); + setFailure(result, pathToLoad); return result; } @@ -184,7 +205,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, pathToLoad); + setFailure(result, pathToLoad); return result; } @@ -196,7 +217,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, pathToLoad); + setFailure(result, pathToLoad); return result; } @@ -240,7 +261,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, loadedPath); + setFailure(result, loadedPath); return result; } @@ -342,7 +363,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, loadedPath); + setFailure(result, loadedPath); return result; } @@ -360,7 +381,7 @@ class Model if (result.failed()) { - setStatus(ModelStatus::FAILURE, loadedPath); + setFailure(result, loadedPath); return result; } diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index bf171c31..09823686 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -911,17 +911,56 @@ class GradioClient : public Client return "[" + severity + "] " + message; } - static String formatProcessMessage(const String& messageType) + // Formats a "log" SSE event payload (e.g. {"log": "...", "level": "info"}) + // into a user-visible status string like "[info] Starting inference...". + // Returns empty string if the payload is not a valid log event. + static String formatLogEventPayload(const String& payload) + { + String normalizedPayload = payload.trim(); + + if (normalizedPayload.isEmpty() || normalizedPayload.equalsIgnoreCase("null")) + { + return ""; + } + + var parsed = JSON::parse(normalizedPayload); + auto* dict = parsed.getDynamicObject(); + + if (dict == nullptr) + { + return ""; + } + + String message = extractFirstNonEmptyField(dict, { "log", "message", "detail" }); + + if (message.isEmpty()) + { + return ""; + } + + String level = + extractFirstNonEmptyField(dict, { "level", "type", "severity" }).toLowerCase(); + + if (level.isEmpty()) + { + level = "info"; + } + + return "[" + level + "] " + message; + } + + static String formatProcessMessage(const String& messageType, const String& dataPayload = "") { if (messageType.isEmpty()) { return ""; } - // "log" messages carry info/warning text + // Modern Gradio "log" SSE event: parse the data payload for message + level. + // Emitted when the server calls gr.Info() or gr.Warning(). if (messageType.equalsIgnoreCase("log")) { - return ""; + return formatLogEventPayload(dataPayload); } if (messageType.equalsIgnoreCase("process_starts")) @@ -941,35 +980,67 @@ class GradioClient : public Client return "[process] " + detail; } + if (messageType.equalsIgnoreCase("heartbeat") || messageType.equalsIgnoreCase("complete") + || messageType.equalsIgnoreCase("error")) + { + return ""; + } + return "[status] " + messageType.replace("_", " "); } - void appendProcessMessageFromDataLine(const String& dataLine) + // currentSseEventType: the value from the preceding "event: " line, e.g. "log", + // "process_starts", "heartbeat". Empty if no event: line preceded this. + void appendProcessMessageFromDataLine(const String& dataLine, + const String& currentSseEventType = "") { if (statusMessage == nullptr) { + DBG_AND_LOG( + "GradioClient::appendProcessMessageFromDataLine: statusMessage is null, skipping."); return; } String payload = extractPayload(dataLine); - String messageType = extractMessageTypeFromPayload(payload); String statusText; + // Prefer the SSE event type from the preceding "event:" line (modern Gradio API). + // Fall back to extracting a "msg" field from the JSON payload (old queue API). + String messageType = currentSseEventType.isNotEmpty() + ? currentSseEventType + : extractMessageTypeFromPayload(payload); + + DBG_AND_LOG("GradioClient::appendProcessMessageFromDataLine: payload=\"" + << payload << "\" sseEventType=\"" << currentSseEventType << "\" messageType=\"" + << messageType << "\"."); + if (messageType.isNotEmpty()) { - statusText = formatProcessMessage(messageType); + // For "log" events, pass the full data payload so we can extract the message text. + statusText = formatProcessMessage(messageType, payload); + + DBG_AND_LOG("GradioClient::appendProcessMessageFromDataLine: formatProcessMessage -> \"" + << statusText << "\"."); } if (statusText.isEmpty()) { statusText = extractGradioAlertStatusFromPayload(payload); + + DBG_AND_LOG( + "GradioClient::appendProcessMessageFromDataLine: extractGradioAlertStatus -> \"" + << statusText << "\"."); } if (statusText.isEmpty()) { + DBG_AND_LOG( + "GradioClient::appendProcessMessageFromDataLine: statusText is empty, message dropped."); return; } + DBG_AND_LOG("GradioClient::appendProcessMessageFromDataLine: setting statusMessage -> \"" + << statusText << "\"."); statusMessage->setMessage(statusText); } @@ -995,6 +1066,10 @@ class GradioClient : public Client bool seenAnyDataEvent = false; bool checkedFirstLine = false; + // Track the most recent SSE "event: " line so we can pass it to + // appendProcessMessageFromDataLine when the next "data:" line arrives. + String currentSseEventType; + while (! stream->isExhausted()) { response = stream->readNextLine(); @@ -1005,6 +1080,8 @@ class GradioClient : public Client if (eventLine.isEmpty()) { + // Blank line signals the end of one SSE message block + currentSseEventType = ""; continue; } @@ -1019,6 +1096,14 @@ class GradioClient : public Client } } + // Capture the SSE event type from "event: " lines. + if (eventLine.startsWithIgnoreCase("event:")) + { + currentSseEventType = eventLine.fromFirstOccurrenceOf(":", false, false).trim(); + DBG_AND_LOG("GradioClient::makeGETRequest: SSE event type \"" << currentSseEventType + << "\"."); + } + if (isEventLine(eventLine, GradioEvents::Complete)) { response = extractPayload(stream->readNextLine()); @@ -1065,20 +1150,17 @@ class GradioClient : public Client { seenAnyDataEvent = true; + // Check for process_starts using old-API msg field as fallback. String dataPayload = extractPayload(eventLine); - if (extractMessageTypeFromPayload(dataPayload).equalsIgnoreCase("process_starts")) + if (currentSseEventType.equalsIgnoreCase("process_starts") + || extractMessageTypeFromPayload(dataPayload) + .equalsIgnoreCase("process_starts")) { seenProcessStarts = true; } - appendProcessMessageFromDataLine(eventLine); - } - else - { - // TODO - what other information is available? - // Unhandled SSE events (e.g., heartbeat) - // Examples: - // - event: heartbeat + // Pass the current SSE event type so "log" events are not dropped. + appendProcessMessageFromDataLine(eventLine, currentSseEventType); } } diff --git a/src/utils/Errors.h b/src/utils/Errors.h index 095b94ab..695a5a91 100644 --- a/src/utils/Errors.h +++ b/src/utils/Errors.h @@ -157,20 +157,13 @@ inline String toUserMessage(const HttpError& e) { } - userMessage += " request to endpoint"; - - if (e.endpointPath.isNotEmpty()) - { - userMessage += " \"" + e.endpointPath + "\""; - } - - userMessage += "."; + userMessage += " request."; if (e.request == HttpError::Request::POST) { userMessage += - "\n\nThe Space may be sleeping or temporarily unavailable." - "\n\nTry loading again in a few seconds or Click 'Open URL' to open and check it on Huggingface."; + "\n\nThe server may be sleeping or temporarily unavailable." + "\n\nTry loading again in a few seconds, or click 'Open URL' to view the model's page."; } return userMessage; @@ -221,8 +214,8 @@ inline String toUserMessage(const HttpError& e) if (e.statusCode == 503) { userMessage += - "\n\nThe Hugging Face Space may be sleeping or paused." - "\n\nTry loading again in a few seconds or Click 'Open URL' to open and check it on Huggingface."; + "\n\nThe server may be sleeping, paused, or down due to a build or runtime error." + "\n\nTry loading again in a few seconds, or click 'Open URL' to view the model's page."; } } @@ -251,14 +244,14 @@ inline String toUserMessage(const GradioError& e) { case GradioError::Type::RuntimeError: - userMessage = "The Hugging Face Space reported a runtime error."; + userMessage = "The Gradio server reported a runtime error."; if (e.reason.isNotEmpty()) { userMessage += "\n\nDetails: " + e.reason; } - userMessage += "\n\nPlease open the Space logs for more information."; + userMessage += "\n\nClick 'Open URL' to view the model's page for more information."; return userMessage; diff --git a/src/utils/Logging.h b/src/utils/Logging.h index 9478fd2f..5a774b50 100644 --- a/src/utils/Logging.h +++ b/src/utils/Logging.h @@ -51,9 +51,14 @@ class HARPLogger : private DeletedAtShutdown void clearLog() { File logFile = logger->getLogFile(); - logger.reset(); // release file handle before truncating + logger.reset(); // release file handle before truncating logFile.replaceWithText(""); // truncate - initializeLogger(); // reopen + initializeLogger(); // reopen + + File launchLog = + FileLogger::getSystemLogFileFolder().getChildFile("HARP").getChildFile("launch.log"); + if (launchLog.existsAsFile()) + launchLog.replaceWithText(""); } private: diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index bba39217..890079fb 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -534,7 +534,32 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public SharedResourcePointer sharedChoices; - ComboBox modelPathComboBox; + // A ComboBox that always opens its popup from the top of the list, showing all + // items without scrolling to the currently-selected one first. + struct FullListComboBox : public ComboBox + { + void showPopup() override + { + auto& lf = getLookAndFeel(); + auto label = std::unique_ptr