import os
import sys
import logging
import configparser
from pathlib import Path
from typing import Dict, List, Optional
import re
import time
import random

import requests
from docx import Document
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

# Настройка логирования
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Константы для повторных попыток
MAX_RETRIES = 5
INITIAL_DELAY = 1 # Начальная задержка в секундах
TARGET_CONTENT_LENGTH = random.randint(17000, 22000) # Случайная длина контента от 17000 до 22000
REQUEST_TIMEOUT = 120 # Тайм-аут для запросов

def load_config(config_path: Path) -> Dict[str, str]:
"""Загружает конфигурацию из файла."""
config = configparser.ConfigParser()
if not config_path.is_file():
logger.critical(f"Файл конфигурации '{config_path}' не найден.")
sys.exit(1)
config.read(config_path, encoding='utf-8')
try:
api_config = {
"api_key": config.get("API", "openrouter_api_key").strip(),
"endpoint": config.get("API", "openrouter_endpoint").strip(),
"model": config.get("API", "cloud_model").strip()
}
if not all(api_config.values()):
raise ValueError("Не все параметры API заполнены.")
logger.info("API-ключ, эндпоинт и модель OpenRouter успешно загружены из конфигурации.")
return api_config
except (configparser.NoSectionError, configparser.NoOptionError, ValueError) as e:
logger.critical(f"Ошибка в файле конфигурации: {e}")
sys.exit(1)

def create_requests_session() -> requests.Session:
"""Создает сессию requests для повторного использования соединений."""
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(max_retries=3)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session

def sanitize_filename(name: str) -> str:
"""Удаляет недопустимые символы из имени файла."""
return re.sub(r'[\\/*?:"<>|]', "", name)

def read_docx(file_path: Path) -> Optional[str]:
"""Читает содержимое .docx файла."""
try:
doc = Document(file_path)
text = "\n".join(para.text for para in doc.paragraphs)
logger.info(f"Файл '{file_path}' успешно прочитан.")
return text
except Exception as e:
logger.error(f"Ошибка при чтении файла '{file_path}': {e}")
return None

def parse_course_plan(plan_text: str) -> Dict[str, List[str]]:
"""Парсит текст плана курса на модули и лекции."""
modules = {}
current_module = None
for line in plan_text.splitlines():
module_match = re.match(r"Модуль\s+(\d+)[:\-]\s*(.+)", line.strip(), re.IGNORECASE)
if module_match:
module_number, module_name = module_match.groups()
current_module = f"Модуль {module_number}. {module_name.strip()}"
modules[current_module] = []
logger.debug(f"Найден {current_module}")
elif current_module and line.strip():
modules[current_module].append(line.strip())
logger.debug(f"Добавлена лекция в {current_module}: {line.strip()}")
logger.info(f"Всего модулей: {len(modules)}")
return modules

@retry(
retry=retry_if_exception_type((requests.exceptions.HTTPError, requests.exceptions.ConnectionError, ValueError)),
stop=stop_after_attempt(MAX_RETRIES),
wait=wait_exponential(multiplier=INITIAL_DELAY),
reraise=True
)
def generate_content(session: requests.Session, api_key: str, endpoint: str, prompt: str, model: str) -> str:
"""Отправляет запрос к OpenRouter API и получает сгенерированный контент."""
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 2500,
"temperature": 0.7
}

response = session.post(endpoint, headers=headers, json=payload, timeout=REQUEST_TIMEOUT)
response.raise_for_status()

response_json = response.json()
logger.debug(f"Ответ от API: {response_json}")

if 'choices' not in response_json or not response_json['choices']:
raise ValueError("Ключ 'choices' отсутствует или пуст в ответе API.")

content = response_json['choices'][0]['message']['content'].strip()
return content

def save_docx(content: str, file_path: Path) -> None:
"""Сохраняет текстовый контент в .docx файл."""
try:
document = Document()
lines = content.splitlines()
for line in lines:
if line.strip() == '':
continue # Пропуск пустых строк
paragraph = document.add_paragraph()
if line.startswith('**') and line.endswith('**'):
run = paragraph.add_run(line.strip('*'))
run.bold = True
else:
paragraph.add_run(line)
document.save(file_path)
logger.info(f"Сохранено: '{file_path}'")
except Exception as e:
logger.error(f"Ошибка при сохранении файла '{file_path}': {e}")

def check_and_expand_content(session: requests.Session, api_key: str, endpoint: str, prompt: str, current_content: str, target_length: int, model: str) -> str:
"""Проверяет длину контента и дописывает его, если необходимо."""
current_length = len(current_content)
if current_length < target_length:
logger.info(f"Текущий объем контента: {current_length} символов. Требуется дополнение.")
additional_prompt = f"{prompt}\n\nПродолжи лекцию, чтобы объем был около {target_length} символов."
try:
additional_content = generate_content(session, api_key, endpoint, additional_prompt, model)
logger.info("Контент успешно дополнен.")
return f"{current_content}\n\n{additional_content}"
except Exception as e:
logger.error(f"Не удалось дополнить контент: {e}")
return current_content
return current_content

def main():
base_dir = Path(__file__).parent.resolve()
input_dir = base_dir / "Входные данные"
output_dir = base_dir / "Папка с готовыми лекциями"
config_path = base_dir / "config.ini"

config = load_config(config_path)
api_key = config["api_key"]
endpoint = config["endpoint"]
model = config["model"]

plan_file = input_dir / "План лекций Логопед-афазиолог.docx"
course_plan = read_docx(plan_file)
if course_plan is None:
logger.critical("План курса не был загружен. Завершение работы.")
sys.exit(1)
modules = parse_course_plan(course_plan)

# Чтение инструкций один раз
lecture_rules = read_docx(input_dir / "Правила написания лекции.docx")
custom_instructions = read_docx(input_dir / "custom instructions.docx")
if lecture_rules is None or custom_instructions is None:
logger.critical("Не удалось загрузить инструкции. Завершение работы.")
sys.exit(1)

constant_prompt = f"""
{lecture_rules}


{custom_instructions}

Придерживайся следующих требований:
- Объем: 17000-22000 знаков с пробелами
- Структура: без плана, введения и заключения; сразу раскрывай тему
- Содержание: достаточно текста, не только списки; примеры и объяснения при необходимости
- Стиль: научный, имитирующий стиль российских учебников для вузов
- Форматирование: используй ** для выделения жирным шрифтом подзаголовков и основных определений
- Формулы: корректное визуальное отображение
- Актуальность: соответствие законодательной базе на 2024 год
- Язык: грамотный русский, корректные термины, без речевых штампов
- Уникальность: избегай дублирования материала из других лекций
- Лекция должна начинаться с номера лекции и названия. После номера лекции ставь точку.
- Не включай список литературы.
"""

session = create_requests_session()

for module, lectures in modules.items():
module_dir = output_dir / sanitize_filename(module)
module_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Создана директория модуля: '{module_dir}'")

for i, lecture in enumerate(lectures, 1):
lecture_number = f"Лекция {i}"
prompt = f"""{constant_prompt}

План курса:
{course_plan}

Напишите лекцию:
{lecture_number}. {lecture}
"""
try:
lecture_content = generate_content(session, api_key, endpoint, prompt, model)
if lecture_content:
# Проверка и дополнение контента
final_content = check_and_expand_content(session, api_key, endpoint, prompt, lecture_content, TARGET_CONTENT_LENGTH, model)
file_name = f"{lecture_number}. {sanitize_filename(lecture)}.docx"
file_path = module_dir / file_name
save_docx(final_content, file_path)
else:
logger.error(f"Не удалось сгенерировать контент для лекции '{lecture}' в модуле '{module}'.")
except Exception as e:
logger.error(f"Ошибка при генерации лекции '{lecture}' в модуле '{module}': {e}")
continue # Переход к следующей лекции

time.sleep(5) # Задержка в 5 секунд перед следующим запросом

logger.info("Генерация лекций завершена.")

if __name__ == "__main__":
main() есть проблема лекции пишутся не по плату переделай чтобы план ккждый раз добавлялся в контекст перед написанием каждой лекции и обьём лекции и форматирование её тоже должны строго соблюдаться - реализуй решение по методу триз - придумай