Skip to content

Commit ffe73e0

Browse files
committed
Общая оптимизация: больше мультипроцессинга и асинка
1 parent 267d9f1 commit ffe73e0

File tree

12 files changed

+256
-201
lines changed

12 files changed

+256
-201
lines changed

main.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import asyncio
2+
import glob
23
import os
34
import re
45
from datetime import datetime
6+
from math import ceil
57

68
from src.audio.audio_handling import AudioHandler
79
from src.config.comp_params import ComputerParams
810
from src.config.settings import (
11+
BATCH_VIDEO_PATH,
912
END_BATCH_TO_UPSCALE,
1013
FINAL_VIDEO,
1114
INPUT_BATCHES_DIR,
1215
ORIGINAL_VIDEO,
1316
START_BATCH_TO_UPSCALE,
17+
TMP_VIDEO_PATH,
1418
)
19+
from src.files.file_actions import delete_file
1520
from src.frames.frames_helpers import extract_frames_to_batches, get_fps_accurate
1621
from src.frames.upscale import delete_frames, upscale_batches
1722
from src.utils.logger import logger
@@ -26,7 +31,7 @@ def print_header(title: str) -> None:
2631

2732
def print_bottom(title: str) -> None:
2833
logger.info(f"✅ {title.upper()}".center(50))
29-
logger.info(f"{'=' * 50}")
34+
logger.info(f"{'=' * 50}\n")
3035

3136

3237
async def clean_up(audio: AudioHandler) -> None:
@@ -36,6 +41,12 @@ async def clean_up(audio: AudioHandler) -> None:
3641
asyncio.to_thread(audio.delete_audio_if_exists),
3742
asyncio.to_thread(delete_frames, del_upscaled=False),
3843
asyncio.to_thread(delete_frames, del_upscaled=True),
44+
asyncio.to_thread(
45+
map, delete_file, glob.glob(os.path.join(BATCH_VIDEO_PATH, "*.mp4"))
46+
),
47+
asyncio.to_thread(
48+
map, delete_file, glob.glob(os.path.join(TMP_VIDEO_PATH, "*.mp4"))
49+
),
3950
)
4051
logger.debug("Временные файлы успешно удалены")
4152

@@ -80,13 +91,7 @@ async def process_batches(
8091
logger.success(f"Батчи {start_batch}-{end_batch} успешно апскейлены")
8192

8293
batches_to_perform = [f"batch_{i}" for i in range(start_batch, end_batch + 1)]
83-
short_video = await video.build_short_video(batches_to_perform)
84-
if short_video:
85-
logger.info(f"Видео собрано: {short_video}")
86-
await asyncio.to_thread(delete_frames, del_upscaled=True)
87-
logger.debug(
88-
f"Обработанные кадры удалены из батчей {start_batch}-{end_batch}"
89-
)
94+
video.build_short_video(batches_to_perform)
9095
start_batch += threads
9196

9297

@@ -104,21 +109,21 @@ async def main():
104109
logger.info(
105110
"Параметры системы:"
106111
f"\n\tОС: {my_computer.cpu_name}"
107-
f"\n\tЯдра: {my_computer.cpu_threads}"
112+
f"\n\tCPU потоки: {my_computer.cpu_threads}"
108113
f"\n\tБезопасные потоки: {my_computer.safe_cpu_threads}"
109-
f"\n\tСкорость SSD: {my_computer.ssd_speed} MB/s"
110-
f"\n\tRAM: {my_computer.ram_total} GB"
114+
f"\n\tСкорость SSD: ~{my_computer.ssd_speed} MB/s"
115+
f"\n\tRAM: ~{my_computer.ram_total} GB"
111116
f"\n\tПараметры нейронок: -j {ai_threads}"
112117
f"\n\tПуть к нейронке апскейла: {ai_realesrgan_path}"
113118
)
114119

115120
print_header("извлекаем 'сырьё' из видео...")
116-
audio = AudioHandler(threads=process_threads)
121+
audio = AudioHandler(threads=my_computer.safe_cpu_threads // 2)
117122
await clean_up(audio)
118123

119124
# запускаем извлечение аудио из видео в фоне
120125
asyncio.create_task(audio.extract_audio())
121-
extract_frames_to_batches(process_threads)
126+
extract_frames_to_batches(my_computer.safe_cpu_threads // 2)
122127
fps = await asyncio.to_thread(get_fps_accurate, ORIGINAL_VIDEO)
123128
video = VideoHandler(fps=fps)
124129
print_bottom("`сырьё` из видео извлечено")
@@ -135,11 +140,11 @@ async def main():
135140
START_BATCH_TO_UPSCALE,
136141
end_batch_to_upscale,
137142
)
138-
await asyncio.to_thread(delete_frames, del_upscaled=False)
139143
print_bottom("апскейленные короткие видео сгенерированы")
140144

141145
print_header("начало финальной сборки видео...")
142-
final_merge = video.build_final_video()
146+
total_short_videos = ceil(end_batch_to_upscale / process_threads)
147+
final_merge = await video.build_final_video(total_short_videos)
143148
logger.success(f"Общее видео собрано: {final_merge}")
144149

145150
logger.info("Добавление аудиодорожки к финальному видео")

poetry.lock

Lines changed: 1 addition & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ numpy = "^2.2.6"
1212
imageio = "^2.37.0"
1313
python-dotenv = "^1.1.1"
1414
opencv-python = "^4.12.0.88"
15-
tqdm = "^4.67.1"
16-
colorama = "^0.4.6"
1715
psutil = "^7.0.0"
1816
logging = "==0.4.9.6"
1917

src/audio/audio_handling.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ def extract_audio_sync(self) -> Optional[str]:
6868
"-b:a", self.BITRATE, "-threads", str(self.threads),
6969
"-loglevel", "error", audio_file,
7070
]
71-
7271
try:
7372
result = subprocess.run(
7473
["ffmpeg", *ffmpeg_args],
@@ -82,6 +81,7 @@ def extract_audio_sync(self) -> Optional[str]:
8281
)
8382
self.__check_audio_extracted(audio_file)
8483
self.audio_path = audio_file
84+
logger.success(f"Аудио успешно извлечено {audio_file}")
8585
except subprocess.CalledProcessError as e:
8686
logger.error(f"Ошибка извлечения аудио: {e.stderr.decode()}")
8787
raise RuntimeError(f"Ошибка извлечения аудио: {e.stderr.decode()}")
@@ -124,7 +124,7 @@ def insert_audio(self) -> None:
124124
]
125125
try:
126126
run_ffmpeg_command_with_progress(
127-
cmd, duration, desc="Добавление аудио к видео", unit="сек"
127+
cmd, duration, desc="Импортирование аудиоряда в видео", unit="сек"
128128
)
129129
except subprocess.CalledProcessError as e:
130130
logger.error(f"Ошибка при добавлении аудио: {str(e)}")

src/audio/audio_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def run_ffmpeg_command_with_progress(
4444
or progress_percent >= 100
4545
):
4646
logger.info(
47-
f"{desc}: {progress_percent:.1f}% "
48-
f"({current_time:.1f}/{duration:.1f} {unit})"
47+
f"{desc}: {current_time:.1f}/{duration:.1f}{unit} "
48+
f"({progress_percent:.1f}%)"
4949
)
5050
last_logged_progress = int(progress_percent)
5151
process.wait()

src/config/comp_params.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ def get_optimal_threads(self) -> Tuple[str, int]:
6464
Рассчитывает оптимальные параметры для -j load:proc:save и количество процессов/тредов.
6565
"""
6666
processes = self._calculate_processing_threads()
67-
load_threads = self._calculate_load_threads()
67+
save_threads = self._calculate_save_threads()
6868
proc_threads = self._calculate_proc_threads(processes)
69-
save_threads = max(1, load_threads - 1)
69+
load_threads = max(1, save_threads - 1)
7070

7171
# подбираем load:proc:save
7272
j_params = f"{load_threads}:{proc_threads}:{save_threads}"
@@ -105,15 +105,15 @@ def _estimate_ssd_speed(self) -> float:
105105
finally:
106106
delete_file(test_file)
107107

108-
def _calculate_load_threads(self) -> int:
108+
def _calculate_save_threads(self) -> int:
109109
"""Потоки для загрузки (зависит от SSD)."""
110110
if self.ssd_speed >= 2000: # NVMe
111-
return 3
111+
return 2
112112
return 2 if self.ssd_speed >= 500 else 1 # SATA SSD или HDD
113113

114114
def _calculate_proc_threads(self, processes: int) -> int:
115115
"""Потоки для обработки с учётом количества процессов."""
116-
safe_threads = max(2, self.safe_cpu_threads // max(1, processes))
116+
safe_threads = max(2, self.safe_cpu_threads // max(1, processes) - 1)
117117
return min(8, safe_threads) # Не больше 8 даже для мощных CPU
118118

119119
@staticmethod

src/config/settings.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
ORIGINAL_VIDEO = os.path.join(ROOT_DIR, "data", "input_video", "naruto_test2.mkv")
1010
AUDIO_PATH = os.path.join(ROOT_DIR, "data", "audio")
1111
TMP_VIDEO_PATH = os.path.join(ROOT_DIR, "data", "tmp_video")
12-
FINAL_VIDEO = os.path.join(ROOT_DIR, "data", "output_video", "final_video.mp4")
12+
FINAL_VIDEO = os.path.join(
13+
ROOT_DIR,
14+
"data",
15+
"output_video",
16+
f"{os.path.splitext(os.path.basename(ORIGINAL_VIDEO))[0]}_enhanced.mp4",
17+
)
1318
BATCH_VIDEO_PATH = os.path.join(ROOT_DIR, "data", "video_batches")
1419
INPUT_BATCHES_DIR = os.path.join(ROOT_DIR, "data", "default_frame_batches")
1520
OUTPUT_BATCHES_DIR = os.path.join(ROOT_DIR, "data", "upscaled_frame_batches")

src/frames/frames_helpers.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import os
2-
import sys
32
from concurrent.futures import ThreadPoolExecutor, as_completed
43

54
import cv2
6-
from tqdm import tqdm
75

86
from src.config.settings import (
97
FRAMES_PER_BATCH,
@@ -67,16 +65,14 @@ def extract_frames_to_batches(
6765
logger.info(f"Начало извлечения {total_frames} кадров из видео")
6866
logger.debug(f"Параметры: потоки={threads}, batch_size={batch_size}")
6967

70-
with tqdm(
71-
total=total_frames,
72-
desc="Извлечение фреймов",
73-
ncols=150,
74-
file=sys.stdout,
75-
) as pbar, ThreadPoolExecutor(max_workers=threads) as executor:
68+
with ThreadPoolExecutor(max_workers=threads) as executor:
7669
futures = []
7770
current_batch_dir = make_default_batch_dir(output_dir)
7871
logger.debug(f"Создан первый батч: {current_batch_dir}")
7972

73+
processed_frames = 0
74+
last_logged_percent = 0
75+
8076
for frame_num in range(1, total_frames + 1):
8177
ret, frame = cap.read()
8278
if not ret:
@@ -97,10 +93,20 @@ def extract_frames_to_batches(
9793
if len(futures) >= threads * 2:
9894
for future in as_completed(futures[:threads]):
9995
futures.remove(future)
100-
pbar.update(threads)
96+
processed_frames += threads
97+
98+
current_percent = (processed_frames * 100) // total_frames
99+
if current_percent >= last_logged_percent + 5:
100+
logger.info(
101+
f"Извлечение фреймов: {processed_frames}/{total_frames} "
102+
f"({current_percent}%)"
103+
)
104+
last_logged_percent = current_percent
101105

102106
for _ in as_completed(futures):
103-
pbar.update(1)
107+
processed_frames += 1
108+
109+
logger.info(f"Извлечение фреймов: {total_frames}/{total_frames} " f"(100%)")
104110

105111
cap.release()
106112
logger.success(f"Успешно извлечено {total_frames} кадров")

src/frames/upscale.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
import glob
33
import os
44
import subprocess
5-
import sys
5+
import time
66
from concurrent.futures import ProcessPoolExecutor
77
from pathlib import Path
8-
from tqdm import tqdm
98

109
from src.config.settings import (
1110
INPUT_BATCHES_DIR,
@@ -46,32 +45,54 @@ def delete_frames(del_upscaled: bool, del_only_dirs: bool = True):
4645

4746

4847
async def monitor_progress(
49-
total_frames: int,
50-
is_processing: list,
51-
batch_numbers: range
48+
total_frames: int, is_processing: list, batch_numbers: range
5249
) -> None:
5350
"""Мониторинг прогресса с частым обновлением и визуализацией"""
5451
processed_frames = 0
55-
with tqdm(
56-
total=total_frames,
57-
desc="Обработка батчей "
58-
f"{batch_numbers[0]}-{batch_numbers[-1]}",
59-
unit=f"фрейм",
60-
ncols=150,
61-
file=sys.stdout,
62-
) as pbar:
63-
while is_processing[0]:
64-
if processed_frames >= total_frames:
65-
break
66-
processed_frames = count_frames_in_certain_batches(
67-
OUTPUT_BATCHES_DIR, batch_numbers
68-
)
69-
pbar.n = processed_frames
70-
pbar.refresh()
71-
await asyncio.sleep(5) # Пауза для периодического обновления
72-
pbar.n = processed_frames
73-
pbar.refresh()
74-
pbar.close()
52+
start_time = time.time()
53+
54+
last_logged = 0
55+
log_every_sec = 15 # логировать каждые N секунд
56+
57+
while is_processing[0]:
58+
current_frames = count_frames_in_certain_batches(
59+
OUTPUT_BATCHES_DIR, batch_numbers
60+
)
61+
62+
# Обновляем только если количество изменилось
63+
if current_frames > processed_frames:
64+
processed_frames = current_frames
65+
progress_percent = (processed_frames / total_frames) * 100
66+
67+
# Логируем регулярно или при значительном прогрессе
68+
current_time = time.time()
69+
if (
70+
current_time - last_logged >= log_every_sec
71+
or progress_percent >= 100
72+
or processed_frames == total_frames
73+
):
74+
elapsed = current_time - start_time
75+
fps = processed_frames / elapsed if elapsed > 0 else 0
76+
remaining = (total_frames - processed_frames) / fps if fps > 0 else 0
77+
78+
logger.info(
79+
f"Апскейл фреймов: {processed_frames}/{total_frames} "
80+
f"({progress_percent:.1f}%) | "
81+
f"Прошло: {elapsed:.1f}сек | "
82+
f"Осталось: {remaining:.1f}сек"
83+
)
84+
last_logged = current_time
85+
if processed_frames >= total_frames:
86+
break
87+
await asyncio.sleep(5) # Проверяем прогресс каждые 5 секунд
88+
total_time = time.time() - start_time
89+
logger.info(
90+
f"Апскейл фреймов: {total_frames}/{total_frames} "
91+
f"(100%) | Прошло: {total_time:.1f}сек"
92+
)
93+
logger.success(
94+
f"Обработка завершена. (Средняя скорость: {total_frames / total_time:.1f} FPS)"
95+
)
7596

7697

7798
def _upscale(ai_threads: str, ai_realesrgan_path: str, batch_num: int):
@@ -116,7 +137,7 @@ async def upscale_batches(
116137

117138
logger.info(
118139
f"Начало обработки батчей {start_batch}-{end_batch} "
119-
f"(всего фреймов: {total_frames}, потоков: {process_threads})"
140+
f"(всего фреймов: {total_frames}, процессов: {process_threads})"
120141
)
121142

122143
monitor_task = asyncio.create_task(

0 commit comments

Comments
 (0)