Registra: messaggi di errore azionabili + log tecnici espandibili

Sul sistema dell'utente il comando ffmpeg che usa l'app funziona
correttamente (testato come subprocess identico), ma il vecchio
reporting era confuso quando l'errore era transitorio (BlackHole
non pronto, clock drift, ecc.) o quando la parola 'abort' non
compariva nello stderr.

Cambiamenti:

core/recorder.py:
- _extract_friendly_error riconosce ora piu casi: input/output error
  (BlackHole non pronto -> suggerisce 'sudo killall coreaudiod'),
  device not configured, permission denied, no such device
- rc=-6 su macOS ritorna SEMPRE il messaggio sul permesso Microfono
  (prima richiedeva 'abort' nello stderr, non sempre presente)
- Fallback finale mostra anche le ultime 3 righe di stderr
- Nuova funzione get_last_stderr() per debugging

api/bridge.py:
- Nuovo metodo get_recorder_log() che ritorna stderr per la UI

webui:
- Nuova card di errore persistente con bottoni
  'Mostra log tecnici' / 'Copia log'
- Si resetta automaticamente all'avvio di una nuova registrazione

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
luzadevandClaude Opus 4.7 committed 2026-06-09 01:08:11 +02:00
1 parent 2d8290579b
commit f339197861
5 files changed
+140 -16

No files matched your search

+6
View File
@@ -27,6 +27,7 @@ from core.recorder import (
start_recording,
stop_recording as recorder_stop,
is_recording,
get_last_stderr as recorder_last_stderr,
)
from core.upgrader import (
upgrade_folder,
@@ -376,6 +377,11 @@ class Api:
def is_audio_recording(self) -> dict:
return {"recording": is_recording()}
def get_recorder_log(self) -> dict:
"""Ritorna le ultime righe di stderr di ffmpeg dell'ultima sessione
di registrazione. Utile per diagnosticare un errore."""
return {"ok": True, "lines": recorder_last_stderr(80)}
# ------------------------------------------------------------------
# Audio folder scan (Upgrade tab)
# ------------------------------------------------------------------
+54 -15
View File
@@ -335,39 +335,78 @@ def start_recording(
def _extract_friendly_error(rc: int) -> str:
"""Trasforma rc + stderr di ffmpeg in un messaggio leggibile.
Riconosce i casi tipici: permessi microfono, device occupato,
device non disponibile, ..."""
device non disponibile, clock drift di BlackHole, ecc.
Fallback: se non riconosce il pattern, ritorna le ultime righe
significative dello stderr cosi' l'utente puo' diagnosticare
o aprire un ticket.
"""
with _stderr_lock:
lines = list(_stderr_buf)
text = "\n".join(lines).lower()
is_mac = sys.platform == "darwin"
if "abort" in text and rc == -6 and sys.platform == "darwin":
# ---- Errori specifici (priorita' alta) ----
if "input/output error" in text or "errno 22" in text:
return (
"ffmpeg abortito (errore -6): probabilmente l'app non ha il "
"permesso 'Microfono' di macOS. Vai su Preferenze di Sistema "
"→ Privacy e sicurezza → Microfono e abilita Terminal/Python. "
"Poi riavvia l'app."
"Dispositivo audio non pronto (BlackHole o scheda virtuale). "
"Spesso si risolve cosi':\n"
" 1) Smetti la registrazione e riprova fra 5 secondi\n"
" 2) Se persiste, da Terminale: 'sudo killall coreaudiod'\n"
" 3) Verifica che nessun'altra app stia gia' registrando il device"
)
if any(s in text for s in ("input/output error", "errno 22", "device not configured")):
return "Dispositivo non disponibile (riavvia il servizio audio o ricontrolla la selezione)."
if "device not configured" in text or "device not available" in text:
return "Dispositivo non configurato. Premi 'Aggiorna' e riseleziona."
if "permission" in text or "not authorized" in text or "not permitted" in text:
return "Permesso microfono negato da macOS. Abilita Python/MusicTools in Privacy → Microfono."
if "permission" in text or "not authorized" in text or "not permitted" in text \
or "tcc" in text or "denied" in text:
return _permission_message()
if "no such device" in text or "no such audio device" in text:
return "Dispositivo non trovato. Premi '↻ Aggiorna' e riseleziona."
if "no such device" in text or "no such audio device" in text or "invalid device" in text:
return "Dispositivo non trovato. Premi 'Aggiorna' e riseleziona dalla lista."
# Restituisci l'ultima riga utile dello stderr (esclude le righe di banner/progress)
# ---- Macro per rc=-6 (SIGABRT) su macOS ----
# Su macOS rc=-6 e' quasi sempre il sintomo di un crash di ffmpeg
# dovuto al permesso Microfono mancante. La parola 'abort' non
# sempre compare nello stderr di ffmpeg recenti.
if rc == -6 and is_mac:
return _permission_message()
# ---- Fallback: cerca le ultime righe significative ----
for line in reversed(lines):
l = line.strip()
if not l:
continue
low = l.lower()
# Riga AVFoundation/dshow specifica (es. "[avfoundation @ 0x...] Could not...")
if l.startswith("[avfoundation") or l.startswith("[dshow") or "@ 0x" in l:
return l
if "error" in l.lower() or "fail" in l.lower() or "denied" in l.lower():
if "error" in low or "fail" in low or "denied" in low or "cannot" in low:
return l
return f"ffmpeg exit {rc}"
# Ultima spiaggia: rc + ultime 3 righe di stderr per debug
tail = " | ".join(line for line in lines[-3:] if line.strip()) or "(nessuno)"
return f"ffmpeg exit {rc}. Ultimo stderr: {tail}"
def _permission_message() -> str:
"""Messaggio standardizzato per il problema permesso Microfono macOS."""
return (
"Permesso Microfono mancante. Su macOS:\n"
" Impostazioni di Sistema -> Privacy e sicurezza -> Microfono\n"
" Abilita 'Terminal' (se lanci l'app da terminale) o 'MusicTools'\n"
" (se usi l'app installata). Poi riavvia l'app."
)
def get_last_stderr(max_lines: int = 50) -> list[str]:
"""Espone le ultime righe di stderr ai fini diagnostici.
Chiamato dal bridge per la UI 'Mostra log tecnici'."""
with _stderr_lock:
return list(_stderr_buf[-max_lines:])
def stop_recording() -> dict:
+35
View File
@@ -1410,3 +1410,38 @@ input[type="number"]::-webkit-inner-spin-button {
gap: 10px;
flex-wrap: wrap;
}
/* ============================================================
REGISTRA: card errore con log tecnici
============================================================ */
.rec-error-card {
border-left: 4px solid var(--red, #f87171);
}
.micro-label.red {
color: #ff8a92;
font-weight: 800;
}
.rec-error-msg {
margin-top: 6px;
font-size: 14px;
color: var(--text);
white-space: pre-line;
line-height: 1.55;
}
.rec-error-log {
margin-top: 12px;
background: #0d0d0d;
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
color: #c0c0c0;
max-height: 320px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
+10
View File
@@ -343,6 +343,16 @@
<div class="micro-label">Ultima registrazione</div>
<div class="rec-result" id="recResult"></div>
</div>
<div class="card rec-error-card" id="recErrorCard" hidden>
<div class="micro-label red">Errore registrazione</div>
<div class="rec-error-msg" id="recErrorMsg"></div>
<div class="row" style="gap: 10px; margin-top: 12px;">
<button class="btn btn-ghost pill btn-sm" id="recShowLogBtn">Mostra log tecnici</button>
<button class="btn btn-ghost pill btn-sm" id="recCopyLogBtn" hidden>Copia log</button>
</div>
<pre class="rec-error-log" id="recErrorLog" hidden></pre>
</div>
</section>
<!-- ===== VIEW: METADATI ===== -->
+35 -1
View File
@@ -222,6 +222,7 @@ const bridgeHandlers = {
$("#recRefreshBtn").disabled = true;
$("#recTimer").textContent = "00:00:00";
$("#recStatus").textContent = "● Registrazione in corso…";
$("#recErrorCard").hidden = true; // pulisci eventuale errore precedente
} else if (status === "tick") {
state.rec.seconds = p.seconds || 0;
$("#recTimer").textContent = formatHms(state.rec.seconds);
@@ -247,7 +248,14 @@ const bridgeHandlers = {
$("#recDevice").disabled = false;
$("#recRefreshBtn").disabled = false;
$("#recStatus").textContent = "";
toast("Errore registrazione: " + (p.error || ""), "error");
const msg = p.error || "Errore sconosciuto";
$("#recErrorMsg").textContent = msg;
$("#recErrorCard").hidden = false;
$("#recErrorLog").hidden = true;
$("#recErrorLog").textContent = "";
$("#recCopyLogBtn").hidden = true;
$("#recShowLogBtn").textContent = "Mostra log tecnici";
toast("Errore registrazione", "error");
}
},
};
@@ -695,6 +703,32 @@ $("#recStopBtn").addEventListener("click", async () => {
await window.pywebview.api.stop_audio_recording();
});
$("#recShowLogBtn")?.addEventListener("click", async () => {
const pre = $("#recErrorLog");
if (!pre.hidden) {
pre.hidden = true;
$("#recShowLogBtn").textContent = "Mostra log tecnici";
$("#recCopyLogBtn").hidden = true;
return;
}
const res = await window.pywebview.api.get_recorder_log();
const lines = (res && res.lines) || [];
pre.textContent = lines.length ? lines.join("\n") : "(log vuoto)";
pre.hidden = false;
$("#recShowLogBtn").textContent = "Nascondi log tecnici";
$("#recCopyLogBtn").hidden = false;
});
$("#recCopyLogBtn")?.addEventListener("click", async () => {
const text = $("#recErrorLog").textContent || "";
try {
await navigator.clipboard.writeText(text);
toast("Log copiato negli appunti", "success");
} catch {
toast("Impossibile copiare", "error");
}
});
// ============================================================
// METADATA editor
// ============================================================