SHIKARICHACHA commited on
Commit
516f715
·
verified ·
1 Parent(s): d4a25e5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +304 -62
app.py CHANGED
@@ -1,70 +1,312 @@
1
- import gradio as gr
2
- import os
3
- import requests
4
- from datetime import datetime
 
5
 
6
- # Mistral API setup
7
- MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY", "yQdfM8MLbX9uhInQ7id4iUTwN4h4pDLX")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions"
 
9
 
10
- # Local medical knowledge base
11
- MEDICAL_KNOWLEDGE = {
12
- "headache": {
13
- "assessment": "Could be tension-type, migraine, or other common headache",
14
- "recommendations": [
15
- "Rest in a quiet, dark room",
16
- "Apply cold compress to forehead",
17
- "Hydrate well (at least 8 glasses water/day)",
18
- "Try gentle neck stretches",
19
- "Consider OTC pain relievers (ibuprofen 200-400mg or paracetamol 500mg)"
20
- ],
21
- "warnings": [
22
- "If sudden/severe 'thunderclap' headache",
23
- "If with fever, stiff neck, or confusion",
24
- "If following head injury"
25
- ]
26
- },
27
- "fever": {
28
- "assessment": "Common symptom of infection or inflammation",
29
- "recommendations": [
30
- "Rest and increase fluid intake",
31
- "Use light clothing and keep room cool",
32
- "Sponge with lukewarm water if uncomfortable",
33
- "Paracetamol 500mg every 6 hours (max 4 doses/day)",
34
- "Monitor temperature every 4 hours"
35
- ],
36
- "warnings": [
37
- "Fever >39°C lasting >3 days",
38
- "If with rash, difficulty breathing, or seizures",
39
- "In infants <3 months with any fever"
40
- ]
41
- },
42
- "sore throat": {
43
- "assessment": "Often viral, but could be strep throat",
44
- "recommendations": [
45
- "Gargle with warm salt water 3-4 times daily",
46
- "Drink warm liquids (tea with honey)",
47
- "Use throat lozenges",
48
- "Stay hydrated",
49
- "Rest your voice"
50
- ],
51
- "warnings": [
52
- "Difficulty swallowing or breathing",
53
- "White patches on tonsils",
54
- "Fever >38.5°C with throat pain"
55
- ]
56
- }
57
  }
58
 
59
- def get_local_advice(symptom):
60
- """Get structured medical advice from local knowledge base"""
61
- symptom_lower = symptom.lower()
62
- for key in MEDICAL_KNOWLEDGE:
63
- if key in symptom_lower:
64
- knowledge = MEDICAL_KNOWLEDGE[key]
65
- response = "🔍 [Assessment]\n- " + knowledge["assessment"] + "\n\n"
66
- response += "💡 [Self-Care Recommendations]\n" + "\n".join(["" + item for item in knowledge["recommendations"]]) + "\n\n"
67
- response += "⚠️ [When to Seek Medical Care]\n" + "\n".join(["• " + item for item in knowledge["warnings"]]) + "\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  response += "📅 [Follow-up]\n• Re-evaluate in 2-3 days if not improving\n• See doctor if symptoms worsen or persist beyond 5 days"
69
  return response
70
  return None
 
1
+ """
2
+ Adaptive Music Exercise Generator – Integer Durations Only
3
+ ========================================================
4
+ 1 unit = 1 quarter-note (integer only)
5
+ """
6
 
7
+ # ------------------------------------------------------------------
8
+ # 0. Auto-install deps (unchanged)
9
+ # ------------------------------------------------------------------
10
+ import sys, subprocess
11
+ from typing import Dict, Optional, Tuple, List
12
+
13
+ def install(pkgs: List[str]):
14
+ for p in pkgs:
15
+ try: __import__(p)
16
+ except ImportError:
17
+ subprocess.check_call([sys.executable, "-m", "pip", "install", p])
18
+
19
+ install(["mido", "midi2audio", "pydub", "gradio", "requests"])
20
+
21
+ # ------------------------------------------------------------------
22
+ # 1. Imports
23
+ # ------------------------------------------------------------------
24
+ import random, requests, json, tempfile, mido, re, os, shutil, gradio as gr
25
+ from mido import Message, MidiFile, MidiTrack, MetaMessage
26
+ from midi2audio import FluidSynth
27
+ from pydub import AudioSegment
28
+
29
+ # ------------------------------------------------------------------
30
+ # 2. Config
31
+ # ------------------------------------------------------------------
32
  MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions"
33
+ MISTRAL_API_KEY = "yQdfM8MLbX9uhInQ7id4iUTwN4h4pDLX"
34
 
35
+ SOUNDFONT_URLS = {
36
+ "Trumpet": "https://github.com/FluidSynth/fluidsynth/raw/master/sf2/Trumpet.sf2",
37
+ "Piano": "https://musical-artifacts.com/artifacts/2719/GeneralUser_GS_1.471.sf2",
38
+ "Violin": "https://musical-artifacts.com/artifacts/2744/SalC5Light.sf2",
39
+ "Clarinet": "https://musical-artifacts.com/artifacts/2744/SalC5Light.sf2",
40
+ "Flute": "https://musical-artifacts.com/artifacts/2744/SalC5Light.sf2",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
 
43
+ TICKS_PER_BEAT = 480
44
+ TICKS_PER_UNIT = TICKS_PER_BEAT
45
+
46
+ # ------------------------------------------------------------------
47
+ # 3. Note helpers – fixed regex & cleaner
48
+ # ------------------------------------------------------------------
49
+ NOTE_MAP = {"C":0,"C#":1,"DB":1,"D":2,"D#":3,"EB":3,"E":4,"F":5,"F#":6,"GB":6,
50
+ "G":7,"G#":8,"AB":8,"A":9,"A#":10,"BB":10,"B":11}
51
+
52
+ def sanitise_note_name(n: str) -> str:
53
+ """Convert common variations to canonical form."""
54
+ n = n.strip().upper()
55
+ # Map full words
56
+ n = re.sub(r'\bFLAT\b', 'B', n, flags=re.I)
57
+ n = re.sub(r'\bSHARP\b', '#', n, flags=re.I)
58
+ # Map dash/space forms
59
+ n = re.sub(r'([A-G])\s*-\s*FLAT', r'\1B', n, flags=re.I)
60
+ n = re.sub(r'([A-G])\s*-\s*SHARP', r'\1#', n, flags=re.I)
61
+ return n
62
+
63
+ def note_name_to_midi(n: str) -> int:
64
+ n = sanitise_note_name(n)
65
+ m = re.fullmatch(r"([A-G][#B]?)(\d)", n)
66
+ if not m:
67
+ raise ValueError(f"Invalid note: {n}")
68
+ pitch, octave = m.groups()
69
+ pitch = pitch.replace('B', 'B') # keep BB as B-flat
70
+ if pitch not in NOTE_MAP:
71
+ raise ValueError(f"Invalid pitch: {pitch}")
72
+ return NOTE_MAP[pitch] + (int(octave) + 1) * 12
73
+
74
+ # ------------------------------------------------------------------
75
+ # 4. Integer-only scaler – unchanged
76
+ # ------------------------------------------------------------------
77
+ def scale_json_durations(json_data, target_units: int) -> list:
78
+ ints = [[n, max(1, int(round(d)))] for n, d in json_data]
79
+ total = sum(d for _, d in ints)
80
+ deficit = target_units - total
81
+ if deficit > 0:
82
+ for i in range(deficit):
83
+ ints[i % len(ints)][1] += 1
84
+ elif deficit < 0:
85
+ for i in range(-deficit):
86
+ if ints[i % len(ints)][1] > 1:
87
+ ints[i % len(ints)][1] -= 1
88
+ return ints
89
+
90
+ # ------------------------------------------------------------------
91
+ # 5. JSON → MIDI – unchanged
92
+ # ------------------------------------------------------------------
93
+ def json_to_midi(json_data, instrument, tempo, time_signature, measures):
94
+ mid = MidiFile(ticks_per_beat=TICKS_PER_BEAT)
95
+ track = MidiTrack(); mid.tracks.append(track)
96
+ program = INSTRUMENT_PROGRAMS.get(instrument, 56)
97
+ num, denom = map(int, time_signature.split('/'))
98
+
99
+ track.append(MetaMessage('time_signature', numerator=num, denominator=denom, time=0))
100
+ track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(tempo), time=0))
101
+ track.append(Message('program_change', program=program, time=0))
102
+
103
+ for note_name, dur_units in json_data:
104
+ note_num = note_name_to_midi(note_name)
105
+ ticks = max(int(dur_units * TICKS_PER_UNIT), 1)
106
+ vel = random.randint(60, 100)
107
+ track.append(Message('note_on', note=note_num, velocity=vel, time=0))
108
+ track.append(Message('note_off', note=note_num, velocity=vel, time=ticks))
109
+ return mid
110
+
111
+ # ------------------------------------------------------------------
112
+ # 6. Prompt helpers – always include strict format
113
+ # ------------------------------------------------------------------
114
+ INSTRUMENT_PROGRAMS = {"Piano":0,"Trumpet":56,"Violin":40,"Clarinet":71,"Flute":73}
115
+
116
+ def fallback(instrument, level, key, time_sig, measures):
117
+ patterns = {
118
+ "Trumpet":["C4","D4","E4","G4"],
119
+ "Piano":["C4","E4","G4","C5"],
120
+ "Violin":["G4","A4","B4","D5"],
121
+ "Clarinet":["E4","F4","G4","Bb4"],
122
+ "Flute":["A4","B4","C5","E5"],
123
+ }
124
+ pat = patterns.get(instrument, patterns["Trumpet"])
125
+ numerator = int(time_sig.split('/')[0])
126
+ total = measures * numerator
127
+ notes, durs = [], []
128
+ i = 0
129
+ while sum(durs) < total:
130
+ notes.append(pat[i % len(pat)])
131
+ durs.append(1)
132
+ i += 1
133
+ if sum(durs) > total:
134
+ durs[-1] -= sum(durs) - total
135
+ return json.dumps([[n,d] for n,d in zip(notes,durs)])
136
+
137
+ # ------------------------------------------------------------------
138
+ # 7. Mistral query – strengthened prompt
139
+ # ------------------------------------------------------------------
140
+ def query_mistral(prompt, instrument, level, key, time_sig, measures):
141
+ headers = {"Authorization": f"Bearer {MISTRAL_API_KEY}", "Content-Type": "application/json"}
142
+ numerator = int(time_sig.split('/')[0])
143
+ target_units = measures * numerator
144
+
145
+ strict_format = (
146
+ "Use ONLY standard note names like 'C4', 'F#5', 'Bb3'. "
147
+ "Use ONLY integer durations representing quarter-note beats: "
148
+ "1 = quarter, 2 = half, 4 = whole. "
149
+ f"Sum MUST equal exactly {target_units}. "
150
+ "Output ONLY a JSON array of [note, integer_duration] pairs. "
151
+ "No prose, no explanation."
152
+ )
153
+
154
+ system = f"You are an expert {instrument.lower()} teacher."
155
+ if prompt.strip():
156
+ user = f"{prompt}\n\n{strict_format}"
157
+ else:
158
+ style = random.choice(["simple","jazzy","technical"])
159
+ tech = random.choice(["with long tones","with slurs","with double tonguing"])
160
+ user = f"Create a {style} {instrument.lower()} exercise in {key}, {time_sig}, {tech}. {strict_format}"
161
+
162
+ payload = {
163
+ "model":"mistral-medium",
164
+ "messages":[{"role":"system","content":system},{"role":"user","content":user}],
165
+ "max_tokens":800,
166
+ "temperature":0.6,
167
+ }
168
+ try:
169
+ r = requests.post(MISTRAL_API_URL, headers=headers, json=payload)
170
+ r.raise_for_status()
171
+ return r.json()["choices"][0]["message"]["content"].strip()
172
+ except Exception as e:
173
+ print("Mistral error:", e)
174
+ return fallback(instrument, level, key, time_sig, measures)
175
+
176
+ # ------------------------------------------------------------------
177
+ # 8. Safe JSON parse – unchanged
178
+ # ------------------------------------------------------------------
179
+ def safe_parse(text):
180
+ try:
181
+ text = text.replace("'",'"')
182
+ m = re.search(r"\[(\s*\[.*?\]\s*,?)*\]", text, re.DOTALL)
183
+ return json.loads(m.group(0) if m else text)
184
+ except Exception:
185
+ return None
186
+
187
+ # ------------------------------------------------------------------
188
+ # 9. Main generation – unchanged
189
+ # ------------------------------------------------------------------
190
+ def generate_exercise(instrument, level, key, tempo, ts, measures, custom_prompt, mode):
191
+ try:
192
+ prompt_in = custom_prompt if mode=="Exercise Prompt" else ""
193
+ raw = query_mistral(prompt_in, instrument, level, key, ts, measures)
194
+ parsed = safe_parse(raw)
195
+ if not parsed:
196
+ return "Invalid JSON", None, str(tempo), None, "0", ts
197
+
198
+ numerator = int(ts.split('/')[0])
199
+ target_units = measures * numerator
200
+ scaled = scale_json_durations(parsed, target_units)
201
+ midi = json_to_midi(scaled, instrument, tempo, ts, measures)
202
+
203
+ # MIDI → MP3
204
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mid") as f:
205
+ midi.save(f.name)
206
+ wav = f.name.replace(".mid",".wav")
207
+ mp3 = f.name.replace(".mid",".mp3")
208
+ sf2 = get_soundfont(instrument)
209
+ import subprocess as sp
210
+ sp.run(["fluidsynth","-ni",sf2,f.name,"-F",wav,"-r","44100"], check=True, capture_output=True)
211
+ AudioSegment.from_wav(wav).export(mp3, format="mp3")
212
+ static_mp3 = os.path.join("static", os.path.basename(mp3))
213
+ shutil.move(mp3, static_mp3)
214
+ dur = AudioSegment.from_mp3(static_mp3).duration_seconds
215
+ return json.dumps(scaled, indent=2), static_mp3, str(tempo), midi, f"{dur:.2f}s", ts
216
+ except Exception as e:
217
+ return f"Error: {e}", None, str(tempo), None, "0", ts
218
+
219
+ def get_soundfont(instrument):
220
+ os.makedirs("soundfonts", exist_ok=True)
221
+ sf2 = f"soundfonts/{instrument}.sf2"
222
+ if not os.path.exists(sf2):
223
+ open(sf2,"wb").write(requests.get(SOUNDFONT_URLS[instrument]).content)
224
+ return sf2
225
+
226
+ # ------------------------------------------------------------------
227
+ # 10. Chat helper – unchanged
228
+ # ------------------------------------------------------------------
229
+ def chat(msg, hist, instr, lvl):
230
+ if not msg.strip(): return "", hist
231
+ msgs=[{"role":"system","content":f"You are a {instr} teacher for {lvl} students."}]
232
+ for u,a in hist: msgs.extend([{"role":"user","content":u},{"role":"assistant","content":a}])
233
+ msgs.append({"role":"user","content":msg})
234
+ headers={"Authorization":f"Bearer {MISTRAL_API_KEY}","Content-Type":"application/json"}
235
+ payload={"model":"mistral-medium","messages":msgs,"max_tokens":500}
236
+ try:
237
+ r=requests.post(MISTRAL_API_URL,headers=headers,json=payload)
238
+ r.raise_for_status()
239
+ reply=r.json()["choices"][0]["message"]["content"]
240
+ hist.append((msg,reply))
241
+ return "", hist
242
+ except Exception as e:
243
+ hist.append((msg,f"Error: {e}"))
244
+ return "", hist
245
+
246
+ # ------------------------------------------------------------------
247
+ # 11. Gradio UI – unchanged
248
+ # ------------------------------------------------------------------
249
+ def ui():
250
+ with gr.Blocks(title="Adaptive Music Exercise Generator – Integer Durations") as demo:
251
+ gr.Markdown("# 🎼 Adaptive Music Exercise Generator – Integer Durations")
252
+ mode = gr.Radio(["Exercise Parameters","Exercise Prompt"], value="Exercise Parameters", label="Mode")
253
+ with gr.Row():
254
+ with gr.Column(scale=1):
255
+ with gr.Group(visible=True) as params_group:
256
+ instrument=gr.Dropdown(["Trumpet","Piano","Violin","Clarinet","Flute"],value="Trumpet")
257
+ level = gr.Radio(["Beginner","Intermediate","Advanced"], value="Intermediate")
258
+ key = gr.Dropdown(["C Major","G Major","D Major","F Major","Bb Major","A Minor","E Minor"],value="C Major")
259
+ ts = gr.Dropdown(["3/4","4/4"], value="4/4", label="Time Signature")
260
+ measures = gr.Radio([4,8], value=4, label="Length (measures)")
261
+ with gr.Group(visible=False) as prompt_group:
262
+ prompt_txt = gr.Textbox("", lines=3, label="Custom prompt")
263
+ measures2 = gr.Radio([4,8], value=4, label="Length (measures)")
264
+ gen_btn = gr.Button("Generate Exercise", variant="primary")
265
+ with gr.Column(scale=2):
266
+ with gr.Tabs():
267
+ with gr.TabItem("Player"):
268
+ audio=gr.Audio(autoplay=True)
269
+ bpm=gr.Textbox(label="Tempo (BPM)")
270
+ ts_disp=gr.Textbox(label="Time Signature")
271
+ dur=gr.Textbox(label="Duration")
272
+ with gr.TabItem("Data"):
273
+ js_out=gr.Code(language="json")
274
+ with gr.TabItem("MIDI"):
275
+ midi_file=gr.File()
276
+ dl_btn=gr.Button("Download MIDI")
277
+ with gr.TabItem("Chat"):
278
+ chatbot=gr.Chatbot(height=400)
279
+ chat_in=gr.Textbox(label="Ask the AI")
280
+ chat_btn=gr.Button("Send")
281
+
282
+ mode.change(lambda m:{params_group:gr.update(visible=m=="Exercise Parameters"),
283
+ prompt_group:gr.update(visible=m=="Exercise Prompt")},
284
+ [mode],[params_group,prompt_group])
285
+
286
+ def caller(m,i,l,k,t,ms,p,ms2):
287
+ real_ms = ms2 if m=="Exercise Prompt" else ms
288
+ return generate_exercise(i,l,k,60,t,real_ms,p or "",m)
289
+ gen_btn.click(caller,[mode,instrument,level,key,ts,measures,prompt_txt,measures2],
290
+ [js_out,audio,bpm,gr.State(),dur,ts_disp])
291
+
292
+ def save(json_txt,instr,ts):
293
+ data=safe_parse(json_txt)
294
+ if not data: return None
295
+ num=int(ts.split('/')[0])
296
+ target=sum(int(d) for _,d in data)
297
+ measures_est=max(1,round(target/num))
298
+ scaled=scale_json_durations(data,measures_est*num)
299
+ midi=json_to_midi(scaled,instr,60,ts,measures_est)
300
+ path="static/exercise.mid"
301
+ midi.save(path)
302
+ return path
303
+ dl_btn.click(save,[js_out,instrument,ts],[midi_file])
304
+
305
+ chat_btn.click(chat,[chat_in,chatbot,instrument,level],[chat_in,chatbot])
306
+ return demo
307
+
308
+ if __name__ == "__main__":
309
+ ui().launch() response += "⚠️ [When to Seek Medical Care]\n" + "\n".join(["• " + item for item in knowledge["warnings"]]) + "\n\n"
310
  response += "📅 [Follow-up]\n• Re-evaluate in 2-3 days if not improving\n• See doctor if symptoms worsen or persist beyond 5 days"
311
  return response
312
  return None