Zonza ne tient plus elle-meme le micro qu'elle essaie d'ouvrir
Trois dictees d'affilee sans le moindre son, chacune terminee par la fermeture forcee du halo. Cause : Recorder.start() ecrasait self._stream sans fermer le precedent. Des qu'un stop() se bloquait sur un peripherique accapare, le flux restait ouvert POUR TOUJOURS ; l'essai suivant en empilait un autre par-dessus, et Zonza finissait par tenir le micro qu'elle tentait d'ouvrir. 25 threads dans le process en temoignaient. start() libere donc le flux precedent avant d'en ouvrir un nouveau. La liberation DETACHE la reference d'abord — meme ratee, elle ne doit pas laisser un flux mort barrer le prochain demarrage — et utilise abort() plutot que stop(), ce dernier drainant les tampons et pouvant se bloquer. Elle ne leve jamais. Le flux est desormais injectable, ce qui rend Recorder testable sans peripherique. Prouve sur le vrai micro, en enchainant deux demarrages sans arret entre les deux : 1066 blocs au premier, 1070 au second. Avant, le second en recevait zero. 80 tests passent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ebf3c8e749
commit
19afbe0369
36
engine.py
36
engine.py
@ -80,11 +80,13 @@ class Recorder:
|
||||
(utilisé pour alimenter l'animation en temps réel).
|
||||
"""
|
||||
|
||||
def __init__(self, sample_rate, on_block=None):
|
||||
def __init__(self, sample_rate, on_block=None, ouvrir_flux=None):
|
||||
self.sample_rate = sample_rate
|
||||
self.on_block = on_block
|
||||
self._frames = []
|
||||
self._stream = None
|
||||
# ouvrir_flux : injectable pour les tests (aucun peripherique requis).
|
||||
self._ouvrir_flux = ouvrir_flux or (lambda **kw: sd.InputStream(**kw))
|
||||
|
||||
def _callback(self, indata, frames, time_info, status):
|
||||
block = indata.copy()
|
||||
@ -92,9 +94,29 @@ class Recorder:
|
||||
if self.on_block is not None:
|
||||
self.on_block(block.flatten())
|
||||
|
||||
def _liberer(self):
|
||||
"""Libère le flux courant. Ne lève jamais, ne bloque pas durablement.
|
||||
|
||||
On DÉTACHE la référence avant d'agir : même si la libération échoue, le
|
||||
prochain démarrage ne doit pas retomber sur un flux mort. Et on utilise
|
||||
`abort()` plutôt que `stop()` — ce dernier draine les tampons et peut se
|
||||
bloquer sur un périphérique accaparé, ce qui laissait le micro tenu pour
|
||||
toujours (2026-08-22 : trois dictées muettes d'affilée).
|
||||
"""
|
||||
flux, self._stream = self._stream, None
|
||||
if flux is None:
|
||||
return
|
||||
for action in ("abort", "close"):
|
||||
try:
|
||||
getattr(flux, action)()
|
||||
except Exception as exc:
|
||||
print(f"[audio] {action}() du flux précédent a échoué : {exc}")
|
||||
|
||||
def start(self):
|
||||
# un flux orphelin d'un essai precedent tiendrait le micro : on libere d'abord
|
||||
self._liberer()
|
||||
self._frames = []
|
||||
self._stream = sd.InputStream(
|
||||
self._stream = self._ouvrir_flux(
|
||||
samplerate=self.sample_rate,
|
||||
channels=1,
|
||||
dtype="float32",
|
||||
@ -104,13 +126,11 @@ class Recorder:
|
||||
|
||||
def stop(self):
|
||||
"""Arrête le flux et renvoie l'audio concaténé (numpy 1-D float32)."""
|
||||
if self._stream is not None:
|
||||
self._stream.stop()
|
||||
self._stream.close()
|
||||
self._stream = None
|
||||
if not self._frames:
|
||||
frames, self._frames = self._frames, []
|
||||
self._liberer()
|
||||
if not frames:
|
||||
return np.zeros(0, dtype="float32")
|
||||
return np.concatenate(self._frames, axis=0).flatten()
|
||||
return np.concatenate(frames, axis=0).flatten()
|
||||
|
||||
|
||||
class Transcriber:
|
||||
|
||||
78
tests/test_liberation_flux.py
Normal file
78
tests/test_liberation_flux.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Liberation du flux audio.
|
||||
|
||||
Bug du 2026-08-22 : trois dictees d'affilee sans le moindre son. Cause :
|
||||
`Recorder.start()` ecrasait `self._stream` sans fermer le precedent. Quand un
|
||||
`stop()` se bloquait (peripherique accapare), le flux restait ouvert POUR TOUJOURS
|
||||
— et l'essai suivant en empilait un autre. Zonza finissait par tenir elle-meme le
|
||||
micro qu'elle tentait d'ouvrir.
|
||||
|
||||
On libere donc AVANT d'ouvrir, avec `abort()` (immediat) plutot que `stop()` (qui
|
||||
draine les tampons et peut bloquer), et sans jamais lever.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from engine import Recorder
|
||||
|
||||
|
||||
class FauxFlux:
|
||||
def __init__(self, casse_sur=()):
|
||||
self.actions = []
|
||||
self.casse_sur = casse_sur
|
||||
|
||||
def start(self): self.actions.append("start")
|
||||
def abort(self):
|
||||
self.actions.append("abort")
|
||||
if "abort" in self.casse_sur: raise RuntimeError("peripherique occupe")
|
||||
def close(self):
|
||||
self.actions.append("close")
|
||||
if "close" in self.casse_sur: raise RuntimeError("close impossible")
|
||||
|
||||
|
||||
def _rec(flux_successifs):
|
||||
it = iter(flux_successifs)
|
||||
return Recorder(16000, ouvrir_flux=lambda **kw: next(it))
|
||||
|
||||
|
||||
def test_demarrer_libere_le_flux_precedent():
|
||||
a, b = FauxFlux(), FauxFlux()
|
||||
r = _rec([a, b])
|
||||
r.start()
|
||||
r.start() # second demarrage sans stop entre les deux
|
||||
assert "abort" in a.actions and "close" in a.actions, "l'ancien flux doit etre libere"
|
||||
assert b.actions == ["start"]
|
||||
|
||||
|
||||
def test_on_prefere_abort_a_stop():
|
||||
a = FauxFlux()
|
||||
r = _rec([a])
|
||||
r.start(); r.stop()
|
||||
assert "abort" in a.actions
|
||||
assert "stop" not in a.actions, "stop() draine les tampons et peut bloquer"
|
||||
|
||||
|
||||
def test_un_flux_recalcitrant_ne_fait_pas_echouer_le_demarrage():
|
||||
a, b = FauxFlux(casse_sur=("abort", "close")), FauxFlux()
|
||||
r = _rec([a, b])
|
||||
r.start()
|
||||
r.start() # ne doit rien lever malgre les deux echecs
|
||||
assert b.actions == ["start"]
|
||||
|
||||
|
||||
def test_le_flux_est_detache_meme_si_la_liberation_echoue():
|
||||
a = FauxFlux(casse_sur=("abort", "close"))
|
||||
r = _rec([a])
|
||||
r.start(); r.stop()
|
||||
assert r._stream is None, "une reference morte empecherait le prochain demarrage"
|
||||
|
||||
|
||||
def test_stop_rend_l_audio_capture():
|
||||
a = FauxFlux()
|
||||
r = _rec([a])
|
||||
r.start()
|
||||
r._callback(np.ones((512, 1), dtype="float32"), 512, None, None)
|
||||
audio = r.stop()
|
||||
assert audio.shape == (512,)
|
||||
|
||||
|
||||
def test_stop_sans_demarrage_ne_leve_pas():
|
||||
assert _rec([]).stop().size == 0
|
||||
Loading…
Reference in New Issue
Block a user