File size: 17,478 Bytes
6b46d68
7aaa922
 
6b46d68
7aaa922
 
 
 
6756b6d
7aaa922
 
45d6cf1
7aaa922
 
 
6756b6d
7aaa922
6756b6d
 
 
7aaa922
6756b6d
45d6cf1
7aaa922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6756b6d
7aaa922
 
 
 
 
45d6cf1
7aaa922
6756b6d
7aaa922
 
 
6756b6d
7aaa922
 
6756b6d
7aaa922
 
 
 
6756b6d
7aaa922
 
 
 
 
 
 
 
 
 
6756b6d
 
7aaa922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45d6cf1
7aaa922
 
 
6756b6d
7aaa922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45d6cf1
6756b6d
7aaa922
 
 
 
 
 
 
 
 
 
 
 
 
45d6cf1
7aaa922
 
 
 
 
 
 
 
 
 
6b46d68
7aaa922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6b46d68
 
 
7aaa922
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#!/usr/bin/env python3
"""AFIP v3 - Acoustic Flatulence Intelligence Platform"""
import os, time, json, hashlib, struct, sqlite3, tempfile, wave
from pathlib import Path
from datetime import datetime, timezone
from typing import List, Tuple
import numpy as np
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware

DB = Path(os.getenv("DATABASE_PATH", "./afip.db"))
OUTPUT = Path("./output"); OUTPUT.mkdir(exist_ok=True)
B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

def b58(v: bytes) -> str:
    n = int.from_bytes(v, "big")
    if n == 0: return B58[0]
    s = ""
    while n: n, r = divmod(n, 58); s = B58[r] + s
    return s

# ── SMF MIDI Writer ──
class SMF:
    def __init__(self, tpq=480):
        self.tpq = tpq; self.tracks: List[List[Tuple[int, bytes]]] = []
    def add(self) -> int: self.tracks.append([]); return len(self.tracks)-1
    def _vlq(self, v: int) -> bytes:
        buf = [v & 0x7F]; v >>= 7
        while v: buf.append((v & 0x7F) | 0x80); v >>= 7
        return bytes(reversed(buf))
    def meta(self, t: int, d: bytes = b"") -> bytes: return bytes([0xFF, t, len(d)]) + d
    def tempo(self, trk: int, tus: int = 500000): self.tracks[trk].append((0, self.meta(0x51, struct.pack(">I", tus)[1:])))
    def prog(self, trk: int, ch: int, p: int): self.tracks[trk].append((0, bytes([0xC0 | (ch & 0x0F), p & 0x7F])))
    def on(self, trk: int, ch: int, n: int, v: int, dt: int = 0): self.tracks[trk].append((dt, bytes([0x90 | (ch & 0x0F), n & 0x7F, v & 0x7F])))
    def off(self, trk: int, ch: int, n: int, v: int = 0, dt: int = 0): self.tracks[trk].append((dt, bytes([0x80 | (ch & 0x0F), n & 0x7F, v & 0x7F])))
    def eot(self, trk: int, dt: int = 0): self.tracks[trk].append((dt, self.meta(0x2F)))
    def save(self, path):
        with open(path, "wb") as f:
            f.write(b"MThd"); f.write(struct.pack(">I", 6)); f.write(struct.pack(">H", 1))
            f.write(struct.pack(">H", len(self.tracks))); f.write(struct.pack(">H", self.tpq))
            for evts in self.tracks:
                data = b"".join(self._vlq(dt) + msg for dt, msg in evts)
                f.write(b"MTrk"); f.write(struct.pack(">I", len(data))); f.write(data)

# ── YIN Pitch Detector ──
class YIN:
    def __init__(self, sr=16000, frame_ms=46):
        self.sr = sr; self.fs = int(sr * frame_ms / 1000)
        self.hs = max(1, self.fs // 4); self.th = 0.15
    def _diff(self, x):
        n, mt = len(x), len(x)//2; d = np.zeros(mt)
        for tau in range(1, mt): d[tau] = np.sum((x[:n-tau] - x[tau:n]) ** 2)
        return d
    def _cmdf(self, df):
        cm = np.ones(len(df)); rs = 0.0
        for tau in range(1, len(df)): rs += df[tau]; cm[tau] = df[tau]/(rs/tau) if rs else 1.0
        return cm
    def pitch(self, frame):
        frame = (frame[:self.fs] if len(frame) >= self.fs else np.pad(frame, (0, self.fs - len(frame)))) * np.hanning(self.fs)
        df = self._diff(frame); cm = self._cmdf(df); est = None
        for tau in range(2, len(cm)):
            if cm[tau] < self.th:
                while tau+1 < len(cm) and cm[tau+1] < cm[tau]: tau += 1
                est = tau; break
        if est is None: est = int(np.argmin(cm[2:])) + 2
        if 1 <= est < len(cm)-1:
            p = 0.5 * (cm[est-1] - cm[est+1]) / (cm[est-1] - 2*cm[est] + cm[est+1])
            est += p
        return self.sr / est if est > 0 else None
    def detect(self, y):
        return [(i / self.sr, self.pitch(y[i:i + self.fs])) for i in range(0, len(y) - self.fs, self.hs)]

# ── WAV I/O ──
def wav_read(path):
    with wave.open(str(path), "rb") as w:
        ch, sw, sr, nf = w.getnchannels(), w.getsampwidth(), w.getframerate(), w.getnframes()
        raw = np.frombuffer(w.readframes(nf), dtype=np.int16)
        if ch == 2: raw = ((raw[0::2] + raw[1::2]) / 2).astype(np.int16)
        return raw.astype(np.float32) / 32768.0, sr

def wav_write(path, y, sr):
    y = np.clip(y * 32767, -32767, 32767).astype(np.int16)
    with wave.open(str(path), "wb") as w:
        w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr); w.writeframes(y.tobytes())

# ── 36-Model Registry ──
REGISTRY = {
    "ml": [
        {"id": "MIT/ast-finetuned-audioset-10-10-0.4593", "task": "audio-classification", "role": "Primary Acoustic Classifier"},
        {"id": "facebook/wav2vec2-base-960h", "task": "asr", "role": "Spectral Transcription"},
        {"id": "microsoft/wavlm-base", "task": "feature-extraction", "role": "Embedding Extractor"},
        {"id": "facebook/hubert-base-ls960", "task": "feature-extraction", "role": "Hidden-Unit BERT"},
        {"id": "google/yamnet", "task": "audio-classification", "role": "Mobile Audio Tagger"},
        {"id": "espnet/owsm_ctc", "task": "asr", "role": "Open Whisper CTC"},
        {"id": "patrickvonplaten/whisper-large-v2", "task": "asr", "role": "Multilingual Whisper"},
        {"id": "openai/whisper-base", "task": "asr", "role": "Baseline Whisper"},
        {"id": "spotify/basic-pitch", "task": "audio-to-audio", "role": "Fundamental Freq Tracker"},
        {"id": "facebook/encodec_24khz", "task": "audio-to-audio", "role": "Neural Codec"},
        {"id": "speechbrain/sepformer-wsj02mix", "task": "audio-to-audio", "role": "Source Separation"},
        {"id": "m3hrdadfi/wav2vec2-base-100k-gtzan-music-genre", "task": "audio-classification", "role": "Genre Classifier"},
        {"id": "ehcalabres/wav2vec2-lg-xlsr-en-speech-emotion-recognition", "task": "audio-classification", "role": "Emotion Detector"},
        {"id": "superb/wav2vec2-base-superb-er", "task": "audio-classification", "role": "SUPERB Emotion"},
        {"id": "alefiury/wav2vec2-base-960h-gender-recognition-libri", "task": "audio-classification", "role": "Gender Profile"},
        {"id": "facebook/wav2vec2-xlsr-53", "task": "feature-extraction", "role": "XLS-R Encoder"},
        {"id": "jonatasgrosman/wav2vec2-large-xlsr-53-english", "task": "asr", "role": "English ASR"},
        {"id": "facebook/s2t-small-librispeech-asr", "task": "asr", "role": "Speech-to-Text S2T"},
        {"id": "speechbrain/emotion-recognition-wav2vec2-IEMOCAP", "task": "audio-classification", "role": "IEMOCAP Baseline"},
        {"id": "sentence-transformers/all-MiniLM-L6-v2", "task": "feature-extraction", "role": "Semantic Embedding"},
        {"id": "sentence-transformers/all-mpnet-base-v2", "task": "feature-extraction", "role": "MPNet Encoder"},
        {"id": "facebook/bart-base", "task": "feature-extraction", "role": "BART Feature"},
        {"id": "facebook/roberta-base", "task": "feature-extraction", "role": "RoBERTa Context"},
        {"id": "cardiffnlp/twitter-roberta-base-emotion", "task": "text-classification", "role": "Twitter Emotion"},
        {"id": "distilbert-base-uncased-finetuned-sst-2-english", "task": "text-classification", "role": "SST-2 Sentiment"},
        {"id": "dslim/bert-base-NER", "task": "token-classification", "role": "NER Tagger"},
        {"id": "huggingface-course/audio-transformers", "task": "audio-classification", "role": "Course Ref"},
        {"id": "sanchit-gandhi/whisper-medium-finetuned-common-voice-13", "task": "asr", "role": "CV-13 Whisper"},
        {"id": "jonatasgrosman/wavlm-large-xtreme-s", "task": "audio-classification", "role": "XTreme Emotion"},
        {"id": "ehcalabres/wav2vec2-lg-xlsr-en-speech-emotion-recognition", "task": "audio-classification", "role": "Emotion Re-Classifier"},
    ],
    "llm": [
        {"id": "mistralai/Mistral-7B-Instruct-v0.1", "task": "text-generation", "role": "Poetry Engine"},
        {"id": "meta-llama/Llama-2-7b-chat-hf", "task": "text-generation", "role": "Scientific Abstract"},
        {"id": "google/gemma-7b-it", "task": "text-generation", "role": "Naming Conventions"},
        {"id": "HuggingFaceH4/zephyr-7b-beta", "task": "text-generation", "role": "Roast & Critique"},
        {"id": "microsoft/Phi-3-mini-4k-instruct", "task": "text-generation", "role": "Shakespearean Xlator"},
        {"id": "tiiuae/falcon-7b-instruct", "task": "text-generation", "role": "Tokenomics Architect"},
    ],
}
ALL_MODELS = REGISTRY["ml"] + REGISTRY["llm"]

def stub_infer(m: dict, seed: int) -> dict:
    rng = np.random.default_rng(seed)
    t = m["task"]
    if t == "audio-classification":
        return {"label": str(rng.choice(["toot","brap","poot","squeak","rumble","whistle","plop","thunder"])), "score": round(float(rng.random()*0.4+0.5),4)}
    if t == "asr":
        return {"text": str(rng.choice(["brrrraaaaap","pfffffttt","prrrrrrrt","squeeeeeak","thunderclap"]))}
    if t == "feature-extraction":
        return {"dims": 768, "preview": [round(float(x),6) for x in rng.random(4)]}
    if t == "audio-to-audio":
        return {"output": "synthetic_reconstruction.wav", "quality": round(float(rng.random()),4)}
    if t == "text-classification":
        return {"label": str(rng.choice(["POSITIVE","NEGATIVE","NEUTRAL"])), "score": round(float(rng.random()),4)}
    if t == "token-classification":
        return {"entities": [{"word": "fart", "label": "B-FART", "score": 0.99}]}
    if t == "text-generation":
        return {"generated_text": f"[stub] {m['role']} says: beep boop"}
    return {"stub": True}

# ── DB ──
def init_db():
    with sqlite3.connect(DB, check_same_thread=False) as c:
        c.execute("PRAGMA journal_mode=WAL")
        c.execute("CREATE TABLE IF NOT EXISTS farts (id TEXT PRIMARY KEY, ts TEXT, audio_hash TEXT, fingerprint TEXT, fartscore INTEGER, midi_path TEXT, note_count INTEGER, duration REAL, report JSON, prev_hash TEXT, receipt_hash TEXT)")
        c.execute("CREATE TABLE IF NOT EXISTS analyses (id INTEGER PRIMARY KEY, fart_id TEXT, model_id TEXT, task TEXT, role TEXT, result JSON, latency_ms REAL, ts TEXT)")
        c.execute("CREATE INDEX IF NOT EXISTS idx_farts_ts ON farts(ts)")
        c.execute("CREATE INDEX IF NOT EXISTS idx_analyses_fart ON analyses(fart_id)")

def db_conn():
    c = sqlite3.connect(DB, check_same_thread=False)
    c.row_factory = sqlite3.Row
    return c

def latest_receipt() -> str:
    with db_conn() as c:
        r = c.execute("SELECT receipt_hash FROM farts ORDER BY ts DESC LIMIT 1").fetchone()
        return r["receipt_hash"] if r else ""

def insert_fart(fid, ah, fp, fscore, midi, notes, dur, report, prev, receipt):
    with db_conn() as c:
        c.execute("INSERT INTO farts VALUES (?,?,?,?,?,?,?,?,?,?,?)", (fid, datetime.now(timezone.utc).isoformat(), ah, fp, fscore, midi, notes, dur, json.dumps(report), prev, receipt))
        c.commit()

def insert_analysis(fid, m, res, lat):
    with db_conn() as c:
        c.execute("INSERT INTO analyses (fart_id, model_id, task, role, result, latency_ms, ts) VALUES (?,?,?,?,?,?,?)", (fid, m["id"], m["task"], m["role"], json.dumps(res), lat, datetime.now(timezone.utc).isoformat()))
        c.commit()

def list_farts(limit=50):
    with db_conn() as c:
        return [dict(r) for r in c.execute("SELECT * FROM farts ORDER BY ts DESC LIMIT ?", (limit,)).fetchall()]

def get_fart(fid):
    with db_conn() as c:
        r = c.execute("SELECT * FROM farts WHERE id=?", (fid,)).fetchone()
        return dict(r) if r else None

def leaderboard():
    with db_conn() as c:
        return [dict(r) for r in c.execute("SELECT fingerprint, fartscore, ts, note_count FROM farts ORDER BY fartscore DESC LIMIT 20").fetchall()]

# ── Ledger ──
def fingerprint(audio_bytes: bytes) -> tuple:
    h = hashlib.sha256(audio_bytes).digest()
    score = int(hashlib.sha256(h).hexdigest(), 16) % 101
    return "Fart" + b58(h)[:38], score

def receipt(fid: str, ah: str, fp: str, fscore: int, prev: str) -> str:
    return hashlib.sha256(f"{fid}:{ah}:{fp}:{fscore}:{prev}".encode()).hexdigest()

# ── Note Segmentation ──
def segment_notes(pitches):
    notes = []; active = False; nstart = 0.0; cur = None
    for t, p in pitches:
        if p and 40 <= p <= 2000:
            mn = max(0, min(127, int(69 + 12 * np.log2(p / 440))))
            vel = min(127, max(30, int(70 + np.random.randn() * 20)))
            if not active: active, nstart, cur = True, t, mn
            elif abs(mn - cur) > 2:
                if t - nstart >= 0.05: notes.append((nstart, t - nstart, cur, vel))
                nstart, cur = t, mn
        else:
            if active and t - nstart >= 0.05: notes.append((nstart, t - nstart, cur, vel))
            active = False
    if active and pitches and pitches[-1][0] - nstart >= 0.05:
        notes.append((nstart, pitches[-1][0] - nstart, cur, vel))
    return notes

def build_midi(notes, path):
    if not notes: return None
    w = SMF(); t = w.add(); w.tempo(t); w.prog(t, 0, 58)
    notes = sorted(notes, key=lambda x: x[0])
    tps = 480 * (120 / 60.0); last = 0
    for s, d, n, v in notes:
        on, dur = int(s * tps), max(1, int(d * tps))
        w.on(t, 0, n, v, on - last); w.off(t, 0, n, 0, dur); last = on + dur
    w.eot(t, 0); w.save(path); return path

# ── Full Pipeline ──
def run_pipeline(wav_path: Path):
    y, sr = wav_read(wav_path)
    dur = len(y) / sr
    audio_bytes = open(wav_path, "rb").read()
    ah = hashlib.sha256(audio_bytes).hexdigest()
    fp, fscore = fingerprint(audio_bytes)
    prev = latest_receipt() or ""

    pitches = YIN(sr=sr).detect(y)
    notes = segment_notes(pitches)
    midi_file = OUTPUT / f"{fp[:12]}_{int(time.time())}.mid"
    build_midi(notes, midi_file)

    seed = int(hashlib.md5(audio_bytes[:4096]).hexdigest(), 16) % (2**31)
    ml_out, llm_out = [], []
    for m in REGISTRY["ml"]:
        t0 = time.time()
        res = stub_infer(m, seed)
        lat = (time.time() - t0) * 1000
        ml_out.append({"model": m["id"], "role": m["role"], "task": m["task"], "result": res, "latency_ms": round(lat, 2)})
        insert_analysis(fp[:16], m, res, lat)

    prompts = [
        "Write a haiku about this fart.",
        "Name this fart like a startup.",
        "Write a fake Nature abstract about this acoustic emission.",
        "Roast this fart mercilessly.",
        "Translate this fart into Shakespearean English.",
        "Write Solana memecoin tokenomics for this fart.",
    ]
    for m, pr in zip(REGISTRY["llm"], prompts):
        t0 = time.time()
        res = stub_infer(m, int(hashlib.md5((audio_bytes[:4096] + pr.encode())).hexdigest(), 16) % (2**31))
        lat = (time.time() - t0) * 1000
        llm_out.append({"model": m["id"], "role": m["role"], "prompt": pr, "result": res, "latency_ms": round(lat, 2)})
        insert_analysis(fp[:16], m, res, lat)

    rec = receipt(fp[:16], ah, fp, fscore, prev)
    report = {"fingerprint": fp, "fartscore": fscore, "duration": dur, "note_count": len(notes), "models": ml_out + llm_out}
    insert_fart(fp[:16], ah, fp, fscore, str(midi_file), len(notes), dur, report, prev, rec)
    return {
        "fart_id": fp[:16], "fingerprint": fp, "fartscore": fscore,
        "duration_sec": dur, "note_count": len(notes),
        "midi_url": f"/output/{midi_file.name}" if midi_file.exists() else None,
        "receipt": rec, "prev_receipt": prev,
        "model_outputs": ml_out, "llm_outputs": llm_out,
        "notes": [{"start": s, "duration": d, "midi": n, "velocity": v} for s, d, n, v in notes],
    }

# ── FastAPI ──
app = FastAPI(title="AFIP", version="3.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
init_db()

app.mount("/output", StaticFiles(directory="output"), name="output")
if Path("static").exists():
    app.mount("/static", StaticFiles(directory="static"), name="static")

@app.get("/")
def root():
    if Path("static/index.html").exists():
        return FileResponse("static/index.html")
    return {"name": "AFIP", "version": "3.0.0", "models": len(ALL_MODELS)}

@app.get("/health")
def health(): return {"status": "ok", "models_loaded": len(ALL_MODELS), "db": str(DB)}

@app.get("/registry")
def registry(): return {"ml_models": REGISTRY["ml"], "llm_models": REGISTRY["llm"], "total": len(ALL_MODELS)}

@app.get("/history")
def history(limit: int = 50): return list_farts(limit=limit)

@app.get("/leaderboard")
def lb(): return leaderboard()

@app.get("/fart/{fart_id}")
def get_fart_api(fart_id: str):
    r = get_fart(fart_id)
    if not r: raise HTTPException(status_code=404, detail="Fart not found")
    return r

@app.post("/analyze")
async def analyze(file: UploadFile = File(...)):
    tmp = Path(tempfile.gettempdir()) / f"afip_{int(time.time()*1000)}.wav"
    try:
        open(tmp, "wb").write(await file.read())
        return run_pipeline(tmp)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        if tmp.exists(): tmp.unlink()

@app.get("/mock")
def mock():
    t = np.linspace(0, 1.5, int(16000 * 1.5))
    y = np.sin(2 * np.pi * 120 * t) * np.exp(-t * 2) + np.sin(2 * np.pi * 85 * t) * 0.5
    y += np.random.randn(len(y)) * 0.02
    tmp = OUTPUT / f"mock_{int(time.time())}.wav"
    wav_write(tmp, y, 16000)
    return run_pipeline(tmp)

@app.get("/download/{fname}")
def download(fname: str):
    p = OUTPUT / fname
    if not p.exists(): raise HTTPException(status_code=404)
    return FileResponse(p, media_type="audio/midi", filename=fname)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "8080")))