Open Project
-
+
From 1e4fdfc8c5c4df1e71c434d52eaa80b21e71d7d6 Mon Sep 17 00:00:00 2001
From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com>
Date: Mon, 20 Apr 2026 12:09:57 -0700
Subject: [PATCH 25/30] refactor: replace purple accent color with light blue
theme and beautify Open Project modal
---
src/components/ChatBox.css | 22 ++++-----
src/components/Toolbar.css | 14 ++++--
src/components/common/FileImportModal.css | 17 +++++--
src/components/common/OpenProjectModal.css | 56 +++++++++++-----------
src/components/piano-roll/PianoRoll.css | 19 +++++---
src/components/track/Track.css | 20 ++++----
src/index.css | 16 ++-----
src/styles/shared.css | 19 ++++++--
8 files changed, 104 insertions(+), 79 deletions(-)
diff --git a/src/components/ChatBox.css b/src/components/ChatBox.css
index 6018761..185459b 100644
--- a/src/components/ChatBox.css
+++ b/src/components/ChatBox.css
@@ -15,7 +15,8 @@
.chatbox-header {
display: flex;
align-items: center;
- justify-content: space-between; /* left title, right actions */
+ justify-content: space-between;
+ /* left title, right actions */
background-color: #3a3a3a;
height: 40px;
border-bottom: 1px solid #4a4a4a;
@@ -229,7 +230,7 @@
.abort-link {
background: none !important;
border: none !important;
- color: #7b68ee !important;
+ color: #5a9fd4 !important;
text-decoration: underline !important;
cursor: pointer !important;
padding: 0 !important;
@@ -288,14 +289,12 @@
/* Processing wave animation */
.processing-wave {
- background: linear-gradient(
- 90deg,
- rgba(135, 206, 250, 0.5) 0%,
- rgba(135, 206, 250, 0.8) 25%,
- rgb(171, 218, 250) 50%,
- rgba(135, 206, 250, 0.8) 75%,
- rgba(135, 206, 250, 0.5) 100%
- );
+ background: linear-gradient(90deg,
+ rgba(135, 206, 250, 0.5) 0%,
+ rgba(135, 206, 250, 0.8) 25%,
+ rgb(171, 218, 250) 50%,
+ rgba(135, 206, 250, 0.8) 75%,
+ rgba(135, 206, 250, 0.5) 100%);
background-size: 200% 100%;
background-clip: text;
-webkit-background-clip: text;
@@ -307,7 +306,8 @@
0% {
background-position: 200% 0%;
}
+
100% {
background-position: -200% 0%;
}
-}
+}
\ No newline at end of file
diff --git a/src/components/Toolbar.css b/src/components/Toolbar.css
index e3a8b5c..20c9e8f 100644
--- a/src/components/Toolbar.css
+++ b/src/components/Toolbar.css
@@ -9,7 +9,9 @@
border-bottom: 1px solid #3a3a3a;
}
-.toolbar-left, .toolbar-center, .toolbar-right {
+.toolbar-left,
+.toolbar-center,
+.toolbar-right {
display: flex;
align-items: center;
}
@@ -44,7 +46,8 @@
border: none;
color: #e0e0e0;
margin: 0 5px;
- padding: 5px;
+ width: 28px;
+ height: 28px;
cursor: pointer;
display: flex;
align-items: center;
@@ -63,6 +66,11 @@
cursor: not-allowed;
}
+.toolbar button:active {
+ background-color: #5a5a5a;
+ transform: scale(0.95);
+}
+
.record-btn {
color: #ff4444;
}
@@ -198,4 +206,4 @@
color: #e0e0e0;
min-width: 20px;
text-align: center;
-}
+}
\ No newline at end of file
diff --git a/src/components/common/FileImportModal.css b/src/components/common/FileImportModal.css
index 6000beb..29ec85b 100644
--- a/src/components/common/FileImportModal.css
+++ b/src/components/common/FileImportModal.css
@@ -30,6 +30,7 @@
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
+
to {
opacity: 1;
transform: scale(1) translateY(0);
@@ -41,6 +42,7 @@
opacity: 1;
transform: scale(1) translateY(0);
}
+
to {
opacity: 0;
transform: scale(0.95) translateY(-10px);
@@ -48,8 +50,13 @@
}
@keyframes fileImportOverlayFadeOut {
- from { opacity: 1; }
- to { opacity: 0; }
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
}
.file-import-overlay-closing {
@@ -106,12 +113,12 @@
}
.file-import-drop-zone:hover {
- border-color: #7b68ee;
+ border-color: #5a9fd4;
background-color: rgba(123, 104, 238, 0.05);
}
.file-import-drop-zone.drag-over {
- border-color: #7b68ee;
+ border-color: #5a9fd4;
background-color: rgba(123, 104, 238, 0.1);
transform: scale(1.02);
}
@@ -182,4 +189,4 @@
background-color: #4a8fc4;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(90, 159, 212, 0.3);
-}
+}
\ No newline at end of file
diff --git a/src/components/common/OpenProjectModal.css b/src/components/common/OpenProjectModal.css
index a6cb7a5..2e1f28b 100644
--- a/src/components/common/OpenProjectModal.css
+++ b/src/components/common/OpenProjectModal.css
@@ -69,7 +69,6 @@
justify-content: space-between;
align-items: center;
padding: 20px;
- border-bottom: 1px solid #3a3a3a;
background-color: #252525;
flex-shrink: 0;
}
@@ -104,7 +103,6 @@
.open-project-filter {
padding: 10px 12px;
background-color: #252525;
- border-bottom: 1px solid #3a3a3a;
flex-shrink: 0;
}
@@ -127,7 +125,7 @@
}
.open-project-filter input:focus {
- border-color: #7b68ee;
+ border-color: #5a9fd4;
}
.open-project-filter input::placeholder {
@@ -137,8 +135,7 @@
/* View toggle (Projects / Trash) */
.open-project-view-toggle {
display: flex;
- border: 1px solid #3a3a3a;
- border-radius: 4px;
+ border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
}
@@ -147,14 +144,11 @@
background-color: #1e1e1e;
color: #999;
border: none;
- padding: 5px 12px;
- font-size: 12px;
+ padding: 8px 20px;
+ font-size: 14px;
+ font-weight: 500;
cursor: pointer;
- transition: background-color 0.1s, color 0.1s;
-}
-
-.open-project-toggle-btn:not(:last-child) {
- border-right: 1px solid #3a3a3a;
+ transition: all 0.2s ease;
}
.open-project-toggle-btn:hover {
@@ -162,7 +156,7 @@
}
.open-project-toggle-btn.active {
- background-color: #7b68ee;
+ background-color: #5a9fd4;
color: #fff;
}
@@ -170,7 +164,6 @@
.open-project-trash-hint {
padding: 6px 12px;
background-color: #3a2a1a;
- border-bottom: 1px solid #5a3a1a;
color: #d4a054;
font-size: 11px;
flex-shrink: 0;
@@ -182,7 +175,6 @@
align-items: center;
padding: 6px 12px;
background-color: #252525;
- border-bottom: 1px solid #3a3a3a;
font-size: 11px;
color: #999;
text-transform: uppercase;
@@ -205,7 +197,7 @@
}
.open-project-sort-col.active {
- color: #7b68ee;
+ color: #5a9fd4;
}
.open-project-sort-col-name {
@@ -230,7 +222,6 @@
display: flex;
align-items: center;
padding: 10px 12px;
- border-bottom: 1px solid #333;
cursor: pointer;
transition: background-color 0.1s;
}
@@ -272,32 +263,37 @@
/* Open button */
.open-project-item-open-btn {
- background-color: #7b68ee;
+ background-color: #5a9fd4;
color: #fff;
border: none;
- border-radius: 4px;
- padding: 5px 14px;
- font-size: 12px;
+ border-radius: 6px;
+ padding: 8px 20px;
+ font-size: 14px;
+ font-weight: 500;
cursor: pointer;
+ transition: all 0.2s ease;
}
.open-project-item-open-btn:hover {
- background-color: #6a58d6;
+ background-color: #4a8fc4;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(90, 159, 212, 0.3);
}
/* Icon action buttons (duplicate, delete, restore, perm-delete) */
.open-project-item-action-btn {
background: transparent;
border: 1px solid #555;
- border-radius: 4px;
+ border-radius: 6px;
color: #bbb;
- padding: 5px 8px;
- font-size: 12px;
+ padding: 8px 12px;
+ font-size: 14px;
+ font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
- transition: background-color 0.1s, color 0.1s, border-color 0.1s;
+ transition: all 0.2s ease;
}
.open-project-item-action-btn:hover {
@@ -306,12 +302,18 @@
border-color: #777;
}
-.open-project-item-permdelete-btn:hover {
+.open-project-item-delete-btn:hover {
background-color: #5a2020;
color: #ff6666;
border-color: #ff4444;
}
+.open-project-item-permdelete-btn:hover {
+ background-color: #7a1010;
+ color: #ff4444;
+ border-color: #ff2222;
+}
+
.open-project-item-restore-btn:hover {
background-color: #1a3a2a;
color: #66cc88;
diff --git a/src/components/piano-roll/PianoRoll.css b/src/components/piano-roll/PianoRoll.css
index 386b265..fbb676b 100644
--- a/src/components/piano-roll/PianoRoll.css
+++ b/src/components/piano-roll/PianoRoll.css
@@ -1,6 +1,6 @@
/* For the piano roll (to be implemented later) */
.piano-roll {
- background-color: #7b68ee;
+ background-color: #5a9fd4;
height: 100%;
}
@@ -18,7 +18,8 @@
/* Piano notes */
.piano-note {
position: absolute;
- background-color: #ff5555; /* Red color */
+ background-color: #ff5555;
+ /* Red color */
border: 1px solid #999;
border-radius: 2px;
z-index: 10;
@@ -72,7 +73,8 @@
padding: 0 10px;
user-select: none;
position: relative;
- z-index: 100; /* Ensure toolbar and its dropdowns appear above piano roll content */
+ z-index: 100;
+ /* Ensure toolbar and its dropdowns appear above piano roll content */
}
/* Override pointer-events for piano roll toolbar sections */
@@ -138,7 +140,8 @@
position: sticky;
top: 0;
z-index: 20;
- cursor: pointer; /* Show pointer cursor on hover to indicate interactivity */
+ cursor: pointer;
+ /* Show pointer cursor on hover to indicate interactivity */
}
.piano-grid-header:hover {
@@ -153,8 +156,10 @@
.piano-roll-body {
display: flex;
- min-height: calc(8 * 12 * var(--region-piano-key-height)); /* 8 octaves * 12 notes * piano key height */
- width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width)); /* 32 bars * 160px width + piano keys width */
+ min-height: calc(8 * 12 * var(--region-piano-key-height));
+ /* 8 octaves * 12 notes * piano key height */
+ width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width));
+ /* 32 bars * 160px width + piano keys width */
}
.piano-keys-container {
@@ -262,4 +267,4 @@
align-items: center;
justify-content: center;
z-index: 100;
-}
+}
\ No newline at end of file
diff --git a/src/components/track/Track.css b/src/components/track/Track.css
index d273679..c78f7d4 100644
--- a/src/components/track/Track.css
+++ b/src/components/track/Track.css
@@ -2,13 +2,13 @@
.track-grid {
display: block;
height: 120px;
- min-width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width)); /* 32 bars * --track-grid-bar-width */
+ min-width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width));
+ /* 32 bars * --track-grid-bar-width */
background-size: var(--track-grid-bar-width) 120px;
background-image:
/* Vertical lines for bars */
linear-gradient(to right,
- transparent calc(var(--track-grid-bar-width) - 1px), #3a3a3a calc(var(--track-grid-bar-width) - 1px), #3a3a3a var(--track-grid-bar-width)
- );
+ transparent calc(var(--track-grid-bar-width) - 1px), #3a3a3a calc(var(--track-grid-bar-width) - 1px), #3a3a3a var(--track-grid-bar-width));
position: relative;
border-bottom: 1px solid #3a3a3a;
}
@@ -73,11 +73,11 @@
/* Drag and drop styles */
.track-info.drag-over {
- border-top: 2px solid #7b68ee;
+ border-top: 2px solid #5a9fd4;
}
.track-grid.drag-over {
- border-top: 2px solid #7b68ee;
+ border-top: 2px solid #5a9fd4;
}
/* Dragging state */
@@ -103,7 +103,7 @@
}
.track-name:hover {
- color: #7b68ee;
+ color: #5a9fd4;
}
.track-name:hover::before {
@@ -200,11 +200,13 @@
}
.pan-controls button.solo.active {
- background: #c8a400; /* yellow */
+ background: #c8a400;
+ /* yellow */
color: #101010;
}
.pan-controls button.mute.active {
- background: #d32f2f; /* red */
+ background: #d32f2f;
+ /* red */
color: #ffffff;
-}
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 26e4e0a..a25c276 100644
--- a/src/index.css
+++ b/src/index.css
@@ -19,7 +19,8 @@
box-sizing: border-box;
}
-html, body {
+html,
+body {
height: 100%;
width: 100%;
overflow: hidden;
@@ -49,15 +50,6 @@ button {
transition: border-color 0.25s;
}
-button:hover {
- border-color: #646cff;
-}
-
-button:focus,
-button:focus-visible {
- outline: 2px auto rgba(100, 108, 255, 0.5);
-}
-
textarea {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 12px;
@@ -71,7 +63,7 @@ input[type="range"] {
height: 4px;
border-radius: 2px;
outline: none;
- }
+}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
@@ -81,4 +73,4 @@ input[type="range"]::-webkit-slider-thumb {
background: #e0e0e0;
border-radius: 50%;
cursor: pointer;
-}
+}
\ No newline at end of file
diff --git a/src/styles/shared.css b/src/styles/shared.css
index 2f1253e..30ee42a 100644
--- a/src/styles/shared.css
+++ b/src/styles/shared.css
@@ -1,6 +1,7 @@
/* Shared toolbar layout classes (used by Toolbar and PianoRollToolbar) */
-.toolbar-left, .toolbar-right {
+.toolbar-left,
+.toolbar-right {
display: flex;
align-items: center;
z-index: 1;
@@ -119,7 +120,15 @@
}
@keyframes buttonBlink {
- 0% { background-color: #333; }
- 50% { background-color: #555; }
- 100% { background-color: #333; }
-}
+ 0% {
+ background-color: #333;
+ }
+
+ 50% {
+ background-color: #555;
+ }
+
+ 100% {
+ background-color: #333;
+ }
+}
\ No newline at end of file
From a71de0c7899a4b3f47937ee2fa3fb6dfbd8fff52 Mon Sep 17 00:00:00 2001
From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com>
Date: Mon, 20 Apr 2026 15:33:02 -0700
Subject: [PATCH 26/30] fix: minor updated color palette for some components;
updated default region/track names for stem separation.
---
src/components/ChatBox.css | 2 +-
src/components/KGOnePanel.css | 23 +++++----
src/components/KGOnePanel.tsx | 5 +-
.../settings/sections/GeneralSettings.tsx | 48 +++++++++----------
src/components/track/RegionItem.tsx | 2 +-
.../commands/region/ImportStemsCommand.ts | 3 +-
src/styles/shared.css | 2 +-
7 files changed, 47 insertions(+), 38 deletions(-)
diff --git a/src/components/ChatBox.css b/src/components/ChatBox.css
index 185459b..4f61eec 100644
--- a/src/components/ChatBox.css
+++ b/src/components/ChatBox.css
@@ -149,7 +149,7 @@
}
.chatbox-input:focus {
- border-color: #6a6a6a;
+ border-color: #5a9fd4;
background-color: #4a4a4a;
}
diff --git a/src/components/KGOnePanel.css b/src/components/KGOnePanel.css
index 1cbca22..d37a191 100644
--- a/src/components/KGOnePanel.css
+++ b/src/components/KGOnePanel.css
@@ -59,7 +59,7 @@
.kgone-tab.active {
color: #e0e0e0;
- border-bottom-color: #7a9cff;
+ border-bottom-color: #5a9fd4;
}
/* Scrollable body */
@@ -102,7 +102,7 @@
.kgone-input:focus,
.kgone-textarea:focus,
.kgone-select:focus {
- border-color: #5a6aaa;
+ border-color: #5a9fd4;
}
.kgone-textarea {
@@ -277,7 +277,7 @@
}
.kgone-player-play-btn:hover {
- color: #7a9cff;
+ color: #5a9fd4;
}
.kgone-player-progress-track {
@@ -296,7 +296,7 @@
.kgone-player-progress-fill {
height: 100%;
- background-color: #7a9cff;
+ background-color: #5a9fd4;
border-radius: 2px;
pointer-events: none;
transition: width 0.1s linear;
@@ -336,7 +336,7 @@
}
.kgone-player-download-btn:hover {
- color: #7a9cff;
+ color: #5a9fd4;
}
/* ── Generation state feedback ─────────────────────────────────────────────── */
@@ -364,8 +364,13 @@
}
@keyframes kgone-spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
}
/* Stem players (Separator tab) */
@@ -390,7 +395,7 @@
.kgone-checkbox-row input[type="checkbox"] {
width: 14px;
height: 14px;
- accent-color: #7a9cff;
+ accent-color: #5a9fd4;
cursor: pointer;
}
@@ -399,4 +404,4 @@
font-size: 11px;
cursor: pointer;
user-select: none;
-}
+}
\ No newline at end of file
diff --git a/src/components/KGOnePanel.tsx b/src/components/KGOnePanel.tsx
index 927609e..59d5de6 100644
--- a/src/components/KGOnePanel.tsx
+++ b/src/components/KGOnePanel.tsx
@@ -826,6 +826,7 @@ const SeparatorTab: React.FC = () => {
const taskIdRef = useRef
('');
const originalRegionRef = useRef<{
regionName: string;
+ trackName: string;
startFromBeat: number;
trackIndex: number;
} | null>(null);
@@ -866,6 +867,7 @@ const SeparatorTab: React.FC = () => {
// Capture snapshot before anything changes — selection may shift during generation
originalRegionRef.current = {
regionName: selectedAudioRegion.region.getName(),
+ trackName: selectedAudioRegion.trackName,
startFromBeat: selectedAudioRegion.region.getStartFromBeat(),
trackIndex: selectedAudioRegion.trackIndex,
};
@@ -1043,7 +1045,8 @@ const SeparatorTab: React.FC = () => {
await KGAudioFileStorage.storeAudioFile(projectName, fileId, audioFile);
return {
- trackName: `${snap.regionName} (${stem.name})`,
+ trackName: `${snap.trackName} - ${stem.name}`,
+ regionName: `${snap.regionName} - ${stem.name}`,
audioFileId: fileId,
audioFileName: fileName,
audioDurationSeconds: toneBuffer.duration,
diff --git a/src/components/settings/sections/GeneralSettings.tsx b/src/components/settings/sections/GeneralSettings.tsx
index 664b9ba..a9a644c 100644
--- a/src/components/settings/sections/GeneralSettings.tsx
+++ b/src/components/settings/sections/GeneralSettings.tsx
@@ -203,17 +203,17 @@ const GeneralSettings: React.FC = () => {
General
-
+
LLM Provider
-
+
LLM Provider
-
handleLlmProviderChange(e.target.value)}
>
@@ -245,13 +245,13 @@ const GeneralSettings: React.FC = () => {
-
+
Model
@@ -365,13 +365,13 @@ const GeneralSettings: React.FC = () => {
-
+
Model
@@ -423,13 +423,13 @@ const GeneralSettings: React.FC = () => {
-
+
Base URL
- handleCompatibleBaseUrlChange(e.target.value)}
/>
-
+
Model
-
{
e.preventDefault();
handleSoundfontBaseUrlChange('https://cdn.jsdelivr.net/npm/soundfont-for-samplers/FluidR3_GM/');
}}
- style={{ color: '#646cff', textDecoration: 'underline', cursor: 'pointer' }}
+ style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }}
>
Restore default
diff --git a/src/components/track/RegionItem.tsx b/src/components/track/RegionItem.tsx
index 7930edb..d6f6934 100644
--- a/src/components/track/RegionItem.tsx
+++ b/src/components/track/RegionItem.tsx
@@ -548,7 +548,7 @@ const RegionItem: React.FC
= ({
data-is-dragging={isDragging}
>
- {audioRegion ? audioRegion.getAudioFileName() : name}
+ {name}
{!audioRegion && (
diff --git a/src/core/commands/region/ImportStemsCommand.ts b/src/core/commands/region/ImportStemsCommand.ts
index 070cbf5..090f280 100644
--- a/src/core/commands/region/ImportStemsCommand.ts
+++ b/src/core/commands/region/ImportStemsCommand.ts
@@ -12,6 +12,7 @@ import { KGAudioInterface } from '../../audio-interface/KGAudioInterface';
*/
export interface StemImportEntry {
trackName: string;
+ regionName: string;
audioFileId: string;
audioFileName: string;
audioDurationSeconds: number;
@@ -128,7 +129,7 @@ export class ImportStemsCommand extends KGCommand {
regionId,
trackId.toString(),
track.getTrackIndex(),
- stem.audioFileName,
+ stem.regionName,
this.insertBeat,
durationInBeats,
stem.audioFileId,
diff --git a/src/styles/shared.css b/src/styles/shared.css
index 30ee42a..00ce7a2 100644
--- a/src/styles/shared.css
+++ b/src/styles/shared.css
@@ -27,7 +27,7 @@
align-items: center;
justify-content: center;
z-index: 1005;
- margin-left: -50px;
+ margin-left: -120px;
}
/* Tool buttons (used by Toolbar and PianoRollToolbar) */
From f7f79cdb32cbdd66a2834f67e85b22aec7a27402 Mon Sep 17 00:00:00 2001
From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com>
Date: Mon, 20 Apr 2026 21:49:25 -0700
Subject: [PATCH 27/30] fix: extract stems from trimmed audio is using full
song
---
src/components/KGOnePanel.tsx | 31 ++++++++++++++++++---
src/util/audioUtil.ts | 52 +++++++++++++++++++++++++++++++++++
2 files changed, 79 insertions(+), 4 deletions(-)
create mode 100644 src/util/audioUtil.ts
diff --git a/src/components/KGOnePanel.tsx b/src/components/KGOnePanel.tsx
index 59d5de6..db30a23 100644
--- a/src/components/KGOnePanel.tsx
+++ b/src/components/KGOnePanel.tsx
@@ -10,6 +10,7 @@ import { KGAudioFileStorage } from '../core/io/KGAudioFileStorage';
import { ConfigManager } from '../core/config/ConfigManager';
import { DEBUG_MODE } from '../constants/uiConstants';
import { fetchWithRetry } from '../util/retryUtil';
+import { sliceAudioToWav } from '../util/audioUtil';
import type { KeySignature } from '../core/KGProject';
import { ImportStemsCommand } from '../core/commands';
import type { StemImportEntry } from '../core/commands';
@@ -915,18 +916,40 @@ const SeparatorTab: React.FC = () => {
const audioFileId = selectedAudioRegion.region.getAudioFileId();
const audioFileName = selectedAudioRegion.region.getAudioFileName();
- const arrayBuffer = await KGAudioFileStorage.loadAudioFile(projectName, audioFileId);
- const audioFile = new File([arrayBuffer], audioFileName, { type: 'audio/mpeg' });
+ const clipStart = selectedAudioRegion.region.getClipStartOffsetSeconds();
+ const fullDuration = selectedAudioRegion.region.getAudioDurationSeconds();
+ const regionLengthSec = selectedAudioRegion.region.getLength() * (60 / bpm);
+ const effectiveDuration = Math.min(regionLengthSec, fullDuration - clipStart);
+
+ const rawBuffer = await KGAudioFileStorage.loadAudioFile(projectName, audioFileId);
+ const needsSlice = clipStart > 0.01 || effectiveDuration < fullDuration - 0.01;
+
+ let uploadBuffer: ArrayBuffer;
+ let uploadFileName: string;
+ let uploadMimeType: string;
+
+ if (needsSlice) {
+ setGenHint('Trimming audio to region range...');
+ uploadBuffer = await sliceAudioToWav(rawBuffer, clipStart, effectiveDuration);
+ uploadFileName = audioFileName.replace(/\.[^.]+$/, '.wav');
+ uploadMimeType = 'audio/wav';
+ } else {
+ uploadBuffer = rawBuffer;
+ uploadFileName = audioFileName;
+ uploadMimeType = 'audio/mpeg';
+ }
+
+ const audioFile = new File([uploadBuffer], uploadFileName, { type: uploadMimeType });
const formData = new FormData();
- formData.append('file', audioFile, audioFileName);
+ formData.append('file', audioFile, uploadFileName);
formData.append('model_filename', model);
if (signal.aborted) return;
setGenHint('Submitting separation request...');
- kgoneLog('REQ', 'POST /v1/separator/separate', { file: audioFileName, model_filename: model });
+ kgoneLog('REQ', 'POST /v1/separator/separate', { file: uploadFileName, model_filename: model });
const sepResp = await fetch(`${baseUrl}/v1/separator/separate`, {
method: 'POST',
body: formData,
diff --git a/src/util/audioUtil.ts b/src/util/audioUtil.ts
new file mode 100644
index 0000000..e3ec6ca
--- /dev/null
+++ b/src/util/audioUtil.ts
@@ -0,0 +1,52 @@
+function writeString(view: DataView, offset: number, str: string) {
+ for (let i = 0; i < str.length; i++)
+ view.setUint8(offset + i, str.charCodeAt(i));
+}
+
+export async function sliceAudioToWav(
+ arrayBuffer: ArrayBuffer,
+ startSeconds: number,
+ durationSeconds: number
+): Promise
{
+ const audioCtx = new AudioContext();
+ const decoded = await audioCtx.decodeAudioData(arrayBuffer);
+ audioCtx.close();
+
+ const { sampleRate, numberOfChannels: numCh } = decoded;
+ const startSample = Math.floor(startSeconds * sampleRate);
+ const numSamples = Math.min(
+ Math.floor(durationSeconds * sampleRate),
+ decoded.length - startSample
+ );
+
+ const bytesPerSample = 2; // 16-bit PCM
+ const dataSize = numSamples * numCh * bytesPerSample;
+ const buf = new ArrayBuffer(44 + dataSize);
+ const view = new DataView(buf);
+
+ writeString(view, 0, 'RIFF');
+ view.setUint32(4, 36 + dataSize, true);
+ writeString(view, 8, 'WAVE');
+ writeString(view, 12, 'fmt ');
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true); // PCM
+ view.setUint16(22, numCh, true);
+ view.setUint32(24, sampleRate, true);
+ view.setUint32(28, sampleRate * numCh * bytesPerSample, true);
+ view.setUint16(32, numCh * bytesPerSample, true);
+ view.setUint16(34, 16, true);
+ writeString(view, 36, 'data');
+ view.setUint32(40, dataSize, true);
+
+ let offset = 44;
+ for (let i = 0; i < numSamples; i++) {
+ for (let ch = 0; ch < numCh; ch++) {
+ const s = decoded.getChannelData(ch)[startSample + i];
+ const c = Math.max(-1, Math.min(1, s));
+ view.setInt16(offset, c < 0 ? c * 32768 : c * 32767, true);
+ offset += 2;
+ }
+ }
+
+ return buf;
+}
From d6001a6ceb01463537aa7c4fee5dae24f697246f Mon Sep 17 00:00:00 2001
From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com>
Date: Tue, 21 Apr 2026 19:03:40 -0700
Subject: [PATCH 28/30] chore: added two debugger automation utilities
---
src/core/KGDebugger.ts | 148 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 148 insertions(+)
diff --git a/src/core/KGDebugger.ts b/src/core/KGDebugger.ts
index 6fc188a..55cf488 100644
--- a/src/core/KGDebugger.ts
+++ b/src/core/KGDebugger.ts
@@ -29,6 +29,8 @@ export class KGDebugger {
'testExtractXMLFromString(input)',
'testToolCall(jsonInput)',
'inputChatBox(content, interval?)',
+ 'inputKGOneCaption(content, interval?)',
+ 'inputKGOneLyrics(content, interval?)',
'opfs(command)',
'startShell()',
]);
@@ -346,6 +348,8 @@ export class KGDebugger {
console.log(" testExtractXMLFromString(input) - Test XML extraction from string");
console.log(" testToolCall(input) - Execute tool call(s) from JSON and show results");
console.log(" inputChatBox(content, interval?) - Type into ChatBox textarea and submit with Enter");
+ console.log(" inputKGOneCaption(content, interval?) - Type into KGOne Caption textarea (Full Song tab)");
+ console.log(" inputKGOneLyrics(content, interval?) - Type into KGOne Lyrics textarea (Full Song tab)");
console.log(" opfs(command) - OPFS file browser (pwd, ls, cd, cat, dl, rm)");
console.log(" startShell() - Start interactive OPFS shell (prompt-based loop)");
console.log(" help() - Show this help");
@@ -359,6 +363,10 @@ export class KGDebugger {
console.log(' await KGStudio.KGDebugger.testToolCall(\'{"name":"read_music","arguments":{"start":0,"length":8}}\')');
console.log(' await KGStudio.KGDebugger.testToolCall({name:"add_notes",arguments:{notes:[{pitch:"C4",start:0,length:1}]}})');
console.log(' await KGStudio.KGDebugger.testToolCall([{name:"remove_notes",arguments:{start:0,end_beat:4}},{name:"read_music",arguments:{}}])');
+ console.log("");
+ console.log("💡 KGOne input examples:");
+ console.log(' await KGStudio.KGDebugger.inputKGOneCaption("Genre: Eurodance, 90s dance-pop, upbeat electronic...", 30)');
+ console.log(' await KGStudio.KGDebugger.inputKGOneLyrics("[Verse 1]\\nYour lyrics here...\\n\\n[Chorus]\\n...", 30)');
}
/**
@@ -451,6 +459,146 @@ export class KGDebugger {
}
}
+ /**
+ * Type content into K.G.One Caption textarea character-by-character.
+ * Honors auto-resize (by dispatching 'input' events).
+ * Note: Unlike ChatBox, this does NOT auto-submit (no Enter key dispatch).
+ * @param content - The text to type into the caption input
+ * @param interval - Delay in ms between characters (default 30ms)
+ */
+ public async inputKGOneCaption(content: string, interval: number = 30): Promise {
+ try {
+ const label = Array.from(document.querySelectorAll('label.kgone-label')).find(
+ el => el.textContent?.trim() === 'Caption'
+ );
+ if (!label) {
+ console.error('❌ Caption label not found. Ensure KGOne panel is visible with Full Song tab selected.');
+ return;
+ }
+
+ const textarea = label.nextElementSibling as HTMLTextAreaElement | null;
+ if (!textarea || textarea.tagName !== 'TEXTAREA') {
+ console.error('❌ Caption textarea not found after the label.');
+ return;
+ }
+
+ textarea.focus();
+
+ const valueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
+ const setValue = (val: string) => {
+ if (valueSetter) {
+ valueSetter.call(textarea, val);
+ } else {
+ textarea.value = val;
+ }
+ };
+
+ const dispatchInput = (data?: string) => {
+ const ev = typeof InputEvent !== 'undefined'
+ ? new InputEvent('input', { bubbles: true, data, inputType: 'insertText' })
+ : new Event('input', { bubbles: true });
+ textarea.dispatchEvent(ev);
+ };
+
+ setValue('');
+ dispatchInput('');
+
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+ let typed = '';
+ for (let i = 0; i < content.length; i++) {
+ typed += content[i];
+ setValue(typed);
+ dispatchInput(content[i]);
+
+ try {
+ textarea.selectionStart = textarea.selectionEnd = typed.length;
+ } catch {
+ }
+
+ textarea.scrollTop = textarea.scrollHeight;
+
+ if (interval > 0) {
+ await sleep(interval);
+ }
+ }
+
+ await sleep(Math.max(30, interval));
+ } catch (error) {
+ console.error('❌ Error in inputKGOneCaption:', error);
+ }
+ }
+
+ /**
+ * Type content into K.G.One Lyrics textarea character-by-character.
+ * Honors auto-resize (by dispatching 'input' events).
+ * Note: Unlike ChatBox, this does NOT auto-submit (no Enter key dispatch).
+ * @param content - The text to type into the lyrics input
+ * @param interval - Delay in ms between characters (default 30ms)
+ */
+ public async inputKGOneLyrics(content: string, interval: number = 30): Promise {
+ try {
+ const label = Array.from(document.querySelectorAll('label.kgone-label')).find(
+ el => el.textContent?.trim() === 'Lyrics'
+ );
+ if (!label) {
+ console.error('❌ Lyrics label not found. Ensure KGOne panel is visible with Full Song tab selected.');
+ return;
+ }
+
+ const textarea = label.nextElementSibling as HTMLTextAreaElement | null;
+ if (!textarea || textarea.tagName !== 'TEXTAREA') {
+ console.error('❌ Lyrics textarea not found after the label.');
+ return;
+ }
+
+ textarea.focus();
+
+ const valueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
+ const setValue = (val: string) => {
+ if (valueSetter) {
+ valueSetter.call(textarea, val);
+ } else {
+ textarea.value = val;
+ }
+ };
+
+ const dispatchInput = (data?: string) => {
+ const ev = typeof InputEvent !== 'undefined'
+ ? new InputEvent('input', { bubbles: true, data, inputType: 'insertText' })
+ : new Event('input', { bubbles: true });
+ textarea.dispatchEvent(ev);
+ };
+
+ setValue('');
+ dispatchInput('');
+
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+ let typed = '';
+ for (let i = 0; i < content.length; i++) {
+ typed += content[i];
+ setValue(typed);
+ dispatchInput(content[i]);
+
+ try {
+ textarea.selectionStart = textarea.selectionEnd = typed.length;
+ } catch {
+ }
+
+ textarea.scrollTop = textarea.scrollHeight;
+
+ if (interval > 0) {
+ await sleep(interval);
+ }
+ }
+
+ await sleep(Math.max(30, interval));
+ } catch (error) {
+ console.error('❌ Error in inputKGOneLyrics:', error);
+ }
+ }
+
/**
* Start an interactive OPFS shell using browser prompt() dialogs.
* Each prompt shows the current working directory. Type commands and click OK.
From 3bce450637f04b503b0eafed3a67a96fc5281a6b Mon Sep 17 00:00:00 2001
From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com>
Date: Fri, 24 Apr 2026 20:03:18 -0700
Subject: [PATCH 29/30] fix: added base url presents for OpenAI Compatible
settings
---
.../settings/sections/GeneralSettings.tsx | 46 +++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/src/components/settings/sections/GeneralSettings.tsx b/src/components/settings/sections/GeneralSettings.tsx
index a9a644c..cbdc30a 100644
--- a/src/components/settings/sections/GeneralSettings.tsx
+++ b/src/components/settings/sections/GeneralSettings.tsx
@@ -455,6 +455,52 @@ const GeneralSettings: React.FC = () => {
value={compatibleBaseUrl}
onChange={(e) => handleCompatibleBaseUrlChange(e.target.value)}
/>
+
From 3261356bc9db336a8dcc900d80d4e5c1772ec6c3 Mon Sep 17 00:00:00 2001
From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com>
Date: Fri, 24 Apr 2026 22:00:09 -0700
Subject: [PATCH 30/30] docs: updated README.md, added K.G.One Music Studio
information
---
README.md | 76 +++++++++++++++++++++++++++++++++++++---
docs/KGOne-Demo-GIF.gif | Bin 0 -> 9379997 bytes
2 files changed, 71 insertions(+), 5 deletions(-)
create mode 100644 docs/KGOne-Demo-GIF.gif
diff --git a/README.md b/README.md
index 2a43fab..7c90ecc 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,28 @@
-## K.G.Studio — A Browser-based DAW with AI Assistant
+# K.G.Studio — A Browser-based DAW with AI Assistant
+
+## New!
+
+> ### ✦ [**K.G.One Music Studio is available now.**](https://github.com/KGAudioLab/K.G.One) ✦
+> [**K.G.One Music Studio**](https://github.com/KGAudioLab/K.G.One) is a fully local, open-source integrated platform built around **K.G.Studio** (this project). It bundles [ACE-Step 1.5](https://github.com/ace-step/ACE-Step-1.5) for full-song generation, [Foundation-1](https://huggingface.co/RoyalCities/Foundation-1) for audio clip and MIDI loop generation, and [python-audio-separator (UVR5)](https://github.com/nomadkaraoke/python-audio-separator) for stem separation — bringing GPU-accelerated AI music generation directly into your browser-based production workflow.
+
+## What is K.G.Studio?
K.G.Studio is a lightweight, modern DAW that runs entirely in the browser with **K.G.Studio Musician Assistant** at its core. It features realistic instrument playback via Tone.js samplers, a piano‑roll editor, track and region management with full undo/redo, project persistence to IndexedDB, a configurable settings panel, and an integrated AI assistant with tool execution.
**K.G.Studio Musician Assistant** is an AI assistance agent for harmony, arrangement, and note editing — but not full auto‑composition.
+
+
+
+
+> Note: Full-Song Generation Feature, Audio Clip Generation Feature, and Stem Separation Feature requires [**K.G.One Music Studio**](https://github.com/KGAudioLab/K.G.One) integration.
+
## Latest Updates
+- **2026.04.24**: Added [**K.G.One Music Studio**](https://github.com/KGAudioLab/K.G.One) integration! When K.G.Studio connects to a local K.G.One server, the **K.G.One Music Generator** panel (magic wand button ✦ in the toolbar) becomes available with three AI-powered tools: **Full Song Generation** (powered by ACE-Step 1.5 — generate full-length songs from text prompts), **Clip Generation** (powered by Foundation-1 — generate instrument clips and MIDI loops from text), and **Stem Separation** (powered by python-audio-separator — split any audio into vocals, instrumentals, and more). Generated audio and MIDI can be previewed instantly and dragged directly onto your tracks. K.G.One runs entirely on your own machine (Windows/Linux, CUDA GPU required); see the [K.G.One repository](https://github.com/KGAudioLab/K.G.One) for setup instructions.
- **2026.04.11**: Migrated project storage from IndexedDB to OPFS (Origin Private File System) with a folder-based structure for better media file handling. Added audio track support with WAV/MP3 import, playback, looping, and non-destructive region trimming. Added bounce-to-WAV/MP3 export via offline rendering.
- **2026.04.05**: Migrated the AI agent from XML-based tool calling to native OpenAI SDK function calling for improved reliability and compatibility. Added new LLM model options including GPT-5.4 series.
- **2026.01.23**: Implemented seamless loop playback! Drag on the bar numbers to set loop range, or toggle loop mode with the Loop button in the toolbar. Loop playback uses `Tone.js`'s native looping for sample-accurate, gap-free looping.
@@ -52,10 +66,12 @@ This project investigates how AI-human collaboration can enhance creative music-
- [Click here to get a free OpenRouter API Key](https://openrouter.ai/keys) (you may need an OpenRouter account).
- In **Settings ⚙️ → General → LLM Provider**, select **OpenAI Compatible**.
- In **OpenAI Compatible Server → Key**, paste your key. (Note: on non‑localhost, your key isn't persisted by default for security; you can enable "Persist API Keys on Non-Localhost" in Settings to persist them, though this may increase XSS risk.)
- - In **OpenAI Compatible Server → Model**, enter `qwen/qwen3.6-plus:free`. (Note: this is a free model [qwen3.6-plus](https://openrouter.ai/qwen/qwen3.6-plus:free); non‑free models may require billing; free model providers may collect your data, check the model page for details; this project is not affiliated with OpenRouter or any model provider.)
+ - In **OpenAI Compatible Server → Model**, enter `openai/gpt-oss-120b:free`. (Note: this is a free model; non‑free models may require billing; free model providers may collect your data, check the model page for details; this project is **not** affiliated with OpenRouter or any model provider.)
- In **OpenAI Compatible Server → Base URL**, enter `https://openrouter.ai/api/v1`.
-(Alternatively, you can use the official OpenAI API, other OpenAI‑compatible services, or your own hosted LLM server. e.g., Ollama, vLLM)
+**Tips:**
+- You can also use the official OpenAI API, other OpenAI-compatible services, or a self-hosted LLM server (e.g., Ollama, vLLM). Note that model quality varies — not all models perform equally well for music editing tasks. For local hosting, we recommend `qwen3.5-35b-a3b` as a good balance between generation quality and hardware requirements.
+- If you have an active subscription with OpenAI or another LLM provider, you can use [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) to run a local proxy server that routes requests through your existing subscription, without needing a separate API key.
### Basic DAW operations
- Double‑click (or hold Ctrl/Cmd and click) on a track to create a region.
@@ -220,7 +236,10 @@ OpenRouter is a platform that provides unified access to a wide range of languag
5. Enter your chosen model name in **OpenAI Compatible Server → Model**. Recommended model series include:
- `Anthropic: Claude Sonnet 4.6` (`anthropic/claude-sonnet-4.6`: [Link](https://openrouter.ai/anthropic/claude-sonnet-4.6)) — best balance of quality and cost for the Claude series
- `Qwen: Qwen3.5-35B-A3B` (`qwen/qwen3.5-35b-a3b`: [Link](https://openrouter.ai/qwen/qwen3.5-35b-a3b)) — recommended open source model
- - `Qwen: Qwen3.6 Plus` (FREE MODEL: `qwen/qwen3.6-plus:free`: [Link](https://openrouter.ai/qwen/qwen3.6-plus:free)) — recommended free model; note that free model providers may collect your data, check the model page for details
+ - `Qwen: Qwen3-Next-80B-A3B` (FREE MODEL: `qwen/qwen3-next-80b-a3b-instruct:free`: [Link](https://openrouter.ai/qwen/qwen3-next-80b-a3b-instruct:free)) — recommended free model
+ - `OpenAI: GPT-OSS 120B` (FREE MODEL: `openai/gpt-oss-120b:free`: [Link](https://openrouter.ai/openai/gpt-oss-120b:free)) — recommended free model
+ - Note: free model providers may collect your data; check the model page for details before use
+ - Note: free model availability changes frequently — for the latest free options, visit the [OpenRouter Models Page](https://openrouter.ai/models) and use the **Prompt Pricing** filter to find currently free models
6. Input the base URL `https://openrouter.ai/api/v1` **OpenAI Compatible Server → Base URL**.
### About the agent and LLM providers
@@ -231,6 +250,53 @@ For security, when using K.G.Studio from a non‑local host, API keys are not pe
K.G.Studio does not provide or host any of the models listed above, nor is it affiliated with any model provider. All data is stored locally on your device; K.G.Studio does not collect or transmit your data. You are solely responsible for any data you provide to third‑party model providers.
+## K.G.One Music Generator
+
+> **Requires [K.G.One Music Studio](https://github.com/KGAudioLab/K.G.One) integration.** The K.G.One Music Generator panel is only available when K.G.Studio is connected to a running K.G.One server. See the [K.G.One repository](https://github.com/KGAudioLab/K.G.One) for setup instructions.
+
+The **K.G.One Music Generator** panel provides three GPU-accelerated AI tools for music generation and audio processing. Click the **✦ (magic wand)** button in the toolbar to open it. The panel is mutually exclusive with the AI Assistant chatbox — opening one will close the other.
+
+> **Note:** The first time you use each tool, the server needs to load the corresponding AI model, which can take 60 seconds or longer depending on your hardware. Switching between tabs may also trigger a model reload.
+
+### Full Song Generation
+
+Generate a complete, full-length song from a text description and optional lyrics. Powered by [ACE-Step 1.5](https://github.com/ace-step/ACE-Step-1.5).
+
+- In the **Full Song** tab, enter a **Caption** describing the desired style, mood, tempo, instrumentation, and structure in natural language. For example: `Genre: Eurodance, 90s dance-pop, upbeat electronic. Tempo: ~130 BPM. Instrumentation: driving kick drum, eurodance bassline...`
+- Optionally, enter **Lyrics**. Use `[Intro]`, `[Verse]`, `[Chorus]`, `[Bridge]` tags to mark sections. Check **Instrumental** to skip vocals entirely.
+- Click **Generate Song**. A progress indicator shows the generation stage and percentage in real time.
+- Once complete, a preview player appears. Drag it onto an **audio track** to import the song as a region.
+- Dropping onto a MIDI track is not supported for full song generation.
+- **Advanced Settings** (expandable): Inference Steps, Guidance Scale, Seed, and Thinking (CoT metadata generation).
+
+### Clip Generation
+
+Generate short instrument clips and MIDI loops from text descriptions. Powered by [Foundation-1](https://huggingface.co/RoyalCities/Foundation-1).
+
+- In the **Clip** tab, enter a **Prompt** describing the clip using comma-separated tags covering instrument family, sub-type, timbre, effects, length, BPM, and key. For example: `Gritty, Acid, Bassline, 303, Synth Lead, FM, Sub, High Reverb, 8 Bars, 140 BPM, E minor`
+- Optionally, enter a **Negative Prompt** to steer the generation away from unwanted characteristics (e.g. `distortion, noise`).
+- Select **Bars**: 4 or 8. The BPM and key signature are pre-filled from your project settings and can be adjusted in **Advanced Settings**.
+- Click **Generate Clip**. Once complete, a preview player appears with a drag handle on the left and a download button on the right.
+- **To import**: drag the player onto a track on the timeline.
+ - Drop onto an **audio track** to import as a WAV audio region (recommended).
+ - Drop onto a **MIDI track** to import as a MIDI region. Note that the MIDI is transcribed from the audio and may not be perfectly accurate.
+- **Advanced Settings** (expandable): Note, Scale, BPM, Steps, CFG Scale, Seed (`-1` for random), Sampler Type, Sigma Min/Max, and CFG Rescale.
+
+### Stem Separation
+
+Split an existing audio region into individual stems (e.g. vocals, instruments, drums). Powered by [python-audio-separator (UVR5)](https://github.com/nomadkaraoke/python-audio-separator).
+
+- **Select an audio region** on the timeline before opening this tab. The selected region and its track name are shown at the top of the **Separator** tab. Only audio regions are supported — MIDI regions cannot be separated.
+- Choose a **Separation Model**:
+ - **Vocal and Instrument (Medium Accuracy)** (`UVR-MDX-NET-Inst_HQ_3`) — fast two-stem split (vocal / instrumental).
+ - **Vocal and Instrument (High Accuracy)** (`MDX23C-8KFFT-InstVoc_HQ`) — higher-quality two-stem split, slower.
+ - **Vocal, Drums, Bass, Guitar, Piano, and Others** (`htdemucs_6s`) — full six-stem separation.
+- Click **Separate Stems**. If the selected region has a clip start offset or is trimmed, the audio is automatically sliced to match the region range before uploading.
+- Once complete, each stem appears as a labelled preview player with a drag handle. You can preview each stem individually before importing.
+- **To import stems**:
+ - Drag each stem player individually onto an **audio track** to place it where you want.
+ - Or click **Import All Stems to Timeline** to create one new audio track per stem automatically, positioned immediately below the source track and aligned to the same start beat as the original region. This is a single undoable operation.
+
## Upcoming Features
Feature priorities might change.
@@ -240,7 +306,7 @@ Feature priorities might change.
- [X] Intelligent Chord Assistant with functional harmony guidance (T/S/D)
- [ ] Support track control automations (e.g. sustain, volume, pan, etc.)
- [ ] Support MIDI control events (e.g. CC, pitch bend, etc.)
-- [ ] Support WAV audio tracks
+- [X] Support WAV audio tracks
- [ ] Filters and effects
- [ ] MCP Support
- [X] Add support for OpenAI's open source models (`gpt-oss-20b` and `gpt-oss-120b`)
diff --git a/docs/KGOne-Demo-GIF.gif b/docs/KGOne-Demo-GIF.gif
new file mode 100644
index 0000000000000000000000000000000000000000..67303484dae1b2652e1eb20ea2becd250298f19c
GIT binary patch
literal 9379997
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