Files
tai/tai_pkg/edge.py
T
retoor 8df5cf7a45 Split monolithic tai.py into tai_pkg package, keep tai-orig.py as original
tai.py (8643 lines) is now a thin facade over tai_pkg/ (33 feature modules, one class or feature per file). tai-orig.py preserves the pre-split single-file version byte for byte (sha256 verified against snapshot); import tai and ./tai.py behavior unchanged.

Equivalence proof, all run on this tree immediately pre-commit: differential battery tai-orig.py vs facade 3718 checks 0 failures (surface, pure fuzz, tool dispatch, store/audit, release ops, fake-HOME install roundtrip, CLI runs); test_tai.py 261 OK; test_seal.py 16 OK; --version/--help byte-identical; dir(tai) zero removals.

Fix included: opencode wire-protocol schema echo leaked into replies; opencode_parse now scans with JSONDecoder.raw_decode capped at 200 scans (13 OpencodeParseTests). Test count 261 = 237 HEAD + 8 pre-existing uncommitted ToolArgumentsTests (now committed with this change, disclosed) + 13 OpencodeParseTests + 3 EmbeddedStringsTests. Patch-target-only test migration (24 hunks), zero assertion changes. tai_pkg clean-regenerated after mid-verification tamper was found and reverted.
2026-10-10 19:25:02 +02:00

246 lines
9.8 KiB
Python

"""Edge voice synthesis and local audio capture/playback."""
import base64
import hashlib
import html
import os
import random
import re
import shutil
import socket
import ssl
import struct
import subprocess
import sys
import time
import uuid
from tai_pkg.consts import Consts
from tai_pkg.errors import BackendError
from tai_pkg.text import Text
__all__ = ["EdgeTts", "Audio"]
class EdgeTts:
"""EdgeTts."""
@staticmethod
def edge_token(skew=0):
ticks = int(time.time()) + skew + 11644473600
ticks -= ticks % 300
ticks *= 10000000
return hashlib.sha256(("%d%s" % (ticks, Consts.EDGE_TRUSTED_TOKEN)).encode("ascii")).hexdigest().upper()
@staticmethod
def edge_timestamp():
return time.strftime("%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)", time.gmtime())
@staticmethod
def edge_ssml(text, voice):
safe = html.escape(text, quote=False).replace("\r", " ").replace("\n", " ")
return "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'><voice name='%s'><prosody pitch='+0Hz' rate='+0%%' volume='+0%%'>%s</prosody></voice></speak>" % (voice, safe)
@staticmethod
def edge_chunks(text, limit=4000):
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
parts = []
current = ""
for sentence in sentences:
trial = (current + " " + sentence).strip()
if len(trial.encode("utf-8")) > limit and current:
parts.append(current)
current = sentence
else:
current = trial
if current:
parts.append(current)
return parts or [text]
@staticmethod
def ws_send(sock, payload):
if isinstance(payload, str):
payload = payload.encode("utf-8")
mask = os.urandom(4)
header = bytes([0x81])
length = len(payload)
if length < 126:
header += bytes([0x80 | length])
elif length < 65536:
header += bytes([0x80 | 126]) + struct.pack(">H", length)
else:
header += bytes([0x80 | 127]) + struct.pack(">Q", length)
masked = bytes(piece ^ mask[pos % 4] for pos, piece in enumerate(payload))
sock.sendall(header + mask + masked)
@staticmethod
def ws_recv_exact(sock, count):
data = b""
while len(data) < count:
piece = sock.recv(count - len(data))
if not piece:
raise BackendError("edge connection closed")
data += piece
return data
@staticmethod
def ws_recv(sock):
opcode = 0
payload = b""
first = True
while True:
head = EdgeTts.ws_recv_exact(sock, 2)
done = head[0] & 0x80
part = head[0] & 0x0F
masked = head[1] & 0x80
length = head[1] & 0x7F
if length == 126:
length = struct.unpack(">H", EdgeTts.ws_recv_exact(sock, 2))[0]
elif length == 127:
length = struct.unpack(">Q", EdgeTts.ws_recv_exact(sock, 8))[0]
key = EdgeTts.ws_recv_exact(sock, 4) if masked else b""
body = EdgeTts.ws_recv_exact(sock, length) if length else b""
if masked:
body = bytes(piece ^ key[pos % 4] for pos, piece in enumerate(body))
if part == 0x8:
raise BackendError("edge closed stream")
if part == 0x9:
sock.sendall(bytes([0x8A, 0x00]))
continue
if part in (0x1, 0x2) and first:
opcode = part
first = False
payload += body
if done:
return opcode, payload
@staticmethod
def edge_skew(header_text):
match = re.search(r"Date:\s*(.+?)\r?$", header_text, re.MULTILINE | re.IGNORECASE)
if not match:
return 0
try:
stamp = time.mktime(time.strptime(match.group(1).strip(), "%a, %d %b %Y %H:%M:%S %Z"))
return int(stamp - time.time())
except (ValueError, OverflowError):
return 0
@staticmethod
def edge_connect():
skew = 0
for attempt in range(2):
path = "%s?TrustedClientToken=%s&ConnectionId=%s&Sec-MS-GEC=%s&Sec-MS-GEC-Version=1-%s" % (Consts.EDGE_WS_PATH, Consts.EDGE_TRUSTED_TOKEN, uuid.uuid4().hex, EdgeTts.edge_token(skew), Consts.EDGE_CHROMIUM)
raw = socket.create_connection((Consts.EDGE_HOST, 443), timeout=20)
sock = ssl.create_default_context().wrap_socket(raw, server_hostname=Consts.EDGE_HOST)
sock.settimeout(30)
key = base64.b64encode(os.urandom(16)).decode("ascii")
muid = "".join(random.choice("0123456789ABCDEF") for _ in range(32))
handshake = "\r\n".join([
"GET %s HTTP/1.1" % path,
"Host: %s" % Consts.EDGE_HOST,
"Upgrade: websocket",
"Connection: Upgrade",
"Sec-WebSocket-Version: 13",
"Sec-WebSocket-Key: %s" % key,
"Pragma: no-cache",
"Cache-Control: no-cache",
"Origin: chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold",
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0",
"Accept-Encoding: gzip, deflate, br, zstd",
"Accept-Language: en-US,en;q=0.9",
"Cookie: muid=%s;" % muid,
"", "",
])
sock.sendall(handshake.encode("ascii"))
answer = b""
while b"\r\n\r\n" not in answer:
piece = sock.recv(4096)
if not piece:
break
answer += piece
header_text = answer.decode("latin-1", "replace")
if " 101 " in header_text.split("\r\n", 1)[0]:
return sock
sock.close()
if " 403 " in header_text and attempt == 0:
skew = EdgeTts.edge_skew(header_text)
continue
raise BackendError("edge handshake failed: %s" % header_text.split("\r\n", 1)[0][:200])
raise BackendError("edge handshake failed after retry")
@staticmethod
def edge_synthesize(text, voice):
sock = EdgeTts.edge_connect()
audio = bytearray()
try:
config = "X-Timestamp:%s\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n{\"context\":{\"synthesis\":{\"audio\":{\"metadataoptions\":{\"sentenceBoundaryEnabled\":\"false\",\"wordBoundaryEnabled\":\"true\"},\"outputFormat\":\"audio-24khz-48kbitrate-mono-mp3\"}}}}" % EdgeTts.edge_timestamp()
EdgeTts.ws_send(sock, config)
for chunk in EdgeTts.edge_chunks(text):
message = "X-RequestId:%s\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:%sZ\r\nPath:ssml\r\n\r\n%s" % (uuid.uuid4().hex, EdgeTts.edge_timestamp(), EdgeTts.edge_ssml(chunk, voice))
EdgeTts.ws_send(sock, message)
while True:
opcode, payload = EdgeTts.ws_recv(sock)
if opcode == 0x2 and len(payload) > 2:
head_len = int.from_bytes(payload[0:2], "big")
audio += payload[2 + head_len:]
elif opcode == 0x1 and "Path:turn.end" in payload.decode("utf-8", "replace"):
break
return bytes(audio)
finally:
try:
sock.close()
except OSError:
pass
class Audio:
"""Audio."""
@staticmethod
def play_audio(path):
if sys.platform == "darwin" and shutil.which("afplay"):
runner = ["afplay", path]
elif shutil.which("paplay"):
runner = ["paplay", path]
elif shutil.which("aplay"):
runner = ["aplay", "-q", path]
elif shutil.which("pw-play"):
runner = ["pw-play", path]
elif sys.platform == "win32":
runner = ["powershell", "-c", "(New-Object Media.SoundPlayer '%s').PlaySync();" % path]
else:
return False
try:
subprocess.run(runner, timeout=300, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
return True
except (OSError, subprocess.SubprocessError):
return False
@staticmethod
def listen_audio(seconds, workdir):
recorder = next((item for item in Consts.RECORDERS if shutil.which(item[0])), None)
transcriber = next((name for name in Consts.TRANSCRIBERS if shutil.which(name)), "")
if recorder is None or not transcriber:
missing = []
if recorder is None:
missing.append("audio recorder (install arecord, sox, or ffmpeg)")
if not transcriber:
missing.append("transcriber (install whisper-cpp or whisper)")
return "listening unavailable, missing: " + ", ".join(missing)
path = os.path.join(workdir, "listen-%d.wav" % int(time.time()))
command = [part.replace("{seconds}", str(seconds)).replace("{path}", path) for part in recorder[1]]
try:
subprocess.run(command, timeout=seconds + 15, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
done = subprocess.run([transcriber, path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=180)
except (OSError, subprocess.SubprocessError) as exc:
return "listening failed: " + Text.short_error(exc)
finally:
try:
os.remove(path)
except OSError:
pass
if done.returncode != 0:
return "transcriber error: " + (done.stderr or "")[:500]
text = (done.stdout or "").strip()
return Text.truncate(text, 3000, 500) if text else "heard nothing"