вот какие таски сделай присваивать id к захоронению в таблице и фото
Вытаскивать из фото широту и долготу и добавлять данные в таблицу
интерфейс выбор край город кладбище 
В таблице :край, город, кладбище, ID, url , Ф, и, о, дата р, дата смерти, широта, долгота, статус
чтобы сохранялось еще и в таблицу xlsx  вот код import base64
import requests
import os
import csv
import re
import json
import threading
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
from configparser import ConfigParser
from concurrent.futures import ThreadPoolExecutor, as_completed
from PIL import Image, ImageOps
from tqdm import tqdm
import logging

# Настройка логирования с явным указанием кодировки UTF-8
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(levelname)s:%(message)s',
    handlers=[
        logging.FileHandler("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()
    if not os.path.exists(config_path):
        # Создаем стандартный конфиг, если его нет
        config['openai'] = {
            'api_key': '',
            'detail': 'low',
            'prompt_path': '',
            'photos_path': ''
        }
        with open(config_path, 'w', encoding='utf-8') as configfile:
            config.write(configfile)
        return '', 'low', '', ''
    config.read(config_path)
    api_key = config.get('openai', 'api_key', fallback='')
    detail = config.get('openai', 'detail', fallback='low').lower()
    prompt_path = config.get('openai', 'prompt_path', fallback='')
    photos_path = config.get('openai', 'photos_path', fallback='')

    if detail not in ['low', 'high']:
        logging.warning(f"Некорректное значение 'detail' в конфигурации: {detail}. Используется значение по умолчанию 'low'.")
        detail = 'low'
    return api_key, detail, prompt_path, photos_path

def save_config(api_key, detail, prompt_path, photos_path, config_path='config.ini'):
    config = ConfigParser()
    config['openai'] = {
        'api_key': api_key,
        'detail': detail,
        'prompt_path': prompt_path,
        'photos_path': photos_path
    }
    with open(config_path, 'w', encoding='utf-8') as configfile:
        config.write(configfile)

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

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 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 extract_and_process_image_stream(image_path, api_key, prompt):
    try:
        # Кодирование изображения в base64
        base64_image = encode_image(image_path)

        # Подготовка заголовков и полезной нагрузки для API-запроса
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}"
        }

        payload = {
            "model": "gpt-4o",
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}", "detail": "high"}}
                    ]
                }
            ],
            "max_tokens": 300,
            "response_format": { 
                "type": "json_object"
            }
        }

        response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
                
        response_json = response.json()

        if response.status_code != 200:
            logging.error(f"Ошибка: {response_json}")
            return None, 0, 0

        content = response_json['choices'][0]['message']['content']
        usage = response_json.get('usage', {})
        prompt_tokens = usage.get('prompt_tokens', 0)
        completion_tokens = usage.get('completion_tokens', 0)
        total_tokens = usage.get('total_tokens', 0)

        return content, prompt_tokens, completion_tokens

    except Exception as e:
        logging.error(f"Ошибка при обработке изображения: {e}")
        return None, 0, 0

def parse_extracted_text(text):
    objects_pattern = re.compile(r'\{\s*([^}]+)\s*\}')
    key_value_pattern = re.compile(r'\"([^\"]+)\": \"([^\"]*)\"')
    
    objects = objects_pattern.findall(text)
    parsed_results = []
    
    for obj in objects:
        parsed_data = {}
        for match in key_value_pattern.finditer(obj):
            key = match.group(1).lower()
            value = match.group(2)
            parsed_data[key] = value
        
        parsed_results.append(parsed_data)
    
    return parsed_results

def process_image_and_write_to_csv_stream(filename, api_key, csv_writer, prompt, totals):
    image_path = os.path.join(os.getcwd(), 'photos', filename)
    result, prompt_tokens, completion_tokens = extract_and_process_image_stream(image_path, api_key, prompt)
    
    if result:
        logging.info(f"Результат для {filename}:\n{result}\n")
        cleaned_result = result.strip('```json').strip('```').strip()
        parsed_results = parse_extracted_text(cleaned_result)
        
        for parsed_data in parsed_results:
            # Убедитесь, что все ключи присутствуют в разобранных данных
            last_name = parsed_data.get('фамилия', '-')
            first_name = parsed_data.get('имя', '-')
            middle_name = parsed_data.get('отчество', '-')
            birth_date = parsed_data.get('дата рождения', '-')
            death_date = parsed_data.get('дата смерти', '-')
            status = parsed_data.get('статус', 'Обработка')

            csv_writer.writerow([filename, last_name, first_name, middle_name, birth_date, death_date, status])
        
        # Расчет стоимости
        cost = calculate_cost(prompt_tokens, completion_tokens)
        
        # Обновление общих сумм
        totals['prompt_tokens'] += prompt_tokens
        totals['completion_tokens'] += completion_tokens
        totals['total_tokens'] += (prompt_tokens + completion_tokens)
        totals['cost'] += cost

        # Вывод информации по изображению
        logging.info(f"Токены для {filename}: Входящие = {prompt_tokens}, Исходящие = {completion_tokens}, Всего = {prompt_tokens + completion_tokens}")
        logging.info(f"Стоимость для {filename}: ${cost:.6f}\n")
    else:
        logging.error(f"Ошибка обработки изображения: {filename}")

# Функции для GUI

def create_batch_ui(state_manager, log_widget):
    def create_batch():
        try:
            log_widget.insert(tk.END, "Начало создания батча...\n")
            log_widget.see(tk.END)
            state_manager.load_state()

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

            # Проверка путей
            if not prompt_path:
                msg = "Файл промпта не задан. Перейдите в вкладку 'Настройки' и выберите файл промпта."
                logging.error(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                return

            if not photos_path:
                msg = "Папка с фотографиями не задана. Перейдите в вкладку 'Настройки' и выберите папку с фотографиями."
                logging.error(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                return

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

            # Список файлов оригинальных изображений
            image_files = [f for f in os.listdir(photos_path) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
            if not image_files:
                msg = f"Нет файлов для обработки в папке '{photos_path}'."
                logging.info(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                return

            # Если состояние ещё не инициализировано, то инициализируем батчи
            if not state_manager.state["batches"]:
                # Загрузка и сжатие изображений, удаление метаданных
                log_widget.insert(tk.END, "Сжатие изображений и удаление метаданных...\n")
                log_widget.see(tk.END)
                prepared_files = []
                for filename in tqdm(image_files, desc="Обработка изображений", unit="изображение"):
                    original_path = os.path.join(photos_path, 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_template = load_prompt_from_file(prompt_path)
                if not prompt_template:
                    msg = "Промпт не был загружен. Завершение работы."
                    logging.error(msg)
                    log_widget.insert(tk.END, msg + "\n")
                    log_widget.see(tk.END)
                    return

                # Разделение подготовленных файлов на батчи
                log_widget.insert(tk.END, "Разделение файлов на батчи...\n")
                log_widget.see(tk.END)
                batches = get_batches(prepared_files, max_size_mb=100)
                log_widget.insert(tk.END, f"Всего батчей: {len(batches)}\n")
                log_widget.see(tk.END)
                
                # Инициализация общих сумм
                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:
                log_widget.insert(tk.END, "Загружаем существующие батчи из состояния.\n")
                log_widget.see(tk.END)

            # Загрузка промпта из файла (в любом случае)
            prompt_template = load_prompt_from_file(prompt_path)
            if not prompt_template:
                msg = "Промпт не был загружен. Завершение работы."
                logging.error(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                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"]
                
                log_widget.insert(tk.END, f"Обработка батча {batch_num} с {len(batch_files)} файлами...\n")
                log_widget.see(tk.END)
                
                # Создание задач для текущего батча с использованием глобального индекса и параметра 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)
                log_widget.insert(tk.END, f"Запись задач в файл {tasks_file_path}...\n")
                log_widget.see(tk.END)
                write_jsonl_file(tasks, tasks_file_path)
                
                # Загрузка файла на OpenAI
                log_widget.insert(tk.END, "Загрузка задач на OpenAI...\n")
                log_widget.see(tk.END)
                try:
                    batch_file_id = upload_file(tasks_file_path, api_key)
                    log_widget.insert(tk.END, f"Файл батча {batch_num} загружен. ID файла: {batch_file_id}\n")
                    log_widget.see(tk.END)
                    # Обновление состояния батча
                    state_manager.update_batch(batch_num, status="submitted", file_id=batch_file_id)
                except Exception as e:
                    logging.error(f"Ошибка загрузки файла батча {batch_num} на OpenAI: {e}")
                    log_widget.insert(tk.END, f"Ошибка загрузки файла батча {batch_num} на OpenAI: {e}\n")
                    log_widget.see(tk.END)
                    state_manager.update_batch(batch_num, status="failed")
                    continue  # Переходим к следующему батчу

                # Создание Batch Job
                log_widget.insert(tk.END, "Создание Batch Job...\n")
                log_widget.see(tk.END)
                try:
                    batch_job_id = create_batch_job(batch_file_id, api_key)
                    log_widget.insert(tk.END, f"Batch Job для батча {batch_num} создан. ID задания: {batch_job_id}\n")
                    log_widget.see(tk.END)
                    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}")
                    log_widget.insert(tk.END, f"Ошибка создания Batch Job для батча {batch_num}: {e}\n")
                    log_widget.see(tk.END)
                    state_manager.update_batch(batch_num, status="failed")
                    continue  # Переходим к следующему батчу

            log_widget.insert(tk.END, "Создание батча завершено.\n")
            log_widget.see(tk.END)

        except Exception as e:
            logging.error(f"Неожиданная ошибка при создании батча: {e}")
            log_widget.insert(tk.END, f"Неожиданная ошибка при создании батча: {e}\n")
            log_widget.see(tk.END)

    # Запуск в отдельном потоке
    threading.Thread(target=create_batch, daemon=True).start()

def check_batch_ui(state_manager, log_widget):
    def check_batch():
        try:
            log_widget.insert(tk.END, "Начало проверки батча...\n")
            log_widget.see(tk.END)
            state_manager.load_state()

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

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

            for batch in submitted_batches:
                batch_num = batch["batch_num"]
                job_id = batch["job_id"]
                starting_index = batch["starting_index"]
                log_widget.insert(tk.END, f"Отслеживание состояния Batch Job для батча {batch_num}, Job ID: {job_id}...\n")
                log_widget.see(tk.END)
                
                try:
                    status_info = check_batch_job_status(job_id, api_key)
                    status = status_info.get('status')
                    log_widget.insert(tk.END, f"Текущий статус батча {batch_num}: {status}\n")
                    log_widget.see(tk.END)
                    
                    if status == 'completed':
                        log_widget.insert(tk.END, f"Batch Job батча {batch_num} успешно завершено.\n")
                        log_widget.see(tk.END)
                        result_file_id = status_info.get('output_file_id')
                        if not result_file_id:
                            msg = f"Не найден 'output_file_id' в статусе Batch Job для батча {batch_num}."
                            logging.error(msg)
                            log_widget.insert(tk.END, msg + "\n")
                            log_widget.see(tk.END)
                            state_manager.update_batch(batch_num, status="failed")
                            continue  # Переходим к следующему батчу
                        
                        # Загрузка результатов
                        log_widget.insert(tk.END, f"Загрузка результатов из файла ID: {result_file_id} для батча {batch_num}...\n")
                        log_widget.see(tk.END)
                        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}")
                            log_widget.insert(tk.END, f"Ошибка загрузки результатов Batch Job для батча {batch_num}: {e}\n")
                            log_widget.see(tk.END)
                            state_manager.update_batch(batch_num, status="failed")
                            continue  # Переходим к следующему батчу
                        
                        # Парсинг результатов
                        log_widget.insert(tk.END, "Парсинг результатов...\n")
                        log_widget.see(tk.END)
                        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}")
                            log_widget.insert(tk.END, f"Ошибка парсинга результатов батча {batch_num}: {e}\n")
                            log_widget.see(tk.END)
                            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"
                        log_widget.insert(tk.END, f"Запись результатов в CSV файл: {csv_output_path}...\n")
                        log_widget.see(tk.END)
                        write_results_to_csv(results, csv_output_path, image_files_batch, starting_index, state_manager.state["totals"])
                        log_widget.insert(tk.END, f"Результаты батча {batch_num} записаны в CSV.\n")
                        log_widget.see(tk.END)
                    elif status == 'failed':
                        msg = f"Batch Job батча {batch_num} завершилось с ошибкой."
                        logging.error(msg)
                        log_widget.insert(tk.END, msg + "\n")
                        log_widget.see(tk.END)
                        log_widget.insert(tk.END, f"{json.dumps(status_info, ensure_ascii=False)}\n")
                        log_widget.see(tk.END)
                        state_manager.update_batch(batch_num, status="failed")
                    else:
                        # Статус все ещё in_progress или другой
                        log_widget.insert(tk.END, f"Batch Job батча {batch_num} всё ещё обрабатывается. Ждём...\n")
                        log_widget.see(tk.END)
                except Exception as e:
                    logging.error(f"Ошибка проверки статуса Batch Job для батча {batch_num}: {e}")
                    log_widget.insert(tk.END, f"Ошибка проверки статуса Batch Job для батча {batch_num}: {e}\n")
                    log_widget.see(tk.END)
                    continue  # Переходим к следующему батчу

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

            log_widget.insert(tk.END, "Проверка батча завершена.\n")
            log_widget.see(tk.END)

        except Exception as e:
            logging.error(f"Неожиданная ошибка при проверке батча: {e}")
            log_widget.insert(tk.END, f"Неожиданная ошибка при проверке батча: {e}\n")
            log_widget.see(tk.END)

    # Запуск в отдельном потоке
    threading.Thread(target=check_batch, daemon=True).start()

def process_images_ui(log_widget):
    def process_images():
        try:
            log_widget.insert(tk.END, "Начало стримовой обработки изображений...\n")
            log_widget.see(tk.END)

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

            # Проверка путей
            if not prompt_path:
                msg = "Файл промпта не задан. Перейдите в вкладку 'Настройки' и выберите файл промпта."
                logging.error(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                return

            if not photos_path:
                msg = "Папка с фотографиями не задана. Перейдите в вкладку 'Настройки' и выберите папку с фотографиями."
                logging.error(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                return

            # Список файлов изображений
            image_files = [f for f in os.listdir(photos_path) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
            if not image_files:
                msg = f"Нет файлов для обработки в папке '{photos_path}'."
                logging.info(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                return

            # Загрузка промпта из файла
            prompt = load_prompt_from_file(prompt_path)
            if not prompt:
                msg = "Промпт не был загружен. Завершение работы."
                logging.error(msg)
                log_widget.insert(tk.END, msg + "\n")
                log_widget.see(tk.END)
                return

            # Открытие файла результатов в режиме добавления
            csv_output_path = 'stream_results.csv'
            with open(csv_output_path, 'a', newline='', encoding='utf-8') as results_file:
                writer = csv.writer(results_file)
                
                # Запись заголовка, если файл пуст
                if os.stat(csv_output_path).st_size == 0:
                    writer.writerow(['Filename', 'Фамилия', 'Имя', 'Отчество', 'Дата рождения', 'Дата смерти', 'Статус'])

                # Инициализация общих сумм
                totals = {
                    'prompt_tokens': 0,
                    'completion_tokens': 0,
                    'total_tokens': 0,
                    'cost': 0.0
                }

                # Обработка изображений с использованием ThreadPoolExecutor для многопоточности
                with ThreadPoolExecutor(max_workers=5) as executor:
                    futures = [executor.submit(process_image_and_write_to_csv_stream, filename, api_key, writer, prompt, totals) for filename in image_files]
                    for future in as_completed(futures):
                        future.result()

            # Вывод общих сумм после обработки всех изображений
            summary = (
                "Общие результаты стримовой обработки:\n"
                f"Всего входящих токенов: {totals['prompt_tokens']}\n"
                f"Всего исходящих токенов: {totals['completion_tokens']}\n"
                f"Всего токенов: {totals['total_tokens']}\n"
                f"Общая стоимость: ${totals['cost']:.6f}\n"
            )
            logging.info(summary)
            log_widget.insert(tk.END, summary + "\n")
            log_widget.see(tk.END)
            logging.info("Стримовая обработка завершена успешно.")

        except Exception as e:
            logging.error(f"Неожиданная ошибка при стримовой обработке: {e}")
            log_widget.insert(tk.END, f"Неожиданная ошибка при стримовой обработке: {e}\n")
            log_widget.see(tk.END)

    # Запуск в отдельном потоке
    threading.Thread(target=process_images, daemon=True).start()

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 main_gui():
    # Создание экземпляра StateManager
    state_manager = StateManager()

    # Создание основного окна
    root = tk.Tk()
    root.title("Batch and Stream Processing Interface")
    root.geometry("900x700")

    # Создание Notebook (вкладок)
    notebook = ttk.Notebook(root)
    notebook.pack(fill=tk.BOTH, expand=True)

    # Вкладка "Обработка"
    processing_frame = ttk.Frame(notebook)
    notebook.add(processing_frame, text="Обработка")

    # Вкладка "Настройки"
    settings_frame = ttk.Frame(notebook)
    notebook.add(settings_frame, text="Настройки")

    # Создание фрейма для кнопок обработки в вкладке "Обработка"
    button_frame = ttk.Frame(processing_frame)
    button_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)

    # Кнопка "Создать батч"
    create_button = ttk.Button(button_frame, text="Создать батч", command=lambda: create_batch_ui(state_manager, log_widget))
    create_button.pack(side=tk.LEFT, padx=5)

    # Кнопка "Проверить батч"
    check_button = ttk.Button(button_frame, text="Проверить батч", command=lambda: check_batch_ui(state_manager, log_widget))
    check_button.pack(side=tk.LEFT, padx=5)

    # Кнопка "Стримовая обработка"
    stream_button = ttk.Button(button_frame, text="Стримовая обработка", command=lambda: process_images_ui(log_widget))
    stream_button.pack(side=tk.LEFT, padx=5)

    # Создание области для логов в вкладке "Обработка"
    log_frame = ttk.Frame(processing_frame)
    log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

    log_label = ttk.Label(log_frame, text="Логи:")
    log_label.pack(anchor=tk.W)

    log_widget = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD)
    log_widget.pack(fill=tk.BOTH, expand=True)

    # Функция для перенаправления логов в виджет
    class TextHandler(logging.Handler):
        def __init__(self, text_widget):
            super().__init__()
            self.text_widget = text_widget

        def emit(self, record):
            msg = self.format(record)
            def append():
                self.text_widget.insert(tk.END, msg + '\n')
                self.text_widget.see(tk.END)
            self.text_widget.after(0, append)

    # Добавление обработчика для отображения логов в GUI
    text_handler = TextHandler(log_widget)
    text_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s:%(message)s'))
    logging.getLogger().addHandler(text_handler)

    # Функции для вкладки "Настройки"

    def select_prompt_file():
        file_path = filedialog.askopenfilename(
            title="Выберите файл промпта",
            filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
        )
        if file_path:
            prompt_entry_var.set(file_path)
            # Сохранение в конфиг
            api_key, detail, _, photos_path = load_config()
            save_config(api_key, detail, file_path, photos_path)
            logging.info(f"Выбран файл промпта: {file_path}")
            log_widget.insert(tk.END, f"Выбран файл промпта: {file_path}\n")
            log_widget.see(tk.END)

    def select_photos_folder():
        folder_path = filedialog.askdirectory(
            title="Выберите папку с фотографиями"
        )
        if folder_path:
            photos_entry_var.set(folder_path)
            # Сохранение в конфиг
            api_key, detail, prompt_path, _ = load_config()
            save_config(api_key, detail, prompt_path, folder_path)
            logging.info(f"Выбрана папка с фотографиями: {folder_path}")
            log_widget.insert(tk.END, f"Выбрана папка с фотографиями: {folder_path}\n")
            log_widget.see(tk.END)

    # Создание фрейма для настроек в вкладке "Настройки"
    settings_content = ttk.Frame(settings_frame)
    settings_content.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)

    # Настройка выбора файла промпта
    prompt_label = ttk.Label(settings_content, text="Файл промпта:")
    prompt_label.grid(row=0, column=0, sticky=tk.W, pady=5)

    prompt_entry_var = tk.StringVar()
    prompt_entry = ttk.Entry(settings_content, textvariable=prompt_entry_var, width=60)
    prompt_entry.grid(row=0, column=1, pady=5, padx=5)

    prompt_button = ttk.Button(settings_content, text="Выбрать файл", command=select_prompt_file)
    prompt_button.grid(row=0, column=2, padx=5, pady=5)

    # Настройка выбора папки с фотографиями
    photos_label = ttk.Label(settings_content, text="Папка с фотографиями:")
    photos_label.grid(row=1, column=0, sticky=tk.W, pady=5)

    photos_entry_var = tk.StringVar()
    photos_entry = ttk.Entry(settings_content, textvariable=photos_entry_var, width=60)
    photos_entry.grid(row=1, column=1, pady=5, padx=5)

    photos_button = ttk.Button(settings_content, text="Выбрать папку", command=select_photos_folder)
    photos_button.grid(row=1, column=2, padx=5, pady=5)

    # Загрузка настроек при запуске
    def load_settings():
        api_key, detail, prompt_path, photos_path = load_config()
        prompt_entry_var.set(prompt_path)
        photos_entry_var.set(photos_path)
        logging.info("Настройки загружены.")
        log_widget.insert(tk.END, "Настройки загружены.\n")
        log_widget.see(tk.END)

    load_settings()

    # Запуск основного цикла обработки событий
    root.mainloop()

if __name__ == "__main__":
    main_gui()