Make the audio file tab remember, and its Stop stop

The two switches were written to disk by the Save button at the far end of
the window, so a file transcribed with timestamps and cleanup was
transcribed without either the next time. They belong to the run rather than
to the form: they go to disk as they are ticked now, and the folder the last
file came from goes with them.

Stop only set a flag that was looked at between chunks, and a file under ten
minutes is one chunk, so for most files it was looked at after the work it
was meant to stop had already finished. Nothing that blocks is reached by a
flag. The request is inside urlopen, ffmpeg is inside communicate, and a
whisper on this machine is a process of ours that would grind on to the end
of the chunk with nobody left to hand the answer to. So the socket is shut
down under the read, ffmpeg is killed, and a local server is stopped and
left for the next run to start again.

Shutting the socket down rather than closing it is the point: close() alone
leaves a thread already inside recv() waiting for bytes that are never
coming now. The connection is registered before it has a socket, so a stop
landing in the few lines between making a connection and blocking on it
refuses the connection rather than missing it and letting urllib quietly
open another.
This commit is contained in:
yusufipk
2026-08-02 09:29:12 +03:00
parent fc333fcb8c
commit 7a507c52eb
7 changed files with 438 additions and 33 deletions
+122
View File
@@ -3,10 +3,17 @@
Nothing here reaches the network. What is checked is the request that would have
gone out, because that is what a new provider changes and what an old one
notices: the URL, the headers, the fields of the multipart body, the JSON.
Stopping one is the exception. Cutting a request off is done to the socket it
is blocked on, and a faked urlopen has no socket to cut, so those tests talk to
a server of their own on the loopback interface.
"""
import http.server
import json
import os
import threading
import time
import unittest
import api
@@ -578,3 +585,118 @@ class TranscribeHere(DikteTest):
with fake_urlopen({"segments": [{"start": 0, "end": 1, "text": " hi"}]}) as calls:
api.transcribe_segments(LOCAL, self.wav)
self.assertEqual(multipart_fields(calls[0])["model"], "ggml-base.bin")
class Stopping(unittest.TestCase):
"""The Stop button, from the far end: a request already blocked on a reply.
The one that matters is a whisper on this machine, which answers minutes
after it was asked, so it is a real socket here rather than a fake urlopen.
Nothing leaves the loopback interface.
"""
def setUp(self):
answering = threading.Event()
class Slow(http.server.BaseHTTPRequestHandler):
def do_POST(self):
self.rfile.read(int(self.headers.get("Content-Length") or 0))
answering.set()
time.sleep(30) # the model, thinking
def log_message(self, *args):
pass
self.answering = answering
self.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Slow)
threading.Thread(target=self.server.serve_forever, daemon=True).start()
self.addCleanup(self.server.server_close)
self.addCleanup(self.server.shutdown)
self.url = f"http://127.0.0.1:{self.server.server_address[1]}/v1/x"
def post(self, aborter, out):
try:
api._request(self.url, b"{}", {}, timeout=30, aborter=aborter)
out.append("answered")
except BaseException as exc: # noqa: BLE001 - the type is the result
out.append(type(exc).__name__)
def test_a_request_waiting_on_a_reply_is_cut_off(self):
aborter, out = api.Aborter(), []
thread = threading.Thread(target=self.post, args=(aborter, out))
thread.start()
self.assertTrue(self.answering.wait(10))
aborter.abort()
thread.join(timeout=10)
self.assertFalse(thread.is_alive())
self.assertEqual(out, ["Aborted"])
def test_a_request_that_starts_after_the_stop_never_goes_out(self):
aborter, out = api.Aborter(), []
aborter.abort()
self.post(aborter, out)
self.assertEqual(out, ["Aborted"])
self.assertFalse(self.answering.is_set())
def test_without_one_the_request_is_the_plain_urllib_one(self):
"""Everything that is not stoppable keeps the opener it always had."""
with fake_urlopen({"text": "hi"}) as calls:
api._request(self.url, b"{}", {})
self.assertEqual(len(calls), 1)
class Sockets(unittest.TestCase):
"""The few lines urllib takes between making a connection and blocking on
it. A stop that lands in there must not leave the request waiting out its
hour-long local timeout."""
class FakeConn:
auto_open = 1
sock = None
closed = False
def close(self):
self.closed = True
def test_a_connection_opened_after_the_stop_is_refused(self):
sockets = api._Sockets()
sockets.cut()
with self.assertRaises(api.Aborted):
sockets.add(self.FakeConn())
def test_one_that_is_already_open_is_closed_where_it_stands(self):
sockets, conn = api._Sockets(), self.FakeConn()
sockets.add(conn)
sockets.cut()
self.assertTrue(conn.closed)
def test_one_with_no_socket_yet_is_stopped_from_making_another(self):
"""close() leaves auto_open on, and the next line would reconnect."""
sockets, conn = api._Sockets(), self.FakeConn()
sockets.add(conn)
sockets.cut()
self.assertEqual(conn.auto_open, 0)
class Aborter(unittest.TestCase):
def test_what_was_registered_is_run_once_the_stop_lands(self):
aborter, cut = api.Aborter(), []
with aborter.holding(lambda: cut.append(True)):
aborter.abort()
self.assertEqual(cut, [True])
def test_a_block_that_ended_is_not_cut_afterwards(self):
aborter, cut = api.Aborter(), []
with aborter.holding(lambda: cut.append(True)):
pass
aborter.abort()
self.assertEqual(cut, [])
def test_a_stop_that_already_landed_stops_the_next_step_too(self):
aborter = api.Aborter()
aborter.abort()
with self.assertRaises(api.Aborted):
aborter.check()
with self.assertRaises(api.Aborted):
with aborter.holding(lambda: None):
pass