pachet commited on
Commit
cea354c
·
1 Parent(s): 6535ffb

trying end phrase detection and processing with python

Browse files
Files changed (2) hide show
  1. app.py +24 -111
  2. phrase_catching.py +320 -0
app.py CHANGED
@@ -1,10 +1,13 @@
1
  import gradio as gr
2
  from fastapi import FastAPI, Request
3
  import json
4
-
 
 
5
  # ✅ Create FastAPI App
6
  app = FastAPI()
7
 
 
8
  # ✅ MIDI Processing Function in Python
9
  @app.post("/midi_input")
10
  async def process_midi(request: Request):
@@ -31,30 +34,6 @@ async def process_midi(request: Request):
31
  print(f"🚨 Error processing MIDI: {str(e)}")
32
  return {"status": "error", "message": str(e)}
33
 
34
- @app.post("/midi_phrase")
35
- async def process_midi_phrase(request: Request):
36
- try:
37
- data = await request.json()
38
- phrase = data["phrase"] # List of MIDI notes
39
-
40
- print(f"🎹 Received MIDI Phrase ({len(phrase)} notes)")
41
-
42
- # 🚀 Process Phrase: Example - Transpose Each Note Up by 3 Semitones
43
- generated_phrase = [
44
- {
45
- "note": (note["note"] + 3) % 128,
46
- "velocity": note["velocity"],
47
- "duration": note["duration"], # ✅ Keep original duration
48
- "inter_onset": note["inter_onset"] # ✅ Keep inter-onset time
49
- }
50
- for note in phrase
51
- ]
52
-
53
- return {"status": "success", "generated_phrase": generated_phrase}
54
-
55
- except Exception as e:
56
- print(f"🚨 Error processing MIDI phrase: {str(e)}")
57
- return {"status": "error", "message": str(e)}
58
 
59
  # ✅ JavaScript to Capture and Send MIDI Data
60
  midi_js = """
@@ -110,7 +89,7 @@ function updateMIDIDevices() {
110
  function selectMIDIInput() {
111
  let inputSelect = document.getElementById("midiInput");
112
  let inputId = inputSelect.value;
113
-
114
  if (selectedInput) {
115
  selectedInput.onmidimessage = null;
116
  }
@@ -159,102 +138,35 @@ function playMIDINote(note, velocity) {
159
 
160
  // ✅ Send MIDI Data to Python
161
  // ✅ Handle Incoming MIDI Messages and Send to Python
162
-
163
- let midiPhrase = []; // ✅ Buffer to store incoming notes
164
- let activeNotes = new Map(); // ✅ Track active notes with start times
165
- let phraseTimeout = null; // ✅ Timer to detect phrase end
166
- let lastNoteOnTime = null; // ✅ Track previous note-on time for inter-onset calculation
167
-
168
- // ✅ Handle Incoming MIDI Messages (Phrase Buffering)
169
  function handleMIDIMessage(event) {
170
- let command = event.data[0] & 0xf0; // Extract MIDI command
171
- let note = event.data[1];
172
  let velocity = event.data[2];
173
- let timestamp = performance.now(); // ✅ Get precise timing
174
-
175
- if (command === 0x90 && velocity > 0) {
176
- // ✅ Note On: Store start time and inter-onset interval
177
- let interOnsetTime = lastNoteOnTime ? timestamp - lastNoteOnTime : 0;
178
- lastNoteOnTime = timestamp;
179
- console.log(interOnsetTime);
180
-
181
- activeNotes.set(note, timestamp); // ✅ Store note start time
182
-
183
- midiPhrase.push({ note, velocity, start_time: timestamp, duration: null, inter_onset: interOnsetTime });
184
-
185
- console.log(`🎤 Note ON: ${note}, Velocity ${velocity}, IOT ${interOnsetTime}ms`);
186
- }
187
- else if (command === 0x80 || (command === 0x90 && velocity === 0)) {
188
- // ✅ Note Off: Calculate duration and update phrase
189
- if (activeNotes.has(note)) {
190
- let startTime = activeNotes.get(note);
191
- let duration = timestamp - startTime;
192
- activeNotes.delete(note);
193
-
194
- // ✅ Find the note in the phrase and update duration
195
- let noteIndex = midiPhrase.findIndex(n => n.note === note && n.duration === null);
196
- if (noteIndex !== -1) {
197
- midiPhrase[noteIndex].duration = duration;
198
- console.log(`🎹 Note OFF: ${note}, Duration ${duration}ms`);
199
- }
200
- }
201
-
202
- // ✅ If no active notes, start phrase timeout (1s delay before sending)
203
- if (activeNotes.size === 0) {
204
- if (phraseTimeout) clearTimeout(phraseTimeout);
205
- phraseTimeout = setTimeout(sendPhraseToPython, 1000);
206
- }
207
- }
208
- }
209
 
210
- // Send the Phrase to Python After 1 Second of Silence
211
- function sendPhraseToPython() {
212
- if (midiPhrase.length === 0 || activeNotes.size > 0) return; // ✅ Do not send if notes are still active
 
213
 
214
- console.log("📨 Sending MIDI phrase to Python:", midiPhrase);
215
 
216
- fetch("/midi_phrase", {
 
217
  method: "POST",
218
  headers: { "Content-Type": "application/json" },
219
- body: JSON.stringify({ phrase: midiPhrase })
220
  })
221
  .then(response => response.json())
222
  .then(data => {
223
- console.log("📩 Python Response:", data);
224
- if (data.generated_phrase) {
225
- playGeneratedPhrase(data.generated_phrase);
 
 
226
  }
227
  })
228
- .catch(error => console.error("🚨 Error sending MIDI phrase:", error));
229
-
230
- // ✅ Clear the phrase buffer
231
- midiPhrase = [];
232
- lastNoteOnTime = null;
233
-
234
  }
235
 
236
- // ✅ Play Generated MIDI Phrase from Python (Keeping Timing)
237
- function playGeneratedPhrase(phrase) {
238
- if (!selectedOutput) {
239
- console.warn("⚠️ No MIDI output selected.");
240
- return;
241
- }
242
-
243
- console.log("🎵 Playing Generated Phrase:", phrase);
244
-
245
- phrase.forEach((noteData, index) => {
246
- let delay = noteData.inter_onset; // ✅ Use stored inter-onset time
247
- setTimeout(() => {
248
- let noteOnMessage = [0x90, noteData.note, noteData.velocity];
249
- let noteOffMessage = [0x80, noteData.note, 0];
250
-
251
- selectedOutput.send(noteOnMessage);
252
- setTimeout(() => selectedOutput.send(noteOffMessage), noteData.duration); // ✅ Use stored duration
253
- }, delay);
254
- });
255
- }
256
-
257
-
258
  // ✅ Attach Generate Button Event
259
  function attachButtonEvent() {
260
  let generateButton = document.getElementById("generateButton");
@@ -264,7 +176,7 @@ function attachButtonEvent() {
264
 
265
  generateButton.addEventListener("click", function () {
266
  console.log("🎹 Generate button clicked.");
267
-
268
  if (!selectedOutput) {
269
  alert("⚠️ Please select a MIDI Output first!");
270
  return;
@@ -310,7 +222,8 @@ with gr.Blocks() as demo:
310
  # ✅ Mount FastAPI with Gradio
311
  app = gr.mount_gradio_app(app, demo, path="/")
312
 
 
313
  if __name__ == "__main__":
314
  import uvicorn
315
- print("🚀 Starting FastAPI with Gradio...")
316
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  import gradio as gr
2
  from fastapi import FastAPI, Request
3
  import json
4
+ # THIS VERSION WORKS
5
+ # AND SENDS BACK A NOTE A FIFTH HIGHER THAN THE INPUT NOT ON THE SELECTED PORT
6
+ #
7
  # ✅ Create FastAPI App
8
  app = FastAPI()
9
 
10
+
11
  # ✅ MIDI Processing Function in Python
12
  @app.post("/midi_input")
13
  async def process_midi(request: Request):
 
34
  print(f"🚨 Error processing MIDI: {str(e)}")
35
  return {"status": "error", "message": str(e)}
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  # ✅ JavaScript to Capture and Send MIDI Data
39
  midi_js = """
 
89
  function selectMIDIInput() {
90
  let inputSelect = document.getElementById("midiInput");
91
  let inputId = inputSelect.value;
92
+
93
  if (selectedInput) {
94
  selectedInput.onmidimessage = null;
95
  }
 
138
 
139
  // ✅ Send MIDI Data to Python
140
  // ✅ Handle Incoming MIDI Messages and Send to Python
 
 
 
 
 
 
 
141
  function handleMIDIMessage(event) {
142
+ let originalNote = event.data[1];
 
143
  let velocity = event.data[2];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ let midiData = {
146
+ note: originalNote,
147
+ velocity: velocity
148
+ };
149
 
150
+ console.log(`🎤 MIDI Input: Note ${originalNote}, Velocity ${velocity}`);
151
 
152
+ // ✅ Send MIDI data to Python backend
153
+ fetch("/midi_input", {
154
  method: "POST",
155
  headers: { "Content-Type": "application/json" },
156
+ body: JSON.stringify(midiData)
157
  })
158
  .then(response => response.json())
159
  .then(data => {
160
+ console.log("📨 MIDI sent to Python. Response:", data);
161
+
162
+ // ✅ Play the generated MIDI response
163
+ if (data.status === "success") {
164
+ playMIDINote(data.generated_note, data.generated_velocity);
165
  }
166
  })
167
+ .catch(error => console.error("🚨 Error sending MIDI data:", error));
 
 
 
 
 
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  // ✅ Attach Generate Button Event
171
  function attachButtonEvent() {
172
  let generateButton = document.getElementById("generateButton");
 
176
 
177
  generateButton.addEventListener("click", function () {
178
  console.log("🎹 Generate button clicked.");
179
+
180
  if (!selectedOutput) {
181
  alert("⚠️ Please select a MIDI Output first!");
182
  return;
 
222
  # ✅ Mount FastAPI with Gradio
223
  app = gr.mount_gradio_app(app, demo, path="/")
224
 
225
+ # ✅ Run the app
226
  if __name__ == "__main__":
227
  import uvicorn
228
+
229
+ uvicorn.run(app, host="0.0.0.0", port=7860)
phrase_catching.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from fastapi import FastAPI, Request
3
+ import json
4
+
5
+ # ✅ Create FastAPI App
6
+ app = FastAPI()
7
+
8
+
9
+ # ✅ MIDI Processing Function in Python
10
+ @app.post("/midi_input")
11
+ async def process_midi(request: Request):
12
+ try:
13
+ midi_data = await request.json()
14
+ note = midi_data["note"]
15
+ velocity = midi_data["velocity"]
16
+
17
+ print(f"🎹 Received MIDI Note: {note}, Velocity: {velocity}")
18
+
19
+ # 🚀 Process MIDI data (example: Transpose + Generate New Notes)
20
+ generated_note = (note + 5) % 128 # Transpose up by 3 semitones
21
+ generated_velocity = min(velocity + 10, 127) # Increase velocity slightly
22
+
23
+ # ✅ Send MIDI Response Back to Client
24
+ return {
25
+ "status": "success",
26
+ "generated_note": generated_note,
27
+ "generated_velocity": generated_velocity,
28
+ "original_note": note
29
+ }
30
+
31
+ except Exception as e:
32
+ print(f"🚨 Error processing MIDI: {str(e)}")
33
+ return {"status": "error", "message": str(e)}
34
+
35
+
36
+ @app.post("/midi_phrase")
37
+ async def process_midi_phrase(request: Request):
38
+ try:
39
+ data = await request.json()
40
+ phrase = data["phrase"] # List of MIDI notes
41
+
42
+ print(f"🎹 Received MIDI Phrase ({len(phrase)} notes)")
43
+
44
+ # 🚀 Process Phrase: Example - Transpose Each Note Up by 3 Semitones
45
+ generated_phrase = [
46
+ {
47
+ "note": (note["note"] + 3) % 128,
48
+ "velocity": note["velocity"],
49
+ "duration": note["duration"], # ✅ Keep original duration
50
+ "inter_onset": note["inter_onset"] # ✅ Keep inter-onset time
51
+ }
52
+ for note in phrase
53
+ ]
54
+
55
+ return {"status": "success", "generated_phrase": generated_phrase}
56
+
57
+ except Exception as e:
58
+ print(f"🚨 Error processing MIDI phrase: {str(e)}")
59
+ return {"status": "error", "message": str(e)}
60
+
61
+
62
+ # ✅ JavaScript to Capture and Send MIDI Data
63
+ midi_js = """
64
+ <script>
65
+ let midiAccess = null;
66
+ let selectedInput = null;
67
+ let selectedOutput = null;
68
+
69
+ // ✅ Request MIDI Access
70
+ navigator.requestMIDIAccess()
71
+ .then(access => {
72
+ console.log("✅ MIDI Access Granted!");
73
+ midiAccess = access;
74
+ updateMIDIDevices();
75
+ midiAccess.onstatechange = updateMIDIDevices;
76
+ })
77
+ .catch(err => console.error("🚨 MIDI API Error:", err));
78
+
79
+ // ✅ Update MIDI Input & Output Menus
80
+ function updateMIDIDevices() {
81
+ let inputSelect = document.getElementById("midiInput");
82
+ let outputSelect = document.getElementById("midiOutput");
83
+
84
+ if (!inputSelect || !outputSelect) {
85
+ console.error("❌ MIDI dropdowns not found!");
86
+ return;
87
+ }
88
+
89
+ // Clear existing options
90
+ inputSelect.innerHTML = '<option value="">Select MIDI Input</option>';
91
+ outputSelect.innerHTML = '<option value="">Select MIDI Output</option>';
92
+
93
+ // Populate MIDI Input Devices
94
+ midiAccess.inputs.forEach((input, key) => {
95
+ let option = document.createElement("option");
96
+ option.value = key;
97
+ option.textContent = input.name || `MIDI Input ${key}`;
98
+ inputSelect.appendChild(option);
99
+ });
100
+
101
+ // Populate MIDI Output Devices
102
+ midiAccess.outputs.forEach((output, key) => {
103
+ let option = document.createElement("option");
104
+ option.value = key;
105
+ option.textContent = output.name || `MIDI Output ${key}`;
106
+ outputSelect.appendChild(option);
107
+ });
108
+
109
+ console.log("🎛 Updated MIDI Input & Output devices.");
110
+ }
111
+
112
+ // ✅ Handle MIDI Input Selection
113
+ function selectMIDIInput() {
114
+ let inputSelect = document.getElementById("midiInput");
115
+ let inputId = inputSelect.value;
116
+
117
+ if (selectedInput) {
118
+ selectedInput.onmidimessage = null;
119
+ }
120
+
121
+ if (midiAccess.inputs.has(inputId)) {
122
+ selectedInput = midiAccess.inputs.get(inputId);
123
+ selectedInput.onmidimessage = handleMIDIMessage;
124
+ console.log(`🎤 MIDI Input Selected: ${selectedInput.name}`);
125
+ }
126
+ }
127
+
128
+ // ✅ Handle MIDI Output Selection
129
+ function selectMIDIOutput() {
130
+ let outputSelect = document.getElementById("midiOutput");
131
+ let outputId = outputSelect.value;
132
+
133
+ if (midiAccess.outputs.has(outputId)) {
134
+ selectedOutput = midiAccess.outputs.get(outputId);
135
+ console.log(`🎹 MIDI Output Selected: ${selectedOutput.name}`);
136
+ }
137
+ }
138
+
139
+
140
+ // ✅ Play a MIDI Note Sent Back from Python
141
+ function playMIDINote(note, velocity) {
142
+ if (!selectedOutput) {
143
+ console.warn("⚠️ No MIDI output selected.");
144
+ return;
145
+ }
146
+
147
+ let noteOnMessage = [0x90, note, velocity]; // Note On
148
+ let noteOffMessage = [0x80, note, 0]; // Note Off
149
+
150
+ console.log(`🎵 Playing Generated MIDI Note ${note}, Velocity ${velocity}`);
151
+
152
+ try {
153
+ selectedOutput.send(noteOnMessage);
154
+ setTimeout(() => {
155
+ selectedOutput.send(noteOffMessage);
156
+ console.log(`🔇 Note Off ${note}`);
157
+ }, 500);
158
+ } catch (error) {
159
+ console.error("🚨 Error playing MIDI note:", error);
160
+ }
161
+ }
162
+
163
+ // ✅ Send MIDI Data to Python
164
+ // ✅ Handle Incoming MIDI Messages and Send to Python
165
+
166
+ let midiPhrase = []; // ✅ Buffer to store incoming notes
167
+ let activeNotes = new Map(); // ✅ Track active notes with start times
168
+ let phraseTimeout = null; // ✅ Timer to detect phrase end
169
+ let lastNoteOnTime = null; // ✅ Track previous note-on time for inter-onset calculation
170
+
171
+ // ✅ Handle Incoming MIDI Messages (Phrase Buffering)
172
+ function handleMIDIMessage(event) {
173
+ let command = event.data[0] & 0xf0; // Extract MIDI command
174
+ let note = event.data[1];
175
+ let velocity = event.data[2];
176
+ let timestamp = performance.now(); // ✅ Get precise timing
177
+
178
+ if (command === 0x90 && velocity > 0) {
179
+ // ✅ Note On: Store start time and inter-onset interval
180
+ let interOnsetTime = lastNoteOnTime ? timestamp - lastNoteOnTime : 0;
181
+ lastNoteOnTime = timestamp;
182
+ console.log(interOnsetTime);
183
+
184
+ activeNotes.set(note, timestamp); // ✅ Store note start time
185
+
186
+ midiPhrase.push({ note, velocity, start_time: timestamp, duration: null, inter_onset: interOnsetTime });
187
+
188
+ console.log(`🎤 Note ON: ${note}, Velocity ${velocity}, IOT ${interOnsetTime}ms`);
189
+ }
190
+ else if (command === 0x80 || (command === 0x90 && velocity === 0)) {
191
+ // ✅ Note Off: Calculate duration and update phrase
192
+ if (activeNotes.has(note)) {
193
+ let startTime = activeNotes.get(note);
194
+ let duration = timestamp - startTime;
195
+ activeNotes.delete(note);
196
+
197
+ // ✅ Find the note in the phrase and update duration
198
+ let noteIndex = midiPhrase.findIndex(n => n.note === note && n.duration === null);
199
+ if (noteIndex !== -1) {
200
+ midiPhrase[noteIndex].duration = duration;
201
+ console.log(`🎹 Note OFF: ${note}, Duration ${duration}ms`);
202
+ }
203
+ }
204
+
205
+ // ✅ If no active notes, start phrase timeout (1s delay before sending)
206
+ if (activeNotes.size === 0) {
207
+ if (phraseTimeout) clearTimeout(phraseTimeout);
208
+ phraseTimeout = setTimeout(sendPhraseToPython, 1000);
209
+ }
210
+ }
211
+ }
212
+
213
+ // ✅ Send the Phrase to Python After 1 Second of Silence
214
+ function sendPhraseToPython() {
215
+ if (midiPhrase.length === 0 || activeNotes.size > 0) return; // ✅ Do not send if notes are still active
216
+
217
+ console.log("📨 Sending MIDI phrase to Python:", midiPhrase);
218
+
219
+ fetch("/midi_phrase", {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/json" },
222
+ body: JSON.stringify({ phrase: midiPhrase })
223
+ })
224
+ .then(response => response.json())
225
+ .then(data => {
226
+ console.log("📩 Python Response:", data);
227
+ if (data.generated_phrase) {
228
+ playGeneratedPhrase(data.generated_phrase);
229
+ }
230
+ })
231
+ .catch(error => console.error("🚨 Error sending MIDI phrase:", error));
232
+
233
+ // ✅ Clear the phrase buffer
234
+ midiPhrase = [];
235
+ lastNoteOnTime = null;
236
+
237
+ }
238
+
239
+ // ✅ Play Generated MIDI Phrase from Python (Keeping Timing)
240
+ function playGeneratedPhrase(phrase) {
241
+ if (!selectedOutput) {
242
+ console.warn("⚠️ No MIDI output selected.");
243
+ return;
244
+ }
245
+
246
+ console.log("🎵 Playing Generated Phrase:", phrase);
247
+
248
+ phrase.forEach((noteData, index) => {
249
+ let delay = noteData.inter_onset; // ✅ Use stored inter-onset time
250
+ setTimeout(() => {
251
+ let noteOnMessage = [0x90, noteData.note, noteData.velocity];
252
+ let noteOffMessage = [0x80, noteData.note, 0];
253
+
254
+ selectedOutput.send(noteOnMessage);
255
+ setTimeout(() => selectedOutput.send(noteOffMessage), noteData.duration); // ✅ Use stored duration
256
+ }, delay);
257
+ });
258
+ }
259
+
260
+
261
+ // ✅ Attach Generate Button Event
262
+ function attachButtonEvent() {
263
+ let generateButton = document.getElementById("generateButton");
264
+
265
+ if (generateButton) {
266
+ console.log("✅ Generate button found! Attaching event listener...");
267
+
268
+ generateButton.addEventListener("click", function () {
269
+ console.log("🎹 Generate button clicked.");
270
+
271
+ if (!selectedOutput) {
272
+ alert("⚠️ Please select a MIDI Output first!");
273
+ return;
274
+ }
275
+
276
+ let randomNote = 60 + Math.floor(Math.random() * 12); // Random note from C4 to B4
277
+ console.log(`🎵 Generating MIDI Note: ${randomNote}`);
278
+ playMIDINote(randomNote, 100);
279
+ });
280
+
281
+ } else {
282
+ console.log("⏳ Waiting for button to be available...");
283
+ setTimeout(attachButtonEvent, 500); // Try again in 500ms
284
+ }
285
+ }
286
+
287
+ // ✅ Ensure the Button and Menus Are Loaded
288
+ window.onload = function() {
289
+ console.log("✅ Page fully loaded. Checking for elements...");
290
+ updateMIDIDevices();
291
+ attachButtonEvent();
292
+ };
293
+ </script>
294
+
295
+ <!-- 🎛 MIDI Input & Output Selection -->
296
+ <div>
297
+ <label for="midiInput">MIDI Input: </label>
298
+ <select id="midiInput" onchange="selectMIDIInput()"></select>
299
+
300
+ <label for="midiOutput">MIDI Output: </label>
301
+ <select id="midiOutput" onchange="selectMIDIOutput()"></select>
302
+ </div>
303
+
304
+ <!-- 🎶 "Generate MIDI" Button -->
305
+ <button id="generateButton">🎵 Generate MIDI Note</button>
306
+
307
+ """
308
+
309
+ # ✅ Inject JavaScript and HTML
310
+ with gr.Blocks() as demo:
311
+ gr.HTML(midi_js)
312
+
313
+ # ✅ Mount FastAPI with Gradio
314
+ app = gr.mount_gradio_app(app, demo, path="/")
315
+
316
+ if __name__ == "__main__":
317
+ import uvicorn
318
+
319
+ print("🚀 Starting FastAPI with Gradio...")
320
+ uvicorn.run(app, host="0.0.0.0", port=7860)