From c1ee2f82e46c899284a9934db62717d0b7b9d4e1 Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Fri, 19 Jun 2026 16:29:52 +0300 Subject: [PATCH] add ACC (GT3/GT4) support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - game_of() + load_prompt() with acc→f1 fallback in report_common - technique/setup/race: skip ERS/setup sections for ACC; compare: routes prompt - --game {auto,f1,acc} CLI flag - 8 ACC prompt files (ru/en × 4 modes) - GUI: ttk.Notebook F1 22/ACC tabs, isolated state, game-mismatch warning - rename: normalise ACC full-word events to compact tokens (Practice→P, Qualifying→Q, Race→R, Hotlap→HL, Hotstint→HS) - tests: ACC fixture + 9 new tests (87 total); all F1 regressions green - docs: README + ARCHITECTURE bilingual updates --- ARCHITECTURE.md | 15 +- ARCHITECTURE.ru.md | 15 +- README.md | 27 +++- README.ru.md | 27 +++- prompts/en/acc/compare.md | 62 ++++++++ prompts/en/acc/race.md | 92 ++++++++++++ prompts/en/acc/setup.md | 80 ++++++++++ prompts/en/acc/technique.md | 92 ++++++++++++ prompts/ru/acc/compare.md | 64 ++++++++ prompts/ru/acc/race.md | 95 ++++++++++++ prompts/ru/acc/setup.md | 80 ++++++++++ prompts/ru/acc/technique.md | 92 ++++++++++++ telemetry/__main__.py | 18 ++- telemetry/gui.py | 275 +++++++++++++++++++++------------- telemetry/rename.py | 46 +++++- telemetry/report_common.py | 25 +++- telemetry/report_compare.py | 8 +- telemetry/report_race.py | 12 +- telemetry/report_setup.py | 17 ++- telemetry/report_technique.py | 26 ++-- tests/conftest.py | 10 ++ tests/fixtures/acc_lap.csv | 40 +++++ tests/test_rename.py | 47 +++++- tests/test_reports.py | 55 +++++++ 24 files changed, 1156 insertions(+), 164 deletions(-) create mode 100644 prompts/en/acc/compare.md create mode 100644 prompts/en/acc/race.md create mode 100644 prompts/en/acc/setup.md create mode 100644 prompts/en/acc/technique.md create mode 100644 prompts/ru/acc/compare.md create mode 100644 prompts/ru/acc/race.md create mode 100644 prompts/ru/acc/setup.md create mode 100644 prompts/ru/acc/technique.md create mode 100644 tests/fixtures/acc_lap.csv diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index caec867..1ca752f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ # Architecture -A telemetry CSV becomes a Markdown report through a small linear pipeline. Everything is stdlib Python; there is no framework and no global state. +A telemetry CSV (F1 22 or ACC) becomes a Markdown report through a small linear pipeline. Everything is stdlib Python; there is no framework and no global state. ## Files @@ -13,15 +13,16 @@ telemetry/ segments.py Lap -> corners & straights. Zone detection + numbering. corner_metrics.py Per-zone number crunching: braking point, lock/spin, coast, temps. resample.py Adaptive distance grid + channel interpolation for the trace. - report_common.py Header block, legend strings, md_table, token_estimate, load_prompt, atomic write. + report_common.py Header block, legend strings, md_table, token_estimate, game_of, load_prompt, atomic write. report_technique.py Driving report (corners, straights, cost, trace). report_setup.py Car report (setup, balance, tyres, brakes, suspension, phases). report_compare.py Two+ laps side by side, per-corner and cumulative delta. report_race.py A folder of laps: lap times (with position), stints, wear, thermals, ERS. Dedups duplicate lap numbers. report_profile.py A folder of laps: cheap cross-lap corner tendencies (median per corner, per dominant compound) + auto-picked deep-dive laps. No trace. rename.py Insert session type + lap number (P1_L7) into filenames; with races_dir, sort into races/// and recurse subdirectories. - gui.py tkinter window; per-mode browse memory; runs generation/rename on a worker thread. + gui.py tkinter window with two tabs (F1 22 / ACC); per-mode browse memory; runs generation/rename on a worker thread. Warns (non-blocking) when the detected game doesn't match the active tab. prompts/ LLM analysis prompts, ru/ and en/ subdirectories, one file per mode. Auto-appended to every report. + ACC-specific overrides live in prompts//acc/.md; missing files fall back to the F1 prompt. tests/ pytest suite + fixture laps. ``` @@ -31,7 +32,7 @@ tests/ pytest suite + fixture laps. 2. **`parser.load_lap`** reads the file as UTF-8 and splits it into four meta blocks (session, track, setup) plus the 108-channel telemetry table. Channel names are normalised by stripping the ` [unit]` suffix. Every column that is entirely zero is dropped — the game writes many dead channels (see Gotchas). 3. **`segments.detect_corners`** walks the samples, marks each as "cornering" when `|Steer| > STEER_ON` or `Brake > BRAKE_ON`, merges adjacent zones closer than `MERGE_GAP_M`, drops zones shorter than `MIN_LEN_M`, and numbers what's left T1..Tn by distance. For each zone it calls **`corner_metrics.corner_from_zone`**, which extracts the braking point, apex, gear, full-throttle point, peak brake/gLat, relative lock%/spin%, coast distance, and rear tyre temperature peak. `straights` fills the gaps between corners. 4. **`resample.adaptive_points`** builds a distance grid that is dense inside (and on approach to) corners and sparse on straights — the step sizes default to the lap's own median sample spacing via `auto_steps`, so denser raw telemetry yields a denser trace. **`sample_at`** interpolates each channel onto that grid (nearest-sample for discrete channels like Gear/DRS). -5. The **`report_.generate(…, lang, include_prompt=True)`** function assembles Markdown from `report_common` helpers (`header_block`, `md_table`, `legend`) and the data above. When `include_prompt` is set it calls **`load_prompt(mode, lang)`** to read the matching prompt from `prompts//.md` and appends it after a `---` separator (CLI `--no-prompt` / GUI checkbox turn this off). Finally **`write_report`** writes atomically (temp file + `os.replace`) and returns `(abs_path, tokens)`. Before generating, `__main__` skips the run via **`is_fresh(report, sources)`** when an up-to-date report exists (override with `--force`). +5. The **`report_.generate(…, lang, include_prompt=True)`** function assembles Markdown from `report_common` helpers (`header_block`, `md_table`, `legend`) and the data above. **`game_of(lap)`** inspects the `Game` session field and returns `'acc'` (if the field starts with `"ACC"`) or `'f1'`; the report generators use this to suppress DRS/ERS/hybrid sections for ACC and to replace the setup parameter table with a note when all setup values are zero (as ACC always exports). When `include_prompt` is set it calls **`load_prompt(mode, lang, game)`** which first tries `prompts//acc/.md` for ACC and falls back to `prompts//.md` if absent, then appends the result after a `---` separator (CLI `--no-prompt` / GUI checkbox turn this off). Finally **`write_report`** writes atomically (temp file + `os.replace`) and returns `(abs_path, tokens)`. Before generating, `__main__` skips the run via **`is_fresh(report, sources)`** when an up-to-date report exists (override with `--force`). 6. **`__main__`** prints that path and `~N tokens (budget 60k)`. The **GUI** shows the same tuple in its log instead of printing. ## Adding things @@ -74,3 +75,9 @@ If a future track needs a different corner grouping, `merge_gap_for` already ada **Rename with `races_dir` sorts into folders and walks subdirectories.** `rename_unprocessed(targets, races_dir)` when `races_dir` is set: (1) recursively scans all subdirectories via `os.walk`, (2) reads metadata from each CSV (`read_metadata` → lap, event, track), (3) inserts tokens if absent, (4) creates `races//Practice|Qualifying|Race|Sprint/` and moves the file there, (5) even already-renamed files are moved if they're in the wrong folder, (6) only skips files that are already in the right place with the right name. Without `races_dir` it works as before — renames in-place in the original folder. **Reports go next to the input, not next to the program.** `report_path` derives the `reports/` directory from the *input CSV's* folder. `race` mode puts the report inside the race session folder (`/reports/`). Moving the tool does not move where reports land. + +**Game is derived from the `Game` metadata field and threaded through report generation.** `game_of(lap)` reads `lap.session['Game']`; the field starts with `"ACC"` for Assetto Corsa Competizione and is something else for F1 22. Every report generator receives the lap and calls `game_of` to decide which sections to emit. The CLI `--game` flag can override detection by patching the session field before dispatch. + +**ACC setup is all zeros and is intentionally not rendered as a parameter table.** ACC's CSV export always writes zeros for every setup field. `report_setup` checks whether all setup values are zero and, when true, replaces the parameter table with a single explanatory note rather than printing a table of meaningless zeros. + +**ACC session events use full words; F1 22 uses short codes.** F1 22 writes session codes like `P1`, `Q1`, `R` directly. ACC writes `Practice`, `Qualifying`, `Race`. The rename module normalises ACC full-word events to compact canonical codes (Practice→P, Qualifying→Q, Race→R) before inserting them into the filename and choosing the destination folder. The inserted token is the same compact code that the session-pattern recogniser expects, so re-running rename on already-processed ACC files is idempotent (they are skipped, not double-tagged). diff --git a/ARCHITECTURE.ru.md b/ARCHITECTURE.ru.md index 336933d..007c59d 100644 --- a/ARCHITECTURE.ru.md +++ b/ARCHITECTURE.ru.md @@ -2,7 +2,7 @@ # Архитектура -CSV-телеметрия превращается в Markdown-отчёт через короткий линейный конвейер. Всё на стандартной библиотеке Python; ни фреймворка, ни глобального состояния. +CSV-телеметрия (F1 22 или ACC) превращается в Markdown-отчёт через короткий линейный конвейер. Всё на стандартной библиотеке Python; ни фреймворка, ни глобального состояния. ## Файлы @@ -13,15 +13,16 @@ telemetry/ segments.py Lap -> повороты и прямые. Детекция зон + нумерация. corner_metrics.py Подсчёт по зоне: точка торможения, lock/spin, накат, температуры. resample.py Адаптивная сетка по дистанции + интерполяция каналов для трассы. - report_common.py Шапка, строки легенды, md_table, token_estimate, load_prompt, атомарная запись. + report_common.py Шапка, строки легенды, md_table, token_estimate, game_of, load_prompt, атомарная запись. report_technique.py Отчёт о пилотировании (повороты, прямые, расход, трасса). report_setup.py Отчёт о машине (сетап, баланс, шины, тормоза, подвеска, фазы). report_compare.py Два+ круга рядом, дельта по поворотам и накопленная. report_race.py Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS. Дедуп дублей номеров кругов. report_profile.py Папка кругов: дешёвые тенденции по поворотам (медиана на поворот, по доминирующему составу) + авто-выбор кругов для разбора. Без трассы. rename.py Вставка типа сессии + номера круга (P1_L7) в имя; с races_dir — сортировка в races/// и рекурсия по подпапкам. - gui.py Окно tkinter; память последней папки по режиму; генерация/переименование в потоке. + gui.py Окно tkinter с двумя вкладками (F1 22 / ACC); память последней папки по режиму; генерация/переименование в потоке. Предупреждает (без блокировки), если определённая игра не совпадает с активной вкладкой. prompts/ Промпты LLM, подпапки ru/ и en/, по одному файлу на режим. Добавляются в конец каждого отчёта. + Переопределения для ACC лежат в prompts//acc/.md; при отсутствии — откат к F1-промпту. tests/ Набор pytest + фикстуры-круги. ``` @@ -31,7 +32,7 @@ tests/ Набор pytest + фикстуры-круги. 2. **`parser.load_lap`** читает файл как UTF-8 и делит на четыре мета-блока (session, track, setup) плюс таблицу телеметрии на 108 каналов. Имена каналов нормализуются срезанием суффикса ` [unit]`. Каждая колонка, целиком состоящая из нулей, отбрасывается — игра пишет много мёртвых каналов (см. «Подводные камни»). 3. **`segments.detect_corners`** проходит по сэмплам, помечает сэмпл как «в повороте», если `|Steer| > STEER_ON` или `Brake > BRAKE_ON`, сливает соседние зоны с зазором меньше `MERGE_GAP_M`, отбрасывает зоны короче `MIN_LEN_M` и нумерует оставшиеся T1..Tn по дистанции. Для каждой зоны вызывается **`corner_metrics.corner_from_zone`**, которая извлекает точку торможения, апекс, передачу, точку полного газа, пик тормоза/gLat, относительные lock%/spin%, дистанцию наката и пик температуры задней шины. `straights` заполняет промежутки между поворотами. 4. **`resample.adaptive_points`** строит сетку по дистанции — плотную внутри поворотов (и на подходе) и редкую на прямых; шаги по умолчанию берутся из медианного интервала сэмплов круга через `auto_steps`, так что более плотная сырая телеметрия даёт более плотную трассу. **`sample_at`** интерполирует каждый канал на эту сетку (ближайший сэмпл для дискретных вроде Gear/DRS). -5. Функция **`report_<режим>.generate(…, lang, include_prompt=True)`** собирает Markdown из помощников `report_common` (`header_block`, `md_table`, `legend`) и данных выше. При `include_prompt` вызывает **`load_prompt(mode, lang)`**, которая читает промпт из `prompts//.md` и добавляет его после разделителя `---` (CLI `--no-prompt` / чекбокс GUI это отключают). Финальный текст пишется через **`write_report`** атомарно (временный файл + `os.replace`), возвращается `(abs_path, tokens)`. Перед генерацией `__main__` пропускает прогон через **`is_fresh(report, sources)`**, если свежий отчёт уже есть (override — `--force`). +5. Функция **`report_<режим>.generate(…, lang, include_prompt=True)`** собирает Markdown из помощников `report_common` (`header_block`, `md_table`, `legend`) и данных выше. **`game_of(lap)`** читает поле `Game` из сессии и возвращает `'acc'` (если значение начинается с `"ACC"`) или `'f1'`; генераторы отчётов используют это, чтобы убрать разделы DRS/ERS/гибрид для ACC и заменить таблицу сетапа пометкой, когда все значения нулевые (ACC всегда так экспортирует). При `include_prompt` вызывает **`load_prompt(mode, lang, game)`**, которая для ACC сначала ищет `prompts//acc/.md` и откатывается к `prompts//.md` при его отсутствии, затем добавляет результат после разделителя `---` (CLI `--no-prompt` / чекбокс GUI это отключают). Финальный текст пишется через **`write_report`** атомарно (временный файл + `os.replace`), возвращается `(abs_path, tokens)`. Перед генерацией `__main__` пропускает прогон через **`is_fresh(report, sources)`**, если свежий отчёт уже есть (override — `--force`). 6. **`__main__`** печатает путь и `~N tokens (budget 60k)`. **GUI** показывает тот же кортеж в логе вместо печати. ## Как добавлять @@ -74,3 +75,9 @@ tests/ Набор pytest + фикстуры-круги. **Rename с `races_dir` распределяет по папкам и рекурсит.** `rename_unprocessed(targets, races_dir)` когда `races_dir` установлен: (1) сканирует всё дерево подпапок (`os.walk`), (2) читает метаданные из CSV (`read_metadata` → lap, event, track), (3) вставляет токены если их нет, (4) создаёт `races//Practice|Qualifying|Race|Sprint/` и перемещает туда файл, (5) даже уже переименованные файлы переносятся если лежат не там где надо, (6) пропускаются только файлы, которые уже находятся в нужной папке с нужным именем. Без `races_dir` работает как раньше — переименовывает в исходной папке. **Отчёты выходят рядом с входом, а не рядом с программой.** `report_path` выводит папку `reports/` из папки *входного CSV*. Режим `race` кладёт отчёт внутрь папки сессии (`/reports/`). Перемещение инструмента не меняет, куда попадают отчёты. + +**Игра определяется из поля `Game` в метаданных и передаётся сквозь всю генерацию отчёта.** `game_of(lap)` читает `lap.session['Game']`; для Assetto Corsa Competizione поле начинается с `"ACC"`, для F1 22 — иначе. Каждый генератор отчёта вызывает `game_of`, чтобы решить, какие секции выводить. Флаг CLI `--game` позволяет переопределить определение, патча поле сессии до диспатча. + +**Сетап ACC — всегда нули, и таблица параметров намеренно не рендерится.** CSV-экспорт ACC записывает нули во все поля сетапа. `report_setup` проверяет, все ли значения нулевые, и, если да, заменяет таблицу параметров поясняющей пометкой вместо вывода бессмысленных нулей. + +**События сессии в ACC — полные слова; в F1 22 — короткие коды.** F1 22 пишет коды вроде `P1`, `Q1`, `R`. ACC пишет `Practice`, `Qualifying`, `Race`. Модуль переименования нормализует полные слова ACC до компактных канонических кодов (Practice→P, Qualifying→Q, Race→R) перед вставкой в имя файла и выбором папки назначения. Вставляемый токен совпадает с тем, что ожидает распознаватель паттерна сессии, поэтому повторный запуск rename на уже обработанных файлах ACC идемпотентен (они пропускаются, а не получают двойной тег). diff --git a/README.md b/README.md index ae91235..1e94f49 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [Русский](README.ru.md) -# F1 Telemetry Analyzer +# F1 22 / ACC Telemetry Analyzer -Turns raw F1 22 lap telemetry into compact Markdown reports you can paste into an LLM for analysis. A full raw lap is ~1M tokens; these reports are 1–35k. +Turns raw F1 22 and Assetto Corsa Competizione lap telemetry into compact Markdown reports you can paste into an LLM for analysis. A full raw lap is ~1M tokens; these reports are 1–35k. ```bash # from the tool/ folder @@ -21,23 +21,31 @@ Download or clone the repository. Keep your telemetry CSVs in the parent folder ## What it does -Six modes. Five produce reports, one renames files. +Six modes. Five produce reports, one renames files. Both F1 22 and ACC (GT3/GT4) CSVs are supported — the game is detected automatically from each file's metadata. | Mode | Input | Use it to analyse | |------|-------|-------------------| | `technique` | one lap | **how** you drive — braking points, apex speeds, throttle application, lockups, resource cost | | `setup` | one lap | **what** is set — balance (US/OS), tyre temps, brakes, suspension rake & roll | | `compare` | two+ laps | **where** one lap is faster — per-corner and cumulative delta, plus a deterministic `Verdict` (fuel-corrected) | -| `race` | a folder of laps | the whole race — pace, stint degradation, tyre wear, ERS per lap | +| `race` | a folder of laps | the whole race — pace, stint degradation, tyre wear, ERS per lap (F1) | | `profile` | a folder of laps | **cheap** (~400 tokens) cross-lap driver tendencies per tyre compound (median coast/lock/spin per corner, flagged relative to the session) + auto-picked laps to deep-dive. Track-agnostic: corners are clustered by apex distance, not index | | `rename` | files/folder | insert session type and lap number into each filename (`..._74.074_...` → `..._P1_L7_74.074_...`) | Reports include a legend explaining every column and have the analysis prompt auto-appended at the end — just drop the file into an LLM. Prompts come in RU and EN; pick with `--lang en` on the CLI or the language radio in the GUI. -**Flags:** `--no-prompt` omits the embedded analysis prompt (smaller files for agent pipelines; in the GUI it's the **Include prompt** checkbox). `--force` regenerates even when an up-to-date report already exists (runs are otherwise skipped as `cached`). +**Flags:** `--no-prompt` omits the embedded analysis prompt (smaller files for agent pipelines; in the GUI it's the **Include prompt** checkbox). `--force` regenerates even when an up-to-date report already exists (runs are otherwise skipped as `cached`). `--game {auto,f1,acc}` overrides the auto-detected game (default `auto`). > Typical optimized flow: run `profile` first to see chronic tendencies and which laps matter, then generate `technique`/`setup`/`compare` only for those laps with `--no-prompt`. +### ACC vs F1 22 differences + +GT3/GT4 cars have no hybrid system, and ACC does not export the car setup: + +- `technique` and `race` reports omit the DRS / ERS / MGU-K sections. +- `setup` mode: ACC's in-game setup export is all zeros, so the parameter table is replaced by a note ("ACC does not export setup — analysis derived from telemetry"); the telemetry-derived sections (corner balance, tyres, brakes, suspension) are retained. +- Fuel is in **litres** (GT) rather than kg (F1). Tyres are Pirelli dry/wet compounds, not F1 C1–C5. + ### Folder layout ``` @@ -62,7 +70,7 @@ Telemetry/ ## Handy things -**GUI** — double-click `run-gui.bat` (or `run-gui.pyw` for no console window, or run `python -m telemetry gui`). Pick a mode and language (RU/EN), browse to the file/folder, click Generate. The file browser remembers the last used directory per mode. The **Rename unprocessed** button adds the session type and lap number to files that don't have them yet and leaves the rest alone. +**GUI** — double-click `run-gui.bat` (or `run-gui.pyw` for no console window, or run `python -m telemetry gui`). The window has two tabs — **F1 22** and **ACC** — each with its own independent controls and log. Pick a mode and language (RU/EN), browse to the file/folder, click Generate. The file browser remembers the last used directory per mode. The **Rename unprocessed** button adds the session type and lap number to files that don't have them yet and leaves the rest alone. If a file's detected game doesn't match the active tab, the GUI logs a warning but proceeds. **CLI** — from the `tool/` folder: @@ -77,6 +85,9 @@ python -m telemetry rename races/Zandvoort/Practice # or individual files # choose prompt language (default: ru) python -m telemetry technique races/Zandvoort/Practice/lap.csv --lang en +# override game detection (default: auto) +python -m telemetry technique ACC/races/Hungaroring/Practice/lap.csv --game acc + # agent pipeline: drop the prompt, reuse fresh reports python -m telemetry technique races/Zandvoort/Practice/lap.csv --no-prompt python -m telemetry technique races/Zandvoort/Practice/lap.csv --force # ignore cache @@ -101,7 +112,7 @@ Re-running on the same folder is safe — in-place files are left alone. ## Input format -F1 22 UDP telemetry exported to CSV (tested with [F1Laps](https://www.f1laps.com)). UTF-8, ~1800 rows per lap (≈29 Hz), 108 channels. One file = one lap. For `race` mode, drop all of a race's lap files into one folder. +F1 22 UDP telemetry or ACC telemetry, both exported to CSV (F1 22 tested with [F1Laps](https://www.f1laps.com)). UTF-8, 108 channels, one file = one lap (F1 22 is ≈1800 rows/lap at ≈29 Hz; ACC laps are denser, ~14k rows — reports stay compact via adaptive resampling). For `race` mode, drop all of a race's lap files into one folder. The CSV format is identical between F1 22 and ACC; the game is detected from the `Game` metadata field in each file. ## For developers @@ -117,7 +128,7 @@ Adding a mode: drop a `telemetry/report_.py` exposing `generate(...) -> (a ## Credits -Built for analysing personal F1 22 hotlaps. Telemetry export format from [F1Laps](https://www.f1laps.com). +Built for analysing personal F1 22 and ACC hotlaps. Telemetry export format from [F1Laps](https://www.f1laps.com). In-game telemetry captured with Telemetry Tool by Iasione. ## License diff --git a/README.ru.md b/README.ru.md index d617798..66d9973 100644 --- a/README.ru.md +++ b/README.ru.md @@ -1,8 +1,8 @@ [English](README.md) -# F1 Telemetry Analyzer +# F1 22 / ACC Telemetry Analyzer -Превращает сырую телеметрию круга F1 22 в компактные Markdown-отчёты для анализа через LLM. Сырой круг — около 1М токенов; отчёты — 1–35k. +Превращает сырую телеметрию круга F1 22 и Assetto Corsa Competizione в компактные Markdown-отчёты для анализа через LLM. Сырой круг — около 1М токенов; отчёты — 1–35k. ```bash # из папки tool/ @@ -21,23 +21,31 @@ python -m telemetry technique "races/Zandvoort/Практика/zandvoort_P1_L7_ ## Что умеет -Шесть режимов. Пять строят отчёты, один переименовывает файлы. +Шесть режимов. Пять строят отчёты, один переименовывает файлы. Поддерживаются CSV из F1 22 и ACC (GT3/GT4) — игра определяется автоматически из метаданных каждого файла. | Режим | Вход | Для анализа | |-------|------|-------------| | `technique` | один круг | **как** едешь — точки торможения, скорости в апексах, работа газом, блокировки, расход ресурсов | | `setup` | один круг | **что** настроено — баланс (US/OS), температуры шин, тормоза, rake и крен подвески | | `compare` | два+ круга | **где** один круг быстрее — дельта по поворотам и накопленная, плюс детерминированный `Verdict` (с топливной поправкой) | -| `race` | папка кругов | вся гонка — темп, деградация в стинтах, износ резины, ERS по кругам | +| `race` | папка кругов | вся гонка — темп, деградация в стинтах, износ резины, ERS по кругам (F1) | | `profile` | папка кругов | **дёшево** (~400 токенов) тенденции пилота по каждому составу (медиана coast/lock/spin на поворот, флаги относительно сессии) + авто-выбор кругов. Универсально: повороты кластеризуются по дистанции апекса, не по индексу | | `rename` | файлы/папка | вставить тип сессии и номер круга в имя файла (`..._74.074_...` → `..._P1_L7_74.074_...`) | Отчёты содержат легенду с пояснением каждой колонки, а промпт анализа добавляется в конец файла автоматически — достаточно скинуть файл в LLM. Промпты доступны на RU и EN; выбирается флагом `--lang en` в CLI или переключателем языка в GUI. -**Флаги:** `--no-prompt` убирает встроенный промпт (меньше файлы для агентных конвейеров; в GUI — чекбокс **Включить промпт**). `--force` перегенерирует, даже если свежий отчёт уже есть (иначе прогон пропускается как `cached`). +**Флаги:** `--no-prompt` убирает встроенный промпт (меньше файлы для агентных конвейеров; в GUI — чекбокс **Включить промпт**). `--force` перегенерирует, даже если свежий отчёт уже есть (иначе прогон пропускается как `cached`). `--game {auto,f1,acc}` переопределяет автоопределение игры (по умолчанию `auto`). > Оптимальный порядок: сначала `profile` — увидеть хронические тенденции и какие круги важны, затем `technique`/`setup`/`compare` только по этим кругам с `--no-prompt`. +### Отличия ACC от F1 22 + +Машины GT3/GT4 не имеют гибридной системы, а ACC не экспортирует настройки машины: + +- Отчёты `technique` и `race` не содержат разделов DRS / ERS / MGU-K. +- Режим `setup`: экспорт настроек из ACC — все нули, поэтому таблица параметров заменяется пометкой («ACC не экспортирует сетап — анализ построен по телеметрии»); разделы, основанные на телеметрии (баланс в поворотах, шины, тормоза, подвеска), остаются. +- Топливо в **литрах** (GT), а не в кг (F1). Шины — составы Pirelli dry/wet, а не F1 C1–C5. + ### Структура папок ``` @@ -62,7 +70,7 @@ Telemetry/ ## Полезное -**GUI** — двойной клик по `run-gui.bat` (или `run-gui.pyw` без окна консоли, либо `python -m telemetry gui`). Выбери режим и язык промпта (RU/EN), укажи файл/папку, нажми Generate. Диалог открывается в последней использованной папке для каждого режима отдельно. Кнопка **Rename unprocessed** добавляет тип сессии и номер круга к файлам, у которых их ещё нет, и не трогает остальные. +**GUI** — двойной клик по `run-gui.bat` (или `run-gui.pyw` без окна консоли, либо `python -m telemetry gui`). Окно разделено на две вкладки — **F1 22** и **ACC** — у каждой свои независимые элементы управления и лог. Выбери режим и язык промпта (RU/EN), укажи файл/папку, нажми Generate. Диалог открывается в последней использованной папке для каждого режима отдельно. Кнопка **Rename unprocessed** добавляет тип сессии и номер круга к файлам, у которых их ещё нет, и не трогает остальные. Если игра в файле не совпадает с активной вкладкой, GUI выводит предупреждение, но продолжает работу. **CLI** — из папки `tool/`: @@ -77,6 +85,9 @@ python -m telemetry rename races/Zandvoort/Практика # или отд # выбор языка промпта (по умолчанию: ru) python -m telemetry technique races/Zandvoort/Практика/lap.csv --lang en +# переопределить определение игры (по умолчанию: auto) +python -m telemetry technique ACC/races/Hungaroring/Practice/lap.csv --game acc + # агентный конвейер: убрать промпт, переиспользовать свежие отчёты python -m telemetry technique races/Zandvoort/Практика/lap.csv --no-prompt python -m telemetry technique races/Zandvoort/Практика/lap.csv --force # игнорировать кэш @@ -101,7 +112,7 @@ C:\...\Telemetry\reports\lap_technique.md ## Входной формат -Телеметрия F1 22 (UDP), экспортированная в CSV (тестировалось на [F1Laps](https://www.f1laps.com)). UTF-8, ~1800 строк на круг (≈29 Гц), 108 каналов. Один файл = один круг. Для режима `race` сложи все круги гонки в одну папку. +Телеметрия F1 22 (UDP) или ACC, экспортированная в CSV (F1 22 тестировалось на [F1Laps](https://www.f1laps.com)). UTF-8, 108 каналов, один файл = один круг (F1 22 — ≈1800 строк/круг при ≈29 Гц; круги ACC плотнее, ~14k строк — отчёты остаются компактными за счёт адаптивного ресэмплинга). Для режима `race` сложи все круги гонки в одну папку. Формат CSV одинаков у F1 22 и ACC; игра определяется из поля `Game` в метаданных каждого файла. ## Для разработчиков @@ -117,7 +128,7 @@ python -m telemetry gui # запустить GUI ## Благодарности -Сделано для разбора личных хотлапов в F1 22. Формат экспорта телеметрии — [F1Laps](https://www.f1laps.com). +Сделано для разбора личных хотлапов в F1 22 и ACC. Формат экспорта телеметрии — [F1Laps](https://www.f1laps.com). Телеметрия в игре записывается через Telemetry Tool от Iasione. ## Лицензия diff --git a/prompts/en/acc/compare.md b/prompts/en/acc/compare.md new file mode 100644 index 0000000..23bf86f --- /dev/null +++ b/prompts/en/acc/compare.md @@ -0,0 +1,62 @@ +You are a race engineer comparing two laps from the same driver in Assetto Corsa Competizione (GT3/GT4). Lap 1 is the reference. Find where and why one lap is faster than the other and what it reveals about the driving. + +## Data format + +**Header** — comparison table: laptime, sectors S1/S2/S3, tyre, fuel at lap start [L], tyre wear, weather. + +**Corners** — per corner: +- `Vmin L1 / Vmin L2` — minimum apex speed [km/h] +- `time L1 / time L2` — time in corner zone [s] +- `Δt seg` — time delta in this corner (L2 − L1): negative = L2 faster, positive = L1 faster +- `Δt cum` — cumulative delta at end of this corner: shows running score to this point in the lap + +**Delta trace** — per sample by distance: +- `dist` — distance [m] +- `spd_L1 / spd_L2` — speed per lap [km/h] +- `Δt` — cumulative delta L2 − L1 [s]: negative = L2 leads, positive = L1 leads + +--- + +## Task + +Analyse each point. Back every conclusion with specific Δt numbers, distances, speeds. + +### 1. Overview + +- Overall time difference and which sector it was built in +- Was the gap steady across the lap or created sharply in one area +- Fuel and tyre wear effect: if laps are from different stints, account for load and degradation + +### 2. Corner-by-corner + +Per corner with non-zero `Δt seg`: +- Who is faster and by how much +- `Vmin L1 vs L2`: higher apex speed = either later braking or better line +- High Vmin but higher `time` = faster apex but worse exit (traction or trajectory loss) +- Lower Vmin but lower `time` = better exit compensating for the slower apex +- `Δt cum` after corner: does the gap grow, or does the next section recover it + +### 3. Delta trace analysis + +Working with `Δt` by distance: +- **Where delta builds sharply** (steep kink): critical points — braking or traction where styles diverge +- **Where delta is flat**: both laps equivalent, lower priority zones +- **Braking zones**: Δt shrinking under braking = L2 brakes later (or more effectively) +- **Traction zones**: after apex `spd_L2 > spd_L1` = L2 traction better (earlier throttle or less wheelspin); in GT cars this matters especially at slow-corner exits where mechanical grip is the limiting factor +- **Straights**: speed difference on a straight = result of the preceding corner exit, not the straight itself + +### 4. Patterns + +- One lap systematically better in slow corners, the other in fast? +- One style risks entries, the other risks exits? +- Any corner where the "slower" lap is actually faster at that specific point — and why? + +### 5. Summary and priorities + +- **What to take from the faster lap**: specific corners and technique +- **Priority zones to work on**: biggest `|Δt seg|` opportunities +- **What not to copy**: corners where the "faster" lap is still slower than the reference — note them + +--- + +Below is the attached comparison report. Begin analysis. diff --git a/prompts/en/acc/race.md b/prompts/en/acc/race.md new file mode 100644 index 0000000..25eda36 --- /dev/null +++ b/prompts/en/acc/race.md @@ -0,0 +1,92 @@ +You are a race engineer analysing full-race telemetry in Assetto Corsa Competizione (GT3/GT4). The report covers all laps: lap times, stints, tyre wear, and thermals. Perform a detailed race analysis. + +## Data format + +**Lap times** — per lap: +- `Lap` — lap number +- `Time` — lap time `M:SS.mmm` +- `S1 / S2 / S3` — sector times [s] +- `Tyre` — compound (dry/wet type) +- `Age` — laps on this set (1 = first lap on fresh set) +- `Fuel L` — fuel at lap start [L] +- `Note` — flags: `FAST` = fastest valid lap, `PIT` = in-lap, `INV` = invalidated + +**Stint summary** — per stint: +- `Stint` — stint number +- `Laps` — laps in stint +- `Tyre` — compound +- `Laps on tyre` — total laps on this set +- `Avg pace` — average pace (excluding pit laps and invalidated) +- `Best / Worst` — best and worst lap in stint +- `Deg s/lap` — degradation slope per tyre age lap (positive = slowing, negative = improving) + +**Tyre wear per lap** — `FL / FR / RL / RR` — wear % at end of each lap + +**Thermal summary per lap**: +- `Tf avg°C` — average peak front tyre temperature for the lap +- `Tr avg°C` — rear +- `Brk FL max°C` — peak front-left brake temperature +- `Brk RR max°C` — peak rear-right brake temperature + +--- + +## Task + +Analyse each point. Back every conclusion with specific numbers from the tables. + +### 1. Race overview + +- How did pace evolve through the race: progression or degradation? +- Were there outliers (abnormally slow or fast laps) — name the lap numbers and reason (PIT, INV, safety car?) +- Compare best vs average pace: how large is the gap? +- One-sentence overall race assessment + +### 2. Stint analysis + +Per stint from `Stint summary`: +- Average pace and relative quality (best/worst stint?) +- `Best–Worst` spread: large spread = inconsistent pace, small = consistent +- Compare stints: where was the driver faster and why (tyre, fuel level, track condition?) +- Was the pit stop timed well: not too early/late given degradation trend + +### 3. Tyre degradation + +From `Deg s/lap` and `Tyre wear`: +- Which stint has the most/least degradation? +- Compare `Deg s/lap` vs actual wear increment per lap — consistent? +- When did the tyre "die": identify the lap where pace dropped sharply +- Wear evenness: FL/FR or RL/RR asymmetry? + +### 4. Fuel effect + +GT cars carry fuel in litres; as fuel burns off the car gets lighter. +- For the first (or long) stint: correlate `Fuel L` and `Time` per lap +- Is there a visible correlation between fuel burn and improving pace as the car lightens? +- If early-stint pace is poor — tyre warm-up or a heavy car? +- Check whether pace dynamics are consistent with the expected weight-reduction benefit + +### 5. Thermal management + +From `Thermal summary`: +- How do tyre temps evolve through each stint: warm-up, stable, overheating? +- Pirelli GT tyre operating window is approximately ~70–90°C; a sharp rise above this with simultaneous pace drop signals overheating +- Brake temps: rising through the race distance or stable? FL vs RR asymmetry? +- Which laps had critical thermal conditions? + +### 6. Best and worst laps + +- `FAST` lap: which stint, what tyre age, what fuel level? +- Why did the best lap happen when it did — track rubber, clean air, cool conditions? +- Worst valid lap (not INV, not PIT): reason — degradation, traffic, driver error? +- Patterns: "out lap" always slow, "in lap" pace dip? + +### 7. Summary and recommendations + +- **What worked**: strategy and driving that was executed well +- **Main losses**: specific laps and zones where the most time was lost +- **Strategic recommendations**: pit timing, compound choice, stint length +- **Driving technique**: if degradation is abnormal — what to change in driving style + +--- + +Below is the attached race report. Begin analysis. diff --git a/prompts/en/acc/setup.md b/prompts/en/acc/setup.md new file mode 100644 index 0000000..f764a10 --- /dev/null +++ b/prompts/en/acc/setup.md @@ -0,0 +1,80 @@ +You are an experienced race engineer. The following is a telemetry report for one lap in Assetto Corsa Competizione (GT3/GT4). Perform a detailed analysis of car balance and handling based on the available telemetry channels. + +## Important note + +ACC does not export car setup parameters to telemetry — the in-game setup table is all zeros and is omitted from the report. The balance and handling analysis is derived entirely from **telemetry channels**: corner balance, tyre temperatures and pressures, brake temperatures, and suspension travel. + +## Data format + +**Corner balance** — per corner: +- `gLat_max` — peak lateral G-force [G] +- `steer/g` — steering angle to lateral G ratio: high = steering a lot for the G = understeer +- `front|slip|` / `rear|slip|` — average absolute slip of front/rear axle in the corner zone +- `balance` — outcome: US (front axle slips more), OS (rear), — (neutral) + +**Tyres** — per wheel: +- `wearΔ%` — wear over the lap +- `surf min/avg/max °C` — surface temperature +- `carc min/avg/max °C` — carcass temperature +- `pressure` — hot pressure [PSI] + +**Brakes** — min/avg/max temperature per wheel [°C] + +**Suspension**: +- Rake — average front vs rear ride height [mm]; positive rake = nose higher than tail +- Roll per corner — body roll in each corner [mm]; shows outer-wheel loading + +**Lap phases** — average tyre surface temperature across three equal lap segments + +--- + +## Task + +Analyse each point. Back every conclusion with numbers from the report. + +### 1. Mechanical balance + +- Corner balance: systematic US or OS across most corners? +- Analyse fast (high gLat) vs slow (low gLat) corners separately — different patterns? +- High `steer/g` = car demands more steering for the load level = understeer tendency; low `steer/g` with high rear slip = oversteer +- GT cars are sensitive to front/rear spring and anti-roll bar balance: abnormal Roll per corner may indicate a soft spring or weak anti-roll bar on one axle + +### 2. Differential and traction + +- From `spin%` in the corner table: high rear slip on exit indicates an aggressive on-throttle differential lock or premature throttle application +- If `spin%` is consistently high in slow corners but modest in fast corners — on-throttle differential setting is the likely cause +- Check whether rear slip is symmetric across left and right sides — asymmetry suggests a geometry or tyre pressure issue + +### 3. Tyre thermal window + +- Pirelli GT tyre operating window is approximately ~70–90°C surface; outside this range grip is compromised +- Are tyres in window; who overheats, who underheats +- Surface vs carcass delta: large gap = tyre overheating on the outside but cold carcass (delamination risk), or vice versa +- Wear: even across 4 wheels — abnormally high wear on one wheel signals load imbalance +- Lap phases: normal warm-up pattern — should rise in phase 1 and stabilise + +### 4. Tyre pressures + +- Hot pressure (from Tyres): too high = overheating and loss of contact patch; too low = under-temperature, tyre wanders +- F/R pressure balance: asymmetry affects mechanical balance +- L/R pressure difference on the same axle — sign of asymmetric heating or incorrect cold starting pressure + +### 5. Brakes + +- Temperature range: <200°C = cold (poor braking), 350–800°C = normal GT working range, >900°C = dangerous overheating +- L/R asymmetry: different temperatures on same axle = uneven pad wear or brake balance issue +- If rears are consistently hotter than fronts — bias is too far rearward; if fronts dominate — bias is forward; both reduce braking stability + +### 6. Suspension + +- Rake: moderate positive rake is standard for GT; very high rake increases rear aerodynamic load but can cause a lazy front on turn-in +- Roll per corner: typical roll 3–8 mm; large roll = soft suspension or weak anti-roll bars; small = stiff, sensitive to bumps +- L/R roll asymmetry in the same corner indicates uneven suspension travel or load distribution + +### 7. Recommendations + +Per identified issue — what specifically to change (direction of adjustment) and expected effect. Prioritise by lap time impact. + +--- + +Below is the attached report. Begin analysis. diff --git a/prompts/en/acc/technique.md b/prompts/en/acc/technique.md new file mode 100644 index 0000000..1cf8e23 --- /dev/null +++ b/prompts/en/acc/technique.md @@ -0,0 +1,92 @@ +You are an experienced racing engineer. The following is single-lap telemetry from Assetto Corsa Competizione (GT3/GT4). Perform a detailed driving technique analysis. + +## Data format + +The report contains: + +**Corners** — corner table. Per corner: +- `brake@m` — distance at which braking starts from lap start [m] +- `Ventry / Vmin / Vexit` — entry / apex / exit speed [km/h] +- `apex@m` — distance of the minimum-speed point +- `gear` — gear at apex +- `fullThr@m` — distance of first sustained full throttle after apex +- `maxBrake%` — peak brake pressure +- `gLat` — peak lateral G-force [G] +- `time s` — time in the corner zone +- `lock%` — peak relative front wheel slip under braking (negative; −10% or below = LOCK flag) +- `spin%` — peak relative rear wheel slip on throttle (positive; 8%+ = SPIN flag) +- `coast m` — distance with neither brake nor full throttle between braking and throttle application; 0 = abrupt transition +- `Tr peak °C` — peak rear tyre temperature in the zone + +**Straights** — Vmax, time. + +**Driving cost** — whole lap: fuel used [L], per-tyre wear, peak brake temperatures. + +**Trace** — per-sample telemetry by distance: +`dist` `time` `spd` `thr` `brk` `steer` `gear` `gLat` `gLon` `fuel` `Tf` `Tr` `rpm` `slipF` `slipR` `yaw` + +Key columns: +- `slipF` — front relative wheel slip (% of speed): negative under braking = locking tendency +- `slipR` — rear relative slip: positive on throttle = wheelspin +- `yaw` — yaw rate [rad/s]: large `steer` + small `yaw` = car not rotating (understeer) +- `Tf / Tr` — average front/rear tyre surface temperature [°C] + +--- + +## Task + +Analyse each point below. Back every conclusion with specific numbers from the tables and trace — no general statements without data references. + +### 1. Braking + +Per corner with non-zero `brake@m`: +- Evaluate the braking point and how fully the brakes are used (`maxBrake%`) +- Analyse `lock%`: how severe is the lockup, isolated or repeated across corners +- Evaluate `coast m`: long coast = cautious transition or trail braking; zero = snap release +- Compare `Ventry` and `Vmin`: large delta = hard braking deep into the corner; small = braking complete before turn-in + +### 2. Apexes + +Per corner: +- `Vmin` in context of the corner's speed class — over-braked or on the limit? +- `gear` at apex — too low (excess torque, wheelspin risk on exit)? +- `gLat` vs `Vmin`: high gLat at low Vmin = good apex loading; low gLat = missed apex + +### 3. Exits + +Per corner: +- `fullThr@m` vs `apex@m`: smaller gap = more aggressive exit +- `spin%`: high value = wheelspin on exit — throttle applied too early or too aggressively +- `Vexit`: final result — does it match the corner's speed potential +- From Trace: how `thr` builds after apex — stepped or smooth; any drops after reaching full throttle + +### 4. Steering and handling + +From Trace (focus on cornering zones): +- `steer`: any sharp sawtooth inputs or excessive corrections +- `steer` + `yaw`: zones where yaw lags far behind steer — specific understeer points +- Corners where the car refuses to rotate (high steer, weak yaw response) + +### 5. Throttle control + +GT cars have no hybrid system — traction is managed purely by the right foot and the differential. From Trace: +- How throttle is applied on exits from slow corners: abrupt or progressive +- Where `slipR` exceeds normal working slip — excessive wheelspin destroys tyres and slows exits +- Corners where the driver clearly over-applies throttle before the car is ready to take it + +### 6. Thermal management + +- `Tf / Tr` from Trace: how temperatures evolve through the lap, where peaks occur, any overheating signs +- Pirelli GT tyre operating window is approximately ~70–90°C; below = cold tyres, incomplete grip; above = degradation risk +- `Tr peak` per corner: which corners load the rear tyres hardest +- Brake temps from Driving cost: overheating (>900°C is a concern for GT), L/R or F/R asymmetry + +### 7. Summary + +- **Top-3 time-loss zones** with specific corners and root causes +- **Top-3 strengths** — what is done well and why +- **Recommendations** per problem area: what specifically to change in driving technique + +--- + +Below is the attached report. Begin analysis. diff --git a/prompts/ru/acc/compare.md b/prompts/ru/acc/compare.md new file mode 100644 index 0000000..5164ecb --- /dev/null +++ b/prompts/ru/acc/compare.md @@ -0,0 +1,64 @@ +Ты — гоночный инженер, сравнивающий два круга одного пилота в Assetto Corsa Competizione (GT3/GT4). Первый круг (Lap 1) — эталонный. Найди, где и почему один круг быстрее другого, и что это говорит о пилотировании. + +## Формат данных + +**Шапка** — сравнительная таблица: laptime, секторы S1/S2/S3, шина, топливо на старте круга [л], износ шин, погода. + +**Corners** — по каждому повороту: +- `Vmin L1 / Vmin L2` — минимальная скорость в апексе [км/ч] +- `time L1 / time L2` — время в зоне поворота [с] +- `Δt seg` — разница времён в данном повороте (L2 − L1): отрицательное = L2 быстрее, положительное = L1 быстрее +- `Δt cum` — накопленная дельта к концу этого поворота: показывает общий счёт на данном участке круга + +**Delta trace** — построчно по дистанции: +- `dist` — дистанция [м] +- `spd_L1 / spd_L2` — скорость каждого круга [км/ч] +- `Δt` — накопленная дельта L2 − L1 [с]: отрицательное = L2 ведёт, положительное = L1 ведёт + +--- + +## Задача + +Разбери каждый пункт. Каждый вывод подкрепляй конкретными числами Δt, дистанциями, скоростями. + +### 1. Общая картина + +- Итоговая разница времён и в каком секторе она сформировалась +- Был ли разрыв стабильным по кругу или резко возник в одном месте +- Влияние топлива и износа шин: если круги из разных стинтов — учти поправку на загрузку и деградацию + +### 2. Разбор по поворотам + +По каждому повороту с ненулевым `Δt seg`: + +- Кто быстрее и на сколько +- `Vmin L1 vs L2`: кто выше скорость в апексе — тот либо позже тормозил, либо лучше нашёл траекторию +- Если Vmin выше, но `time` больше — апекс быстрее, но выход хуже (потеря на тяге или траектории) +- Если Vmin ниже, но `time` меньше — лучший выход компенсировал потерю в апексе +- `Δt cum` после поворота: нарастает ли отрыв или следующий участок его нивелирует + +### 3. Анализ дельта-трассы + +Работай с `Δt` по дистанции: + +- **Где дельта строится резко** (крутой излом): это критические точки — торможение или тяга, где стили расходятся +- **Где дельта стабильна**: оба круга едут одинаково, эти зоны не приоритет +- **Тормозные зоны**: если Δt уменьшается при торможении — L2 тормозит позже (или эффективнее) +- **Тяговые зоны**: если после апекса `spd_L2 > spd_L1` — L2 лучше тянет (раньше газ или меньше буксовки); в GT это особенно важно на выходах из шпилек, где тяговый контроль — ключ +- **Прямые**: разница скоростей на прямой = результат предыдущего выхода из поворота, а не самой прямой + +### 4. Паттерны + +- Один круг систематически лучше в медленных поворотах, другой — в быстрых? +- Один стиль рискует на входах, другой — на выходах? +- Есть ли повороты, где «медленный» круг на самом деле быстрее в этом конкретном месте — и почему? + +### 5. Итог и приоритеты + +- **Что взять из быстрого круга**: конкретные повороты и техника +- **Приоритетные зоны для работы**: где наибольший потенциал (самые большие `|Δt seg|`) +- **Что не копировать**: если в «быстром» круге есть места, где он всё же медленнее — зафиксируй + +--- + +Ниже прикреплён отчёт сравнения. Начинай анализ. diff --git a/prompts/ru/acc/race.md b/prompts/ru/acc/race.md new file mode 100644 index 0000000..aa552e8 --- /dev/null +++ b/prompts/ru/acc/race.md @@ -0,0 +1,95 @@ +Ты — гоночный инженер, анализирующий телеметрию целой гонки в Assetto Corsa Competizione (GT3/GT4). Перед тобой отчёт по всем кругам: времена, стинты, износ шин и тепловой режим. Проведи детальный разбор гонки. + +## Формат данных + +Отчёт содержит: + +**Lap times** — таблица кругов: +- `Lap` — номер круга +- `Time` — время круга в формате `M:SS.mmm` +- `S1 / S2 / S3` — времена секторов [с] +- `Tyre` — тип шины (dry/wet, состав) +- `Age` — количество кругов на данном комплекте (1 = первый круг на новом комплекте) +- `Fuel L` — топливо на старте круга [л] +- `Note` — флаги: `FAST` = быстрейший действительный круг, `PIT` = пит-лэп, `INV` = недействительный круг + +**Stint summary** — сводка по стинтам: +- `Stint` — номер стинта +- `Laps` — количество кругов в стинте +- `Tyre` — тип шины +- `Laps on tyre` — всего кругов на комплекте +- `Avg pace` — средний темп (без пит-лэпов и недействительных) +- `Best / Worst` — лучший и худший круг стинта +- `Deg s/lap` — деградация: наклон тренда времён по тирейджу (положительное = замедление, отрицательное = улучшение) + +**Tyre wear per lap** — износ по кругам: +- `FL / FR / RL / RR` — процент износа каждой шины (последний замер круга) + +**Thermal summary per lap** — тепловой режим: +- `Tf avg°C` — средняя максимальная температура передних шин за круг +- `Tr avg°C` — задних шин +- `Brk FL max°C` — максимальная температура переднего левого тормоза +- `Brk RR max°C` — заднего правого тормоза + +--- + +## Задача + +Разбери каждый из пунктов. Каждый вывод подкрепляй конкретными числами из таблиц — не делай общих утверждений без ссылки на данные. + +### 1. Общая картина гонки + +- Как менялся темп по ходу гонки: прогрессия или деградация? +- Были ли выбросы (аномально медленные или быстрые круги) — назови номера кругов и причину (PIT, INV, safety car?) +- Сравни лучший и средний темп: насколько велик разрыв? +- Итоговая оценка гонки в одном предложении + +### 2. Анализ стинтов + +По каждому стинту из `Stint summary`: +- Средний темп и его относительное качество (лучший / худший стинт?) +- Разброс `Best–Worst`: большой разброс = непостоянный темп, малый = стабильный +- Сравни стинты между собой: где пилот ехал быстрее и почему (шина, топливо, трек, условия?) +- Был ли питстоп своевременным: не слишком ли рано/поздно по тренду деградации? + +### 3. Деградация резины + +По `Deg s/lap` и таблице `Tyre wear`: +- В каком стинте деградация наибольшая и наименьшая? +- Сопоставь `Deg s/lap` с фактическим приростом износа по кругам — соответствуют ли друг другу? +- Когда резина «умерла»: определи круг, после которого темп резко упал +- Равномерность износа: есть ли асимметрия FL/FR или RL/RR? + +### 4. Топливный эффект + +GT-машины заправляются в литрах; по мере выжигания топлива машина становится легче. +- По первому стинту (или длинному стинту): сопоставь `Fuel L` и `Time` по кругам +- Есть ли видимая корреляция между расходом топлива и улучшением темпа по мере облегчения машины? +- Если темп в начале стинта плохой — прогрев шин или тяжёлая машина? +- Оцени, согласуется ли динамика темпа с ожидаемым эффектом снижения веса + +### 5. Тепловой менеджмент + +По `Thermal summary`: +- Как меняются температуры шин по кругам в каждом стинте: прогрев, стабилизация, перегрев? +- Рабочий диапазон шин Pirelli (GT) ориентировочно ~70–90°C; резкий рост выше с одновременным ухудшением темпа — признак перегрева +- Температуры тормозов: рост по дистанции или стабильные? Асимметрия FL vs RR? +- В каких кругах тепловой режим был критическим? + +### 6. Лучшие и худшие круги + +- `FAST` круг: в каком стинте, на каком тирейдже, при каком запасе топлива? +- Почему лучший круг получился лучшим — трек rubber? чистый воздух? прохлада? +- Худший действительный круг (не INV, не PIT): причина — деградация, трафик, ошибка? +- Есть ли интересные паттерны: «out lap» всегда медленный, «in lap» провальный по темпу? + +### 7. Итог и рекомендации + +- **Что сработало:** что в стратегии и пилотировании было сделано хорошо +- **Главные потери:** конкретные круги и зоны, где потеряно больше всего +- **Стратегические рекомендации:** момент питстопа, выбор состава, длина стинтов +- **Техника пилотирования:** если деградация аномальна — что нужно изменить в стиле езды + +--- + +Ниже прикреплён отчёт гонки. Начинай анализ. diff --git a/prompts/ru/acc/setup.md b/prompts/ru/acc/setup.md new file mode 100644 index 0000000..2a235f2 --- /dev/null +++ b/prompts/ru/acc/setup.md @@ -0,0 +1,80 @@ +Ты — опытный гоночный инженер. Перед тобой телеметрический отчёт за один круг в Assetto Corsa Competizione (GT3/GT4). Проведи детальный разбор баланса и поведения машины на основе доступных телеметрических данных. + +## Важное замечание + +ACC не экспортирует параметры сетапа в телеметрию — таблица сетапа в игре содержит нули и в отчёт не включается. Анализ баланса и управляемости строится **исключительно по телеметрическим каналам**: угловой баланс, температуры и давления шин, тормоза, ходы подвески. + +## Формат данных + +**Corner balance** — по каждому повороту: +- `gLat_max` — пиковая боковая перегрузка [G] +- `steer/g` — отношение угла руля к боковой G: высокое = рулишь много на данном G = understeer +- `front|slip|` / `rear|slip|` — средний модуль скольжения переднего / заднего осей в зоне поворота +- `balance` — итог: US (передняя ось проскальзывает сильнее), OS (задняя), — (нейтрально) + +**Tyres** — по каждому колесу: +- `wearΔ%` — износ за круг +- `surf min/avg/max °C` — температура поверхности +- `carc min/avg/max °C` — температура каркаса +- `pressure` — горячее давление [PSI] + +**Brakes** — температура мин/сред/макс по каждому колесу [°C] + +**Suspension**: +- Rake — средняя высота носа vs кормы [мм]; положительный rake = нос выше кормы +- Roll по поворотам — крен кузова в каждом повороте [мм]; показывает загрузку внешних колёс + +**Lap phases** — средняя температура поверхности шин по трём равным отрезкам круга + +--- + +## Задача + +Разбери каждый пункт. Каждый вывод подкрепляй числами из отчёта. + +### 1. Механический баланс + +- Corner balance: есть ли систематический US или OS по большинству поворотов +- Отдельно рассмотри быстрые (высокий gLat) vs медленные (низкий gLat) повороты — разные ли паттерны +- Высокий `steer/g` = машина требует больше руля на данной загрузке = недостаточная поворачиваемость; низкий `steer/g` при высоком скольжении задней оси = избыточная +- GT-машины чувствительны к балансу жёсткостей подвески и антикренов: аномальный Roll в поворотах может указывать на мягкую пружину или слабый стабилизатор на одной оси + +### 2. Дифференциал и тяга + +- По `spin%` из corner table: высокое скольжение задних на выходе указывает на агрессивный блок дифференциала на газу или слишком раннюю подачу газа +- Если `spin%` системно высок в медленных поворотах, но невелик в быстрых — причина, вероятно, в настройке OnThrottle дифференциала +- Оцени, насколько равномерно скольжение по левой и правой сторонам — асимметрия = проблема с геометрией или давлением шин + +### 3. Тепловой режим шин + +- Рабочий диапазон шин Pirelli (GT) ориентировочно ~70–90°C поверхности; выход за границы снижает сцепление +- Попадают ли шины в рабочий диапазон; кто перегревается, кто недогревается +- Разница поверхности и каркаса: большая разница = шина перегревается снаружи, но каркас холодный (риск расслоения), или наоборот +- Износ: равномерный ли по 4 колёсам — аномально высокий износ одного колеса указывает на дисбаланс нагрузки +- Lap phases: нормальный ли прогрев — должен расти в первой фазе и стабилизироваться + +### 4. Давление шин + +- Горячее давление (из Tyres): слишком высокое = перегрев и потеря контакта с дорогой; слишком низкое = недогрев, шина «плывёт» +- Баланс F/R давления: асимметрия влияет на механический баланс +- Разница L/R давления на одной оси — признак асимметричного нагрева или неправильного старта давления в холодном состоянии + +### 5. Тормоза + +- Диапазон температур: <200°C = холодные (плохое торможение), 350–800°C = рабочий диапазон GT, >900°C = опасный перегрев +- Асимметрия L/R: разные температуры на одной оси = неравномерный износ колодок или проблема с балансом тормозного усилия +- Если задние тормоза стабильно горячее передних — bias смещён назад; если передние — вперёд; оба отклонения ухудшают управляемость при торможении + +### 6. Подвеска + +- Rake: умеренный положительный rake стандартен для GT; очень большой rake увеличивает аэродинамическую прижимную силу кормы, но снижает нейтральность на входе +- Roll по поворотам: нормальный крен 3–8 мм; большой крен = мягкая подвеска или слабые антикрены; малый = жёсткая, чувствительная к буграм +- Асимметрия Roll L/R в одном повороте указывает на разные ходы подвески или неравномерную нагрузку + +### 7. Рекомендации + +По каждой выявленной проблеме — что конкретно менять (направление изменения настройки) и каков ожидаемый эффект. Приоритизируй по влиянию на время круга. + +--- + +Ниже прикреплён отчёт. Начинай анализ. diff --git a/prompts/ru/acc/technique.md b/prompts/ru/acc/technique.md new file mode 100644 index 0000000..4f5eaef --- /dev/null +++ b/prompts/ru/acc/technique.md @@ -0,0 +1,92 @@ +Ты — опытный инженер-аналитик гоночной команды. Перед тобой телеметрия одного круга в Assetto Corsa Competizione (GT3/GT4). Проведи максимально детальный разбор стиля пилотирования. + +## Формат данных + +Отчёт содержит: + +**Corners** — таблица поворотов. По каждому: +- `brake@m` — дистанция начала торможения от старта круга +- `Ventry / Vmin / Vexit` — скорость входа / апекса / выхода [км/ч] +- `apex@m` — дистанция точки минимальной скорости +- `gear` — передача в апексе +- `fullThr@m` — дистанция первого устойчивого полного газа после апекса +- `maxBrake%` — пиковое давление на тормоз +- `gLat` — пиковая боковая перегрузка [G] +- `time s` — время в зоне поворота +- `lock%` — пик относительного скольжения передних колёс при торможении (отрицательное; −10% и ниже = флаг LOCK) +- `spin%` — пик относительного скольжения задних колёс на газу (положительное; 8%+ = флаг SPIN) +- `coast m` — дистанция без тормоза и без полного газа между торможением и тягой; 0 = резкий переход +- `Tr peak °C` — пиковая температура задней шины в зоне + +**Straights** — прямые: Vmax, время. + +**Driving cost** — за весь круг: расход топлива [л], износ каждой шины, пиковые температуры тормозов. + +**Trace** — построчная телеметрия по дистанции: +`dist` `time` `spd` `thr` `brk` `steer` `gear` `gLat` `gLon` `fuel` `Tf` `Tr` `rpm` `slipF` `slipR` `yaw` + +Пояснения к ключевым колонкам: +- `slipF` — относительное скольжение передних колёс (% от скорости): уход в минус при торможении = блокировка +- `slipR` — задних колёс: положительный на газу = пробуксовка +- `yaw` — угловая скорость рыскания [рад/с]: большой `steer` + маленький `yaw` = машина не поворачивает (understeer) +- `Tf / Tr` — средняя температура передних / задних шин [°C] + +--- + +## Задача + +Разбери каждый из пунктов ниже. Каждый вывод подкрепляй конкретными числами из таблиц и трассы — не делай общих утверждений без ссылки на данные. + +### 1. Торможения + +По каждому повороту с ненулевым `brake@m`: +- Оцени точку торможения и полноту использования тормозов (`maxBrake%`) +- Проанализируй `lock%`: насколько сильная блокировка, стабильна ли она или разовая +- Оцени `coast m`: длинный накат — осторожный переход или trail braking; нулевой — резкий перенос ноги +- Сравни `Ventry` и `Vmin`: большая разница = активное торможение в повороте; малая = торможение завершено до поворота + +### 2. Апексы + +По каждому повороту: +- `Vmin` в контексте скоростного класса поворота — перетормозил или точно? +- `gear` в апексе — не слишком ли низкая передача (лишний момент, риск буксовки на выходе) +- `gLat` vs `Vmin`: высокий gLat при низком Vmin = хорошая загрузка в апексе; низкий gLat = промах мимо вершины + +### 3. Выходы + +По каждому повороту: +- `fullThr@m` относительно `apex@m`: чем меньше разрыв, тем агрессивнее выход +- `spin%`: высокое значение = буксовка на выходе — слишком рано или слишком резко газ +- `Vexit`: итоговый результат выхода — соответствует ли скоростному потенциалу поворота +- По Trace: как нарастает `thr` после апекса — ступенчато или плавно; есть ли просадки после полного газа + +### 4. Работа руля и поворачиваемость + +По Trace (фокус на зоны поворотов): +- `steer`: нет ли резких пилообразных движений или избыточных коррекций +- `steer` + `yaw`: в каких зонах yaw сильно отстаёт от steer — конкретные точки understeer +- В каких поворотах машина отказывается поворачивать (большой steer, слабый yaw) + +### 5. Тяговый контроль + +GT-машины не имеют гибридных систем — тяга управляется исключительно правой ногой и дифференциалом. По Trace: +- Как подаётся газ на выходе из медленных поворотов: резко или прогрессивно +- Где `slipR` выходит за пределы нормального скольжения — чрезмерная буксовка разрушает шины и замедляет выход +- Есть ли повороты, где пилот явно «переливает» газ раньше, чем машина готова к тяге + +### 6. Тепловой режим + +- `Tf / Tr` по Trace: как меняется температура по кругу, где пики, есть ли перегрев +- Рабочий диапазон шин Pirelli (GT) ориентировочно ~70–90°C; ниже = резина холодная, сцепление неполное; выше = риск деградации +- `Tr peak` по поворотам: какие повороты наиболее нагружают задние шины +- Brake temps из Driving cost: перегрев (>900°C — тревожный порог для GT), асимметрия L/R или F/R + +### 7. Итог + +- **Топ-3 зоны потери времени** с конкретными поворотами и причинами +- **Топ-3 сильные стороны** — что делается хорошо и почему +- **Рекомендации** по каждой проблемной зоне: что конкретно изменить в технике пилотирования + +--- + +Ниже прикреплён отчёт. Начинай анализ. diff --git a/telemetry/__main__.py b/telemetry/__main__.py index 5c41079..49af3ce 100644 --- a/telemetry/__main__.py +++ b/telemetry/__main__.py @@ -61,6 +61,8 @@ def main() -> None: parser.add_argument('mode', choices=['technique', 'setup', 'compare', 'race', 'profile', 'rename', 'gui']) parser.add_argument('files', nargs='*') parser.add_argument('--lang', choices=['ru', 'en'], default='ru') + parser.add_argument('--game', choices=['auto', 'f1', 'acc'], default='auto', + help='override game detection (default: auto, from the Game field)') parser.add_argument('--no-prompt', action='store_true', help='omit the embedded analysis prompt (for agent pipelines)') parser.add_argument('--force', action='store_true', @@ -70,6 +72,7 @@ def main() -> None: mode: str = args.mode files: list[str] = args.files lang: str = args.lang + game: str | None = None if args.game == 'auto' else args.game include_prompt: bool = not args.no_prompt force: bool = args.force @@ -105,18 +108,18 @@ def main() -> None: if mode == 'technique': out = report_path(files[0], 'technique') _emit(out, [files[0]], - lambda: report_technique.generate(load_lap(files[0]), out, lang, include_prompt)) + lambda: report_technique.generate(load_lap(files[0]), out, lang, include_prompt, game)) elif mode == 'setup': out = report_path(files[0], 'setup') _emit(out, [files[0]], - lambda: report_setup.generate(load_lap(files[0]), out, lang, include_prompt)) + lambda: report_setup.generate(load_lap(files[0]), out, lang, include_prompt, game)) elif mode == 'compare': stem_b = os.path.splitext(os.path.basename(files[1]))[0] out = report_path(files[0], 'compare', stem_b) _emit(out, files, - lambda: report_compare.generate([load_lap(f) for f in files], out, lang, include_prompt)) + lambda: report_compare.generate([load_lap(f) for f in files], out, lang, include_prompt, game)) elif mode in ('race', 'profile'): race_dir = os.path.abspath(files[0]) @@ -129,9 +132,12 @@ def main() -> None: dirname = os.path.basename(race_dir.rstrip('/\\')) suffix = '_race.md' if mode == 'race' else '_profile.md' out = os.path.join(race_dir, 'reports', dirname + suffix) - gen_mod = report_race if mode == 'race' else report_profile - _emit(out, csv_paths, - lambda: gen_mod.generate([load_lap(p) for p in csv_paths], out, lang, include_prompt)) + if mode == 'race': + _emit(out, csv_paths, + lambda: report_race.generate([load_lap(p) for p in csv_paths], out, lang, include_prompt, game)) + else: + _emit(out, csv_paths, + lambda: report_profile.generate([load_lap(p) for p in csv_paths], out, lang, include_prompt)) if __name__ == '__main__': diff --git a/telemetry/gui.py b/telemetry/gui.py index eaebefe..7819b44 100644 --- a/telemetry/gui.py +++ b/telemetry/gui.py @@ -5,112 +5,141 @@ import tkinter as tk from tkinter import filedialog, scrolledtext, ttk from .parser import load_lap -from .report_common import report_path +from .report_common import game_of, report_path from . import report_technique, report_setup, report_compare, report_race, report_profile from .rename import rename_unprocessed -# project root = parent of the tool\ dir _PROJECT_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')) _RACES_DIR = os.path.join(_PROJECT_ROOT, 'races') +# Maps tab id → display name used in mismatch warnings. +_TAB_GAME = {'f1': 'F1 22', 'acc': 'ACC'} -def _race_output_path(race_dir: str) -> str: - dirname = os.path.basename(race_dir.rstrip('/\\')) - return os.path.join(race_dir, 'reports', dirname + '_race.md') + +class _TabState: + """All mutable state that belongs to one tab.""" + def __init__(self) -> None: + self.mode = tk.StringVar(value='technique') + self.lang = tk.StringVar(value='ru') + self.include_prompt = tk.BooleanVar(value=True) + self.path_var = tk.StringVar() + self.selection: list[str] = [] + self.last_dir: dict[str, str] = {} + self.log_queue: queue.Queue[str | None] = queue.Queue() + self.busy = False + # widgets set by _build_panel + self.log_widget: scrolledtext.ScrolledText | None = None + self.btn_generate: ttk.Button | None = None + self.btn_rename: ttk.Button | None = None class _App: def __init__(self, root: tk.Tk) -> None: self._root = root - root.title('F1 Telemetry Analyzer') + root.title('Telemetry Analyzer (F1 22 / ACC)') root.resizable(True, True) - self._mode = tk.StringVar(value='technique') - self._lang = tk.StringVar(value='ru') - self._include_prompt = tk.BooleanVar(value=True) - self._selection: list[str] = [] - self._last_dir: dict[str, str] = {} - self._log_queue: queue.Queue[str | None] = queue.Queue() - self._busy = False + self._tabs: dict[str, _TabState] = { + 'f1': _TabState(), + 'acc': _TabState(), + } self._build_ui() - self._poll_log() + self._poll_all_logs() + + # ── UI construction ────────────────────────────────────────────────────── def _build_ui(self) -> None: + nb = ttk.Notebook(self._root) + nb.pack(fill='both', expand=True, padx=6, pady=6) + + for tab_id, label in [('f1', 'F1 22'), ('acc', 'ACC')]: + frame = ttk.Frame(nb) + nb.add(frame, text=label) + self._build_panel(frame, tab_id) + + def _build_panel(self, parent: ttk.Frame, tab_id: str) -> None: + st = self._tabs[tab_id] pad = {'padx': 8, 'pady': 4} - mode_frame = ttk.LabelFrame(self._root, text='Mode') + mode_frame = ttk.LabelFrame(parent, text='Mode') mode_frame.pack(fill='x', **pad) for label, value in [('Technique', 'technique'), ('Setup', 'setup'), ('Compare', 'compare'), ('Race', 'race'), ('Profile', 'profile')]: - ttk.Radiobutton(mode_frame, text=label, variable=self._mode, + ttk.Radiobutton(mode_frame, text=label, variable=st.mode, value=value).pack(side='left', padx=6, pady=4) - lang_frame = ttk.LabelFrame(self._root, text='Prompt language / Язык промпта') + lang_frame = ttk.LabelFrame(parent, text='Prompt language / Язык промпта') lang_frame.pack(fill='x', **pad) - ttk.Radiobutton(lang_frame, text='RU', variable=self._lang, + ttk.Radiobutton(lang_frame, text='RU', variable=st.lang, value='ru').pack(side='left', padx=6, pady=4) - ttk.Radiobutton(lang_frame, text='EN', variable=self._lang, + ttk.Radiobutton(lang_frame, text='EN', variable=st.lang, value='en').pack(side='left', padx=6, pady=4) ttk.Checkbutton(lang_frame, text='Включить промпт в отчёт / Include prompt', - variable=self._include_prompt).pack(side='left', padx=16, pady=4) + variable=st.include_prompt).pack(side='left', padx=16, pady=4) - input_frame = ttk.LabelFrame(self._root, text='Input') + input_frame = ttk.LabelFrame(parent, text='Input') input_frame.pack(fill='x', **pad) - self._path_var = tk.StringVar() - ttk.Entry(input_frame, textvariable=self._path_var, + ttk.Entry(input_frame, textvariable=st.path_var, state='readonly', width=60).pack(side='left', fill='x', expand=True, padx=(6, 2), pady=4) ttk.Button(input_frame, text='Browse…', - command=self._browse).pack(side='left', padx=(2, 6), pady=4) + command=lambda: self._browse(tab_id)).pack(side='left', padx=(2, 6), pady=4) - btn_frame = ttk.Frame(self._root) + btn_frame = ttk.Frame(parent) btn_frame.pack(fill='x', **pad) - self._btn_generate = ttk.Button(btn_frame, text='Generate report', - command=self._on_generate) - self._btn_generate.pack(side='left', padx=(0, 4)) - self._btn_rename = ttk.Button(btn_frame, text='Rename unprocessed (add lap #)', - command=self._on_rename) - self._btn_rename.pack(side='left', padx=4) + st.btn_generate = ttk.Button(btn_frame, text='Generate report', + command=lambda: self._on_generate(tab_id)) + st.btn_generate.pack(side='left', padx=(0, 4)) + st.btn_rename = ttk.Button(btn_frame, text='Rename unprocessed (add lap #)', + command=lambda: self._on_rename(tab_id)) + st.btn_rename.pack(side='left', padx=4) ttk.Button(btn_frame, text='Open races folder', command=self._open_reports).pack(side='left', padx=4) - log_frame = ttk.LabelFrame(self._root, text='Log') + log_frame = ttk.LabelFrame(parent, text='Log') log_frame.pack(fill='both', expand=True, **pad) - self._log = scrolledtext.ScrolledText(log_frame, state='disabled', - wrap='word', height=16) - self._log.pack(fill='both', expand=True, padx=4, pady=4) + st.log_widget = scrolledtext.ScrolledText(log_frame, state='disabled', + wrap='word', height=16) + st.log_widget.pack(fill='both', expand=True, padx=4, pady=4) + + # ── Browse ─────────────────────────────────────────────────────────────── + + def _browse(self, tab_id: str) -> None: + st = self._tabs[tab_id] + mode = st.mode.get() + init = st.last_dir.get(mode, _RACES_DIR) - def _browse(self) -> None: - mode = self._mode.get() - init = self._last_dir.get(mode, _RACES_DIR) if mode in ('race', 'profile'): d = filedialog.askdirectory(title='Select race folder', initialdir=init) if d: - self._selection = [d] - self._path_var.set(d) - self._last_dir[mode] = d + st.selection = [d] + st.path_var.set(d) + st.last_dir[mode] = d elif mode == 'compare': files = filedialog.askopenfilenames( title='Select 2+ CSV files', initialdir=init, filetypes=[('CSV', '*.csv')]) if files: - self._selection = list(files) - self._path_var.set(' | '.join(os.path.basename(f) for f in files)) - self._last_dir[mode] = os.path.dirname(files[0]) + st.selection = list(files) + st.path_var.set(' | '.join(os.path.basename(f) for f in files)) + st.last_dir[mode] = os.path.dirname(files[0]) else: f = filedialog.askopenfilename( title='Select CSV file', initialdir=init, filetypes=[('CSV', '*.csv')]) if f: - self._selection = [f] - self._path_var.set(f) - self._last_dir[mode] = os.path.dirname(f) + st.selection = [f] + st.path_var.set(f) + st.last_dir[mode] = os.path.dirname(f) - def _validate_selection(self) -> str | None: - mode = self._mode.get() - sel = self._selection + # ── Validation / busy state / logging ──────────────────────────────────── + + def _validate_selection(self, tab_id: str) -> str | None: + st = self._tabs[tab_id] + mode = st.mode.get() + sel = st.selection if not sel: return 'No file selected' if mode in ('technique', 'setup') and len(sel) != 1: @@ -121,66 +150,76 @@ class _App: return f'{mode} requires a directory' return None - def _set_busy(self, busy: bool) -> None: - self._busy = busy + def _set_busy(self, tab_id: str, busy: bool) -> None: + st = self._tabs[tab_id] + st.busy = busy state = 'disabled' if busy else 'normal' - self._btn_generate.configure(state=state) - self._btn_rename.configure(state=state) + st.btn_generate.configure(state=state) + st.btn_rename.configure(state=state) - def _log_append(self, text: str) -> None: - self._log.configure(state='normal') - self._log.insert('end', text + '\n') - self._log.see('end') - self._log.configure(state='disabled') + def _log_append(self, tab_id: str, text: str) -> None: + w = self._tabs[tab_id].log_widget + w.configure(state='normal') + w.insert('end', text + '\n') + w.see('end') + w.configure(state='disabled') - def _poll_log(self) -> None: - while True: - try: - msg = self._log_queue.get_nowait() - except queue.Empty: - break - if msg is None: - self._set_busy(False) - else: - self._log_append(msg) - self._root.after(100, self._poll_log) + def _push(self, tab_id: str, msg: str) -> None: + self._tabs[tab_id].log_queue.put(msg) - def _push(self, msg: str) -> None: - self._log_queue.put(msg) + def _poll_all_logs(self) -> None: + for tab_id, st in self._tabs.items(): + while True: + try: + msg = st.log_queue.get_nowait() + except queue.Empty: + break + if msg is None: + self._set_busy(tab_id, False) + else: + self._log_append(tab_id, msg) + self._root.after(100, self._poll_all_logs) - def _on_generate(self) -> None: - err = self._validate_selection() + # ── Generate ───────────────────────────────────────────────────────────── + + def _on_generate(self, tab_id: str) -> None: + err = self._validate_selection(tab_id) if err: - self._log_append(f'✗ {err}') + self._log_append(tab_id, f'✗ {err}') return - mode = self._mode.get() - sel = list(self._selection) - lang = self._lang.get() - inc = self._include_prompt.get() - self._set_busy(True) - threading.Thread(target=self._worker_generate, args=(mode, sel, lang, inc), + st = self._tabs[tab_id] + mode = st.mode.get() + sel = list(st.selection) + lang = st.lang.get() + inc = st.include_prompt.get() + self._set_busy(tab_id, True) + threading.Thread(target=self._worker_generate, + args=(tab_id, mode, sel, lang, inc), daemon=True).start() - def _worker_generate(self, mode: str, sel: list[str], lang: str, inc: bool) -> None: + def _worker_generate(self, tab_id: str, mode: str, sel: list[str], + lang: str, inc: bool) -> None: try: + self._check_game_mismatch(tab_id, mode, sel) + if mode == 'technique': lap = load_lap(sel[0]) out = report_path(sel[0], 'technique') abs_path, tokens = report_technique.generate(lap, out, lang, inc) - self._push(f'✓ {abs_path} (~{tokens} tokens)') + self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)') elif mode == 'setup': lap = load_lap(sel[0]) out = report_path(sel[0], 'setup') abs_path, tokens = report_setup.generate(lap, out, lang, inc) - self._push(f'✓ {abs_path} (~{tokens} tokens)') + self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)') elif mode == 'compare': laps = [load_lap(f) for f in sel] stem_b = os.path.splitext(os.path.basename(sel[1]))[0] out = report_path(sel[0], 'compare', stem_b) abs_path, tokens = report_compare.generate(laps, out, lang, inc) - self._push(f'✓ {abs_path} (~{tokens} tokens)') + self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)') elif mode in ('race', 'profile'): race_dir = os.path.abspath(sel[0]) @@ -188,7 +227,7 @@ class _App: f for f in os.listdir(race_dir) if f.lower().endswith('.csv') ) if not csv_files: - self._push(f'✗ No CSV files found in {race_dir}') + self._push(tab_id, f'✗ No CSV files found in {race_dir}') return laps = [load_lap(os.path.join(race_dir, f)) for f in csv_files] dirname = os.path.basename(race_dir.rstrip('/\\')) @@ -196,39 +235,71 @@ class _App: out = os.path.join(race_dir, 'reports', dirname + suffix) gen_mod = report_race if mode == 'race' else report_profile abs_path, tokens = gen_mod.generate(laps, out, lang, inc) - self._push(f'✓ {abs_path} (~{tokens} tokens)') + self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)') except Exception as e: - self._push(f'✗ {e}') + self._push(tab_id, f'✗ {e}') finally: - self._log_queue.put(None) + self._tabs[tab_id].log_queue.put(None) - def _on_rename(self) -> None: - init = self._last_dir.get('rename', os.path.expanduser('~')) + def _check_game_mismatch(self, tab_id: str, mode: str, sel: list[str]) -> None: + """Warn (but don't block) when the file's game doesn't match the active tab.""" + try: + if mode in ('race', 'profile'): + race_dir = os.path.abspath(sel[0]) + csv_files = sorted( + f for f in os.listdir(race_dir) if f.lower().endswith('.csv') + ) + if not csv_files: + return + probe = os.path.join(race_dir, csv_files[0]) + else: + probe = sel[0] + detected = game_of(load_lap(probe)) + except Exception: + return # if probe fails, let the main worker surface the real error + + tab_game = tab_id # 'f1' or 'acc' + if detected != tab_game: + tab_label = _TAB_GAME[tab_id] + file_label = _TAB_GAME.get(detected, detected.upper()) + self._push(tab_id, + f'⚠ Selected file is {file_label}, but you are on the ' + f'{tab_label} tab — proceeding anyway.') + + # ── Rename ─────────────────────────────────────────────────────────────── + + def _on_rename(self, tab_id: str) -> None: + st = self._tabs[tab_id] + init = st.last_dir.get('rename', os.path.expanduser('~')) d = filedialog.askdirectory(title='Select folder with CSV files', initialdir=init) if not d: return - self._last_dir['rename'] = d - self._set_busy(True) - threading.Thread(target=self._worker_rename, args=(d,), daemon=True).start() + st.last_dir['rename'] = d + self._set_busy(tab_id, True) + threading.Thread(target=self._worker_rename, args=(tab_id, d), + daemon=True).start() - def _worker_rename(self, d: str) -> None: + def _worker_rename(self, tab_id: str, d: str) -> None: try: results = rename_unprocessed([d], races_dir=_RACES_DIR) if not results: - self._push('No CSV files found') + self._push(tab_id, 'No CSV files found') for r in results: if r.status == 'renamed': - new_display = os.path.relpath(r.new, _RACES_DIR) if r.new and r.new.startswith(_RACES_DIR) else r.new - self._push(f'renamed: {r.old} -> {new_display}') + new_display = (os.path.relpath(r.new, _RACES_DIR) + if r.new and r.new.startswith(_RACES_DIR) else r.new) + self._push(tab_id, f'renamed: {r.old} -> {new_display}') elif r.status == 'skipped': - self._push(f'skipped: {r.old} ({r.detail})') + self._push(tab_id, f'skipped: {r.old} ({r.detail})') else: - self._push(f'error: {r.old} ({r.detail})') + self._push(tab_id, f'error: {r.old} ({r.detail})') except Exception as e: - self._push(f'✗ {e}') + self._push(tab_id, f'✗ {e}') finally: - self._log_queue.put(None) + self._tabs[tab_id].log_queue.put(None) + + # ── Misc ───────────────────────────────────────────────────────────────── def _open_reports(self) -> None: target = _RACES_DIR if os.path.isdir(_RACES_DIR) else _PROJECT_ROOT diff --git a/telemetry/rename.py b/telemetry/rename.py index 7c44cbb..2c74753 100644 --- a/telemetry/rename.py +++ b/telemetry/rename.py @@ -4,19 +4,57 @@ from dataclasses import dataclass from .parser import _parse_header_row, _parse_value_row -_SESSION_PAT = re.compile(r'^(FP[1-3]|P[1-3]|Q[1-3]|R|Race|Sprint)$', re.IGNORECASE) +# Canonical token pattern — every token we ever insert must be listed here so +# _is_processed returns True on a second run (idempotency guarantee). +_SESSION_PAT = re.compile( + r'^(FP[1-3]|P[1-3]|P|Q[1-3]|SQ[1-3]|Q|R|Race|Sprint|HL|HS)$', + re.IGNORECASE, +) + +# Maps ACC full-word event values to compact tokens used in filenames. +# Prefix-match on lowercased value so "Practice 1" etc. also normalise. +# F1 short codes pass through unchanged (they won't match any prefix here). +_ACC_NORM: list[tuple[str, str]] = [ + ('practice', 'P'), + ('qualifying', 'Q'), + # Superpole is ACC's single-lap qualifying format → Q folder makes sense. + ('superpole', 'Q'), + ('hotlap', 'HL'), + ('hotstint', 'HS'), + ('race', 'R'), + ('sprint', 'Sprint'), +] + + +def _normalise_event(raw: str) -> str: + """Return the canonical session token for a raw event string. + + F1 short codes (P1, Q2, R, Sprint, FP1 …) are returned as-is. + ACC full-word values are mapped to compact tokens via prefix match. + """ + lo = raw.strip().lower() + for prefix, token in _ACC_NORM: + if lo.startswith(prefix): + return token + # Unknown / F1 code — pass through unchanged so F1 files still work. + return raw.strip() def _session_folder(event: str) -> str: + # event is already the canonical token at call sites inside this module. e = event.upper() - if re.match(r'^(FP[1-3]|P[1-3])$', e): + if re.match(r'^(FP[1-3]|P[1-3]|P)$', e): return 'Practice' - if re.match(r'^(SQ[1-3]|Q[1-3])$', e): + if re.match(r'^(SQ[1-3]|Q[1-3]|Q)$', e): return 'Qualifying' if re.match(r'^SPRINT$', e): return 'Sprint' if re.match(r'^(R|RACE)$', e): return 'Race' + if re.match(r'^HL$', e): + return 'Hotlap' + if re.match(r'^HS$', e): + return 'Hotstint' return 'Other' @@ -150,6 +188,7 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list if not session_type: results.append(RenameResult(name, None, 'error', 'session type not found in CSV')) continue + session_type = _normalise_event(session_type) new_name = insert_tokens(name, session_type, lap) dest = os.path.join(os.path.dirname(p), new_name) else: @@ -161,6 +200,7 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list if not session_type: results.append(RenameResult(name, None, 'error', 'session type not found in CSV')) continue + session_type = _normalise_event(session_type) new_name = name if _is_processed(stem) else insert_tokens(name, session_type, lap) dest_dir = os.path.join(races_dir, _safe_dirname(track_name), _session_folder(session_type)) dest = os.path.join(dest_dir, new_name) diff --git a/telemetry/report_common.py b/telemetry/report_common.py index 2798bf9..e71fa67 100644 --- a/telemetry/report_common.py +++ b/telemetry/report_common.py @@ -4,6 +4,10 @@ import tempfile from .parser import Lap, laptime_s, sector_times +def game_of(lap: Lap) -> str: + return 'acc' if lap.session.get('Game', '').upper().startswith('ACC') else 'f1' + + def header_block(lap: Lap) -> str: s = lap.session t = lap.track @@ -103,14 +107,21 @@ def write_report(path: str, text: str) -> tuple[str, int]: return os.path.abspath(path), tokens -def load_prompt(mode: str, lang: str) -> str: +def load_prompt(mode: str, lang: str, game: str = 'f1') -> str: here = os.path.dirname(__file__) - p = os.path.normpath(os.path.join(here, '..', 'prompts', lang, f'{mode}.md')) - try: - with open(p, encoding='utf-8') as f: - return f.read().strip() - except FileNotFoundError: - return '' + base = os.path.join(here, '..', 'prompts', lang) + # ACC gets its own prompt set, falling back to the F1 prompt when absent. + candidates = [] + if game == 'acc': + candidates.append(os.path.normpath(os.path.join(base, 'acc', f'{mode}.md'))) + candidates.append(os.path.normpath(os.path.join(base, f'{mode}.md'))) + for p in candidates: + try: + with open(p, encoding='utf-8') as f: + return f.read().strip() + except FileNotFoundError: + continue + return '' def report_path(input_path: str, mode: str, other_stem: str = '') -> str: diff --git a/telemetry/report_compare.py b/telemetry/report_compare.py index a358091..e6c9bf7 100644 --- a/telemetry/report_compare.py +++ b/telemetry/report_compare.py @@ -4,7 +4,7 @@ from .parser import Lap, laptime_s, sector_times from .segments import detect_corners, Corner from .resample import adaptive_points, sample_at from .report_common import ( - legend, md_table, write_report, token_estimate, load_prompt, + legend, md_table, write_report, token_estimate, load_prompt, game_of, LEGEND_DIST, LEGEND_SPD, LEGEND_DT_SEG, LEGEND_DT_CUM, ) @@ -230,8 +230,10 @@ def _delta_trace(laps: list[Lap], corners: list[Corner]) -> tuple[str, list[floa return block, final_deltas -def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: bool = True) -> tuple[str, int]: +def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: bool = True, + game: str | None = None) -> tuple[str, int]: lap_a = laps[0] + g = game or game_of(lap_a) corners = detect_corners(lap_a) head_table = _header_table(laps) @@ -257,7 +259,7 @@ def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: b ] text = '\n'.join(parts) if include_prompt: - prompt = load_prompt('compare', lang) + prompt = load_prompt('compare', lang, g) if prompt: text += '\n\n---\n\n' + prompt return write_report(out_path, text) diff --git a/telemetry/report_race.py b/telemetry/report_race.py index a2aaa98..3470cc6 100644 --- a/telemetry/report_race.py +++ b/telemetry/report_race.py @@ -1,5 +1,5 @@ from .parser import Lap, laptime_s, sector_times -from .report_common import md_table, write_report, load_prompt +from .report_common import md_table, write_report, load_prompt, game_of def _fmt_laptime(s: float) -> str: @@ -293,10 +293,12 @@ def _ers_table(laps: list[Lap]) -> str: return md_table(['Lap', 'ERS MJ', 'MGU-K MJ', 'MGU-H MJ'], rows) -def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: bool = True) -> tuple[str, int]: +def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: bool = True, + game: str | None = None) -> tuple[str, int]: laps = _dedup_laps(laps) laps = _sort_laps(laps) stints, ages = _assign_stints(laps) + g = game or (game_of(laps[0]) if laps else 'f1') parts = [ _header(laps), @@ -308,12 +310,12 @@ def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: b _tyre_wear_table(laps), '\n## Thermal summary per lap', _thermal_table(laps), - '\n## ERS per lap', - _ers_table(laps), ] + if g != 'acc': + parts += ['\n## ERS per lap', _ers_table(laps)] text = '\n'.join(parts) if include_prompt: - prompt = load_prompt('race', lang) + prompt = load_prompt('race', lang, g) if prompt: text += '\n\n---\n\n' + prompt return write_report(out_path, text) diff --git a/telemetry/report_setup.py b/telemetry/report_setup.py index 3212209..934d8c2 100644 --- a/telemetry/report_setup.py +++ b/telemetry/report_setup.py @@ -2,7 +2,7 @@ from .parser import Lap from .segments import detect_corners, Corner from .segments import _indices_in_range from .report_common import ( - header_block, legend, md_table, write_report, load_prompt, + header_block, legend, md_table, write_report, load_prompt, game_of, LEGEND_DIST, LEGEND_TIME, LEGEND_SPD, LEGEND_THR_BRK, LEGEND_STEER, LEGEND_GEAR, LEGEND_DRS, LEGEND_GLAT, LEGEND_GLON, LEGEND_FUEL, LEGEND_LOCK, LEGEND_SPIN, LEGEND_STEER_G_CAVEAT, LEGEND_SUSP, @@ -220,14 +220,23 @@ def _suspension_section(lap: Lap, corners: list[Corner]) -> str | None: return rake_line + '\n\n' + roll_table -def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = True) -> tuple[str, int]: +def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = True, + game: str | None = None) -> tuple[str, int]: + g = game or game_of(lap) corners = detect_corners(lap) susp = _suspension_section(lap, corners) + if g == 'acc': + setup_section = ('_ACC does not export car setup — analysis derived from telemetry below._' + if lang == 'en' else + '_ACC не экспортирует сетап — разбор баланса из телеметрии ниже._') + else: + setup_section = _full_setup_table(lap) + parts = [ header_block(lap), '\n## Setup', - _full_setup_table(lap), + setup_section, '\n## Corner balance', _corner_balance(lap, corners), '\n## Tyres', @@ -250,7 +259,7 @@ def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = T ] text = '\n'.join(parts) if include_prompt: - prompt = load_prompt('setup', lang) + prompt = load_prompt('setup', lang, g) if prompt: text += '\n\n---\n\n' + prompt return write_report(out_path, text) diff --git a/telemetry/report_technique.py b/telemetry/report_technique.py index b6d6519..08c4c3d 100644 --- a/telemetry/report_technique.py +++ b/telemetry/report_technique.py @@ -3,7 +3,7 @@ from .segments import detect_corners, straights, Corner, Straight from .resample import adaptive_points, sample_at, format_trace_row from .report_common import ( header_block, setup_summary, legend, md_table, write_report, report_path, - load_prompt, + load_prompt, game_of, LEGEND_DIST, LEGEND_TIME, LEGEND_SPD, LEGEND_THR_BRK, LEGEND_STEER, LEGEND_GEAR, LEGEND_DRS, LEGEND_GLAT, LEGEND_GLON, LEGEND_FUEL, LEGEND_ERS, LEGEND_LOCK, LEGEND_SPIN, LEGEND_TF, LEGEND_RPM, LEGEND_SLIP, LEGEND_YAW, @@ -56,7 +56,7 @@ def _straight_rows(ss: list[Straight]) -> list[list[str]]: return rows -def _driving_cost(lap: Lap, corners: list[Corner]) -> str: +def _driving_cost(lap: Lap, corners: list[Corner], game: str = 'f1') -> str: ch = lap.ch fuel = ch.get('FuelRemaining', []) fuel_used = (fuel[0] - fuel[-1]) if len(fuel) >= 2 else 0.0 @@ -94,7 +94,12 @@ def _driving_cost(lap: Lap, corners: list[Corner]) -> str: f'- Fuel used: {fuel_used:.2f} kg', '- Tyre wear Δ:', *wear_lines, - f'- ERS spent: {ers_mj:.3f} MJ | MGU-K harvested: {mguk_mj:.3f} MJ | MGU-H harvested: {mguh_mj:.3f} MJ', + ] + if game != 'acc': + lines.append( + f'- ERS spent: {ers_mj:.3f} MJ | MGU-K harvested: {mguk_mj:.3f} MJ | MGU-H harvested: {mguh_mj:.3f} MJ' + ) + lines += [ f'- Corners flagged LOCK: {lock_count} | SPIN: {spin_count}', f'- Worst lock%: {worst_lock_str} | Worst spin%: {worst_spin_str}', '- Max brake temps:', @@ -150,7 +155,9 @@ def _trace_block(lap: Lap, corners: list[Corner]) -> str: return '```\n' + header + '\n' + '\n'.join(rows) + '\n```' -def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = True) -> tuple[str, int]: +def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = True, + game: str | None = None) -> tuple[str, int]: + g = game or game_of(lap) corners = detect_corners(lap) ss = straights(corners, lap) @@ -163,15 +170,16 @@ def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = T _straight_rows(ss), ) - parts = [ - header_block(lap), - f'\n**Setup:** {setup_summary(lap)}', + parts = [header_block(lap)] + if g != 'acc': + parts.append(f'\n**Setup:** {setup_summary(lap)}') + parts += [ '\n## Corners', corner_table, '\n## Straights', straight_table, '\n## Driving cost (per lap)', - _driving_cost(lap, corners), + _driving_cost(lap, corners, g), '\n## Trace', _trace_block(lap, corners), '\n' + legend([ @@ -182,7 +190,7 @@ def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = T ] text = '\n'.join(parts) if include_prompt: - prompt = load_prompt('technique', lang) + prompt = load_prompt('technique', lang, g) if prompt: text += '\n\n---\n\n' + prompt return write_report(out_path, text) diff --git a/tests/conftest.py b/tests/conftest.py index 7d7cbdf..b120185 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,3 +18,13 @@ def truncated_path(): @pytest.fixture(scope="session") def sample_lap(sample_path): return load_lap(sample_path) + + +@pytest.fixture(scope="session") +def acc_path(): + return os.path.join(_FIXTURES, "acc_lap.csv") + + +@pytest.fixture(scope="session") +def acc_lap(acc_path): + return load_lap(acc_path) diff --git a/tests/fixtures/acc_lap.csv b/tests/fixtures/acc_lap.csv new file mode 100644 index 0000000..e6eccf1 --- /dev/null +++ b/tests/fixtures/acc_lap.csv @@ -0,0 +1,40 @@ +player,v8,???,0,2026061914565357 +Game,version,date,track,car,event,laptime [s],S1 [s],S2 [s],S3 [s],S4+ [s] +ACC,1.1,2026-06-19 12:01:23,Hungaroring,CaR,Practice,123.702,43.470,45.950,34.282 +TrackID [int],Tracklen [m],EventID [int],EventID2 [int],Weather [txt],TeamID [int],Tyre [txt],Valid [b],Pitlap [b],Online [b],Lap [int],LapsInRace [int],Fuel at Start [kg],MaxGears [int],MaxRevs [rpm],IdleRevs [rpm},TrackTemp [C],AmbientTemp [C],Windspeed [m/s],WindDirection [ ] +7,4381.009765625,0,50,Clear,57,DHA,false,false,false,1,20,60.019676208496094,6,6500.0,1950.0001,27.946645736694336,27.15789031982422,2.18314528465271,75.69888305664062 +FWing,RWing,OnThrottle,OffThrottle,FrontCamber,RearCamber,FrontToe,RearToe,FrontSusp,RearSusp,FrontAntiRoll,RearAntiRoll,FrontSuspH,RearSuspH,BrakePressure,BrakeBias,FLTyrePressure,FRTyrePressure,RLTyrePressure,RRTyrePressure,Ballast,FuelLoad +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +LapDistance [m],TotalDistance [m],LapTime [s],Sector1Time [s],Sector2Time [s],Speed [km/h],EngineRevs [rpm],ThrottlePercentage [%],BrakePercentage [%],Steer [%],Clutch [%],Gear [int],DRS [0/1],CanUseDRS [0/1],KERSLevel [J],EngineTemperature [C],RacePosition [int],Sector [int],InPits [int],FuelRemaining [l],CurrentFlag [int],CurrentLapInvalid [int],X [m],Y [m],Z [m],GForceLatitudinal [G],GForceLongitudinal [G],GForceVertical [G],WheelSpeedRearLeft [km/h],WheelSpeedRearRight [km/h],WheelSpeedFrontLeft [km/h],WheelSpeedFrontRight [km/h],BrakeTemperatureRearLeft [C],BrakeTemperatureRearRight [C],BrakeTemperatureFrontLeft [C],BrakeTemperatureFrontRight [C],TyreTemperatureRearLeft [C],TyreTemperatureRearRight [C],TyreTemperatureFrontLeft [C],TyreTemperatureFrontRight [C],TyreWearRearLeft [%],TyreWearRearRight [%],TyreWearFrontLeft [%],TyreWearFrontRight [%],SuspensionPositionRearLeft [m],SuspensionPositionRearRight [m],SuspensionPositionFrontLeft [m],SuspensionPositionFrontRight [m],SuspensionVelocityRearLeft [m/s],SuspensionVelocityRearRight [m/s],SuspensionVelocityFrontLeft [m/s],SuspensionVelocityFrontRight [m/s],SuspensionAccelerationRearLeft [m/s^2],SuspensionAccelerationRearRight [m/s^2],SuspensionAccelerationFrontLeft [m/s^2],SuspensionAccelerationFrontRight [m/s^2],MGUKHarvested [J],MGUHHarvested [J],ERSSpent [J],ERSMode [int],FuelMixMode [int],Yaw [rad],Roll [rad],Pitch [rad],TyreCarcassTemperatureRearLeft [C],TyreCarcassTemperatureRearRight [C],TyreCarcassTemperatureFrontLeft [C],TyreCarcassTemperatureFrontRight [C],TyrePressureRearLeft [PSI],TyrePressureRearRight [PSI],TyrePressureFrontLeft [PSI],TyrePressureFrontRight [PSI],WheelSlipRearLeft [%],WheelSlipRearRight [%],WheelSlipFrontLeft [%],WheelSlipFrontRight [%],Torque [Nm],Handbrake [%],RearLeftInside [C],RearLeftMiddle [C],RearLeftOutside [C],RearRightInside [C],RearRightMiddle [C],RearRightOutside [C],FrontLeftInside [C],FrontLeftMiddle [C],FrontLeftOutside [C],FrontRightInside [C],FrontRightMiddle [C],FrontRightOutside [C],WoldSpeedX [km/h],WoldSpeedY [km/h],WoldSpeedZ [km/h],LocalAngularVelocityX [rad/s],LocalAngularVelocityY [rad/s],LocalAngularVelocityZ [rad/s],IcePower [W],MgukPower [W],FrontRideHeight [m],RearRideHeight [m],LoadRearLeft [N],LoadRearRight [N],LoadFrontLeft [N],LoadFrontRight [N],Extra1 [m],Extra2 [],Extra3 [],Extra4 [] +11.52857,4036.4016,0.282,0.0,0.0,169.64093,5723.0,100.0,0.0,-0.010964928,0.0,4.0,0.0,0.0,0.0,84.686035,1.0,0.0,0.0,60.019676,0.0,1.0,-109.23591,24.842262,544.1296,-0.036001448,0.2483705,0.025862258,173.77263,173.87315,169.28203,169.32233,305.99573,304.57455,401.18625,396.96573,73.76696,69.08077,70.795746,65.185524,0.0,0.0,0.0,0.0,0.034967814,0.034488626,0.013857091,0.013722197,2.075326E-5,1.89149E-5,-7.246143E-6,-2.5508643E-6,-1.3962284E-7,-1.2725481E-7,4.8750273E-8,1.716159E-8,0.0,0.0,0.0,-1.0,1.0,-2.1652644,-0.03211098,0.0017947316,73.76696,69.08077,70.795746,65.185524,25.10461,24.530798,25.619375,25.080778,0.024355568,0.024948094,-0.0021156704,-0.0018781277,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.042892,0.0030937572,26.396013,-0.0044563767,-5.508619E-4,0.013827926,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +12.014709,4036.7551,0.29,0.0,0.0,169.72768,5726.0,100.0,0.0,0.0018527993,0.0,4.0,0.0,0.0,0.0,84.688995,1.0,0.0,0.0,60.0194,0.0,1.0,-109.5287,24.84228,543.93164,-0.023693023,0.24786447,0.012986496,174.02707,173.90872,169.38522,169.4204,305.94714,304.52625,401.0366,396.81787,73.76947,69.08246,70.79732,65.18582,0.0,0.0,0.0,0.0,0.034994584,0.03462096,0.0141735785,0.013794035,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,1.0,-2.1652737,-0.03226933,0.0018673544,73.76947,69.08246,70.79732,65.18582,25.10485,24.53096,25.619526,25.080807,0.025331153,0.024633894,-0.0020176482,-0.0018104152,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.061405,0.0047848364,26.411741,-0.009338445,-8.7213836E-4,0.017504098,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +12.338801,4037.1086,0.3,0.0,0.0,169.79353,5729.0,100.0,0.0,-0.005238485,0.0,4.0,0.0,0.0,0.0,84.691216,1.0,0.0,0.0,60.019196,0.0,1.0,-109.82159,24.84231,543.73364,-0.019859513,0.24973202,0.0029058661,174.1567,173.91837,169.62521,169.51479,305.89853,304.47787,400.88684,396.66995,73.771355,69.083725,70.79849,65.18604,0.0,0.0,0.0,0.0,0.034860697,0.034790497,0.014345023,0.013859233,-0.0059509445,0.016770637,0.027107334,0.0076130964,-0.33060804,0.931702,1.505963,0.4229498,0.0,0.0,0.0,-1.0,1.0,-2.1652846,-0.03241021,0.0019555911,73.771355,69.083725,70.79849,65.18604,25.105034,24.531082,25.61964,25.08083,0.025696889,0.024293179,-9.913363E-4,-0.0016416886,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.07583,0.0050230986,26.423271,-0.012692683,-0.0012003235,0.017881883,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +12.82494,4037.5803,0.307,0.0,0.0,169.86078,5732.0,100.0,0.0,-0.0020330295,0.0,4.0,0.0,0.0,0.0,84.693436,1.0,0.0,0.0,60.01899,0.0,1.0,-110.21227,24.842361,543.4695,-0.015500802,0.25483412,-0.035387367,174.24815,174.01012,169.7397,169.63368,305.89853,304.47787,400.88684,396.66995,73.77323,69.08499,70.799644,65.18625,0.0,0.0,0.0,0.0,0.034660157,0.034980144,0.014283132,0.01383878,-0.019672163,0.021128532,0.006444314,0.002632082,-1.157186,1.2428548,0.3790773,0.15482835,0.0,0.0,0.0,-1.0,1.0,-2.1652973,-0.03250376,0.0020536045,73.77323,69.08499,70.799644,65.18625,25.105213,24.531204,25.619751,25.080849,0.025829243,0.024427839,-7.1281136E-4,-0.0013369576,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.090744,0.0024308157,26.434696,-0.012943774,-0.0012463271,0.009102043,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +13.149033,4037.9343,0.315,0.0,0.0,169.9762,5737.0,100.0,0.0,0.009160281,0.0,4.0,0.0,0.0,0.0,84.697136,1.0,0.0,0.0,60.018642,0.0,1.0,-110.50542,24.842388,543.27124,-0.032200195,0.26522526,-0.046108786,173.96013,174.05067,169.72847,169.74197,305.8012,304.38113,400.5873,396.37408,73.77638,69.087105,70.80156,65.1866,0.0,0.0,0.0,0.0,0.03464579,0.035214636,0.013901675,0.013589289,-0.014327218,0.028275946,-0.029556515,-0.017996257,-0.95514786,1.885063,-1.9704343,-1.1997504,0.0,0.0,0.0,-1.0,1.0,-2.1653147,-0.032495715,0.0021680715,73.77638,69.087105,70.80156,65.1866,25.105516,24.531408,25.619934,25.080883,0.02343825,0.023970794,-0.0014574057,-0.0013779213,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.115627,-0.0038357505,26.455435,-0.005286799,-0.0010240518,-0.0044617346,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +13.4731245,4038.2883,0.325,0.0,0.0,170.04463,5738.0,100.0,0.0,0.016547244,0.0,4.0,0.0,0.0,0.0,84.699356,1.0,0.0,0.0,60.018436,0.0,1.0,-110.798676,24.842382,543.0729,-0.026762703,0.25481597,-0.0077182837,174.0757,174.09074,169.71399,169.76973,305.8012,304.38113,400.5873,396.37408,73.778275,69.08838,70.80272,65.186806,0.0,0.0,0.0,0.0,0.03487389,0.03536873,0.013778092,0.013468641,0.011874156,0.021588057,-0.028057748,-0.020563291,0.65967536,1.1993365,-1.5587639,-1.142405,0.0,0.0,0.0,-1.0,1.0,-2.165323,-0.03248412,0.0021698782,73.778275,69.08838,70.80272,65.186806,25.105698,24.53153,25.620045,25.080902,0.023705985,0.023794403,-0.0019443971,-0.0016166419,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.130116,-0.0042215604,26.467607,0.0018667248,-0.001168449,9.254145E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +13.959263,4038.7605,0.332,0.0,0.0,170.1122,5739.0,100.0,0.0,0.0083989715,0.0,4.0,0.0,0.0,0.0,84.70158,1.0,0.0,0.0,60.018227,0.0,1.0,-111.18986,24.842344,542.80835,-0.027512636,0.25494838,0.019121999,174.11198,174.16544,169.7345,169.77376,305.75256,304.33273,400.4375,396.2261,73.78017,69.089645,70.80388,65.18702,0.0,0.0,0.0,0.0,0.03509519,0.035522744,0.013817208,0.013471926,0.026435317,0.018123975,-0.004968661,-0.00690373,1.5550187,1.0661162,-0.29227418,-0.40610176,0.0,0.0,0.0,-1.0,1.0,-2.1653323,-0.032541778,0.0021323483,73.78017,69.089645,70.80388,65.18702,25.105879,24.531652,25.620157,25.080923,0.023512656,0.023826884,-0.0022203415,-0.001989509,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.144463,-0.0028066994,26.4798,0.006246484,-0.0014940045,0.0111888945,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +14.283356,4039.1147,0.342,0.0,0.0,170.17862,5739.0,100.0,0.0,-0.0034686916,0.0,4.0,0.0,0.0,0.0,84.7038,1.0,0.0,0.0,60.01802,0.0,1.0,-111.48337,24.842316,542.6098,-0.028546177,0.24940713,0.07171454,174.1544,174.22723,169.84029,169.7952,305.70386,304.28433,400.28775,396.07816,73.78207,69.09092,70.805046,65.18724,0.0,0.0,0.0,0.0,0.035337783,0.035671897,0.014006146,0.013649582,0.027287751,0.017833402,0.013414934,0.010643593,1.6051619,1.0490236,0.78911376,0.6260937,0.0,0.0,0.0,-1.0,1.0,-2.165343,-0.032671917,0.0020802144,73.78207,69.09092,70.805046,65.18724,25.106062,24.531775,25.62027,25.080944,0.023362413,0.02379034,-0.001988125,-0.0022530768,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.158398,0.002200487,26.491964,0.0066807917,-0.001680956,0.019356037,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +14.769494,4039.5874,0.35,0.0,0.0,170.26718,5740.0,100.0,0.0,2.7964596E-4,0.0,4.0,0.0,0.0,0.0,84.70676,1.0,0.0,0.0,60.017742,0.0,1.0,-111.87488,24.84231,542.3449,-0.024774725,0.25113165,0.080288865,174.34059,174.34473,170.01518,169.89398,305.65518,304.23593,400.13797,395.93024,73.784584,69.092606,70.80659,65.187546,0.0,0.0,0.0,0.0,0.03564696,0.03586361,0.014187679,0.013973102,0.030653965,0.0189371,0.020581711,0.02784313,1.702998,1.0520611,1.1434284,1.5468405,0.0,0.0,0.0,-1.0,1.0,-2.1653576,-0.03284161,0.0020180475,73.784584,69.092606,70.80659,65.187546,25.106304,24.531937,25.620417,25.080975,0.023923587,0.023947949,-0.0014799545,-0.002191845,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.17719,0.009847768,26.507994,0.005680933,-0.00166586,0.014170732,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +15.253259,4039.9421,0.357,0.0,0.0,170.3318,5741.0,100.0,0.0,-0.0037863501,0.0,4.0,0.0,0.0,0.0,84.70898,1.0,0.0,0.0,60.017536,0.0,1.0,-112.16864,24.842356,542.1462,-0.01671688,0.24182548,0.051545005,174.62819,174.5305,170.05142,169.97345,305.6065,304.1875,399.98825,395.7823,73.78646,69.09387,70.80774,65.187775,0.0,0.0,0.0,0.0,0.035722785,0.035942208,0.014252585,0.014245591,0.025666753,0.01802072,0.016429275,0.039733946,1.7111169,1.2013813,1.095285,2.6489298,0.0,0.0,0.0,-1.0,1.0,-2.16537,-0.032910272,0.001991446,73.78646,69.09387,70.80774,65.187775,25.106483,24.532059,25.62053,25.080997,0.025223643,0.024650058,-0.0016460561,-0.0021038703,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.191162,0.013625955,26.519114,0.002056227,-0.0017836776,0.0065005226,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +15.567857,4040.2969,0.367,0.0,0.0,170.39563,5744.0,100.0,0.0,2.2442082E-4,0.0,4.0,0.0,0.0,0.0,84.7112,1.0,0.0,0.0,60.01733,0.0,1.0,-112.4625,24.84244,541.9473,-0.01776832,0.24116075,0.024250237,174.77617,174.67091,170.07419,170.066,305.6065,304.1875,399.98825,395.7823,73.78833,69.09513,70.8089,65.18801,0.0,0.0,0.0,0.0,0.035606816,0.03589045,0.01432358,0.014522852,-0.0023613959,0.0015786466,0.007994199,0.032338258,-0.13890564,0.09286156,0.470247,1.9022504,0.0,0.0,0.0,-1.0,1.0,-2.1653836,-0.03292371,0.0020090973,73.78833,69.09513,70.8089,65.18801,25.106663,24.532179,25.62064,25.081018,0.025708094,0.025090385,-0.0018864868,-0.0019345023,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.20509,0.015505659,26.53018,-0.004986443,-0.0016786046,-3.4433513E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +16.039753,4040.7703,0.375,0.0,0.0,170.45953,5748.0,100.0,0.0,0.0040749414,0.0,4.0,0.0,0.0,0.0,84.71342,1.0,0.0,0.0,60.01712,0.0,1.0,-112.854485,24.842587,541.68207,-0.018202942,0.2413529,0.017172432,174.78044,174.73691,170.10362,170.1539,305.5578,304.1391,399.8385,395.6344,73.79021,69.09639,70.81005,65.18824,0.0,0.0,0.0,0.0,0.03545997,0.035773963,0.0144413635,0.014785069,-0.014600862,-0.00934696,0.010487676,0.029970996,-0.811159,-0.51927555,0.58264863,1.6650554,0.0,0.0,0.0,-1.0,1.0,-2.1653988,-0.032894265,0.002088995,73.79021,69.09639,70.81005,65.18824,25.106844,24.532299,25.62075,25.081041,0.025348512,0.025093224,-0.002087924,-0.0017930274,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.21901,0.016820235,26.541273,-0.013632805,-0.0016907271,-0.0055084135,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +16.354351,4041.1255,0.382,0.0,0.0,170.54517,5755.0,100.0,0.0,0.0012987609,0.0,4.0,0.0,0.0,0.0,84.71641,1.0,0.0,0.0,60.016846,0.0,1.0,-113.14859,24.84271,541.48303,-0.022501389,0.2451741,0.0393255,174.67075,174.71301,170.24464,170.33131,305.5091,304.09067,399.68878,395.48648,73.79271,69.09807,70.81159,65.18854,0.0,0.0,0.0,0.0,0.035401274,0.035641193,0.0146766985,0.015073612,-0.013702859,-0.01661703,0.023541227,0.03671733,-0.913524,-1.107802,1.5694151,2.4478219,0.0,0.0,0.0,-1.0,1.0,-2.1654234,-0.032835644,0.002294316,73.79271,69.09807,70.81159,65.18854,25.107084,24.532461,25.6209,25.08107,0.024190536,0.024438366,-0.0017621501,-0.0012539756,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.237606,0.02003613,26.556547,-0.02424377,-0.0018405571,-0.00504553,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +16.66895,4041.4807,0.392,0.0,0.0,170.63432,5761.0,100.0,0.0,-0.012933018,0.0,4.0,0.0,0.0,0.0,84.7194,1.0,0.0,0.0,60.016567,0.0,1.0,-113.4428,24.842848,541.28394,-0.02279238,0.25629854,0.0285197,174.57756,174.62965,170.44597,170.5077,305.46036,304.0422,399.539,395.33853,73.79522,69.099754,70.81311,65.18882,0.0,0.0,0.0,0.0,0.035602823,0.035672452,0.014721351,0.0151250595,0.00840316,-0.005971202,0.016469836,0.01999944,0.49430352,-0.3512472,0.9688139,1.1764377,0.0,0.0,0.0,-1.0,1.0,-2.1654518,-0.032781146,0.0025523542,73.79522,69.099754,70.81311,65.18882,25.107327,24.532623,25.621044,25.081099,0.023109358,0.023414625,-0.0011038778,-7.420546E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.257008,0.022770366,26.57248,-0.025350533,-0.0020437962,-0.00586406,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +17.140846,4041.9546,0.4,0.0,0.0,170.70256,5763.0,100.0,0.0,-0.021631196,0.0,4.0,0.0,0.0,0.0,84.72164,1.0,0.0,0.0,60.016357,0.0,1.0,-113.83525,24.843058,541.0183,-0.01922873,0.25786906,0.008813665,174.71027,174.66154,170.55545,170.61969,305.4116,303.9937,399.3892,395.19055,73.797104,69.10103,70.81424,65.18902,0.0,0.0,0.0,0.0,0.035794284,0.035845757,0.014541981,0.014907652,0.021833926,0.011364619,-0.007484315,-0.009220041,1.2129959,0.63136774,-0.41579527,-0.5122245,0.0,0.0,0.0,-1.0,1.0,-2.165474,-0.032745425,0.0027118518,73.797104,69.10103,70.81424,65.18902,25.107508,24.532745,25.621153,25.081116,0.023477638,0.02319233,-8.6181273E-4,-4.8549226E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.271946,0.0233458,26.584316,-0.01879117,-0.0023562151,-0.003961567,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +17.612743,4042.3103,0.41,0.0,0.0,170.79535,5764.0,100.0,0.0,-0.05187564,0.0,4.0,0.0,0.0,0.0,84.72463,1.0,0.0,0.0,60.01608,0.0,1.0,-114.129715,24.843231,540.819,-0.016898781,0.26488048,-0.03438637,174.85413,174.76385,170.60626,170.6618,305.36285,303.94522,399.23935,395.0425,73.79961,69.10272,70.81574,65.18927,0.0,0.0,0.0,0.0,0.03597383,0.036112472,0.014156679,0.014442531,0.020611411,0.024445562,-0.03137067,-0.03791823,1.1450784,1.3580867,-1.742815,-2.1065683,0.0,0.0,0.0,-1.0,1.0,-2.1655045,-0.032718446,0.0028147027,73.79961,69.10272,70.81574,65.18927,25.107746,24.53291,25.621298,25.08114,0.023764038,0.023235457,-0.0011070737,-7.818763E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.29251,0.020403259,26.600136,-0.005004784,-0.002925523,-0.0024073077,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +18.08464,4042.7847,0.417,0.0,0.0,170.84209,5764.0,100.0,0.0,-0.05187564,0.0,4.0,0.0,0.0,0.0,84.72613,1.0,0.0,0.0,60.01594,0.0,1.0,-114.522514,24.843454,540.5531,-0.017355524,0.2645813,-0.028130485,174.8914,174.82014,170.62549,170.66968,305.36285,303.94522,399.23935,395.0425,73.80086,69.10358,70.81649,65.1894,0.0,0.0,0.0,0.0,0.03605872,0.036253184,0.013992093,0.014216232,0.01555506,0.023966327,-0.032346368,-0.040671732,0.9150035,1.409784,-1.9027275,-2.3924549,0.0,0.0,0.0,-1.0,1.0,-2.1655192,-0.032705612,0.0028123893,73.80086,69.10358,70.81649,65.1894,25.107868,24.532991,25.621368,25.081152,0.023702193,0.023285033,-0.001267824,-0.0010091378,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.3028,0.019186966,26.608093,0.0021187798,-0.002998144,-0.0026582738,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +18.399239,4043.1406,0.427,0.0,0.0,170.9358,5763.0,100.0,0.0,-0.058447283,0.0,4.0,0.0,0.0,0.0,84.72912,1.0,0.0,0.0,60.015663,0.0,1.0,-114.817245,24.843603,540.3536,-0.017351182,0.26599306,-0.019857155,174.96233,174.9395,170.75694,170.68292,305.31406,303.8967,399.08948,394.89447,73.80337,69.10527,70.81798,65.18965,0.0,0.0,0.0,0.0,0.036252815,0.036529295,0.013673156,0.01382406,0.016410999,0.024518983,-0.028442537,-0.036380693,0.9653529,1.4422932,-1.6730905,-2.1400409,0.0,0.0,0.0,-1.0,1.0,-2.1655464,-0.032651495,0.00270092,73.80337,69.10527,70.81798,65.18965,25.108109,24.533154,25.621513,25.081179,0.023555756,0.023422193,-0.0010463779,-0.001479471,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.323437,0.017381918,26.624083,0.01657727,-0.0030503247,-0.008598872,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +18.871134,4043.6155,0.435,0.0,0.0,171.02904,5762.0,100.0,0.0,-0.05161566,0.0,4.0,0.0,0.0,0.0,84.73211,1.0,0.0,0.0,60.015385,0.0,1.0,-115.2104,24.843784,540.0874,-0.019829765,0.2624005,-0.0019270547,175.03053,175.09914,170.74245,170.69574,305.26523,303.84818,398.93958,394.7464,73.80588,69.106964,70.819466,65.18992,0.0,0.0,0.0,0.0,0.036502145,0.036762293,0.013357432,0.013613336,0.024634724,0.028283853,-0.03525894,-0.03349424,1.3685958,1.5713252,-1.9588301,-1.8607912,0.0,0.0,0.0,-1.0,1.0,-2.165567,-0.03249422,0.0024466952,73.80588,69.106964,70.819466,65.18992,25.10835,24.533318,25.621655,25.081203,0.023396553,0.02379777,-0.0016757208,-0.0019487994,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.34369,0.01690556,26.640028,0.030272061,-0.0027883833,-0.018927325,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +19.185734,4043.9717,0.442,0.0,0.0,171.09583,5763.0,100.0,0.0,-0.044326268,0.0,4.0,0.0,0.0,0.0,84.73435,1.0,0.0,0.0,60.01518,0.0,1.0,-115.5054,24.84391,539.88763,-0.01922547,0.24736242,0.008626948,175.27032,175.32149,170.73315,170.74261,305.21643,303.79965,398.7897,394.59833,73.807755,69.10822,70.820595,65.190125,0.0,0.0,0.0,0.0,0.036639597,0.036803745,0.013291526,0.013668455,0.025785467,0.018296639,-0.025441995,-0.01037363,1.7190311,1.2197759,-1.696133,-0.6915753,0.0,0.0,0.0,-1.0,1.0,-2.1655786,-0.03235155,0.002190889,73.807755,69.10822,70.820595,65.190125,25.10853,24.533438,25.621765,25.081223,0.024398552,0.024697684,-0.002119682,-0.0020644295,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.35799,0.017505331,26.651459,0.035164107,-0.0026199582,-0.018351326,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +19.657629,4044.3281,0.452,0.0,0.0,171.18163,5767.0,100.0,0.0,-0.042977434,0.0,4.0,0.0,0.0,0.0,84.73734,1.0,0.0,0.0,60.0149,0.0,1.0,-115.80052,24.844038,539.6878,-0.021774048,0.23947896,-0.0031411645,175.5444,175.60716,170.78072,170.87808,305.16763,303.7511,398.6399,394.45035,73.810234,69.10989,70.822105,65.1904,0.0,0.0,0.0,0.0,0.036588937,0.0365657,0.013468195,0.013896169,0.005105401,-0.011564396,0.006515478,0.016637256,0.3003177,-0.68025863,0.3832634,0.97866213,0.0,0.0,0.0,-1.0,1.0,-2.165594,-0.032213278,0.0018545655,73.810234,69.10989,70.822105,65.1904,25.108768,24.5336,25.62191,25.081251,0.02548629,0.02585287,-0.0023420742,-0.0017732137,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.376392,0.0173921,26.666403,0.031326007,-0.002644165,-0.008772555,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +20.129528,4044.8035,0.46,0.0,0.0,171.24388,5771.0,100.0,0.0,-0.047827054,0.0,4.0,0.0,0.0,0.0,84.739586,1.0,0.0,0.0,60.01469,0.0,1.0,-116.19417,24.844215,539.42126,-0.022482496,0.2338695,-0.03322093,175.69981,175.77182,170.86798,170.89658,305.1188,303.70258,398.4901,394.3024,73.812096,69.11114,70.82324,65.190605,0.0,0.0,0.0,0.0,0.036385402,0.03626933,0.013675446,0.014058191,-0.014121954,-0.029689737,0.02132889,0.021652007,-0.784553,-1.6494298,1.1849384,1.2028893,0.0,0.0,0.0,-1.0,1.0,-2.1656082,-0.032209214,0.0016591968,73.812096,69.11114,70.82324,65.190605,25.108948,24.533718,25.622019,25.08127,0.026021011,0.026441412,-0.002195113,-0.0020281922,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.38975,0.01505751,26.677374,0.022958325,-0.0027453755,0.0028090784,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +20.447374,4045.1602,0.47,0.0,0.0,171.3059,5778.0,100.0,0.0,-0.062244527,0.0,4.0,0.0,0.0,0.0,84.74183,1.0,0.0,0.0,60.01448,0.0,1.0,-116.489525,24.844341,539.2212,-0.02323555,0.23502758,-0.034066625,175.73271,175.83405,170.9717,170.9541,305.06998,303.65402,398.34036,394.15442,73.81395,69.11238,70.82438,65.19081,0.0,0.0,0.0,0.0,0.036146622,0.035968594,0.013882383,0.0142436335,-0.02457305,-0.03317247,0.023010446,0.019303575,-1.3651695,-1.8429149,1.2783581,1.0724208,0.0,0.0,0.0,-1.0,1.0,-2.1656253,-0.03225248,0.001537294,73.81395,69.11238,70.82438,65.19081,25.109125,24.533836,25.622128,25.08129,0.025841586,0.026433097,-0.0019509464,-0.0020535914,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.40316,0.0125868805,26.688421,0.012787492,-0.0027570797,0.0064510647,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +20.925768,4045.636,0.477,0.0,0.0,171.36821,5784.0,100.0,0.0,-0.06566952,0.0,4.0,0.0,0.0,0.0,84.744095,1.0,0.0,0.0,60.01427,0.0,1.0,-116.88349,24.844479,538.9544,-0.020693334,0.23424439,-0.032882582,175.83826,175.87889,171.07758,171.0785,305.06998,303.65402,398.34036,394.15442,73.8158,69.113625,70.8255,65.191025,0.0,0.0,0.0,0.0,0.03591611,0.03568515,0.014066943,0.014416676,-0.027605496,-0.034363393,0.023029251,0.021087334,-1.6238527,-2.0213761,1.3546618,1.2404314,0.0,0.0,0.0,-1.0,1.0,-2.1656444,-0.032297246,0.0014890591,73.8158,69.113625,70.8255,65.191025,25.109304,24.533957,25.622236,25.081312,0.026084458,0.026321536,-0.001695974,-0.001690497,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.416653,0.009974838,26.699299,0.0031771893,-0.0027551234,0.0058646416,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +21.244698,4045.993,0.485,0.0,0.0,171.4524,5792.0,100.0,0.0,-0.073485024,0.0,4.0,0.0,0.0,0.0,84.747116,1.0,0.0,0.0,60.013992,0.0,1.0,-117.17908,24.84456,538.75415,-0.014153696,0.24148393,-0.06599551,175.97643,175.8869,171.22888,171.17216,305.02115,303.60547,398.19058,394.00647,73.81827,69.11529,70.826996,65.19129,0.0,0.0,0.0,0.0,0.035547975,0.03535814,0.014167956,0.014536584,-0.03990978,-0.040697057,0.01903822,0.01953002,-2.660652,-2.7131371,1.2692147,1.3020014,0.0,0.0,0.0,-1.0,1.0,-2.1656735,-0.032350145,0.0015241038,73.81827,69.11529,70.826996,65.19129,25.10954,24.534117,25.62238,25.081337,0.026386462,0.025864357,-0.0013036191,-0.0016344176,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.43546,0.003714017,26.713604,-0.0072334763,-0.0028177002,0.0038037421,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +21.723093,4046.35,0.495,0.0,0.0,171.518,5796.0,100.0,0.0,-0.07679934,0.0,4.0,0.0,0.0,0.0,84.74938,1.0,0.0,0.0,60.013783,0.0,1.0,-117.47478,24.844614,538.5539,-0.013632373,0.25245753,-0.06559541,175.91951,175.82158,171.30203,171.30792,304.9723,303.5569,398.04077,393.8585,73.82012,69.11654,70.8281,65.19148,0.0,0.0,0.0,0.0,0.03531124,0.03522051,0.0140859345,0.01452019,-0.033603773,-0.025813363,0.001055085,0.005750762,-1.8668762,-1.4340757,0.058615834,0.31948677,0.0,0.0,0.0,-1.0,1.0,-2.165697,-0.032351553,0.001600171,73.82012,69.11654,70.8281,65.19148,25.109718,24.534237,25.622486,25.081356,0.025662057,0.02509104,-0.0012592374,-0.0012248752,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.45033,-0.0011681489,26.724682,-0.011095868,-0.0028733583,-0.0014085505,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +22.201488,4046.8264,0.502,0.0,0.0,171.6084,5798.0,100.0,0.0,-0.083627366,0.0,4.0,0.0,0.0,0.0,84.7524,1.0,0.0,0.0,60.013504,0.0,1.0,-117.8692,24.844625,538.2867,-0.013671633,0.25631142,-0.057238538,175.86954,175.81245,171.36868,171.37608,304.92343,303.5083,397.8909,393.71042,73.8226,69.11822,70.829575,65.191734,0.0,0.0,0.0,0.0,0.035153784,0.035233572,0.013865768,0.014293048,-0.023187742,-0.0073274267,-0.01777577,-0.014325659,-1.3639848,-0.4310251,-1.0456336,-0.8426858,0.0,0.0,0.0,-1.0,1.0,-2.16573,-0.032319028,0.0017003558,73.8226,69.11822,70.829575,65.191734,25.109957,24.534399,25.622627,25.081379,0.024830593,0.024497962,-0.0013969055,-0.0013537349,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.47048,-0.00694992,26.739777,-0.008831456,-0.003015354,-0.004377713,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +22.520418,4047.1838,0.51,0.0,0.0,171.67668,5797.0,100.0,0.0,-0.08616711,0.0,4.0,0.0,0.0,0.0,84.75467,1.0,0.0,0.0,60.013294,0.0,1.0,-118.16515,24.844593,538.0862,-0.019227957,0.2582171,-0.044518888,175.88156,175.88544,171.37125,171.43143,304.8745,303.45966,397.741,393.56235,73.82446,69.11948,70.83068,65.191925,0.0,0.0,0.0,0.0,0.035166744,0.035313994,0.013727826,0.014096176,-0.009633104,0.0062321625,-0.023873895,-0.028267564,-0.64220697,0.41547748,-1.591593,-1.8845043,0.0,0.0,0.0,-1.0,1.0,-2.1657543,-0.032279488,0.0017462259,73.82446,69.11948,70.83068,65.191925,25.110136,24.53452,25.622734,25.081398,0.024492983,0.024515623,-0.0017791511,-0.0014285297,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.48551,-0.010167306,26.75147,-0.004878632,-0.0030249038,-0.005207439,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +22.839348,4047.5415,0.517,0.0,0.0,171.74548,5795.0,100.0,0.0,-0.08860586,0.0,4.0,0.0,0.0,0.0,84.756935,1.0,0.0,0.0,60.013084,0.0,1.0,-118.46122,24.844532,537.8856,-0.016487962,0.26103613,-0.022542765,175.92683,175.91576,171.46367,171.48314,304.82562,303.41104,397.5911,393.41428,73.826324,69.120735,70.83179,65.19211,0.0,0.0,0.0,0.0,0.03527517,0.03543609,0.013676049,0.01391935,0.008092324,0.013501197,-0.012647919,-0.02491319,0.53948826,0.9000798,-0.8431946,-1.6608794,0.0,0.0,0.0,-1.0,1.0,-2.165778,-0.032253243,0.0017618624,73.826324,69.120735,70.83179,65.19211,25.110315,24.534641,25.62284,25.081415,0.024346262,0.024281703,-0.0016409023,-0.0015274936,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.500732,-0.011831859,26.763191,-6.815667E-4,-0.0031339019,-0.00269921,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +23.317741,4047.8992,0.527,0.0,0.0,171.83765,5792.0,100.0,0.0,-0.09496216,0.0,4.0,0.0,0.0,0.0,84.75996,1.0,0.0,0.0,60.012806,0.0,1.0,-118.7574,24.844452,537.68494,-0.016842874,0.26147953,-0.011131329,176.03027,176.0295,171.57098,171.57745,304.77667,303.36237,397.44116,393.26617,73.828804,69.12241,70.83325,65.19235,0.0,0.0,0.0,0.0,0.03545368,0.03564789,0.013607756,0.013733252,0.016878633,0.019641045,-0.007062931,-0.021348488,0.99286073,1.1553556,-0.41546655,-1.2557933,0.0,0.0,0.0,-1.0,1.0,-2.1658087,-0.032234173,0.0017262091,73.828804,69.12241,70.83325,65.19235,25.110554,24.534801,25.622982,25.08144,0.024398785,0.0243943,-0.0015518327,-0.001514221,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.520996,-0.013003436,26.778904,0.005979049,-0.0031720053,-0.0017585277,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +23.796137,4048.3765,0.535,0.0,0.0,171.88348,5792.0,100.0,0.0,-0.09539544,0.0,4.0,0.0,0.0,0.0,84.76147,1.0,0.0,0.0,60.012665,0.0,1.0,-119.15249,24.844328,537.41724,-0.018743541,0.25865552,-0.018018097,176.11974,176.11406,171.6248,171.62872,304.77667,303.36237,397.44116,393.26617,73.83004,69.12325,70.833984,65.192474,0.0,0.0,0.0,0.0,0.035528727,0.035723917,0.013566235,0.013642805,0.014086564,0.015990395,-0.0061007836,-0.015363615,0.7825869,0.88835526,-0.33893242,-0.85353416,0.0,0.0,0.0,-1.0,1.0,-2.165823,-0.032226935,0.0016839136,73.83004,69.12325,70.833984,65.192474,25.110672,24.534883,25.62305,25.081451,0.024646044,0.024612974,-0.0015050384,-0.0014821178,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.530987,-0.014075285,26.786732,0.009123739,-0.0031545938,-0.001318757,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 +24.115067,4048.7346,0.542,0.0,0.0,171.97446,5793.0,100.0,0.0,-0.101628795,0.0,4.0,0.0,0.0,0.0,84.76449,1.0,0.0,0.0,60.012386,0.0,1.0,-119.44893,24.844227,537.2164,-0.016709078,0.25675243,-0.04038965,176.28296,176.25171,171.69395,171.72241,304.72772,303.3137,397.29123,393.11807,73.83251,69.12493,70.83545,65.19272,0.0,0.0,0.0,0.0,0.03555339,0.035814002,0.01346909,0.013442952,0.00664716,0.011074047,-0.009244432,-0.019353317,0.443144,0.73826975,-0.61629546,-1.2902211,0.0,0.0,0.0,-1.0,1.0,-2.1658506,-0.032227688,0.0015559354,73.83251,69.12493,70.83545,65.19272,25.11091,24.535046,25.623194,25.081476,0.025053147,0.0248715,-0.001631055,-0.0014655663,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.550945,-0.017810632,26.80214,0.014740507,-0.0031819611,0.0013267533,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0 diff --git a/tests/test_rename.py b/tests/test_rename.py index 83e80c7..ec41e86 100644 --- a/tests/test_rename.py +++ b/tests/test_rename.py @@ -42,7 +42,11 @@ def test_lap_token_present(stem, expected): ("zandvoort_Q2_L3_ferrari", True), ("zandvoort_R_L1_ferrari", True), ("zandvoort_FP1_L5_ferrari", True), - ("zandvoort_P_74.074_ferrari", False), # bare P without digit not matched + # ACC compact tokens + ("hungaroring_P_L1_123.702_ktm_gt4", True), # bare P (ACC Practice) + ("hungaroring_Q_L1_123.702_ktm_gt4", True), # bare Q (ACC Qualifying) + ("hungaroring_HL_L1_123.702_ktm_gt4", True), # Hotlap + ("hungaroring_HS_L1_123.702_ktm_gt4", True), # Hotstint ("zandvoort_L7_74.074_ferrari", False), ("notoken", False), ]) @@ -176,3 +180,44 @@ def test_rename_unprocessed_walks_subdirs(tmp_path, sample_path): assert len(results) == 1 assert results[0].status == 'renamed' assert 'Practice' in results[0].new + + +# --- ACC support --- + +def test_acc_rename_inserts_compact_token(tmp_path, acc_path): + """ACC 'Practice' event normalises to 'P' and lap reads as L1.""" + src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv") + shutil.copy(acc_path, src) + results = rename_unprocessed([src]) + assert len(results) == 1 + r = results[0] + assert r.status == 'renamed' + assert r.new is not None + new_stem = os.path.splitext(os.path.basename(r.new))[0] + tokens = new_stem.split('_') + assert 'P' in tokens + assert 'L1' in tokens + + +def test_acc_rename_lands_in_practice_folder(tmp_path, acc_path): + """ACC 'Practice' event files into races//Practice/, not Other.""" + src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv") + shutil.copy(acc_path, src) + races_dir = str(tmp_path / 'races') + results = rename_unprocessed([src], races_dir=races_dir) + assert len(results) == 1 + r = results[0] + assert r.status == 'renamed' + assert r.new is not None + assert 'Practice' in r.new + assert 'Other' not in r.new + assert os.path.exists(r.new) + + +def test_acc_rename_idempotent(tmp_path, acc_path): + """Second rename_unprocessed run on an already-renamed ACC file returns skipped.""" + src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv") + shutil.copy(acc_path, src) + rename_unprocessed([src]) + results = rename_unprocessed([str(tmp_path)]) + assert all(r.status == 'skipped' for r in results) diff --git a/tests/test_reports.py b/tests/test_reports.py index 30c30b5..91abf21 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -8,6 +8,8 @@ import telemetry.report_race as rr import telemetry.report_profile as rp from telemetry.__main__ import is_fresh from telemetry.parser import Lap +from telemetry import report_common +from telemetry.report_common import game_of, load_prompt def _check(result, path): @@ -179,6 +181,59 @@ def test_is_fresh_cache(tmp_path): assert is_fresh(str(rep), [str(src)]) is False +class TestACC: + """ACC support: game detection, prompt fallback, game-aware report sections.""" + + def test_game_of_acc_vs_f1(self, acc_lap, sample_lap): + assert game_of(acc_lap) == 'acc' + assert game_of(sample_lap) == 'f1' + + def test_load_prompt_acc_falls_back_to_f1(self): + # ACC requests resolve to the ACC prompt if present, else the F1 prompt — + # never empty while an F1 prompt exists. + base = os.path.normpath(os.path.join( + os.path.dirname(report_common.__file__), '..', 'prompts', 'ru')) + for mode in ('technique', 'setup', 'compare', 'race'): + acc_file = os.path.join(base, 'acc', f'{mode}.md') + f1_file = os.path.join(base, f'{mode}.md') + expected = open(acc_file if os.path.exists(acc_file) else f1_file, + encoding='utf-8').read().strip() + assert load_prompt(mode, 'ru', 'acc') == expected + assert load_prompt(mode, 'ru', 'f1') == open(f1_file, encoding='utf-8').read().strip() + + def test_acc_setup_no_param_table_keeps_balance(self, acc_lap, tmp_path): + out = str(tmp_path / "acc_setup.md") + rs.generate(acc_lap, out, 'ru', include_prompt=False) + text = open(out, encoding="utf-8").read() + assert "| Parameter | Value |" not in text # zero-filled setup table dropped + assert "не экспортирует сетап" in text # explanatory note instead + assert "## Corner balance" in text # telemetry-derived sections kept + assert "## Tyres" in text + + def test_acc_technique_no_setup_line_no_ers(self, acc_lap, tmp_path): + out = str(tmp_path / "acc_tech.md") + rt.generate(acc_lap, out, 'ru', include_prompt=False) + text = open(out, encoding="utf-8").read() + assert "## Corners" in text + assert "**Setup:**" not in text # zero-filled setup summary skipped + assert "ERS spent:" not in text # hybrid block dropped + + def test_f1_technique_keeps_setup_and_ers(self, sample_lap, tmp_path): + # regression: F1 path unchanged + out = str(tmp_path / "f1_tech.md") + rt.generate(sample_lap, out, 'ru', include_prompt=False) + text = open(out, encoding="utf-8").read() + assert "**Setup:**" in text + assert "ERS spent:" in text + + def test_acc_race_skips_ers_table(self, acc_lap, tmp_path): + out = str(tmp_path / "acc_race.md") + rr.generate([acc_lap], out, 'ru', include_prompt=False) + text = open(out, encoding="utf-8").read() + assert "## ERS per lap" not in text + assert "## Lap times" in text + + class TestRegressionReports: """Regression: report generators must not crash on valid single-lap input."""