"""Qwen2.5 small text LLM engine for math / reasoning / OCR cleanup.""" from __future__ import annotations import os from typing import Optional from captcha_solver.engines.base import BaseEngine from captcha_solver.config import get_settings class QwenEngine(BaseEngine): name = "qwen" def __init__(self) -> None: super().__init__() self._model = None self._tokenizer = None def _do_load(self) -> None: import torch from transformers import AutoModelForCausalLM, AutoTokenizer s = get_settings() os.environ.setdefault("HF_HOME", str(s.cache_dir / "hf")) dtype = {"float32": torch.float32, "float16": torch.float16, "bfloat16": torch.bfloat16}[ s.qwen_torch_dtype ] self._tokenizer = AutoTokenizer.from_pretrained(s.qwen_model) self._model = AutoModelForCausalLM.from_pretrained( s.qwen_model, torch_dtype=dtype, low_cpu_mem_usage=True, ).to(s.qwen_device).eval() def _do_unload(self) -> None: self._model = None self._tokenizer = None def generate( self, prompt: str, system: Optional[str] = None, max_new_tokens: int = 64, temperature: float = 0.0, ) -> str: """Generate a short response. Best for math, OCR cleanup, simple reasoning.""" if not self._loaded: self.load() import torch assert self._model is not None and self._tokenizer is not None messages = [] if system: messages.append({"role": "system", "content": system}) messages.append({"role": "user", "content": prompt}) text = self._tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = self._tokenizer(text, return_tensors="pt").to(self._model.device) with torch.no_grad(): gen = self._model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=temperature > 0, temperature=temperature or 1.0, pad_token_id=self._tokenizer.eos_token_id, ) out = gen[0][inputs["input_ids"].shape[1]:] return self._tokenizer.decode(out, skip_special_tokens=True).strip()