короче сделай венв помоги а то я делаю pyinstaller и он всякой хуйни лишней делает по типу пайгейм откуда он вообще короче венв чтобы лищшнее не компилилось у меня. import base64
import requests
import os
import csv
import json
from configparser import ConfigParser
import logging
from PIL import Image, ImageOps
from tqdm import tqdm

# Настройка логирования с явным указанием кодировки UTF-8
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(levelname)s:%(message)s',
    handlers=[
        logging.FileHandler("batch_processing.log", encoding='utf-8'),
        logging.StreamHandler()
    ]
)

# Путь к файлу состояния
STATE_FILE = 'batch_state.json'

class StateManager:
    def __init__(self, state_file=STATE_FILE):
        self.state_file = state_file
        self.state = {
            "batches": [],
            "totals": {
                'prompt_tokens': 0,
                'completion_tokens': 0,
                'cached_tokens': 0,
                'total_tokens': 0,
                'cost': 0.0
            },
            "csv_written": False
        }
        self.load_state()

    def load_state(self):
        if os.path.exists(self.state_file):
            try:
                with open(self.state_file, 'r', encoding='utf-8') as f:
                    self.state = json.load(f)
                logging.info("Состояние загружено из файла.")
            except Exception as e:
                logging.error(f"Не удалось загрузить состояние из файла: {e}")
        else:
            logging.info("Файл состояния не найден. Будет создано новое состояние.")

    def save_state(self):
        try:
            with open(self.state_file, 'w', encoding='utf-8') as f:
                json.dump(self.state, f, ensure_ascii=False, indent=4)
            logging.info("Состояние сохранено.")
        except Exception as e:
            logging.error(f"Не удалось сохранить состояние: {e}")

    def initialize_batches(self, batches):
        starting_index = 0
        self.state["batches"] = []
        for i, batch in enumerate(batches):
            self.state["batches"].append({
                "batch_num": i + 1,
                "file_paths": batch,
                "tasks_file_path": f"data/batch_tasks_{i + 1}.jsonl",
                "status": "pending",  # pending, submitted, in_progress, completed, failed
                "job_id": None,
                "result_file_id": None,
                "starting_index": starting_index
            })
            starting_index += len(batch)
        self.save_state()

    def update_batch(self, batch_num, **kwargs):
        for batch in self.state["batches"]:
            if batch["batch_num"] == batch_num:
                for key, value in kwargs.items():
                    batch[key] = value
                break
        self.save_state()

    def get_batches_by_status(self, statuses):
        return [batch for batch in self.state["batches"] if batch["status"] in statuses]

    def all_batches_completed(self):
        return all(batch["status"] in ["completed", "failed"] for batch in self.state["batches"])

def load_config(config_path='config.ini'):
    config = ConfigParser()
    config.read(config_path)
    api_key = config.get('openai', 'api_key')
    detail = config.get('openai', 'detail', fallback='low').lower()
    if detail not in ['low', 'high']:
        logging.warning(f"Некорректное значение 'detail' в конфигурации: {detail}. Используется значение по умолчанию 'low'.")
        detail = 'low'
    return api_key, detail

def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

def create_batch_tasks(image_files, directory_path, prompt_template, detail, starting_index=0):
    tasks = []
    for index, filename in enumerate(image_files):
        image_path = os.path.join(directory_path, filename)
        base64_image = encode_image(image_path)
        
        task = {
            "custom_id": f"task-{starting_index + index}",  # Уникальный идентификатор
            "method": "POST",
            "url": "/v1/chat/completions",
            "body": {
                "model": "gpt-4o",
                "messages": [
                    {
                        "role": "user",
                        "content": [
                            {"type": "text", "text": prompt_template},
                            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}", "detail": detail}}
                        ]
                    }
                ],
                "max_tokens": 300,
                "response_format": {
                    "type": "json_object"
                }
            }
        }
        tasks.append(task)
    return tasks

def write_jsonl_file(tasks, file_path):
    with open(file_path, 'w', encoding='utf-8') as f:
        for task in tasks:
            f.write(json.dumps(task, ensure_ascii=False) + '\n')

def upload_file(file_path, api_key):
    headers = {
        "Authorization": f"Bearer {api_key}"
    }
    with open(file_path, "rb") as f:
        response = requests.post(
            "https://api.openai.com/v1/files",
            headers=headers,
            files={
                'file': (os.path.basename(file_path), f, 'application/jsonl')
            },
            data={
                'purpose': 'batch'
            }
        )
    response.raise_for_status()
    return response.json()['id']

def create_batch_job(file_id, api_key):
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    payload = {
        "input_file_id": file_id,
        "endpoint": "/v1/chat/completions",
        "completion_window": "24h"  # Максимальное время выполнения
    }
    response = requests.post(
        "https://api.openai.com/v1/batches",
        headers=headers,
        json=payload
    )
    response.raise_for_status()
    return response.json()['id']

def check_batch_job_status(job_id, api_key):
    headers = {
        "Authorization": f"Bearer {api_key}"
    }
    response = requests.get(
        f"https://api.openai.com/v1/batches/{job_id}",
        headers=headers
    )
    response.raise_for_status()
    return response.json()

def download_batch_results(result_file_id, api_key):
    headers = {
        "Authorization": f"Bearer {api_key}"
    }
    response = requests.get(
        f"https://api.openai.com/v1/files/{result_file_id}/content",
        headers=headers
    )
    response.raise_for_status()
    return response.content

def parse_results(result_content):
    results = []
    for line in result_content.decode('utf-8').splitlines():
        result = json.loads(line)
        # Извлекаем содержание и использование токенов, если они есть
        response_content = result.get('response', {}).get('body', {})
        choices = response_content.get('choices', [])
        usage = response_content.get('usage', {})

        if choices:
            content = choices[0].get('message', {}).get('content', '')
        else:
            content = ''

        # Проверяем, существует ли 'prompt_tokens_details' и 'cached_tokens'
        prompt_tokens_details = usage.get('prompt_tokens_details', {})
        cached_tokens = prompt_tokens_details.get('cached_tokens', 0)

        result_dict = {
            'custom_id': result.get('custom_id', ''),
            'content': content,
            'prompt_tokens': usage.get('prompt_tokens', 0),
            'completion_tokens': usage.get('completion_tokens', 0),
            'cached_tokens': cached_tokens,
            'total_tokens': usage.get('total_tokens', 0)
        }
        results.append(result_dict)
    return results

def write_results_to_csv(results, csv_path, image_files, starting_index, totals):
    # Создаем или открываем CSV файл в режиме добавления
    file_exists = os.path.isfile(csv_path)
    with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile:
        writer = csv.writer(csvfile)
        # Если файл не существует, записываем заголовок
        if not file_exists:
            writer.writerow(['Filename', 'Фамилия', 'Имя', 'Отчество', 'Дата рождения', 'Дата смерти', 'Статус', 'Входящие токены', 'Исходящие токены', 'Кешированные токены', 'Всего токенов', 'Стоимость ($)'])
        # Создаем словарь для быстрого доступа по custom_id
        results_dict = {res['custom_id']: res for res in results}
        for index, filename in enumerate(image_files):
            task_id = f"task-{starting_index + index}"
            res = results_dict.get(task_id, None)
            if res and 'content' in res:
                try:
                    content = res['content']
                    # Удаляем тройные обратные кавычки, если они есть
                    content = content.strip('```json').strip('```').strip()
                    parsed = json.loads(content)
                    # Проверяем, является ли parsed списком и содержит ли он элементы
                    if isinstance(parsed, list) and len(parsed) > 0:
                        parsed_data = parsed  # Список, каждый элемент - отдельная могила
                    elif isinstance(parsed, dict):
                        parsed_data = [parsed]  # Преобразуем в список для униформности
                    else:
                        parsed_data = []  # В противном случае, пустой список

                    for data in parsed_data:
                        # Извлекаем данные из parsed_data с форматированием имен
                        last_name = format_name(data.get('фамилия', '-'))
                        first_name = format_name(data.get('имя', '-'))
                        middle_name = format_name(data.get('отчество', '-'))
                        birth_date = data.get('дата рождения', '-')
                        death_date = data.get('дата смерти', '-')
                        status = data.get('статус', 'Обработка')
                        # Извлечение токенов
                        prompt_tokens = res.get('prompt_tokens', 0)
                        completion_tokens = res.get('completion_tokens', 0)
                        cached_tokens = res.get('cached_tokens', 0)
                        total_tokens = res.get('total_tokens', 0)
                        # Расчет стоимости
                        cost = calculate_cost(prompt_tokens, completion_tokens)
                        # Обновление общих сумм
                        totals['prompt_tokens'] += prompt_tokens
                        totals['completion_tokens'] += completion_tokens
                        totals['cached_tokens'] += cached_tokens
                        totals['total_tokens'] += total_tokens
                        totals['cost'] += cost
                        # Запись строки в CSV
                        writer.writerow([
                            filename,
                            last_name,
                            first_name,
                            middle_name,
                            birth_date,
                            death_date,
                            status,
                            prompt_tokens,
                            completion_tokens,
                            cached_tokens,
                            total_tokens,
                            f"{cost:.6f}"
                        ])
                except Exception as e:
                    logging.error(f"Ошибка парсинга результата для {filename}: {e}")
                    logging.error(f"Результат: {res}")
                    # Запись пустых значений с статусом 'Обработка'
                    writer.writerow([filename, '-', '-', '-', '-', '-', 'Обработка', 0, 0, 0, 0, "0.000000"])
            else:
                # Если результат отсутствует или возникла ошибка
                writer.writerow([filename, '-', '-', '-', '-', '-', 'Обработка', 0, 0, 0, 0, "0.000000"])

def format_name(name):
    if name == '-' or not name:
        return '-'
    return name.capitalize()

def compress_and_remove_metadata(input_path, output_path, quality=85):
    try:
        with Image.open(input_path) as img:
            # Корректировка ориентации изображения согласно EXIF
            img = ImageOps.exif_transpose(img)
            # Сжатие изображения и удаление всех метаданных
            img.save(output_path, "JPEG", quality=quality, optimize=True)
    except Exception as e:
        logging.error(f"Ошибка при обработке изображения {input_path}: {e}")

def load_prompt_from_file(prompt_file_path):
    try:
        with open(prompt_file_path, 'r', encoding='utf-8') as file:
            return file.read()
    except Exception as e:
        logging.error(f"Ошибка загрузки промпта из файла: {e}")
        return None

def get_batches(file_paths, max_size_mb=100):
    batches = []
    current_batch = []
    current_size = 0
    for file in file_paths:
        size_mb = os.path.getsize(file) / (1024 * 1024)
        if current_size + size_mb > max_size_mb:
            if current_batch:
                batches.append(current_batch)
            current_batch = [file]
            current_size = size_mb
        else:
            current_batch.append(file)
            current_size += size_mb
    if current_batch:
        batches.append(current_batch)
    return batches

def calculate_cost(prompt_tokens, completion_tokens):
    # Установите цены в зависимости от используемой модели
    # Пример для модели gpt-4o (цены могут изменяться, уточни на сайте OpenAI)
    PROMPT_COST_PER_1000 = 0.00125  # Цена за 1000 входящих токенов
    COMPLETION_COST_PER_1000 = 0.005  # Цена за 1000 исходящих токенов
    cost = (prompt_tokens / 1000) * PROMPT_COST_PER_1000 + (completion_tokens / 1000) * COMPLETION_COST_PER_1000
    return cost

def main():
    state_manager = StateManager()

    # Загрузка конфигурации (API ключ и detail)
    try:
        api_key, detail = load_config()
    except Exception as e:
        logging.error(f"Ошибка загрузки конфигурации: {e}")
        return

    # Путь к папке с оригинальными изображениями
    original_dir = os.path.join(os.getcwd(), 'photos2')
    # Путь к папке для подготовленных изображений
    prepared_dir = os.path.join(os.getcwd(), 'photos_prepared')
    os.makedirs(prepared_dir, exist_ok=True)

    # Проверка существования папки с изображениями
    if not os.path.exists(original_dir):
        logging.error(f"Папка с изображениями '{original_dir}' не существует.")
        return

    # Список файлов оригинальных изображений
    image_files = [f for f in os.listdir(original_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
    if not image_files:
        logging.info("Нет файлов для обработки в папке 'photos2'.")
        return

    # Если состояние ещё не инициализировано, то инициализируем батчи
    if not state_manager.state["batches"]:
        # Загрузка и сжатие изображений, удаление метаданных
        logging.info("Сжатие изображений и удаление метаданных...")
        prepared_files = []
        for filename in tqdm(image_files, desc="Обработка изображений", unit="изображение"):
            original_path = os.path.join(original_dir, filename)
            prepared_path = os.path.join(prepared_dir, f"prepared_{filename}")
            compress_and_remove_metadata(original_path, prepared_path)
            prepared_files.append(prepared_path)

        # Загрузка промпта из файла
        prompt_file_path = 'prompt.txt'
        prompt_template = load_prompt_from_file(prompt_file_path)
        if not prompt_template:
            logging.error("Промпт не был загружен. Завершение работы.")
            return

        # Разделение подготовленных файлов на батчи
        logging.info("Разделение файлов на батчи...")
        batches = get_batches(prepared_files, max_size_mb=100)
        logging.info(f"Всего батчей: {len(batches)}")
        
        # Инициализация общих сумм
        state_manager.state["totals"] = {
            'prompt_tokens': 0,
            'completion_tokens': 0,
            'cached_tokens': 0,
            'total_tokens': 0,
            'cost': 0.0
        }

        # Инициализация батчей в состоянии
        state_manager.initialize_batches(batches)
    else:
        logging.info("Загружаем существующие батчи из состояния.")

    # Загрузка промпта из файла (в любом случае)
    prompt_file_path = 'prompt.txt'
    prompt_template = load_prompt_from_file(prompt_file_path)
    if not prompt_template:
        logging.error("Промпт не был загружен. Завершение работы.")
        return

    # Перебор батчей по статусам
    pending_batches = state_manager.get_batches_by_status(["pending"])
    submitted_batches = state_manager.get_batches_by_status(["submitted", "in_progress"])

    # Обработка pending батчей: загрузка и создание задач
    for batch in pending_batches:
        batch_num = batch["batch_num"]
        batch_files = batch["file_paths"]
        batch_filenames = [os.path.basename(f) for f in batch_files]
        starting_index = batch["starting_index"]
        
        logging.info(f"Обработка батча {batch_num} с {len(batch_files)} файлами...")
        
        # Создание задач для текущего батча с использованием глобального индекса и параметра detail
        tasks = create_batch_tasks(batch_filenames, prepared_dir, prompt_template, detail=detail, starting_index=starting_index)
        
        # Запись задач в JSONL файл
        tasks_file_path = batch["tasks_file_path"]
        os.makedirs(os.path.dirname(tasks_file_path), exist_ok=True)
        logging.info(f"Запись задач в файл {tasks_file_path}...")
        write_jsonl_file(tasks, tasks_file_path)
        
        # Загрузка файла на OpenAI
        logging.info("Загрузка задач на OpenAI...")
        try:
            batch_file_id = upload_file(tasks_file_path, api_key)
            logging.info(f"Файл батча {batch_num} загружен. ID файла: {batch_file_id}")
            # Обновление состояния батча
            state_manager.update_batch(batch_num, status="submitted", file_id=batch_file_id)
        except Exception as e:
            logging.error(f"Ошибка загрузки файла батча {batch_num} на OpenAI: {e}")
            state_manager.update_batch(batch_num, status="failed")
            continue  # Переходим к следующему батчу

        # Создание Batch Job
        logging.info("Создание Batch Job...")
        try:
            batch_job_id = create_batch_job(batch_file_id, api_key)
            logging.info(f"Batch Job для батча {batch_num} создан. ID задания: {batch_job_id}")
            state_manager.update_batch(batch_num, status="in_progress", job_id=batch_job_id)
        except Exception as e:
            logging.error(f"Ошибка создания Batch Job для батча {batch_num}: {e}")
            state_manager.update_batch(batch_num, status="failed")
            continue  # Переходим к следующему батчу

    # Обработка submitted/in_progress батчей: проверка статуса и загрузка результатов
    for batch in submitted_batches:
        batch_num = batch["batch_num"]
        job_id = batch["job_id"]
        starting_index = batch["starting_index"]
        logging.info(f"Отслеживание состояния Batch Job для батча {batch_num}, Job ID: {job_id}...")
        
        try:
            status_info = check_batch_job_status(job_id, api_key)
            status = status_info.get('status')
            logging.info(f"Текущий статус батча {batch_num}: {status}")
            
            if status == 'completed':
                logging.info(f"Batch Job батча {batch_num} успешно завершено.")
                result_file_id = status_info.get('output_file_id')
                if not result_file_id:
                    logging.error(f"Не найден 'output_file_id' в статусе Batch Job для батча {batch_num}.")
                    state_manager.update_batch(batch_num, status="failed")
                    continue  # Переходим к следующему батчу
                
                # Загрузка результатов
                logging.info(f"Загрузка результатов из файла ID: {result_file_id} для батча {batch_num}...")
                try:
                    result_content = download_batch_results(result_file_id, api_key)
                    state_manager.update_batch(batch_num, status="completed", result_file_id=result_file_id)
                except Exception as e:
                    logging.error(f"Ошибка загрузки результатов Batch Job для батча {batch_num}: {e}")
                    state_manager.update_batch(batch_num, status="failed")
                    continue  # Переходим к следующему батчу
                
                # Парсинг результатов
                logging.info("Парсинг результатов...")
                try:
                    results = parse_results(result_content)
                    # Обновление общих сумм
                    state_manager.state["totals"]['prompt_tokens'] += sum(res['prompt_tokens'] for res in results)
                    state_manager.state["totals"]['completion_tokens'] += sum(res['completion_tokens'] for res in results)
                    state_manager.state["totals"]['cached_tokens'] += sum(res['cached_tokens'] for res in results)
                    state_manager.state["totals"]['total_tokens'] += sum(res['total_tokens'] for res in results)
                    # Расчет и обновление стоимости
                    for res in results:
                        cost = calculate_cost(res['prompt_tokens'], res['completion_tokens'])
                        state_manager.state["totals"]['cost'] += cost
                    # Сохранение состояния после обновления сумм
                    state_manager.save_state()
                except Exception as e:
                    logging.error(f"Ошибка парсинга результатов батча {batch_num}: {e}")
                    state_manager.update_batch(batch_num, status="failed")
                    continue  # Переходим к следующему батчу

                # Чтение списка файлов, соответствующих батчу
                image_files_batch = [os.path.basename(f) for f in batch["file_paths"]]
                
                # Запись результатов в CSV
                csv_output_path = "results.csv"
                logging.info(f"Запись результатов в CSV файл: {csv_output_path}...")
                write_results_to_csv(results, csv_output_path, image_files_batch, starting_index, state_manager.state["totals"])
            elif status == 'failed':
                logging.error(f"Batch Job батча {batch_num} завершилось с ошибкой.")
                logging.error(status_info)
                state_manager.update_batch(batch_num, status="failed")
            else:
                # Статус все ещё in_progress или другой
                logging.info(f"Batch Job батча {batch_num} всё ещё обрабатывается. Ждём...")
        except Exception as e:
            logging.error(f"Ошибка проверки статуса Batch Job для батча {batch_num}: {e}")
            continue  # Переходим к следующему батчу

    # Если все батчи завершены и CSV ещё не был записан, вывести общие суммы
    if state_manager.all_batches_completed() and not state_manager.state.get("csv_written", False):
        totals = state_manager.state["totals"]
        print("Общие результаты:")
        print(f"Всего входящих токенов: {totals['prompt_tokens']}")
        print(f"Всего исходящих токенов: {totals['completion_tokens']}")
        print(f"Всего кешированных токенов: {totals['cached_tokens']}")
        print(f"Всего токенов: {totals['total_tokens']}")
        print(f"Общая стоимость: ${totals['cost']:.6f}")
        logging.info("Обработка всех батчей завершена успешно.")
        state_manager.state["csv_written"] = True
        state_manager.save_state()

if __name__ == "__main__":
    main()