uri410's picture
Deploy NovelReader Translate Tool
abd39d3 verified
Raw History Blame Contribute Delete
2.41 kB
"""Audio consumer queue, bounded backlog, multi-worker support, and deterministic shutdown.
Only the translation producer mutates the book scope. The job stays active until
close() joins the consumer workers, so reset/delete/rename cannot race audio output.
"""
import queue
import threading
class AudioQueue:
def __init__(self, consume, checkpoint, cancel, maxsize=2, workers=1):
self.consume = consume
self.checkpoint = checkpoint
self.cancel = cancel
self.queue = queue.Queue(maxsize=maxsize)
self.error = None
self.closed = False
self.workers = []
for i in range(max(1, int(workers))):
t = threading.Thread(target=self._run, name=f'chapter-audio-{i}', daemon=True)
t.start()
self.workers.append(t)
@property
def worker(self):
"""Backward compatibility for checks like `q.worker.is_alive()`."""
return self.workers[0] if self.workers else None
def submit(self, item):
if self.closed:
raise RuntimeError('Audio queue is closed')
while any(w.is_alive() for w in self.workers):
if self.error:
raise RuntimeError('Audio worker failed') from self.error
if not self.checkpoint():
self.cancel(item)
return False
try:
self.queue.put(item, timeout=0.2)
return True
except queue.Full:
continue
raise RuntimeError('Audio worker stopped unexpectedly') from self.error
def _run(self):
while True:
item = self.queue.get()
try:
if item is None:
return
if self.error is not None or not self.checkpoint():
self.cancel(item)
else:
self.consume(item)
except Exception as exc:
# Do not abandon a full queue and deadlock its producer.
self.error = exc
finally:
self.queue.task_done()
def close(self):
if not self.closed:
self.closed = True
for _ in self.workers:
self.queue.put(None)
for w in self.workers:
w.join()
if self.error:
raise RuntimeError('Audio worker failed') from self.error