CoolFace
Apppublic

alecomma/Logos

sourceHugging Facemitupdated 10mo agoView on Hugging Face
0likes
app.py793 linesDownload Raw Back to root
1#!/usr/bin/env python32# -*- coding: utf-8 -*-3 4import os5import json6import tempfile7 8import numpy as np9import parselmouth10from parselmouth.praat import call11import librosa12 13from flask import Flask, request, render_template_string14 15app = Flask(__name__)16 17# ==========================18#     FUNZIONE FEATURE19# ==========================20 21def estrai_feature(path_audio: str) -> dict:22    """23    Estrae un set di feature acustiche da un file audio.24    Richiede: praat-parselmouth, librosa, numpy25    """26 27    if not os.path.exists(path_audio):28        raise FileNotFoundError(f"File audio non trovato: {path_audio}")29 30    # Prova a caricare con Praat31    try:32        snd = parselmouth.Sound(path_audio)33    except Exception as e:34        msg = (35            f"Il file esiste ma Praat/parselmouth non riesce a leggerlo. "36            f"Probabilmente NON è un WAV PCM (magari un m4a/mp3 rinominato). "37            f"Esporta come WAV 16-bit PCM mono e riprova. "38            f"Dettagli tecnici: {e}"39        )40        raise RuntimeError(msg)41 42    duration = snd.duration  # durata in secondi43 44    # Caricamento per librosa (MFCC, spettro, pause)45    y, sr = librosa.load(path_audio, sr=None, mono=True)46 47    features = {}48 49    # ==========================50    #      1) MFCC (librosa)51    # ==========================52    mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)53    mfcc_mean = mfcc.mean(axis=1)  # media per coefficiente54 55    for i, val in enumerate(mfcc_mean):56        features[f"mfcc_{i+1}_mean"] = float(val)57 58    # ==========================59    #      2) PITCH (F0)60    # ==========================61    pitch = snd.to_pitch()62    f0_mean = call(pitch, "Get mean", 0, 0, "Hertz")63    f0_stdev = call(pitch, "Get standard deviation", 0, 0, "Hertz")64    f0_min = call(pitch, "Get minimum", 0, 0, "Hertz", "Parabolic")65    f0_max = call(pitch, "Get maximum", 0, 0, "Hertz", "Parabolic")66 67    features["f0_mean_Hz"] = float(f0_mean)68    features["f0_stdev_Hz"] = float(f0_stdev)69    features["f0_min_Hz"] = float(f0_min)70    features["f0_max_Hz"] = float(f0_max)71    features["f0_range_Hz"] = float(f0_max - f0_min)72 73    # ==========================74    #      3) INTENSITÀ75    # ==========================76    intensity = snd.to_intensity()77    int_mean = call(intensity, "Get mean", 0, 0)78    int_stdev = call(intensity, "Get standard deviation", 0, 0)79 80    features["intensity_mean_dB"] = float(int_mean)81    features["intensity_stdev_dB"] = float(int_stdev)82 83    # ==========================84    #   4) JITTER & SHIMMER85    # ==========================86    try:87        point_process = call(snd, "To PointProcess (periodic, cc)", 75, 500)88 89        jitter_local = call(90            point_process,91            "Get jitter (local)",92            0.0,     # tmin93            0.0,     # tmax (0 = fino alla fine)94            0.0001,  # periodo minimo95            0.02,    # periodo massimo96            1.3      # max period factor97        )98 99        shimmer_local = call(100            [snd, point_process],101            "Get shimmer (local)",102            0.0,103            0.0,104            0.0001,105            0.02,106            1.3,107            1.6108        )109 110        features["jitter_local"] = float(jitter_local)111        features["shimmer_local"] = float(shimmer_local)112 113    except Exception as e:114        features["jitter_local"] = float("nan")115        features["shimmer_local"] = float("nan")116        features["jitter_shimmer_error"] = str(e)117 118    # ==========================119    #        5) HNR120    # ==========================121    harmonicity = call(snd, "To Harmonicity (cc)", 0.01, 75, 0.1, 1.0)122    hnr_mean = call(harmonicity, "Get mean", 0, 0)123    features["hnr_mean_dB"] = float(hnr_mean)124 125    # ==========================126    #    6) FEATURE SPETTRALI127    # ==========================128    spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr).mean()129    spectral_rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.85).mean()130    spectral_flatness = librosa.feature.spectral_flatness(y=y).mean()131 132    features["spectral_centroid_Hz"] = float(spectral_centroid)133    features["spectral_rolloff_Hz"] = float(spectral_rolloff)134    features["spectral_flatness"] = float(spectral_flatness)135 136    # ==========================137    #   7) PAUSE / SILENZI138    # ==========================139    rms = librosa.feature.rms(y=y)[0]  # energia per frame140    frame_duration = 512 / sr  # hop_length default = 512141 142    thr = 0.03 * float(rms.max()) if rms.max() > 0 else 0.0143    silent_flags = rms < thr144 145    MIN_PAUSE_DUR = 0.2  # secondi146 147    total_silence = 0.0148    num_pauses = 0149    current_silence_frames = 0150 151    for s in silent_flags:152        if s:153            current_silence_frames += 1154        else:155            if current_silence_frames > 0:156                dur = current_silence_frames * frame_duration157                if dur >= MIN_PAUSE_DUR:158                    num_pauses += 1159                    total_silence += dur160                current_silence_frames = 0161 162    if current_silence_frames > 0:163        dur = current_silence_frames * frame_duration164        if dur >= MIN_PAUSE_DUR:165            num_pauses += 1166            total_silence += dur167 168    silence_ratio = total_silence / duration if duration > 0 else float("nan")169    mean_pause_dur = total_silence / num_pauses if num_pauses > 0 else 0.0170 171    features["num_pauses"] = int(num_pauses)172    features["total_silence_s"] = float(total_silence)173    features["silence_ratio"] = float(silence_ratio)174 175    speaking_time = max(duration - total_silence, 0.0)176    speaking_ratio = speaking_time / duration if duration > 0 else float("nan")177 178    features["mean_pause_duration_s"] = float(mean_pause_dur)179    features["speaking_time_s"] = float(speaking_time)180    features["speaking_ratio"] = float(speaking_ratio)181 182    # ==========================183    #   8) INDICI COMPOSITI184    # ==========================185    prosody_range_index = (features["f0_range_Hz"] + features["intensity_stdev_dB"]) / 2.0186    features["prosody_range_index"] = float(prosody_range_index)187 188    flattening_index = 1.0 / (1.0 + features["f0_stdev_Hz"] + abs(features["intensity_stdev_dB"]))189    features["flattening_index"] = float(flattening_index)190 191    # ==========================192    #        INFO DI BASE193    # ==========================194    features["duration_s"] = float(duration)195    features["sr_Hz"] = int(sr)196 197    return features198 199 200# ==========================201#     HTML TEMPLATE CANVA-STYLE + "by AC"202# ==========================203 204TEMPLATE = """205<!doctype html>206<html lang="it">207<head>208  <meta charset="utf-8">209  <title>Analisi vocale psichiatria · by AC</title>210  <link rel="preconnect" href="https://fonts.googleapis.com">211  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>212  <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap" rel="stylesheet">213  <style>214    * {215      box-sizing: border-box;216    }217 218    body {219      margin: 0;220      min-height: 100vh;221      font-family: 'Nunito', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;222      background: radial-gradient(circle at 0% 0%, #fdf2ff 0, #e0f2fe 35%, #fef3c7 70%, #f5f3ff 100%);223      display: flex;224      align-items: center;225      justify-content: center;226      padding: 24px;227      color: #111827;228    }229 230    .shell {231      width: 100%;232      max-width: 1150px;233      background: rgba(255, 255, 255, 0.9);234      border-radius: 28px;235      box-shadow:236        0 18px 45px rgba(148, 163, 184, 0.4),237        0 0 0 1px rgba(255, 255, 255, 0.9);238      padding: 26px 26px 18px;239      position: relative;240      overflow: hidden;241      backdrop-filter: blur(14px);242    }243 244    .shell::before,245    .shell::after {246      content: "";247      position: absolute;248      width: 260px;249      height: 260px;250      border-radius: 999px;251      filter: blur(40px);252      opacity: 0.6;253      pointer-events: none;254      z-index: 0;255    }256 257    .shell::before {258      background: linear-gradient(135deg, #a855f7, #f97316);259      top: -90px;260      right: -50px;261    }262 263    .shell::after {264      background: linear-gradient(135deg, #22c55e, #3b82f6);265      bottom: -110px;266      left: -40px;267    }268 269    .content {270      position: relative;271      z-index: 1;272    }273 274    .header {275      display: flex;276      justify-content: space-between;277      gap: 16px;278      margin-bottom: 18px;279      align-items: center;280    }281 282    .title-block h1 {283      margin: 6px 0 0;284      font-size: 1.9rem;285      letter-spacing: 0.03em;286      display: flex;287      align-items: center;288      gap: 8px;289      flex-wrap: wrap;290    }291 292    .logo-circle {293      width: 30px;294      height: 30px;295      border-radius: 999px;296      background: conic-gradient(from 180deg, #6366f1, #f97316, #ec4899, #22c55e, #6366f1);297      display: flex;298      align-items: center;299      justify-content: center;300      color: white;301      font-size: 0.9rem;302      box-shadow: 0 6px 14px rgba(79, 70, 229, 0.7);303    }304 305    .title-pill {306      font-size: 0.75rem;307      text-transform: uppercase;308      letter-spacing: 0.16em;309      background: rgba(236, 72, 153, 0.07);310      color: #db2777;311      padding: 4px 11px;312      border-radius: 999px;313      border: 1px solid rgba(236, 72, 153, 0.4);314      display: inline-flex;315      align-items: center;316      gap: 6px;317    }318 319    .title-pill span.dot {320      width: 6px;321      height: 6px;322      border-radius: 999px;323      background: #ec4899;324    }325 326    .subtitle {327      margin-top: 6px;328      font-size: 0.9rem;329      color: #4b5563;330    }331 332    .badge-group {333      display: flex;334      flex-direction: column;335      align-items: flex-end;336      gap: 6px;337      font-size: 0.78rem;338      color: #6b7280;339    }340 341    .pill {342      padding: 4px 10px;343      border-radius: 999px;344      background: linear-gradient(135deg, rgba(129, 140, 248, 0.16), rgba(56, 189, 248, 0.16));345      color: #1d4ed8;346      border: 1px solid rgba(129, 140, 248, 0.7);347      display: inline-flex;348      align-items: center;349      gap: 6px;350    }351 352    .status-dot {353      width: 7px;354      height: 7px;355      border-radius: 999px;356      background: #22c55e;357      box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.25);358    }359 360    .status-text {361      display: inline-flex;362      align-items: center;363      gap: 6px;364      padding: 3px 9px;365      border-radius: 999px;366      background: rgba(15, 23, 42, 0.03);367    }368 369    .signature {370      font-size: 0.95rem;371      color: #6366f1;372      font-weight: 700;373      margin-left: 6px;374      display: inline-flex;375      align-items: center;376      gap: 4px;377    }378 379    .signature span.dot {380      width: 6px;381      height: 6px;382      border-radius: 999px;383      background: #6366f1;384    }385 386    .layout {387      display: grid;388      grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.15fr);389      gap: 18px;390    }391 392    @media (max-width: 880px) {393      .layout {394        grid-template-columns: 1fr;395      }396      .badge-group {397        align-items: flex-start;398      }399    }400 401    .card {402      border-radius: 22px;403      border: 1px solid rgba(148, 163, 184, 0.5);404      padding: 16px 16px 14px;405      background: rgba(255, 255, 255, 0.9);406      position: relative;407      overflow: hidden;408    }409 410    .card::before {411      content: "";412      position: absolute;413      inset: 0;414      background: radial-gradient(circle at top left, rgba(129, 140, 248, 0.14), transparent 55%);415      opacity: 1;416      pointer-events: none;417    }418 419    .card-inner {420      position: relative;421      z-index: 1;422    }423 424    .card h2 {425      font-size: 1.02rem;426      margin: 0 0 6px;427      display: flex;428      align-items: center;429      gap: 8px;430    }431 432    .card h2 span.icon {433      font-size: 1.3rem;434    }435 436    .chip {437      font-size: 0.72rem;438      padding: 3px 8px;439      border-radius: 999px;440      background: rgba(248, 250, 252, 0.9);441      border: 1px solid rgba(148, 163, 184, 0.7);442      margin-left: auto;443    }444 445    .card-header-row {446      display: flex;447      align-items: center;448      gap: 8px;449    }450 451    .card-desc {452      margin: 3px 0 12px;453      font-size: 0.84rem;454      color: #6b7280;455    }456 457    label {458      font-size: 0.85rem;459      font-weight: 600;460      color: #374151;461    }462 463    input[type="file"] {464      margin-top: 8px;465      font-size: 0.84rem;466    }467 468    .btn {469      margin-top: 14px;470      padding: 9px 18px;471      border-radius: 999px;472      border: none;473      background: linear-gradient(135deg, #6366f1, #ec4899);474      color: white;475      cursor: pointer;476      font-size: 0.9rem;477      font-weight: 600;478      display: inline-flex;479      align-items: center;480      gap: 8px;481      box-shadow:482        0 10px 25px rgba(79, 70, 229, 0.55),483        0 0 0 1px rgba(255, 255, 255, 0.6);484      transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;485    }486 487    .btn span.icon {488      font-size: 1rem;489    }490 491    .btn:hover {492      filter: brightness(1.05);493      transform: translateY(-1px);494      box-shadow:495        0 12px 28px rgba(79, 70, 229, 0.7),496        0 0 0 1px rgba(255, 255, 255, 0.9);497    }498 499    .btn:active {500      transform: translateY(0);501      box-shadow:502        0 8px 18px rgba(79, 70, 229, 0.8),503        0 0 0 1px rgba(255, 255, 255, 0.9);504    }505 506    .error {507      margin-top: 10px;508      font-size: 0.8rem;509      color: #b91c1c;510      background: #fef2f2;511      border-radius: 12px;512      padding: 8px 10px;513      border: 1px solid #fecaca;514    }515 516    table {517      border-collapse: collapse;518      width: 100%;519      font-size: 0.78rem;520      background: #ffffff;521      border-radius: 14px;522      overflow: hidden;523      border: 1px solid rgba(209, 213, 219, 0.9);524    }525 526    th, td {527      padding: 6px 8px;528      border-bottom: 1px solid #e5e7eb;529    }530 531    th {532      background: linear-gradient(135deg, #eef2ff, #e0f2fe);533      text-align: left;534      font-weight: 700;535      color: #374151;536    }537 538    tr:nth-child(even) td {539      background: #f9fafb;540    }541 542    tr:last-child td {543      border-bottom: none;544    }545 546    .feature-key {547      white-space: nowrap;548      color: #4b5563;549      font-weight: 600;550    }551 552    .feature-val {553      font-family: "SF Mono", ui-monospace, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;554      color: #111827;555    }556 557    pre {558      margin: 0;559      background: #020617;560      color: #e5e7eb;561      padding: 10px 12px;562      border-radius: 14px;563      font-size: 0.74rem;564      max-height: 360px;565      overflow: auto;566      border: 1px solid rgba(15, 23, 42, 0.9);567    }568 569    .json-card {570      margin-top: 16px;571      border-radius: 22px;572      border: 1px solid rgba(15, 23, 42, 0.9);573      background: radial-gradient(circle at 0% 0%, #0f172a 0, #020617 45%, #111827 100%);574      padding: 14px 16px;575      color: #e5e7eb;576    }577 578    .json-title-row {579      display: flex;580      align-items: center;581      justify-content: space-between;582      margin-bottom: 8px;583      font-size: 0.8rem;584      color: #9ca3af;585    }586 587    .json-pill {588      padding: 3px 9px;589      border-radius: 999px;590      border: 1px solid rgba(148, 163, 184, 0.9);591      background: rgba(15, 23, 42, 0.3);592      font-size: 0.7rem;593    }594 595    .footer {596      margin-top: 10px;597      text-align: right;598      font-size: 0.8rem;599      color: #9ca3af;600    }601 602    .footer span.brand {603      font-weight: 700;604      color: #6366f1;605    }606 607    /* Watermark by AC stile Canva */608    .watermark {609      position: fixed;610      bottom: 14px;611      left: 14px;612      font-size: 0.8rem;613      opacity: 0.55;614      color: #6366f1;615      font-weight: 700;616      display: inline-flex;617      align-items: center;618      gap: 4px;619    }620 621    .watermark span.dot {622      width: 6px;623      height: 6px;624      border-radius: 999px;625      background: #ec4899;626    }627  </style>628</head>629<body>630  <div class="shell">631    <div class="content">632      <div class="header">633        <div class="title-block">634          <div class="title-pill">635            <span class="dot"></span>636            VOCE e PSICHIATRIA637          </div>638          <h1>639            <div class="logo-circle">V</div>640            LOGOS: analisi vocale clinica641 642            643            <span class="signature">644              <span class="dot"></span>645              by AC646            </span>647          </h1>648          <p class="subtitle">649            Carica un file <strong>WAV (16-bit PCM, mono)</strong> per estrarre feature acustiche650            su prosodia, pause, jitter/shimmer e parametri spettrali, pronte per l'analisi statistica.651          </p>652        </div>653        <div class="badge-group">654          <div class="pill">655            <span>🎓 Tesi sperimentale</span>656            <span>·</span>657            <span>by AC</span>658          </div>659          <div class="status-text">660            <span class="status-dot"></span>661            <span>662              {{ 'Risultati caricati · pronto per interpretazione' if features else 'In attesa di un file audio' }}663            </span>664          </div>665        </div>666      </div>667 668      <div class="layout">669        <div class="card">670          <div class="card-inner">671            <div class="card-header-row">672              <h2><span class="icon">🎙️</span>Carica audio</h2>673              <div class="chip">Input .wav</div>674            </div>675            <p class="card-desc">676              Seleziona il file del colloquio / lettura. Ideale: voce chiara, senza rumori677              di fondo importanti. I parametri vengono calcolati localmente sul tuo computer.678            </p>679            <form method="post" enctype="multipart/form-data">680              <label for="audio_file">File audio (.wav)</label><br>681              <input type="file" id="audio_file" name="audio_file" accept=".wav" required>682              <br>683              <button type="submit" class="btn">684                <span class="icon">✨</span>685                Analizza file686              </button>687            </form>688            {% if error %}689              <p class="error">{{ error }}</p>690            {% endif %}691          </div>692        </div>693 694        <div class="card">695          <div class="card-inner">696            <div class="card-header-row">697              <h2><span class="icon">📊</span>Risultati sintetici</h2>698              <div class="chip">Feature numeriche</div>699            </div>700            <p class="card-desc">701              Valori numerici pronti per essere copiati in un foglio dati.702              Ogni riga corrisponde a una feature acustica del file caricato.703            </p>704 705            {% if features %}706              <table>707                <tr><th>Feature</th><th>Valore</th></tr>708                {% for k, v in features.items() %}709                  <tr>710                    <td class="feature-key">{{ k }}</td>711                    <td class="feature-val">{{ v }}</td>712                  </tr>713                {% endfor %}714              </table>715            {% else %}716              <p style="font-size:0.8rem; color:#6b7280; margin-top:4px;">717                Nessun risultato ancora. Carica un file sulla sinistra per iniziare.718              </p>719            {% endif %}720          </div>721        </div>722      </div>723 724      {% if features %}725      <div class="json-card">726        <div class="json-title-row">727          <span>JSON completo delle feature estratte</span>728          <span class="json-pill">Copia-incolla diretto in script · Python / R</span>729        </div>730        <pre>{{ features_json }}</pre>731      </div>732      {% endif %}733 734      <div class="footer">735        by <span class="brand">AC</span>736      </div>737    </div>738  </div>739 740  <div class="watermark">741    <span class="dot"></span>742    by AC743  </div>744</body>745</html>746"""747 748# ==========================749#         ROUTE WEB750# ==========================751 752@app.route("/", methods=["GET", "POST"])753def index():754    features = None755    features_json = None756    error = None757 758    if request.method == "POST":759        if "audio_file" not in request.files:760            error = "Nessun file ricevuto."761        else:762            file = request.files["audio_file"]763            if file.filename == "":764                error = "Nessun file selezionato."765            else:766                # Salva il file in un temporaneo767                with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:768                    file.save(tmp.name)769                    tmp_path = tmp.name770 771                try:772                    feat = estrai_feature(tmp_path)773                    # ordina le feature per chiave774                    features = dict(sorted(feat.items(), key=lambda x: x[0]))775                    features_json = json.dumps(features, indent=2, ensure_ascii=False)776                except Exception as e:777                    error = str(e)778                finally:779                    # ripulisci il file temporaneo780                    if os.path.exists(tmp_path):781                        os.remove(tmp_path)782 783    return render_template_string(784        TEMPLATE,785        features=features,786        features_json=features_json,787        error=error,788    )789 790 791if __name__ == "__main__":792    app.run(debug=True)793