Напиши документацию к этому коду

import os
import sys
from os.path import dirname

sys.path.append(dirname(os.path.abspath(__file__)) + "/../pylib")
sys.path.append(dirname(os.path.abspath(__file__)) + "/../srv-gpu-vutils")
MY_DIR = os.path.dirname(os.path.abspath(__file__))

import re
import requests
import time
import json
from datetime import datetime, timedelta
import pytz
import multiprocessing
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
import subprocess
from subprocess import CalledProcessError

import argparse
import random
from pathlib import Path

import vol1
import s_heartbeat
import vhelpers

# import srv_gpu_task
import dse_client


ENTERPRISE_ID = os.environ['ENTERPRISE_ID']
if not ENTERPRISE_ID:
    print("set ENTERPRISE_ID before")
    os._exit(1)

"""
    ЭТО СКРИПТ поиска начала и конца диалога по stt.

    пример

        !!! python3.6 и выше обязателен

        python3.6 <script.py>  <id поставщика>  -mode=select-source -max-try=10 -json-cfg={....}

    При запуске - он делает итераций -max-try
    на каждую итерацию пробует рандомно взять из конфига ОДИН микрофон и один день (рандомно от сегодня до х) 
    и для этого дня и микрофона собрать файл в статистику и положить на стор результат.

    Этот скрипт можно смело запускать в множественном количестве - он будет работать только с данными обновленными и локировать объект результирубщий перед началом работы.
"""
# пример конфига для отладки
"""
[
   {
        "tz":"Europe/Moscow",
        "start_time": "10:00",
        "end_time": "21:00",
        "sources": [
            10.8.6.82_badge.marya2
        ]
    }
]
"""

_OUTPUT_EXT = ".stt.sedet.json"

_PREV_FILE_CUT_SECS = 10  # сколько захватывает с конца предыдущего файла
_MAX_PROCESSING_DURATION = 60  # максимальная длина обрабатываемого за раз сегмента

parser = argparse.ArgumentParser(description='dialog_start_end supplier')

parser.add_argument('supplier_id')
# parser.add_argument("--stream-mode", "-sm", choices=['mpegts', 'png', 'jpg'], help="How send data to GPU worker. Mpegts is the most efficient and default", required=False, default="mpegts")
parser.add_argument('-json-cfg', type=str, help='json config', required=True)
parser.add_argument('-mode', type=str, default="select-source", choices=['select-source', 'build-source-results'],
                    help='mode to run "select-source" / "build-source-results" ', required=True)
parser.add_argument('-index', type=str, help='index of json config to process', required=False)
parser.add_argument('-source', type=str, help='index of json config to process', required=False)
parser.add_argument('-max-try', type=int, default=15, help='index of json config to process', required=False)
parser.add_argument('-high-priority', dest='high_priority', action='store_true')
parser.add_argument('-params', help='language and speakers', required=False)
args = parser.parse_args()
config = json.loads(args.json_cfg)

if args.params:
    params = json.loads(args.params)
else:
    params = None

# !!!!! ОБЯЗАТЕЛЬНО ВСЕГДА делать сидирование - иначе некоторые части конфига НИКОГДА не будут браться
random.seed(time.time())


# Получаем объекты и расширения файлов
def get_object_ts_and_extention(object_name):
    ext_m = re.search(".+_([0-9]{10}).*(\\.[^\\.]+)$", object_name)
    if not ext_m:
        return None
    return int(ext_m[1]), ext_m[2]


# Обработка файла
def ProcessFile(selected_object, output_object_path, source_json_params, is_high_priority, reversed_object_list,
                params=None):
    
    download_folder = "/tmp/downloads/"
    import shutil
    try:
        shutil.rmtree(download_folder, ignore_errors=False)
    except FileNotFoundError as e:
        print("No folder to remove. It is normal: ", download_folder)

    # создаем новую директорию
    os.mkdir(download_folder)

    # получаем временную метку и расширение текущего объекта
    object_ts, object_ext = get_object_ts_and_extention(selected_object)

    # для микрофонов с потоковой записью (в отличие от телефонных звонков у нас может быть ситуация что надо
    # конец предыдущего файла добавить в начало чтобы слова не попадали в промежуток между
    # FIXME для телефонных записей это не нужно будет, также если
    prev_object_name = getPrevTsObjectSameExt(reversed_object_list, selected_object)

    # предыдущий файл может уже протухнуть или в список не попасть - но это очень редкое событие и поэтому мы
    # игнорируем его и просто тогда не обрабатываем текущий
    # также предыдущий файл может быть такой длины, что не подходит к текущему -> тогда его надо игнорировать
    prev_local_file = None
    prev_object_ts = None
    if prev_object_name:
        prev_local_file = download_folder + Path(prev_object_name).name
        if not vol1.download_object_to_file(prev_object_name, prev_local_file, timeout=30):
            print("ERROR: Could not download PREV object:", prev_object_name, prev_local_file)
            return False
        print("Donwloaded PREV object to local file", prev_object_name, prev_local_file)

    local_file = download_folder + Path(selected_object).name
    if not vol1.download_object_to_file(selected_object, local_file, timeout=30):
        print("ERROR: Could not download object:", selected_object, local_file)
        return False

    print("Downloaded object to local file", selected_object, local_file)

    # Для скаченного stt получаем результат, отдаем текущий объект и предыдущий с тем же расширением
    result = dse_client.DialogStartEnd(prev_file_path=prev_local_file, file_path=local_file, file_start_ts=object_ts, is_high_priority=is_high_priority, params=params)
    
    print('RESULT: ', result['result'])

    print(output_object_path)

    # сохраняем результат по переданному выходному пути
    how_much_to_store = 60 * 60 * 24 * 60  # храним 60 дней на всякий случай
    put_result = vol1.put_object(object_name=output_object_path, content=json.dumps(result, ensure_ascii=False),
            headers={
               "X-Delete-After": str(how_much_to_store),
               "Content-Type": "application/json"
            }, timeout=300)
    
    print("FINISHED, result", put_result)
    if not put_result:
      print("ERROR: failed upload", output_object_path, result_file)
      return False

    return True


def get_sources_codes(sources):
    codes = []
    for item in sources:
        if isinstance(item, str):
            codes.append(item)
        elif isinstance(item, dict):
            # этот сорс в виде объекта передан
            codes.extend(item.keys())
        else:
            print("ERROR: wrong source item", item)
            os._exit(1)

    return codes


def get_source_params(sources, code):
    for item in sources:
        if isinstance(item, str):
            if item == code:
                # у строкового элемента нет параметров дополнительных
                return None
        elif isinstance(item, dict):
            for sub_item in item:
                if sub_item == code:
                    return item[sub_item]

    print("ERROR: wrong source code", code)
    os._exit(1)


# Получаем новый список файлов и сравниваем его со старым
# Если в новом списке есть файлы, не входящие в старый список, то возвращаем их
def filter_new_files(source_key, last_object):
    prefix = ENTERPRISE_ID + "/" + source_key
    # разбиваем /raw-uploads/enterprise-id/file-name
    path_parts = last_object.split('/')
    file_name = path_parts[len(path_parts) - 1]
    # имя последнего файла, только без /raw-uploads/, потому что уже прописан бакет raw-uploads
    marker = ENTERPRISE_ID + '/' + file_name
    # выставлено 9999999999, как заглушка, потому что если вернуть None, то end_marker не задействуется
    # и попадают лишние файлы, например 10.8.3.103_rtsp.camera.10_TEST_1644853800 вместо 10.8.3.103_rtsp.camera.10_1644853800
    # end_marker = None
    end_marker = prefix + "_" + str(9999999999)

    print("PREFIX:", prefix)
    object_list = vol1.get_object_list(bucket="raw-uploads",
                                       prefix=prefix,
                                       marker=marker,
                                       end_marker=end_marker,
                                       extensions=[".stt.json", _OUTPUT_EXT])
    return object_list


# Ищем в списке предыдущий объект с тем же расширением
def getPrevTsObjectSameExt(reversed_object_list, object_name):
    # таймпстемп и расширения для исследуемого объекта
    object_ts, object_ext = get_object_ts_and_extention(object_name)
    # обработка объекта без расширения
    if not object_ext:
        print("Extention not found... Code error")
        raise Exception("Extention not found. Error:", object_name)
    print(" Object extension: ", object_ext)

    for prev_object_name in reversed_object_list:
        print("PREV:", prev_object_name)
        if prev_object_name == object_name:
            continue
        prev_object_ts, prev_object_ext = get_object_ts_and_extention(prev_object_name)
        if prev_object_ext == object_ext:
            print("Prev object found:", prev_object_name)
        else:
            continue

        if int(object_ts) - int(prev_object_ts) > (60 * 60 * 4):
            # если предыдущий объект более чем на 4 часа старше (его начало) -
            # то считаем что он явно не может быть сопряжет с текущим
            print("Prev object timestamp is too old.")
            return None

        return prev_object_name

    return None


def Process(processed_sources_keys):
    source = None
    index = None

    retValueOnAlreadyProcessed = False
    all_sources = []

    if args.mode == "select-source":

        # чтобы равномерно распределять запуски на камеру - надо собрать все камеры в один массив и уже на нем сделать рандомизацию
        print("Get random index of the config entry from 0 to", len(config) - 1)
        if len(config) < 1:
            print("No config items")
            os._exit(1)  # это ошибка конфига

        for cfg_index in range(len(config)):
            cfg_item = config[cfg_index]
            sources_keys = get_sources_codes(cfg_item["sources"])
            if len(sources_keys) < 1:
                print("No sources")
                return False  # Это потенциальная ошибка конфига

            for sources_key in sources_keys:
                # добавляем таппл с индексом и камерой
                all_sources.append((cfg_index, sources_key))

        (cfg_index, sources_key) = random.choice(all_sources)

        print("========================================")
        print(cfg_index)
        print(sources_key)
        index = cfg_index
        source = sources_key

        retValueOnAlreadyProcessed = True


    else:

        index = args.index
        source = args.source
        all_sources.append(
            (index, source))  # добавляем в массив один - тк тут все источники у нас только один указанный

    # проверяем если у нас все источники уже обработаны по разу - то выходим сразу - иначе скрипт будет висеть долго
    # и занимать место
    if len(all_sources) == len(processed_sources_keys):
        print("ALL SOURCES ALREADY PROCESSED. EXIT")
        sys.exit(0)

    # проверяем что если уже обрабатывали выбранный источник - то просто пропускаем его
    if (index, source) not in processed_sources_keys:
        processed_sources_keys.append((index, source))
    else:
        print("Already processed key: ", source, len(all_sources), len(processed_sources_keys))
        # если уже процессили просто возвращает Try если рандомом выбирали камеру
        return retValueOnAlreadyProcessed

    MAX_PROCESS_TRY = args.max_try

    print("Input source:", source)
    print("Input index:", index)

    # по идее в этот скрипт придет только один конфига часть !!!! только по одной камере секция -
    # это для распарралеливания проще!!!

    """
     1. На этом этапе мы взяли одну камеру из одного конфига
     2. Берем список ее файлов на сторе за последние 2 дня
     3. Нам нужен файл без _OUTPUT_EXT  (с конца который ближе к now)
     4. Находим пробуем лочить - если не смогли залочить - берем следующий - тут можно хоть до последнего дойти
     5. Скачиваем медию и подготавливаем (выравниваем)
     6. Запускаем скрипт который детекции делает
     7. По успеху заливаем на стор.
    """

    cfg = config[int(index)]
    source_key = source
    source_json_params = get_source_params(cfg["sources"], source_key)
    print("JSON PARAMS:", source_json_params, source_key)

    tz = "UTC"
    if "tz" in cfg:
        tz = cfg["tz"]

    start_time_cfg = "00:00"
    if "start_time" in cfg:
        start_time_cfg = cfg["start_time"]

    end_time_cfg = "24:00"
    if "end_time" in cfg:
        end_time_cfg = cfg["end_time"]
    if end_time_cfg == "24:00":
        end_time_cfg = "00:00"  # либа не поймет 24:00 - для нее это 00:00

    what_day_ago_to_process = 35
    print("DAY AGO TO PROCESS:", what_day_ago_to_process)

    # парсим конфиг по времени в таймстемпы
    (utc_start_timestamp, utc_end_timestamp, utc_day_start) = vhelpers.get_start_end_utc_ts(tz, what_day_ago_to_process,
                                                                                            start_time_cfg,
                                                                                            end_time_cfg)

    prefix = ENTERPRISE_ID + "/" + source_key
    marker = prefix + "_" + str(utc_start_timestamp)
    # выставлено 9999999999, как заглушка, потому что если вернуть None, то end_marker не задействуется
    # и попадают лишние файлы, например 10.8.3.103_rtsp.camera.10_TEST_1644853800 вместо
    # 10.8.3.103_rtsp.camera.10_1644853800
    # end_marker = None
    end_marker = prefix + "_" + str(9999999999)
    print("PPREFIX:", prefix)
    object_list = vol1.get_object_list(bucket="raw-uploads",
                                       prefix=prefix,
                                       marker=marker,
                                       end_marker=end_marker,
                                       extensions=[".stt.json", _OUTPUT_EXT])

    # теперь бежим с конца и пробуем найти медиа файл без детекций
    curr_try = 0
    exit_code = False
    selected_object = None

    list_not_empty = len(object_list) > 0
    # если список пуст - выходим
    if not list_not_empty:
        return

    # разворачиваем список свежими объектами вперед
    _object_list = list(reversed(object_list))
    # запоминаем ts свежего файла, для запроса еще более свежих
    last_object = _object_list[0]
    while list_not_empty:
        # каждый раз достаем из списка самый первый объект и удаляем его из списка
        object_name = get_last_not_processed_from_list(_object_list)

        # это когда список стал пустым
        if not object_name:
            list_not_empty = False
            break

        if object_name.endswith(_OUTPUT_EXT):  # ".det.json"):
            continue

        # нам надо понять, для объекта текущего - попадает ли он в нужный для нас отрезок времени
        # (при условии, что он может кончаться позже чем требуемое время (т.е. может пересекаться
        object_start_timestamp_utc = vol1.get_name_start_timestamp(object_name)
        # надо взять его день, получить диапазон дня, сравнить таймстемпы (пока без предсказаний длинны файла -
        # просто заложи 30-60 минут если нет следующего?)
        (obj_utc_start_day_timestamp, obj_utc_end_day_timestamp,
         obj_utc_day_start) = vhelpers.get_start_end_utc_ts_for_timestamp(tz, object_start_timestamp_utc,
                                                                          start_time_cfg, end_time_cfg)

        # сравниваем, что время файла попадает в расписание рабочее, тк файл может быть начат раньше начала
        # рабочего дня и содержать данные то берем для начала запас 60 минут
        if obj_utc_start_day_timestamp <= (
                object_start_timestamp_utc + (60 * 60)) and object_start_timestamp_utc <= obj_utc_end_day_timestamp:
            print("GOOD timestamp to process", object_start_timestamp_utc, obj_utc_start_day_timestamp,
                  obj_utc_end_day_timestamp)
        else:
            print("BAD timestamp to process. Skip file", object_start_timestamp_utc, obj_utc_start_day_timestamp,
                  obj_utc_end_day_timestamp)
            continue

        if object_name + _OUTPUT_EXT in object_list:
            print("Already prepared for", object_name)
            continue

        selected_object = object_name

        print("Selected object:", selected_object)
        if not selected_object:
            print("No object without " + _OUTPUT_EXT + " found. Exit")
            os._exit(0)

        # пробуем захватить хеартбит для него
        output_object_path = selected_object + _OUTPUT_EXT
        print("Output PATH: ", output_object_path)

        heartbeat = s_heartbeat.CreateHeartbeat("detector_py_" + output_object_path)
        if not heartbeat:
            print("Could not lock target file. Skip. Try again.")
            continue

        # проверяем что объекта все еще нет (его могли создать пока мы предыдущий обрабатывали)
        (object_exists, object_name2, content, headers) = vol1.get_object_head(object_name=output_object_path)
        print(" Object existence check:", output_object_path, object_exists)
        if object_exists :
            print("Object exists already. It could be created by a parallel thread while we processed previous.")
            heartbeat.release()
            continue

        # Камеры с КЛ должны обрабатываться в приоритете
        high_priority = ""  # srv-gpu принимает только отсутствие параметра high_priority
        # для низкоприоритетных запросов
        if 'high_priority' in config[index]:
            if source_key in config[index]['high_priority']:
                high_priority = "1"

        # На всякий случай меняем тип данных
        # if params is not None:
        #     params["min_spkrs"] = int(params["min_spkrs"])
        #     params["max_spkrs"] = int(params["max_spkrs"])

        det_result = ProcessFile(selected_object=selected_object, output_object_path=output_object_path,
                                 source_json_params=source_json_params, is_high_priority=high_priority,
                                 reversed_object_list=_object_list, params=params)

        exit_code = exit_code or det_result

        heartbeat.release()

        curr_try += 1

        if curr_try >= MAX_PROCESS_TRY:
            return exit_code
            # sys.exit(0 if exit_code else 1)

        # обновляем список файлов - добавляем новые, если есть, в начало списка
        new_files = filter_new_files(source_key, last_object)
        if len(new_files) > 0:
            print("Update list with new files")
            # добавляем в начало списка
            for item in new_files:
                # если объект еще не попадал в обработку
                if item not in object_list:
                    # ставим в начало очереди
                    _object_list.insert(0, item)
                    # кладем в изначальный массив, чтобы в нем находить _OUTPUT_EXT и не переделывать еще раз файл
                    object_list.insert(0, item)
                    # запоминаем самый свежий файл
                    last_object = item

        print("============================= NEXT FILE =============================")

        continue


# Возвращает первый элемент списка, если список не пуст, иначе False
# obj_list - список с объектами
def get_last_not_processed_from_list(obj_list):
    if len(obj_list) > 0:
        return obj_list.pop(0)
    else:
        return False


MIN_TIME_TO_WORK_SEC_ = 60  # 10 minutes

start_time = time.time()
print("START TIMESTAMP: ", start_time)

ret_ = False

processed_sources = []
print("TIME_DIFF:", abs(time.time() - start_time))
while abs(time.time() - start_time) < MIN_TIME_TO_WORK_SEC_:
    print("CALL PROCESS with processed_sources:", len(processed_sources))
    ret_ = Process(processed_sources)

print("READY TO EXIT with ret_: ", ret_)

sys.exit(1 if ret_ is False else 0)