Spaces:
Running
Running
| import os | |
| import spaces | |
| import gradio as gr | |
| MODEL_ID = "zieglerd/RussianConcsExt" | |
| MAX_INPUT_TOKENS = 16_384 | |
| IS_ZERO_GPU = os.getenv("SPACES_ZERO_GPU") == "1" | |
| tokenizer = None | |
| model = None | |
| load_error = None | |
| if IS_ZERO_GPU: | |
| try: | |
| import torch | |
| from peft import AutoPeftModelForCausalLM | |
| from transformers import AutoTokenizer, BitsAndBytesConfig | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) | |
| quantization_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_use_double_quant=True, | |
| bnb_4bit_compute_dtype=torch.bfloat16, | |
| ) | |
| model = AutoPeftModelForCausalLM.from_pretrained( | |
| MODEL_ID, | |
| quantization_config=quantization_config, | |
| device_map="cuda", | |
| dtype=torch.bfloat16, | |
| attn_implementation="sdpa", | |
| ).eval() | |
| except Exception as exc: | |
| load_error = f"{type(exc).__name__}: {exc}" | |
| print(f"Model loading failed: {load_error}", flush=True) | |
| EXAMPLE_CONTRACT = """ДОГОВОР ПОСТАВКИ №458/26 | |
| ООО «Поставщик» обязуется поставить ООО «Заказчик» | |
| общехозяйственные товары согласно спецификации. | |
| Стоимость договора составляет 34 132 686,08 рублей, | |
| в том числе НДС 5%. | |
| Оплата производится следующим образом: | |
| 85% — авансовый платеж. | |
| 15% — после поставки товара.""" | |
| def _extract_json(text: str): | |
| """Parse the first complete JSON object from model output when possible.""" | |
| import json | |
| cleaned = text.strip() | |
| if cleaned.startswith("```"): | |
| lines = cleaned.splitlines() | |
| if lines and lines[0].startswith("```"): | |
| lines = lines[1:] | |
| if lines and lines[-1].strip() == "```": | |
| lines = lines[:-1] | |
| cleaned = "\n".join(lines).strip() | |
| try: | |
| return json.loads(cleaned) | |
| except json.JSONDecodeError: | |
| pass | |
| start = cleaned.find("{") | |
| if start < 0: | |
| return None | |
| depth = 0 | |
| in_string = False | |
| escaped = False | |
| for index, char in enumerate(cleaned[start:], start=start): | |
| if escaped: | |
| escaped = False | |
| continue | |
| if char == "\\" and in_string: | |
| escaped = True | |
| continue | |
| if char == '"': | |
| in_string = not in_string | |
| elif not in_string: | |
| if char == "{": | |
| depth += 1 | |
| elif char == "}": | |
| depth -= 1 | |
| if depth == 0: | |
| try: | |
| return json.loads(cleaned[start : index + 1]) | |
| except json.JSONDecodeError: | |
| return None | |
| return None | |
| def extract_contract(contract_text: str, max_new_tokens: int): | |
| """Extract structured contract data from Russian text and return JSON.""" | |
| if not contract_text or not contract_text.strip(): | |
| return {}, "", "Введите текст договора." | |
| if load_error: | |
| return {}, "", f"Не удалось загрузить модель: {load_error}" | |
| if model is None or tokenizer is None: | |
| return ( | |
| {}, | |
| "", | |
| "Демо ожидает предоставления ZeroGPU. Код и интерфейс уже развернуты.", | |
| ) | |
| messages = [{"role": "user", "content": contract_text.strip()}] | |
| prompt = tokenizer.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| ) | |
| inputs = tokenizer( | |
| prompt, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=MAX_INPUT_TOKENS, | |
| ).to("cuda") | |
| with torch.inference_mode(): | |
| output_ids = model.generate( | |
| **inputs, | |
| max_new_tokens=int(max_new_tokens), | |
| do_sample=False, | |
| repetition_penalty=1.05, | |
| pad_token_id=tokenizer.eos_token_id, | |
| ) | |
| generated_ids = output_ids[0, inputs["input_ids"].shape[1] :] | |
| raw_output = tokenizer.decode(generated_ids, skip_special_tokens=True).strip() | |
| parsed = _extract_json(raw_output) | |
| if parsed is None: | |
| return ( | |
| {"raw_output": raw_output}, | |
| raw_output, | |
| "Модель вернула ответ, но он не является корректным JSON.", | |
| ) | |
| return parsed, raw_output, "Готово. Проверьте результат перед использованием." | |
| with gr.Blocks(title="Russian Contract Extractor") as demo: | |
| gr.Markdown( | |
| """ | |
| # Извлечение данных из российских договоров | |
| Демо LoRA-адаптера | |
| [zieglerd/RussianConcsExt](https://huggingface.co/zieglerd/RussianConcsExt) | |
| для Qwen2.5-32B-Instruct. Вставьте текст договора, чтобы получить | |
| структурированный JSON. | |
| > Результат предназначен для информационного извлечения, не является | |
| > юридической консультацией и требует проверки. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| contract_input = gr.Textbox( | |
| label="Текст договора", | |
| value=EXAMPLE_CONTRACT, | |
| lines=18, | |
| placeholder="Вставьте распознанный или исходный текст договора…", | |
| ) | |
| max_tokens = gr.Slider( | |
| minimum=256, | |
| maximum=2048, | |
| value=1024, | |
| step=128, | |
| label="Максимум токенов ответа", | |
| ) | |
| extract_button = gr.Button("Извлечь данные", variant="primary") | |
| with gr.Column(scale=2): | |
| json_output = gr.JSON(label="Структурированный результат") | |
| status_output = gr.Markdown() | |
| with gr.Accordion("Исходный ответ модели", open=False): | |
| raw_output = gr.Code(label="Ответ", language="json") | |
| extract_button.click( | |
| fn=extract_contract, | |
| inputs=[contract_input, max_tokens], | |
| outputs=[json_output, raw_output, status_output], | |
| api_name="extract_contract", | |
| ) | |
| gr.Examples( | |
| examples=[[EXAMPLE_CONTRACT, 1024]], | |
| inputs=[contract_input, max_tokens], | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(mcp_server=True) | |