Привте короче надо сделать мне интерфейс ui вот тут чтобы все сам мог кликать все методы нужные 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()