'https://...'); */ function med_diag_api_url() { $url = get_option( 'med_diag_api_url', 'https://med.my-dpr.ru' ); return apply_filters( 'med_diag_api_url', untrailingslashit( $url ) ); } /** * Внутренний URL API-бэкенда (для проксирования через PHP). * По умолчанию — внутренний IP сервера, где стоит и WordPress, и API. * Можно переопределить фильтром: add_filter('med_diag_internal_url', fn() => 'http://10.0.0.5:18018'); */ function med_diag_internal_url() { $url = get_option( 'med_diag_internal_url', 'http://127.0.0.1:18018' ); return apply_filters( 'med_diag_internal_url', untrailingslashit( $url ) ); } /** * Режим подключения к бэкенду: * 'proxy' — браузер шлёт запрос на этот же WordPress, PHP проксирует на внутренний IP (по умолчанию); * 'direct' — браузер шлёт запрос напрямую на публичный URL API (нужен CORS на бэкенде). */ function med_diag_mode() { $mode = get_option( 'med_diag_mode', 'proxy' ); return apply_filters( 'med_diag_mode', in_array( $mode, array( 'proxy', 'direct' ), true ) ? $mode : 'proxy' ); } /** * Максимальный размер видео — как на бэкенде (Flask: MAX_CONTENT_LENGTH = 100 МБ). */ function med_diag_max_upload_bytes() { return 100 * 1024 * 1024; } /** * Ошибка в формате JSON: {"error": "..."} — как во Flask-бэкенде. * Фронтенд читает именно поле error (и video_url), а WP_Error отдаёт структуру * {code, message, data} — из-за этого текст ошибки не доходил до пользователя. */ function med_diag_json_error( $message, $status = 500, $extra = array() ) { return new WP_REST_Response( array_merge( array( 'error' => $message ), $extra ), $status ); } /* ─── Регистрация ассетов ─────────────────────────────────── */ add_action( 'wp_enqueue_scripts', 'med_diag_register_assets' ); function med_diag_register_assets() { wp_register_style( 'med-diag-css', MED_DIAG_PLUGIN_URL . 'assets/css/med-diag.css', array(), MED_DIAG_VERSION ); wp_register_script( 'med-diag-js', MED_DIAG_PLUGIN_URL . 'assets/js/med-diag.js', array(), MED_DIAG_VERSION, true // в футере ); wp_localize_script( 'med-diag-js', 'MED_DIAG', array( 'apiUrl' => med_diag_api_url(), 'proxyUrl' => rest_url( 'med-diag/v1' ), 'mode' => med_diag_mode(), ) ); } /* ─── Прокси на внутренний IP (REST) ──────────────────────── */ add_action( 'rest_api_init', 'med_diag_register_routes' ); function med_diag_register_routes() { // Анализ: браузер → этот WordPress → внутренний IP бэкенда register_rest_route( 'med-diag/v1', '/analyze', array( 'methods' => 'POST', 'permission_callback' => '__return_true', 'callback' => 'med_diag_proxy_analyze', ) ); // Скачивания (PDF/DOCX/видео): /download// register_rest_route( 'med-diag/v1', '/download/(?P[^/]+)/(?P[a-z]+)', array( 'methods' => 'GET', 'permission_callback' => '__return_true', 'callback' => 'med_diag_proxy_download', ) ); } function med_diag_proxy_analyze( $request ) { $files = $request->get_file_params(); if ( empty( $files['video'] ) ) { // Если тело запроса превысило post_max_size, PHP отбрасывает его целиком, // и $_FILES пуст — сообщаем о превышении размера (413), как это делает // бэкенд в своём обработчике 413, а не «файл не получен». $content_length = isset( $_SERVER['CONTENT_LENGTH'] ) ? (int) $_SERVER['CONTENT_LENGTH'] : 0; if ( $content_length > med_diag_max_upload_bytes() ) { return med_diag_json_error( 'Видео слишком большое (максимум 100 МБ)', 413 ); } return med_diag_json_error( 'Видео не загружено', 400 ); } $file = $files['video']; $error = isset( $file['error'] ) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE; if ( UPLOAD_ERR_INI_SIZE === $error || UPLOAD_ERR_FORM_SIZE === $error ) { return med_diag_json_error( 'Видео слишком большое (максимум 100 МБ)', 413 ); } if ( UPLOAD_ERR_OK !== $error ) { return med_diag_json_error( 'Ошибка загрузки файла (код ' . $error . ')', 400 ); } if ( isset( $file['size'] ) && (int) $file['size'] > med_diag_max_upload_bytes() ) { return med_diag_json_error( 'Видео слишком большое (максимум 100 МБ)', 413 ); } // Проверяем расширение тем же списком, что и бэкенд (иначе анализ // вернёт ошибку уже после загрузки всего файла). $allowed = array( 'mp4', 'avi', 'mov', 'mkv', 'webm', 'm4v' ); $ext = strtolower( pathinfo( isset( $file['name'] ) ? $file['name'] : '', PATHINFO_EXTENSION ) ); if ( ! in_array( $ext, $allowed, true ) ) { return med_diag_json_error( 'Неподдерживаемый формат. Разрешены: ' . implode( ', ', $allowed ), 400 ); } if ( empty( $file['tmp_name'] ) || ! is_readable( $file['tmp_name'] ) ) { return med_diag_json_error( 'Не удалось прочитать загруженный файл', 500 ); } // Имя файла только из ASCII: в multipart-заголовке не должно быть // неэкранированных кавычек/кириллицы, из-за которых multipart ломается. $filename = 'med-diag-upload.' . $ext; $content = file_get_contents( $file['tmp_name'] ); if ( false === $content || '' === $content ) { return med_diag_json_error( 'Не удалось прочитать загруженный файл', 500 ); } $boundary = wp_generate_password( 24, false ); $body = '--' . $boundary . "\r\n"; $body .= 'Content-Disposition: form-data; name="video"; filename="' . $filename . '"' . "\r\n"; $body .= 'Content-Type: ' . ( ! empty( $file['type'] ) ? $file['type'] : 'application/octet-stream' ) . "\r\n\r\n"; $body .= $content . "\r\n"; $body .= '--' . $boundary . "--\r\n"; $response = wp_remote_post( med_diag_internal_url() . '/analyze', array( 'timeout' => 300, 'headers' => array( 'Content-Type' => 'multipart/form-data; boundary=' . $boundary ), 'body' => $body, ) ); if ( is_wp_error( $response ) ) { return med_diag_json_error( 'Бэкенд недоступен: ' . $response->get_error_message(), 502 ); } $status = (int) wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); // Бэкенд (или nginx) вернул не JSON — например, HTML-страницу ошибки. // Отдаём понятную ошибку в JSON, чтобы фронтенд не падал на response.json() // с «Unexpected token '<'». if ( ! is_array( $data ) ) { return med_diag_json_error( 'Бэкенд вернул некорректный ответ (HTTP ' . $status . ')', 502 ); } // Прокидываем ошибку бэкенда вместе с video_url (ссылка на запись сканирования). if ( $status >= 400 && empty( $data['error'] ) ) { $data['error'] = 'Ошибка анализа (HTTP ' . $status . ')'; } return new WP_REST_Response( $data, $status ); } function med_diag_proxy_download( $request ) { $raw_id = (string) $request->get_param( 'id' ); $id = rawurlencode( $raw_id ); $type = $request->get_param( 'type' ); $response = wp_remote_get( med_diag_internal_url() . '/download/' . $id . '/' . $type, array( 'timeout' => 300, ) ); if ( is_wp_error( $response ) ) { return med_diag_json_error( 'Бэкенд недоступен: ' . $response->get_error_message(), 502 ); } $status = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $status ) { // Бэкенд отдаёт ошибки в JSON — прокидываем текст, если он есть. $data = json_decode( wp_remote_retrieve_body( $response ), true ); $msg = ( is_array( $data ) && ! empty( $data['error'] ) ) ? $data['error'] : 'Ошибка скачивания (HTTP ' . $status . ')'; return med_diag_json_error( $msg, $status > 0 ? $status : 502 ); } $body = wp_remote_retrieve_body( $response ); $content_type = wp_remote_retrieve_header( $response, 'content-type' ); $disposition = wp_remote_retrieve_header( $response, 'content-disposition' ); // Имя файла берём из ответа бэкенда: запись сканирования может быть .mp4 или .webm. if ( $disposition && preg_match( '/filename\*?=(?:UTF-8\'\')?"?([^";]+)"?/i', $disposition, $matches ) ) { $filename = rawurldecode( trim( $matches[1] ) ); } else { $filename = 'diagnostic_' . $raw_id . '.' . ( 'video' === $type ? 'webm' : $type ); } header( 'Content-Type: ' . ( $content_type ? $content_type : 'application/octet-stream' ) ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'Content-Length: ' . strlen( $body ) ); echo $body; exit; } /* ─── Шорткод ─────────────────────────────────────────────── */ add_shortcode( 'med_diagnostics', 'med_diag_shortcode' ); function med_diag_shortcode( $atts ) { $atts = shortcode_atts( array( 'title' => '🩺 Диагностика по видео', ), $atts, 'med_diagnostics' ); wp_enqueue_style( 'med-diag-css' ); wp_enqueue_script( 'med-diag-js' ); ob_start(); ?>

Получите оценку состояния здоровья

📷 Сканирование

Наведите камеру на лицо — мы запишем видео и проведём анализ

📋 Результат анализа

Процесс сканирования: 0%
Поместите лицо в овал целиком: лоб, глаза и подбородок — с запасом. Если голова не помещается — отодвиньте телефон
Не двигайтесь, сканирование займет до 15 секунд.
Обработка видео...
Идёт анализ, это займёт несколько секунд
'string', 'sanitize_callback' => 'esc_url_raw', 'default' => 'https://med.my-dpr.ru', ) ); register_setting( 'med_diag_settings', 'med_diag_internal_url', array( 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'default' => 'http://127.0.0.1:18018', ) ); register_setting( 'med_diag_settings', 'med_diag_mode', array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'proxy', ) ); } function med_diag_settings_page() { ?>

Med Diagnostics


Использование

Вставьте шорткод [med_diagnostics] в любую страницу или запись.

Дополнительные параметры: [med_diagnostics title="Мой заголовок"]

Важно: используйте шорткод один раз на странице.