rename: sort by game — races/F1 22|ACC/<track>/<session>/

read_metadata now returns game_raw; _game_folder maps it to 'F1 22' or 'ACC';
dest_dir gains a game subfolder between races_dir and track
This commit is contained in:
Andrew Pimenov 2026-06-19 16:40:02 +03:00
parent c1ee2f82e4
commit 15bb552166
6 changed files with 55 additions and 33 deletions

View file

@ -19,7 +19,7 @@ telemetry/
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/<track>/<session>/ and recurse subdirectories.
rename.py Insert session type + lap number (P1_L7) into filenames; with races_dir, sort into races/<game>/<track>/<session>/ and recurse subdirectories.
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/<lang>/acc/<mode>.md; missing files fall back to the F1 prompt.
@ -72,7 +72,7 @@ If a future track needs a different corner grouping, `merge_gap_for` already ada
**The setup row has a trailing comma** (an empty `FuelLoad` tail field in some exports). The parser drops trailing empties so the setup dict has no blank-string key.
**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/<track_safe>/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.
**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, game), (3) inserts tokens if absent, (4) creates `races/F1 22|ACC/<track_safe>/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 (`<race_dir>/reports/`). Moving the tool does not move where reports land.

View file

@ -19,7 +19,7 @@ telemetry/
report_compare.py Два+ круга рядом, дельта по поворотам и накопленная.
report_race.py Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS. Дедуп дублей номеров кругов.
report_profile.py Папка кругов: дешёвые тенденции по поворотам (медиана на поворот, по доминирующему составу) + авто-выбор кругов для разбора. Без трассы.
rename.py Вставка типа сессии + номера круга (P1_L7) в имя; с races_dir — сортировка в races/<track>/<session>/ и рекурсия по подпапкам.
rename.py Вставка типа сессии + номера круга (P1_L7) в имя; с races_dir — сортировка в races/<game>/<track>/<session>/ и рекурсия по подпапкам.
gui.py Окно tkinter с двумя вкладками (F1 22 / ACC); память последней папки по режиму; генерация/переименование в потоке. Предупреждает (без блокировки), если определённая игра не совпадает с активной вкладкой.
prompts/ Промпты LLM, подпапки ru/ и en/, по одному файлу на режим. Добавляются в конец каждого отчёта.
Переопределения для ACC лежат в prompts/<lang>/acc/<mode>.md; при отсутствии — откат к F1-промпту.
@ -72,7 +72,7 @@ tests/ Набор pytest + фикстуры-круги.
**Строка сетапа заканчивается лишней запятой** (пустой хвост `FuelLoad` в части экспортов). Парсер отбрасывает пустые хвостовые поля, чтобы в словаре сетапа не было ключа с пустой строкой.
**Rename с `races_dir` распределяет по папкам и рекурсит.** `rename_unprocessed(targets, races_dir)` когда `races_dir` установлен: (1) сканирует всё дерево подпапок (`os.walk`), (2) читает метаданные из CSV (`read_metadata` → lap, event, track), (3) вставляет токены если их нет, (4) создаёт `races/<track_safe>/Practice|Qualifying|Race|Sprint/` и перемещает туда файл, (5) даже уже переименованные файлы переносятся если лежат не там где надо, (6) пропускаются только файлы, которые уже находятся в нужной папке с нужным именем. Без `races_dir` работает как раньше — переименовывает в исходной папке.
**Rename с `races_dir` распределяет по папкам и рекурсит.** `rename_unprocessed(targets, races_dir)` когда `races_dir` установлен: (1) сканирует всё дерево подпапок (`os.walk`), (2) читает метаданные из CSV (`read_metadata` → lap, event, track, game), (3) вставляет токены если их нет, (4) создаёт `races/F1 22|ACC/<track_safe>/Practice|Qualifying|Race|Sprint/` и перемещает туда файл, (5) даже уже переименованные файлы переносятся если лежат не там где надо, (6) пропускаются только файлы, которые уже находятся в нужной папке с нужным именем. Без `races_dir` работает как раньше — переименовывает в исходной папке.
**Отчёты выходят рядом с входом, а не рядом с программой.** `report_path` выводит папку `reports/` из папки *входного CSV*. Режим `race` кладёт отчёт внутрь папки сессии (`<race_dir>/reports/`). Перемещение инструмента не меняет, куда попадают отчёты.

View file

@ -51,6 +51,7 @@ GT3/GT4 cars have no hybrid system, and ACC does not export the car setup:
```
Telemetry/
races/
F1 22/
Zandvoort/
Practice/ <- CSVs go here after renaming
zandvoort_P1_L7_74.074_ferrari.csv
@ -62,6 +63,11 @@ Telemetry/
zandvoort_R_L7_74.152_ferrari.csv
reports/
Race_race.md <- race mode report
ACC/
Hungaroring/
Practice/
hungaroring_P_L1_123.702_ktm_gt4.csv
reports/
tool/ <- this repository
telemetry/
prompts/

View file

@ -51,17 +51,23 @@ python -m telemetry technique "races/Zandvoort/Практика/zandvoort_P1_L7_
```
Telemetry/
races/
F1 22/
Zandvoort/
Практика/ <- CSV-файлы после переименования
Practice/ <- CSV-файлы после переименования
zandvoort_P1_L7_74.074_ferrari.csv
reports/ <- отчёты technique/setup/compare
Квалификация/
Qualifying/
zandvoort_Q1_L3_74.897_ferrari.csv
reports/
Гонка/
Race/
zandvoort_R_L7_74.152_ferrari.csv
reports/
Гонка_race.md <- отчёт режима race
Race_race.md <- отчёт режима race
ACC/
Hungaroring/
Practice/
hungaroring_P_L1_123.702_ktm_gt4.csv
reports/
tool/ <- этот репозиторий
telemetry/
prompts/

View file

@ -40,6 +40,10 @@ def _normalise_event(raw: str) -> str:
return raw.strip()
def _game_folder(game_raw: str) -> str:
return 'ACC' if game_raw.upper().startswith('ACC') else 'F1 22'
def _session_folder(event: str) -> str:
# event is already the canonical token at call sites inside this module.
e = event.upper()
@ -123,8 +127,8 @@ def read_session_type(path: str) -> str:
return session.get('event', '').strip()
def read_metadata(path: str) -> tuple[int, str, str]:
"""Returns (lap_number, event_type, track_name)."""
def read_metadata(path: str) -> tuple[int, str, str, str]:
"""Returns (lap_number, event_type, track_name, game_raw)."""
with open(path, encoding='utf-8') as f:
lines = [f.readline() for _ in range(5)]
sess_keys = _parse_header_row(lines[1])
@ -137,6 +141,7 @@ def read_metadata(path: str) -> tuple[int, str, str]:
int(float(track['Lap'])),
session.get('event', '').strip(),
session.get('track', '').strip(),
session.get('Game', '').strip(),
)
@ -181,7 +186,7 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
results.append(RenameResult(name, None, 'skipped', 'already processed'))
continue
try:
lap, session_type, _ = read_metadata(p)
lap, session_type, _, _game = read_metadata(p)
except Exception as e:
results.append(RenameResult(name, None, 'error', str(e)))
continue
@ -193,7 +198,7 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
dest = os.path.join(os.path.dirname(p), new_name)
else:
try:
lap, session_type, track_name = read_metadata(p)
lap, session_type, track_name, game_raw = read_metadata(p)
except Exception as e:
results.append(RenameResult(name, None, 'error', str(e)))
continue
@ -202,7 +207,8 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
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_dir = os.path.join(races_dir, _game_folder(game_raw),
_safe_dirname(track_name), _session_folder(session_type))
dest = os.path.join(dest_dir, new_name)
if os.path.abspath(p) == os.path.abspath(dest):
results.append(RenameResult(name, None, 'skipped', 'already in place'))

View file

@ -85,10 +85,11 @@ def test_read_session_type_sample(sample_path):
# --- read_metadata ---
def test_read_metadata_sample(sample_path):
lap, event, track = read_metadata(sample_path)
lap, event, track, game = read_metadata(sample_path)
assert lap == 7
assert event == 'P1'
assert track != ''
assert 'F1' in game or game != ''
# --- rename_unprocessed ---
@ -141,6 +142,7 @@ def test_rename_unprocessed_moves_to_races_dir(tmp_path, sample_path):
assert r.new is not None and 'P1' in r.new and 'L7' in r.new
assert os.path.exists(r.new)
assert r.new.startswith(races_dir)
assert 'F1 22' in r.new
assert 'Practice' in r.new
assert not os.path.exists(src)
@ -154,6 +156,7 @@ def test_rename_unprocessed_moves_already_renamed(tmp_path, sample_path):
assert len(results) == 1
r = results[0]
assert r.status == 'renamed'
assert 'F1 22' in r.new
assert 'Practice' in r.new
assert os.path.exists(r.new)
assert not os.path.exists(src)
@ -200,7 +203,7 @@ def test_acc_rename_inserts_compact_token(tmp_path, acc_path):
def test_acc_rename_lands_in_practice_folder(tmp_path, acc_path):
"""ACC 'Practice' event files into races/<Track>/Practice/, not Other."""
"""ACC 'Practice' event files into races/ACC/<Track>/Practice/, not Other."""
src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv")
shutil.copy(acc_path, src)
races_dir = str(tmp_path / 'races')
@ -209,6 +212,7 @@ def test_acc_rename_lands_in_practice_folder(tmp_path, acc_path):
r = results[0]
assert r.status == 'renamed'
assert r.new is not None
assert 'ACC' in r.new
assert 'Practice' in r.new
assert 'Other' not in r.new
assert os.path.exists(r.new)