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_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_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. 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. 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. 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. 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. **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. **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_compare.py Два+ круга рядом, дельта по поворотам и накопленная.
report_race.py Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS. Дедуп дублей номеров кругов. report_race.py Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS. Дедуп дублей номеров кругов.
report_profile.py Папка кругов: дешёвые тенденции по поворотам (медиана на поворот, по доминирующему составу) + авто-выбор кругов для разбора. Без трассы. 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); память последней папки по режиму; генерация/переименование в потоке. Предупреждает (без блокировки), если определённая игра не совпадает с активной вкладкой. gui.py Окно tkinter с двумя вкладками (F1 22 / ACC); память последней папки по режиму; генерация/переименование в потоке. Предупреждает (без блокировки), если определённая игра не совпадает с активной вкладкой.
prompts/ Промпты LLM, подпапки ru/ и en/, по одному файлу на режим. Добавляются в конец каждого отчёта. prompts/ Промпты LLM, подпапки ru/ и en/, по одному файлу на режим. Добавляются в конец каждого отчёта.
Переопределения для ACC лежат в prompts/<lang>/acc/<mode>.md; при отсутствии — откат к F1-промпту. Переопределения для ACC лежат в prompts/<lang>/acc/<mode>.md; при отсутствии — откат к F1-промпту.
@ -72,7 +72,7 @@ tests/ Набор pytest + фикстуры-круги.
**Строка сетапа заканчивается лишней запятой** (пустой хвост `FuelLoad` в части экспортов). Парсер отбрасывает пустые хвостовые поля, чтобы в словаре сетапа не было ключа с пустой строкой. **Строка сетапа заканчивается лишней запятой** (пустой хвост `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/`). Перемещение инструмента не меняет, куда попадают отчёты. **Отчёты выходят рядом с входом, а не рядом с программой.** `report_path` выводит папку `reports/` из папки *входного CSV*. Режим `race` кладёт отчёт внутрь папки сессии (`<race_dir>/reports/`). Перемещение инструмента не меняет, куда попадают отчёты.

View file

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

View file

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

View file

@ -40,6 +40,10 @@ def _normalise_event(raw: str) -> str:
return raw.strip() 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: def _session_folder(event: str) -> str:
# event is already the canonical token at call sites inside this module. # event is already the canonical token at call sites inside this module.
e = event.upper() e = event.upper()
@ -123,8 +127,8 @@ def read_session_type(path: str) -> str:
return session.get('event', '').strip() return session.get('event', '').strip()
def read_metadata(path: str) -> tuple[int, str, str]: def read_metadata(path: str) -> tuple[int, str, str, str]:
"""Returns (lap_number, event_type, track_name).""" """Returns (lap_number, event_type, track_name, game_raw)."""
with open(path, encoding='utf-8') as f: with open(path, encoding='utf-8') as f:
lines = [f.readline() for _ in range(5)] lines = [f.readline() for _ in range(5)]
sess_keys = _parse_header_row(lines[1]) sess_keys = _parse_header_row(lines[1])
@ -137,6 +141,7 @@ def read_metadata(path: str) -> tuple[int, str, str]:
int(float(track['Lap'])), int(float(track['Lap'])),
session.get('event', '').strip(), session.get('event', '').strip(),
session.get('track', '').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')) results.append(RenameResult(name, None, 'skipped', 'already processed'))
continue continue
try: try:
lap, session_type, _ = read_metadata(p) lap, session_type, _, _game = read_metadata(p)
except Exception as e: except Exception as e:
results.append(RenameResult(name, None, 'error', str(e))) results.append(RenameResult(name, None, 'error', str(e)))
continue 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) dest = os.path.join(os.path.dirname(p), new_name)
else: else:
try: try:
lap, session_type, track_name = read_metadata(p) lap, session_type, track_name, game_raw = read_metadata(p)
except Exception as e: except Exception as e:
results.append(RenameResult(name, None, 'error', str(e))) results.append(RenameResult(name, None, 'error', str(e)))
continue continue
@ -202,7 +207,8 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
continue continue
session_type = _normalise_event(session_type) session_type = _normalise_event(session_type)
new_name = name if _is_processed(stem) else insert_tokens(name, session_type, lap) 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) dest = os.path.join(dest_dir, new_name)
if os.path.abspath(p) == os.path.abspath(dest): if os.path.abspath(p) == os.path.abspath(dest):
results.append(RenameResult(name, None, 'skipped', 'already in place')) 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 --- # --- read_metadata ---
def test_read_metadata_sample(sample_path): 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 lap == 7
assert event == 'P1' assert event == 'P1'
assert track != '' assert track != ''
assert 'F1' in game or game != ''
# --- rename_unprocessed --- # --- 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 r.new is not None and 'P1' in r.new and 'L7' in r.new
assert os.path.exists(r.new) assert os.path.exists(r.new)
assert r.new.startswith(races_dir) assert r.new.startswith(races_dir)
assert 'F1 22' in r.new
assert 'Practice' in r.new assert 'Practice' in r.new
assert not os.path.exists(src) 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 assert len(results) == 1
r = results[0] r = results[0]
assert r.status == 'renamed' assert r.status == 'renamed'
assert 'F1 22' in r.new
assert 'Practice' in r.new assert 'Practice' in r.new
assert os.path.exists(r.new) assert os.path.exists(r.new)
assert not os.path.exists(src) 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): 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") src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv")
shutil.copy(acc_path, src) shutil.copy(acc_path, src)
races_dir = str(tmp_path / 'races') 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] r = results[0]
assert r.status == 'renamed' assert r.status == 'renamed'
assert r.new is not None assert r.new is not None
assert 'ACC' in r.new
assert 'Practice' in r.new assert 'Practice' in r.new
assert 'Other' not in r.new assert 'Other' not in r.new
assert os.path.exists(r.new) assert os.path.exists(r.new)