Добавлены итоговый отчёт и архив лабораторной работы
This commit is contained in:
529
lab1/ARCHIVE_README.md
Normal file
529
lab1/ARCHIVE_README.md
Normal file
@@ -0,0 +1,529 @@
|
||||
# Архив лабораторной работы №1
|
||||
|
||||
**Тема:** классификация текстов на основе стилометрических характеристик.
|
||||
**Материал:** реплики персонажей игры Stardew Valley.
|
||||
**Задача:** по одной реплике определить, какому из 34 персонажей она
|
||||
принадлежит, и проверить, какие стилометрические признаки несут этот сигнал.
|
||||
|
||||
Это не авторская атрибуция: все реплики написаны одним автором. Мы различаем
|
||||
**речевые маски персонажей** — намеренно созданные автором различия в лексике,
|
||||
синтаксисе, пунктуации и разговорных маркерах.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что в архиве
|
||||
|
||||
| Путь | Что это |
|
||||
|---|---|
|
||||
| `lab1/report/REPORT.pdf` | Итоговый отчёт, 21 страница. Главный документ. |
|
||||
| `lab1/report/REPORT.docx` | Редактируемая версия отчёта. |
|
||||
| `lab1/report/REPORT.md` | Исходник отчёта в Markdown. |
|
||||
| `lab1/report/figures/` | Рисунки отчёта. |
|
||||
| `lab1/presentation/stardew_stylometry.pdf` | Презентация для защиты. |
|
||||
| `lab1/presentation/stardew_stylometry.pptx` | Редактируемая презентация. |
|
||||
| `lab1/presentation/latex/` | LaTeX-исходник и PDF презентации. |
|
||||
| `lab1/presentation/images/`, `generated/` | Исходные изображения и диаграммы. |
|
||||
| `lab1/stylometry/` | Основной код проекта. |
|
||||
| `lab1/scripts/` | Проверка окружения, запуск серий, сборка отчёта, упаковка. |
|
||||
| `lab1/tests/` | 33 теста (`pytest`). |
|
||||
| `lab1/configs/default.toml` | Конфигурация по умолчанию. |
|
||||
| `lab1/data/dialogues.csv` | Корпус: 2005 реплик, cp1251, разделитель `;`. |
|
||||
| `lab1/data/dialogues.xlsx` | Тот же корпус в Excel. |
|
||||
| `lab1/data/stats_per_character.xlsx` | Статистика по персонажам. |
|
||||
| `lab1/data/metadata/` | Метаданные персонажей для анализа ошибок. |
|
||||
| `lab1/data/corpus_report_*.txt` | Отчёты о состоянии корпуса до/после очистки. |
|
||||
| `lab1/FINAL_RESULTS.csv` | Сводная таблица финальных результатов. |
|
||||
| `lab1/FINAL_REPORT.md` | Расширенная версия отчёта с деталями экспериментов. |
|
||||
| `lab1/EXPERIMENTS.md` | Протокол экспериментов и команд. |
|
||||
| `lab1/FEATURES.md` | Признаки, формулы, обоснование. |
|
||||
| `lab1/METRICS_AND_CLASSIFIERS.md` | Метрики и классификаторы. |
|
||||
| `lab1/TUNING.md` | Настройка гиперпараметров. |
|
||||
| `lab1/RUNBOOK.md` | Подробная инструкция по запуску полной серии. |
|
||||
| `lab1/RESULTS_ANALYSIS.md` | Анализ результатов и ошибок. |
|
||||
| `lab1/FOLLOWUP_EXPERIMENTS.md` | Дополнительные эксперименты. |
|
||||
| `lab1/PRESENTATION_OUTLINE.md` | Структура презентации. |
|
||||
| `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` | Финальные результаты в компактном виде. |
|
||||
| `lab1/requirements.txt` | Основные зависимости. |
|
||||
| `lab1/requirements-report.txt` | Зависимости для пересборки отчёта. |
|
||||
|
||||
Полный список файлов с размерами и SHA-256 — в `MANIFEST.txt`.
|
||||
Контрольные суммы ключевых файлов — в `CHECKSUMS.sha256`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Требования
|
||||
|
||||
### 2.1. Обязательные
|
||||
|
||||
- Python **3.11+** (проверено на 3.14);
|
||||
- Linux, macOS или Windows с WSL;
|
||||
- около 1 ГБ свободного места с виртуальным окружением и NLTK;
|
||||
- интернет — только для установки пакетов и скачивания NLTK.
|
||||
|
||||
### 2.2. Для пересборки отчёта
|
||||
|
||||
- `pandoc` 3.x;
|
||||
- LibreOffice (`libreoffice --headless`);
|
||||
- Python-пакеты `python-docx`, `PyMuPDF` (см. `requirements-report.txt`);
|
||||
- шрифты с кириллицей: Times New Roman или Liberation Serif, Liberation Mono.
|
||||
|
||||
### 2.3. Для пересборки LaTeX-презентации
|
||||
|
||||
- XeLaTeX;
|
||||
- пакеты Beamer, tcolorbox, TikZ, fontspec, babel-russian;
|
||||
- шрифт Liberation Sans.
|
||||
|
||||
---
|
||||
|
||||
## 3. Установка
|
||||
|
||||
Все команды выполняются из каталога `lab1`:
|
||||
|
||||
```bash
|
||||
cd lab1
|
||||
```
|
||||
|
||||
### 3.1. Виртуальное окружение
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
Если окружение уже есть в корне проекта, можно использовать его:
|
||||
|
||||
```bash
|
||||
source ../.venv/bin/activate
|
||||
```
|
||||
|
||||
### 3.2. Зависимости
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Для пересборки отчёта дополнительно:
|
||||
|
||||
```bash
|
||||
python -m pip install -r requirements-report.txt
|
||||
```
|
||||
|
||||
### 3.3. Ресурсы NLTK
|
||||
|
||||
```bash
|
||||
python scripts/download_nltk_data.py
|
||||
```
|
||||
|
||||
Скрипт скачивает `punkt`, `punkt_tab`, `averaged_perceptron_tagger`,
|
||||
`averaged_perceptron_tagger_eng`, `stopwords` в `lab1/nltk_data`.
|
||||
|
||||
Без этих ресурсов POS-признаки отключатся, а результаты будут неполными.
|
||||
Если машина без интернета, скопируйте готовый каталог `nltk_data` в `lab1/`.
|
||||
|
||||
### 3.4. Проверка
|
||||
|
||||
```bash
|
||||
python scripts/check_environment.py
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
|
||||
```text
|
||||
Окружение готово.
|
||||
33 passed
|
||||
```
|
||||
|
||||
`check_environment.py` печатает версии библиотек, SHA-256 корпуса, число
|
||||
реплик, персонажей и словоупотреблений, проверяет токенизатор и POS-теггер.
|
||||
|
||||
---
|
||||
|
||||
## 4. Быстрый запуск
|
||||
|
||||
### 4.1. Аудит корпуса
|
||||
|
||||
```bash
|
||||
python -m stylometry.cli audit
|
||||
```
|
||||
|
||||
Создаёт:
|
||||
|
||||
- `data/processed/audit.md` — отчёт по корпусу;
|
||||
- `data/processed/lines.csv` — нормализованные реплики;
|
||||
- `data/processed/figures/class_distribution.png`.
|
||||
|
||||
Проверьте: 2005 реплик, 34 персонажа, 38 207 словоупотреблений, точных
|
||||
дублей нет.
|
||||
|
||||
### 4.2. Ручные признаки
|
||||
|
||||
```bash
|
||||
python -m stylometry.cli features
|
||||
```
|
||||
|
||||
Создаёт:
|
||||
|
||||
- `features/stylometric.csv` — матрица 2005 × 454;
|
||||
- `features/feature_dictionary.csv` — описание признаков.
|
||||
|
||||
### 4.3. Одна модель
|
||||
|
||||
```bash
|
||||
python -m stylometry.cli run \
|
||||
--preset stylometry_pos_lsa \
|
||||
--model logreg \
|
||||
--folds 5 \
|
||||
--repeats 5
|
||||
```
|
||||
|
||||
Результаты появятся в `results/<preset>_<model>_<timestamp>/`:
|
||||
`summary.md`, `summary.json`, `fold_metrics.csv`, `pooled_metrics.csv`,
|
||||
`per_class_report.csv`, `confusion_matrix.csv`, `oof_predictions.csv`.
|
||||
|
||||
### 4.4. Полная серия
|
||||
|
||||
```bash
|
||||
bash scripts/run_all_experiments.sh
|
||||
```
|
||||
|
||||
Скрипт последовательно запускает все стадии и пишет лог в
|
||||
`results/logs/run_all_<timestamp>.log`. Ориентировочное время — 1,5–3 часа
|
||||
на обычном ноутбуке. Быстрый режим без финальных чисел:
|
||||
|
||||
```bash
|
||||
QUICK=1 bash scripts/run_all_experiments.sh
|
||||
```
|
||||
|
||||
### 4.5. Финальные серии
|
||||
|
||||
```bash
|
||||
python scripts/run_final_experiments.py \
|
||||
--stages nested topic calibration ensemble extra \
|
||||
--out results/final
|
||||
```
|
||||
|
||||
Эта команда воспроизводит вложенный выбор конфигурации, контроль содержания,
|
||||
коррекцию приоров, ансамбли и дополнительные конфигурации. Именно эти
|
||||
результаты сведены в `FINAL_RESULTS.csv` и в отчёте.
|
||||
|
||||
---
|
||||
|
||||
## 5. Что делает каждый модуль
|
||||
|
||||
| Модуль | Назначение |
|
||||
|---|---|
|
||||
| `stylometry/io.py` | Чтение CSV/XLSX, проверка колонок, SHA-256 корпуса. |
|
||||
| `stylometry/cleaning.py` | Unicode-нормализация, плейсхолдеры, пробелы. |
|
||||
| `stylometry/tokenization.py` | NLTK: токенизация, предложения, POS; резервный режим. |
|
||||
| `stylometry/features/` | 454 ручных признака в 11 группах. |
|
||||
| `stylometry/lsa.py` | TF-IDF + TruncatedSVD для word/content/fw/char/pos. |
|
||||
| `stylometry/pipelines.py` | Объединение блоков признаков, модели, сетки параметров. |
|
||||
| `stylometry/cv.py` | Повторная стратифицированная и вложенная кросс-валидация. |
|
||||
| `stylometry/evaluation.py` | Macro-F1, accuracy, balanced accuracy, MCC, каппа, top-k, log loss, Brier, бутстрэп. |
|
||||
| `stylometry/stats.py` | Мак-Немар, Уилкоксон, Фридман, поправка Холма. |
|
||||
| `stylometry/delta.py` | Дельта Барроуза: MFW, z-нормирование, манхэттенское и косинусное расстояние. |
|
||||
| `stylometry/verification.py` | Верификация «свой/чужой»: ROC-AUC, EER, TAR@FAR. |
|
||||
| `stylometry/clustering.py` | Иерархическая и k-means кластеризация, ARI, NMI, силуэт. |
|
||||
| `stylometry/plotting.py` | Диаграммы классов, scree, матрицы ошибок, PCA, t-SNE. |
|
||||
| `stylometry/metadata.py` | Метаданные персонажей, анализ подгрупп. |
|
||||
| `stylometry/cli.py` | Единая точка входа для всех команд. |
|
||||
| `main.py` | Короткая обёртка над `stylometry.cli`. |
|
||||
|
||||
### Группы ручных признаков
|
||||
|
||||
1. Длина.
|
||||
2. Лексика.
|
||||
3. Словарное богатство.
|
||||
4. Символы.
|
||||
5. Пунктуация.
|
||||
6. Функциональные слова.
|
||||
7. Местоимения.
|
||||
8. Части речи.
|
||||
9. Синтаксис.
|
||||
10. Читаемость.
|
||||
11. Разговорные маркеры.
|
||||
|
||||
Формулы и обоснование — в `FEATURES.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Данные и разметка
|
||||
|
||||
### 6.1. Формат
|
||||
|
||||
`data/dialogues.csv`:
|
||||
|
||||
- кодировка cp1251;
|
||||
- разделитель `;`;
|
||||
- колонки `Реплика` и `Персонаж`;
|
||||
- 2005 строк, 34 класса.
|
||||
|
||||
`data/dialogues.xlsx` — тот же корпус для Excel.
|
||||
`data/stats_per_character.xlsx` — число реплик и длины по персонажам.
|
||||
`data/metadata/` — пол, возраст, вид и роль для анализа ошибок; в признаки
|
||||
классификатора эти поля не подаются.
|
||||
|
||||
### 6.2. Правила разметки
|
||||
|
||||
1. Одна реплика — один объект.
|
||||
2. Метка — имя говорящего.
|
||||
3. Один объект относится ровно к одному классу.
|
||||
4. Соответствие реплики и метки проверено вручную.
|
||||
5. Служебные метки `<PLAYER>`, `<FARM>`, `<PET>`, `<SPOUSE>`, `<TIME>`,
|
||||
`<NOUN>`, `<NAME>` заменены нейтральными словами; их число сохранено
|
||||
отдельным признаком.
|
||||
|
||||
### 6.3. Очистка и шум
|
||||
|
||||
Нормализуется только Unicode: варианты кавычек, тире, многоточий и пробелов.
|
||||
Пунктуация, регистр и стоп-слова **сохраняются** — это стилометрический
|
||||
сигнал.
|
||||
|
||||
Шумом считались точные дубликаты, технические артефакты, общие короткие
|
||||
формулы и плейсхолдеры. Корпус прошёл ручную фильтрацию; точные правила
|
||||
фильтрации не сохранились, поэтому исходное распределение восстановить
|
||||
нельзя. Это ограничение зафиксировано в отчёте.
|
||||
|
||||
### 6.4. Ограничение по объёму
|
||||
|
||||
Формальные требования: не менее 90 текстов по 30 предложений и примерно
|
||||
равные классы. В этом корпусе единица анализа — реплика (в среднем 19 слов),
|
||||
классы неравномерны (15–139 реплик). По согласованию с преподавателем
|
||||
эксперимент выполнен на уровне реплик; в отчёте раздел 2.5 честно описывает
|
||||
это отклонение. Для компенсации используются веса классов и macro-F1.
|
||||
|
||||
---
|
||||
|
||||
## 7. Метрики и результаты
|
||||
|
||||
Основная метрика — **macro-F1**: средняя F-мера по 34 классам, каждый
|
||||
персонаж весит одинаково. Дополнительно считаются accuracy, balanced
|
||||
accuracy, top-3/top-5, MCC, каппа Коэна, log loss, Brier, матрица ошибок,
|
||||
бутстрэп-интервалы.
|
||||
|
||||
| Конфигурация | Macro-F1 |
|
||||
|---|---:|
|
||||
| Мягкое голосование трёх моделей | **0,3036 ± 0,0189** |
|
||||
| ExtraTrees, `char_max_features=10000` | 0,2757 ± 0,0219 |
|
||||
| Вложенный выбор конфигурации | 0,2746 ± 0,0233 |
|
||||
| ExtraTrees, полный набор | 0,2499 ± 0,0209 |
|
||||
| Логистическая регрессия, lean + content-LSA | 0,2466 ± 0,0191 |
|
||||
| Случайный лес, `char_max_features=10000` | 0,2583 ± 0,0066 |
|
||||
| Только ручные признаки | 0,1051 ± 0,0038 |
|
||||
| Случайный уровень | 0,029 |
|
||||
|
||||
На отложенной выборке 80/20 мягкое голосование дало 0,3333.
|
||||
|
||||
Контроль содержания: после замены существительных, глаголов, прилагательных,
|
||||
наречий и числительных маркером `CONTENT` macro-F1 падает с 0,2483 до 0,1129.
|
||||
Остаток всё ещё выше случайного уровня, но основной сигнал —
|
||||
лексико-тематический.
|
||||
|
||||
---
|
||||
|
||||
## 8. Как пересобрать отчёт
|
||||
|
||||
Отчёт собирается из `report/REPORT.md`:
|
||||
|
||||
```bash
|
||||
cd lab1
|
||||
python scripts/build_report.py
|
||||
```
|
||||
|
||||
Скрипт:
|
||||
|
||||
1. вызывает `pandoc` и делает `report/build/REPORT_raw.docx`;
|
||||
2. оформляет стили, поля, колонтитулы, таблицы и рисунки;
|
||||
3. сохраняет `report/REPORT.docx`;
|
||||
4. конвертирует DOCX в `report/REPORT.pdf` через LibreOffice;
|
||||
5. проверяет, что в PDF есть все основные разделы.
|
||||
|
||||
Только DOCX:
|
||||
|
||||
```bash
|
||||
python scripts/build_report.py --skip-pdf
|
||||
```
|
||||
|
||||
Требования: `pandoc`, `libreoffice`, `python-docx`, `PyMuPDF`.
|
||||
Рисунки лежат в `report/figures/`; Markdown ссылается на них относительно
|
||||
`report/`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Как пересобрать презентацию
|
||||
|
||||
### 9.1. PPTX
|
||||
|
||||
В архиве лежит **финальная** версия `stardew_stylometry.pptx` (30 слайдов)
|
||||
после ручных правок. Скрипт `build_deck.py` — генератор исходной версии
|
||||
презентации; он создаёт 32 слайда и **перезапишет** финальный файл, если его
|
||||
запустить. Не запускайте скрипт, если хотите сохранить ручные правки: для
|
||||
изменений открывайте PPTX в PowerPoint или LibreOffice.
|
||||
|
||||
Если нужно пересобрать исходную версию с нуля:
|
||||
|
||||
```bash
|
||||
cd lab1/presentation
|
||||
python build_deck.py
|
||||
```
|
||||
|
||||
Скрипт использует `python-pptx`, `Pillow` и `matplotlib`. Диаграммы уже лежат
|
||||
в `presentation/generated/`; для их перегенерации нужны результаты серий
|
||||
(`results/final/...`), которых нет в архиве — сначала запустите эксперименты
|
||||
из раздела 4.
|
||||
|
||||
### 9.2. LaTeX/PDF
|
||||
|
||||
```bash
|
||||
cd lab1/presentation/latex
|
||||
xelatex stardew_stylometry.tex
|
||||
xelatex stardew_stylometry.tex
|
||||
```
|
||||
|
||||
Нужны XeLaTeX, Beamer, tcolorbox, TikZ и шрифт Liberation Sans. Готовый PDF —
|
||||
`stardew_stylometry.pdf`. Изображения подтягиваются из `../images/` и
|
||||
`../generated/`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Где смотреть результаты
|
||||
|
||||
- `lab1/report/REPORT.pdf` — итоговый отчёт со всеми числами и выводами.
|
||||
- `lab1/FINAL_RESULTS.csv` — сводная таблица всех финальных конфигураций.
|
||||
- `lab1/FINAL_REPORT.md` — расширенное описание экспериментов.
|
||||
- `lab1/RESULTS_ANALYSIS.md` — анализ ошибок, классов-аттракторов и длины.
|
||||
- `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` — исходные
|
||||
`summary.json`, `fold_metrics.csv`, `confusion_matrix.csv` и предсказания
|
||||
финальных серий.
|
||||
- `lab1/presentation/generated/` — диаграммы для отчёта и презентации.
|
||||
|
||||
---
|
||||
|
||||
## 11. Частые проблемы
|
||||
|
||||
### `check_environment.py` не находит POS-теггер
|
||||
|
||||
Повторите:
|
||||
|
||||
```bash
|
||||
python scripts/download_nltk_data.py
|
||||
```
|
||||
|
||||
Проверьте, что каталог `lab1/nltk_data` существует и не пуст. Если сеть
|
||||
недоступна, скопируйте `nltk_data` с другой машины.
|
||||
|
||||
### `pytest` не устанавливается
|
||||
|
||||
Убедитесь, что активировано виртуальное окружение и версия Python не ниже
|
||||
3.11:
|
||||
|
||||
```bash
|
||||
python --version
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### `python -m stylometry.cli` не находит модуль
|
||||
|
||||
Команда должна выполняться из каталога `lab1`, а не из корня репозитория:
|
||||
|
||||
```bash
|
||||
cd lab1
|
||||
python -m stylometry.cli audit
|
||||
```
|
||||
|
||||
### LibreOffice не собирает PDF
|
||||
|
||||
Проверьте:
|
||||
|
||||
```bash
|
||||
libreoffice --version
|
||||
```
|
||||
|
||||
На сервере без графической сессии используйте именно `--headless`
|
||||
(это уже делает `build_report.py`). Если LibreOffice нет, отчёт можно
|
||||
открыть в Word/LibreOffice вручную и экспортировать в PDF.
|
||||
|
||||
### Кириллица в PDF выглядит квадратами
|
||||
|
||||
Установите шрифты Liberation Serif/Sans и Liberation Mono. В отчёте
|
||||
используются Times New Roman (с запасным Liberation Serif) и Liberation Mono.
|
||||
|
||||
### Полная серия идёт слишком долго
|
||||
|
||||
Запускайте в `tmux` или `nohup`, см. `RUNBOOK.md`, раздел 5.4. Для проверки
|
||||
работоспособности используйте `QUICK=1`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Что исключено из архива
|
||||
|
||||
Чтобы архив оставался компактным и воспроизводимым, в него не включены:
|
||||
|
||||
- `.venv/` — виртуальное окружение;
|
||||
- `.git/` — история репозитория;
|
||||
- `nltk_data/` — скачивается скриптом;
|
||||
- `data/processed/` — аудит и промежуточные таблицы;
|
||||
- `features/` — матрица признаков (создаётся `features`);
|
||||
- `results/` — промежуточные результаты серий;
|
||||
- большие `exports/*.tar.gz` — кроме компактного финального архива;
|
||||
- `presentation/preview/` — PNG-превью слайдов;
|
||||
- `__pycache__/`, `.pytest_cache/` — кэши;
|
||||
- `presentation/stardew_stylometry.bak` — резервная копия;
|
||||
- служебные файлы LaTeX (`.aux`, `.log`, `.nav`, `.out`, `.snm`, `.toc`).
|
||||
|
||||
Всё это восстанавливается командами из разделов 3–4.
|
||||
|
||||
---
|
||||
|
||||
## 13. Проверка целостности архива
|
||||
|
||||
```bash
|
||||
sha256sum -c CHECKSUMS.sha256
|
||||
```
|
||||
|
||||
`MANIFEST.txt` содержит список всех файлов архива с размерами и SHA-256.
|
||||
Если контрольные суммы совпали, архив распакован полностью.
|
||||
|
||||
Минимальная проверка после распаковки:
|
||||
|
||||
```bash
|
||||
cd lab1
|
||||
python scripts/check_environment.py
|
||||
python -m pytest -q
|
||||
python -m stylometry.cli audit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Атрибуция
|
||||
|
||||
Диалоги Stardew Valley принадлежат ConcernedApe. Корпус используется
|
||||
исключительно в учебных и исследовательских целях. Код проекта можно
|
||||
использовать и изменять для учебных задач.
|
||||
|
||||
---
|
||||
|
||||
## 15. Шпаргалка команд
|
||||
|
||||
```bash
|
||||
# из lab1
|
||||
python scripts/check_environment.py # проверить окружение
|
||||
python -m pytest -q # 33 теста
|
||||
python -m stylometry.cli audit # аудит корпуса
|
||||
python -m stylometry.cli features # 454 ручных признака
|
||||
python -m stylometry.cli run \
|
||||
--preset stylometry_pos_lsa --model logreg --repeats 5
|
||||
bash scripts/run_all_experiments.sh # полная серия
|
||||
python scripts/run_final_experiments.py \
|
||||
--stages nested topic calibration ensemble extra --out results/final
|
||||
python scripts/build_report.py # отчёт DOCX + PDF
|
||||
```
|
||||
Reference in New Issue
Block a user