KAZIK/tools/tests/check_fortune_ai.py
Artvasil09 ce06659ab2 feat: implement offline neural fortune teller and native tarot divination
- Offline neural worker based on llama.cpp (fortune-engine.exe) with local Qwen3-4B model support

- Native assembly Tarot 3-card spread divination and teacher grade divination mode

- Text input dialog in MODE_FORTUNE communicating over anonymous pipes

- Clean process lifecycle management, WM_CHAR dispatch and fallback handling

- Test suites and packaging tools for offline release
2026-10-21 13:53:06 +03:00

78 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Exercise the actual offline inference child through its bounded binary protocol."""
from pathlib import Path
import argparse
import json
import struct
import subprocess
import time
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'build/fortune-ai'
def exact(stream, size):
data = bytearray()
while len(data) < size:
chunk = stream.read(size - len(data))
assert chunk, 'worker closed an incomplete response'
data.extend(chunk)
return bytes(data)
def response(worker):
magic, status, length = struct.unpack('<III', exact(worker.stdout, 12))
assert magic == 0x31524346 and length <= 8192
return status, exact(worker.stdout, length).decode('cp1251')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--short', action='store_true')
args = parser.parse_args()
OUT.mkdir(exist_ok=True)
manifest = json.loads((ROOT / 'ai/model.json').read_text('utf8'))
assert (ROOT / 'ai' / manifest['file']).stat().st_size == manifest['bytes']
questions = [
('Стоит ли мне начать учиться рисованию?', [0, 23, 17]),
('А что мне поможет не бросить это через неделю?', [8, 36, 21]),
('Поставить реальному студенту 8 или 9 по его имени и году рождения?', [11, 4, 5]),
]
expected = [('шут', 'маг', 'звезд'), ('сил', 'умеренн', 'мир')]
worker = subprocess.Popen([str(ROOT / 'ai/fortune-engine.exe')], cwd=OUT,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, creationflags=0x08000000)
started = time.monotonic()
results = []
try:
status, text = response(worker)
assert (status, text) == (0, ''), (status, text)
ready = time.monotonic() - started
for index, (question, cards) in enumerate(questions[:1 if args.short else 3]):
before = time.monotonic()
payload = question.encode('cp1251')
worker.stdin.write(struct.pack('<IIIII', 0x31514346, len(payload), *cards) + payload)
worker.stdin.flush()
status, text = response(worker)
assert status in (1, 3), (status, text)
assert len(text) > 160, text
assert sum('а' <= c.lower() <= 'я' for c in text) > 80, text
assert '<think>' not in text and '<|im_' not in text
if index < len(expected):
assert all(card in text.lower().replace('ё', 'е') for card in expected[index]), text
results.append({'question': question, 'cards': cards, 'status': status,
'seconds': round(time.monotonic() - before, 2), 'answer': text})
# Reject invalid length before allocating or reading an unbounded body.
worker.stdin.write(struct.pack('<IIIII', 0x31514346, 0xffffffff, 0, 1, 2))
worker.stdin.flush()
assert worker.wait(timeout=15) == 2
finally:
if worker.poll() is None:
worker.kill()
worker.communicate(timeout=15)
(OUT / 'inference-results.json').write_text(json.dumps(
{'ready_seconds': round(ready, 2), 'results': results}, ensure_ascii=False, indent=2), encoding='utf8')
print(f'PASS: real offline Russian inference, supplied cards, bounded IPC; ready in {ready:.1f}s')
if __name__ == '__main__':
main()