Spaces:
Running
Running
Fallback MIDI export in embedded Spaces
Browse files- README.md +9 -4
- frontend/app.js +32 -4
- frontend/index.html +3 -3
README.md
CHANGED
|
@@ -96,9 +96,12 @@ intentionally remembers a file, not a folder, because Chrome can reject folder
|
|
| 96 |
access for protected locations.
|
| 97 |
|
| 98 |
If the browser does not support the file-save picker, the export falls back to a
|
| 99 |
-
normal browser download.
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
## High-Level Architecture
|
| 104 |
|
|
@@ -369,7 +372,9 @@ CONTINUATOR_DB_PATH=/data/continuator.sqlite3
|
|
| 369 |
```
|
| 370 |
|
| 371 |
The browser ZIP export does not require persistent Space storage because the
|
| 372 |
-
archive is downloaded to the user.
|
|
|
|
|
|
|
| 373 |
writes inside the container unless `CONTINUATOR_SESSION_EXPORT_DIR` points to a
|
| 374 |
mounted volume, so it is mainly useful for local development or managed storage
|
| 375 |
setups.
|
|
|
|
| 96 |
access for protected locations.
|
| 97 |
|
| 98 |
If the browser does not support the file-save picker, the export falls back to a
|
| 99 |
+
normal browser download. Hugging Face embeds Spaces in a cross-origin frame, so
|
| 100 |
+
the embedded Space also uses the normal browser download flow; open the direct
|
| 101 |
+
Space URL if you want Chrome's file-save picker and `Don't ask again` behavior.
|
| 102 |
+
The backend also keeps `POST /api/sessions/{session_id}/save-midi` as a
|
| 103 |
+
server-side fallback route, but browser users should prefer the ZIP download
|
| 104 |
+
path because it writes to a location they choose locally.
|
| 105 |
|
| 106 |
## High-Level Architecture
|
| 107 |
|
|
|
|
| 372 |
```
|
| 373 |
|
| 374 |
The browser ZIP export does not require persistent Space storage because the
|
| 375 |
+
archive is downloaded to the user. In the embedded Hugging Face Space page,
|
| 376 |
+
browser security rules block Chrome's file-save picker, so exports fall back to
|
| 377 |
+
the browser's normal download flow. The server-side fallback `save-midi` route
|
| 378 |
writes inside the container unless `CONTINUATOR_SESSION_EXPORT_DIR` points to a
|
| 379 |
mounted volume, so it is mainly useful for local development or managed storage
|
| 380 |
setups.
|
frontend/app.js
CHANGED
|
@@ -544,8 +544,27 @@ function safeLocalStorageSet(key, value) {
|
|
| 544 |
}
|
| 545 |
}
|
| 546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
function midiExportZipSaveSupported() {
|
| 548 |
-
return
|
|
|
|
|
|
|
|
|
|
| 549 |
}
|
| 550 |
|
| 551 |
function midiExportZipRememberSupported() {
|
|
@@ -4734,14 +4753,23 @@ async function saveSessionMidi() {
|
|
| 4734 |
setPhraseMessage("MIDI export cancelled.");
|
| 4735 |
return;
|
| 4736 |
}
|
| 4737 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4738 |
throw new Error(
|
| 4739 |
"Chrome blocked that location. Save the ZIP in Downloads, Documents, Desktop, or Music instead of a system, home, or project folder.",
|
| 4740 |
);
|
|
|
|
|
|
|
| 4741 |
}
|
| 4742 |
-
throw error;
|
| 4743 |
}
|
| 4744 |
-
|
|
|
|
|
|
|
| 4745 |
} else {
|
| 4746 |
destinationLabel = `${downloadZipBlob(payload.blob, payload.archive_file_name)} in your browser downloads`;
|
| 4747 |
}
|
|
|
|
| 544 |
}
|
| 545 |
}
|
| 546 |
|
| 547 |
+
function isCrossOriginSubframe() {
|
| 548 |
+
if (window.self === window.top) {
|
| 549 |
+
return false;
|
| 550 |
+
}
|
| 551 |
+
try {
|
| 552 |
+
void window.top.location.href;
|
| 553 |
+
return false;
|
| 554 |
+
} catch {
|
| 555 |
+
return true;
|
| 556 |
+
}
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
function filePickerBlockedByFrame(error) {
|
| 560 |
+
return /cross.?origin|sub.?frames?|file picker/i.test(error?.message || "");
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
function midiExportZipSaveSupported() {
|
| 564 |
+
return (
|
| 565 |
+
typeof window.showSaveFilePicker === "function" &&
|
| 566 |
+
!isCrossOriginSubframe()
|
| 567 |
+
);
|
| 568 |
}
|
| 569 |
|
| 570 |
function midiExportZipRememberSupported() {
|
|
|
|
| 4753 |
setPhraseMessage("MIDI export cancelled.");
|
| 4754 |
return;
|
| 4755 |
}
|
| 4756 |
+
if (filePickerBlockedByFrame(error)) {
|
| 4757 |
+
destinationLabel = `${downloadZipBlob(payload.blob, payload.archive_file_name)} in your browser downloads`;
|
| 4758 |
+
setSessionMidiSaveResult(
|
| 4759 |
+
"File picker is blocked in this embedded page; using browser downloads instead.",
|
| 4760 |
+
);
|
| 4761 |
+
fileHandle = null;
|
| 4762 |
+
} else if (error?.name === "NotAllowedError" || /system files/i.test(error?.message || "")) {
|
| 4763 |
throw new Error(
|
| 4764 |
"Chrome blocked that location. Save the ZIP in Downloads, Documents, Desktop, or Music instead of a system, home, or project folder.",
|
| 4765 |
);
|
| 4766 |
+
} else {
|
| 4767 |
+
throw error;
|
| 4768 |
}
|
|
|
|
| 4769 |
}
|
| 4770 |
+
if (fileHandle) {
|
| 4771 |
+
destinationLabel = await writeZipBlobToFileHandle(payload.blob, fileHandle);
|
| 4772 |
+
}
|
| 4773 |
} else {
|
| 4774 |
destinationLabel = `${downloadZipBlob(payload.blob, payload.archive_file_name)} in your browser downloads`;
|
| 4775 |
}
|
frontend/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
<title>Web Continuator</title>
|
| 7 |
-
<link rel="stylesheet" href="/assets/styles.css?v=20260602-session-midi-
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div class="page-aura"></div>
|
|
@@ -167,7 +167,7 @@
|
|
| 167 |
<p>
|
| 168 |
In Chrome and Edge, <strong>Don't ask again</strong> remembers the
|
| 169 |
selected ZIP file and overwrites it on future saves. Other browsers
|
| 170 |
-
use the normal download flow.
|
| 171 |
</p>
|
| 172 |
</section>
|
| 173 |
|
|
@@ -1159,6 +1159,6 @@
|
|
| 1159 |
</section>
|
| 1160 |
</main>
|
| 1161 |
|
| 1162 |
-
<script type="module" src="/assets/app.js?v=20260602-session-midi-
|
| 1163 |
</body>
|
| 1164 |
</html>
|
|
|
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
<title>Web Continuator</title>
|
| 7 |
+
<link rel="stylesheet" href="/assets/styles.css?v=20260602-session-midi-zip3" />
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div class="page-aura"></div>
|
|
|
|
| 167 |
<p>
|
| 168 |
In Chrome and Edge, <strong>Don't ask again</strong> remembers the
|
| 169 |
selected ZIP file and overwrites it on future saves. Other browsers
|
| 170 |
+
and embedded Hugging Face pages use the normal download flow.
|
| 171 |
</p>
|
| 172 |
</section>
|
| 173 |
|
|
|
|
| 1159 |
</section>
|
| 1160 |
</main>
|
| 1161 |
|
| 1162 |
+
<script type="module" src="/assets/app.js?v=20260602-session-midi-zip3"></script>
|
| 1163 |
</body>
|
| 1164 |
</html>
|