| from matplotlib.animation import FFMpegWriter |
| import matplotlib.pyplot as plt |
| import matplotlib.animation as animation |
| import tempfile |
| import numpy as np |
| import os |
|
|
| def minutes_to_time(minutes, start_time="00:00"): |
| start_hour, start_min = map(int, start_time.split(':')) |
| total_minutes = start_hour * 60 + start_min + minutes |
| hour = (total_minutes // 60) % 24 |
| minute = total_minutes % 60 |
| return f"{hour:02d}:{minute:02d}" |
|
|
| def create_simulation_video(frames, specialists_count, second_model_name, fps=24): |
| if not frames: |
| return None |
|
|
| |
| plt.style.use('seaborn-v0_8-whitegrid') |
| fig, axes = plt.subplots(2, 2, figsize=(16, 10), facecolor='#f8f9fa') |
| plt.subplots_adjust(hspace=0.4, wspace=0.25) |
| plt.close() |
|
|
| def update(i): |
| data = frames[i] |
| for ax in axes.flatten(): |
| ax.clear() |
| ax.set_facecolor('white') |
|
|
| |
| y_inflow = data['inflow_history'] |
| axes[0, 0].fill_between(range(len(y_inflow)), y_inflow, color='#4361ee', alpha=0.3) |
| axes[0, 0].plot(range(len(y_inflow)), y_inflow, color='#4361ee', linewidth=2) |
| axes[0, 0].set_xlim(0, 1440) |
| axes[0, 0].set_title("ДИНАМИКА ПОТОКА (заявок/мин)", fontsize=12, fontweight='bold') |
| axes[0, 0].set_xlabel("Минуты симуляции") |
|
|
| |
| y_load = [v * 100 for v in data['load_history']] |
| axes[0, 1].fill_between(range(len(y_load)), y_load, color='#4cc9f0', alpha=0.3) |
| axes[0, 1].plot(range(len(y_load)), y_load, color='#4cc9f0', linewidth=2) |
| axes[0, 1].axhline(y=80, color='#f72585', linestyle='--', alpha=0.6) |
| axes[0, 1].set_xlim(0, 1440) |
| axes[0, 1].set_ylim(0, 110) |
| axes[0, 1].set_title(f"ЗАГРУЖЕННОСТЬ СПЕЦИАЛИСТОВ %: {y_load[-1]:.1f}%", fontsize=12, fontweight='bold') |
|
|
| |
| states = np.array(data['specialist_states']) |
| cols = 20 |
| rows = int(np.ceil(specialists_count / cols)) |
| z = np.zeros((rows, cols)) |
| for idx, val in enumerate(states[:rows * cols]): |
| z[idx // cols, idx % cols] = val |
|
|
| im = axes[1, 0].imshow(z, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=10) |
| axes[1, 0].set_title(f"МОНИТОРИНГ: {specialists_count} СПЕЦИАЛИСТОВ", fontsize=12, fontweight='bold') |
| axes[1, 0].axis('off') |
|
|
| |
| legend_text = "Цвета: Зеленый (Свободен) → Желтый (3-5 мин) → Красный (8+ мин)" |
| axes[1, 0].text(0.5, -0.1, legend_text, ha='center', transform=axes[1, 0].transAxes, fontsize=10) |
|
|
| |
| ax_stat = axes[1, 1] |
| ax_stat.clear() |
| ax_stat.axis('off') |
|
|
| |
| q_mod_color = '#991b1b' if data['queue'] > 50 else '#166534' |
| q_biz_color = '#991b1b' if data.get('business_queue', 0) > 50 else '#1e293b' |
|
|
| ax_stat.text(0.25, 0.9, "ОЧЕРЕДЬ\n(МОДЕЛИ)", fontsize=10, ha='center', fontweight='bold') |
| ax_stat.text(0.25, 0.78, f"{data['queue']}", fontsize=26, ha='center', fontweight='bold', color=q_mod_color) |
|
|
| ax_stat.text(0.75, 0.9, "ОЧЕРЕДЬ\n(БИЗНЕС ПРАВИЛА)", fontsize=10, ha='center', fontweight='bold') |
| ax_stat.text(0.75, 0.78, f"{data.get('business_queue', 0)}", fontsize=26, ha='center', fontweight='bold', |
| color=q_biz_color) |
|
|
| |
| cum = data['cumulative'] |
| stats_text = ( |
| f"Итоговые показатели к {data['time_str']}\n" |
| f"--------------------------------------\n" |
| f"ОБРАБОТАНО ВСЕГО: {cum['total_processed']}\n" |
| f"Авто-одобрено: {cum['auto_approved']}\n" |
| f"Авто-отказы: {cum['auto_declined']}\n" |
| f"Ручной разбор (модель): {cum['manual_processed']}\n" |
| f"Ручной разбор (бизнес правила): {cum['business_manual_processed']}\n" |
| f"--------------------------------------\n" |
| f"Используемая модель: {second_model_name}" |
| ) |
|
|
| ax_stat.text(0.5, 0.3, stats_text, fontsize=10, fontfamily='monospace', |
| ha='center', va='center', transform=ax_stat.transAxes, |
| bbox=dict(facecolor='#f8f9fa', alpha=1, boxstyle='round,pad=1', edgecolor='#dee2e6')) |
|
|
| return axes.flatten() |
|
|
| ani = animation.FuncAnimation(fig, update, frames=len(frames), interval=1000 / fps) |
| tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') |
|
|
| writer = animation.FFMpegWriter(fps=fps, bitrate=2000, extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p']) |
| ani.save(tmp_file.name, writer=writer) |
| return tmp_file.name |