| name | split-audio |
| description | Split a continuous source mp3 (PA announcements or STA melodies) into per-segment files for a new route, applying naming conventions and updating route.json so the simulator picks them up. |
| triggers | ["/split-audio","split audio","split pa","split sta","split mp3"] |
Purpose
Take a continuous source mp3 + timestamps file the user has prepared, produce one mp3 per segment in audio/<line>/<diagram>/pa/ or sta/, and update route.json so the simulator references them. Apply the established naming conventions so files are self-documenting and song re-use is visible by glob.
When to run
The user points at a working folder like audio_src_<line>_workflow/ containing the source mp3(s) + a timestamps file, and asks to split. May be PA-only, STA-only, or both.
Required input
- Path to the source folder (e.g.,
audio_src_sobu_workflow/)
- Which line + diagram the splits belong to (target:
audio/<line>/<diagram>/)
- If splitter scripts already exist for this folder, ask before overwriting
Source folder layout
Per line/diagram, working files live at the project root in their own folder. Splitter scripts stay with the source files — they document exactly how that diagram was sliced.
audio_src_sobu_workflow/
├── src.mp3 # continuous PA source
├── sta_from_higashichiba.mp3 # STA source (may be partial coverage)
├── timestamps.txt # PA timestamps
├── sta_timestamps.txt # STA timestamps
├── split_pa.py # generated by this skill
└── split_sta.py # generated by this skill
Process
Step 1 — Inspect
ls the source folder. Read each timestamps file. Identify which format applies (PA or STA, see below). If existing split_pa.py / split_sta.py are present, read them — the user may have customized.
Step 2 — Parse + discuss BEFORE acting
Before generating any script or touching route.json, surface to the user:
- Total segment count
- Per-station segment count (especially for PA — don't assume "everyone has 2 timestamps")
- Suspicious gaps — very short (<10s) or very long (>30s within an active stretch) deserve a flag. Could be back-to-back PAs, jingle/voice splits, or non-PA filler in the source.
- For STA: platform mapping per station (
(N) notation), any song names provided
- Any 1-timestamp stations — for PA, those typically use
{prev}-dep (announcement covers "we just left X"). Confirm.
Wait for the user's confirmation on the parse before generating.
Step 3 — Generate splitter script (per-source, ad-hoc)
Splitters are per-source artifacts, not a maintained library. The format of the timestamps file varies between sources — one batch may use 3 timestamps per line (start/cut/end), another may use 2 (start/cut, end implicit). Each source gets its own script reflecting its specific format. Don't try to make one splitter that handles "the format" — there isn't one.
Naming: split_{type}_{describer}.py if multiple sources for the same line (e.g., split_sta_tokyo.py + split_sta_higashichiba.py); plain split_{pa,sta}.py if it's the only one. The script lives in the source folder so the audit trail of "how this batch was split" stays with the data.
Two common patterns to start from — copy the one that matches the format and adapt:
PA splitter — each timestamp = start of one segment, ending at next chronological timestamp; last runs to EOF:
"""Split src.mp3 into N PA segments for audio/<line>/<diagram>/pa/."""
import subprocess, sys
from pathlib import Path
SRC = Path(__file__).parent / "src.mp3"
OUT = Path(__file__).resolve().parents[1] / "audio" / "<line>" / "<diagram>" / "pa"
SEGMENTS = [
("12:22", "tokyo-dep"),
]
def to_sec(ts):
m, s = map(int, ts.split(":"))
return m * 60 + s
def main():
if not SRC.exists():
print(f"ERROR: source not found: {SRC}", file=sys.stderr); return 1
OUT.mkdir(parents=True, exist_ok=True)
for i, (start, name) in enumerate(SEGMENTS):
start_sec = to_sec(start)
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-ss", str(start_sec), "-i", str(SRC)]
if i + 1 < len(SEGMENTS):
cmd += ["-t", str(to_sec(SEGMENTS[i + 1][0]) - start_sec)]
cmd += ["-c", "copy", str(OUT / f"{name}.mp3")]
print(f"[{i+1:2d}/{len(SEGMENTS)}] {name}.mp3 (start={start})")
subprocess.run(cmd, check=True)
print(f"\nDone. {len(SEGMENTS)} files written to {OUT}")
return 0
if __name__ == "__main__":
sys.exit(main())
STA splitter — explicit-end format (3 timestamps per line: start/cut/end). Add an op / archive dest tag per segment so re-runs route operational vs unused recordings to the right folder automatically:
PROJECT = Path(__file__).resolve().parents[1]
OUT_OP = PROJECT / "audio" / "<line>" / "<diagram>" / "sta"
OUT_ARCHIVE = PROJECT / "audio" / "_archive" / "<line>" / "<diagram>" / "sta"
SEGMENTS = [
("0:12", "1:08", "1:18", "tsuga_2_gota-del-vient", "op"),
("5:43", "6:08", "6:17", "narita_5_furawa-shoppu", "archive"),
]
for start, cut, end, name, dest in SEGMENTS:
out_dir = OUT_ARCHIVE if dest == "archive" else OUT_OP
start_sec = to_sec(start)
duration = to_sec(end) - start_sec
sta_cut_sec = to_sec(cut) - start_sec
cmd = ["ffmpeg", "-y", "-loglevel", "error",
"-ss", str(start_sec), "-i", str(SRC),
"-t", str(duration),
"-c", "copy", str(out_dir / f"{name}.mp3")]
print(f"{name}.mp3 [{dest}] start={start} cut={cut} end={end} sta_cut={sta_cut_sec}")
subprocess.run(cmd, check=True)
STA splitter — implicit-end format (2 timestamps per line: start/cut, end = next start, last = EOF):
EOF = "5:18"
SEGMENTS = [
("0:24", "0:40", "tokyo_4_jr-sh5-1", "op"),
("0:47", "1:07", "tokyo_3_twilight", "archive"),
]
for i, (start, cut, name, dest) in enumerate(SEGMENTS):
out_dir = OUT_ARCHIVE if dest == "archive" else OUT_OP
start_sec = to_sec(start)
end = SEGMENTS[i + 1][0] if i + 1 < len(SEGMENTS) else EOF
duration = to_sec(end) - start_sec
sta_cut_sec = to_sec(cut) - start_sec
cmd = ["ffmpeg", "-y", "-loglevel", "error",
"-ss", str(start_sec), "-i", str(SRC),
"-t", str(duration),
"-c", "copy", str(out_dir / f"{name}.mp3")]
print(f"{name}.mp3 [{dest}] start={start} cut={cut} end={end} sta_cut={sta_cut_sec}")
subprocess.run(cmd, check=True)
Print the sta_cut value during the run so it's easy to copy into route.json.
Step 4 — Run + verify file count
Run the splitter. Verify file count matches expected segment count. List the output folder.
Step 5 — Update route.json
Replace placeholder pa arrays (numbered ["1", "2", ...]) with descriptive basenames; replace sta_cut: 9 placeholders with the computed values; replace placeholder sta refs with the new descriptive basenames.
- Termini: omit
sta and sta_cut fields (no departure melody at end of route). Keep time.
- Stations IRL with no departure melody (e.g. 千葉 on the Sobu line): same treatment — omit
sta and sta_cut.
- Passing stations (
pa: []): keep them passing — no sta, no sta_cut, no time.
Archive routing is handled by the splitter's per-segment dest tag in Step 3 — operational files land in audio/<line>/<diagram>/sta/, "archive"-tagged files land in audio/_archive/<line>/<diagram>/sta/ (mirror layout under _archive/, no route.json there). The _ prefix marks "preserved but not shipped" — _archive/ and _mock/ both follow this convention.
Step 6 — Sanity check
Cross-reference route.json refs against files on disk:
PYTHONUTF8=1 python -c "
import json
from pathlib import Path
ROOT = Path('D:/pids_jre_simulator') # absolute path — cwd persists across Bash calls in this harness
route = json.load(open(ROOT / 'audio/<line>/<diagram>/route.json', encoding='utf-8'))
pa_dir = ROOT / 'audio/<line>/<diagram>/pa'
sta_dir = ROOT / 'audio/<line>/<diagram>/sta'
for label, dirpath, key in [('pa', pa_dir, 'pa'), ('sta', sta_dir, 'sta')]:
on_disk = {p.stem for p in dirpath.glob('*.mp3')} if dirpath.exists() else set()
refs = {x for stop in route['stops'] for x in stop.get(key, [])}
print(f'{label}: refs={len(refs)} disk={len(on_disk)} missing={sorted(refs-on_disk)} unused={sorted(on_disk-refs)}')
"
Report any unmatched references. Expected unused on disk: none after Step 5 — unused recordings should already have been moved to audio/_archive/. If a file appears in "unused" here, it's a leftover that needs to be relocated.
Conventions (the rules to apply)
Timestamps file format
PA (timestamps.txt) — each line is one station, with 1 or 2 timestamps:
新日本橋 12:22
錦糸町 13:31
新小岩 14:24 15:08
稲毛 19:17 19:24
成田空港 26:42
- 1 timestamp = single PA at that station (typically
{prev}-dep)
- 2 timestamps = first is
{prev}-dep, second is {this}-arr
- Each timestamp = start of one segment; ends at the next chronological timestamp; last runs to EOF
STA (sta_timestamps.txt) — each line is one segment with explicit boundaries:
0:12 1:08(cut) to 1:18 都賀(2) gota del viento
2:41 3:01 3:06 物井(1) gota del viento
4:28 4:54 4:57 酒々井
6:19 6:47 till the end 空港第2ビル チャイム3B4
- Three timestamps positionally:
start, cut, end. The (cut) and to annotations on some lines are documentation; the compact form (just three numbers) means the same thing.
(N) after the station name = JR platform number where this melody plays IRL. Optional.
- Trailing string = official melody name. Optional. Can be Japanese (katakana / kanji) or English.
- "till the end" in the end slot = segment runs to EOF.
PA filename convention
{prev-station}-dep.mp3 / {this-station}-arr.mp3
- All lowercase, hyphens within compound names, no diacritics (Hepburn with macrons stripped)
- 1-PA mid-route stations: use
{prev}-dep (single PA covers "we just left X, next is Y")
- Terminus single PA: use
{this}-arr (only an arrival announcement at end of line)
- Compound stations:
shin-nihombashi-dep, kita-ageo-arr
STA filename convention
{station}_{platform}_{song-id}.mp3
- Station first (easier to scan when listing files), then platform, then song.
- Underscore between fields, hyphens within a field.
- Only
station is required. platform and song-id are optional — drop along with the separator:
narita_3_soyokaze.mp3 — all three known
narita_3.mp3 — song unknown
shisui_gota-del-vient.mp3 — no platform recorded (rare)
shisui.mp3 — only station known
- Re-use surfaces as a glob on the trailing field:
ls *_gota-del-vient.mp3 shows every station that plays that song
- No metadata sidecar. The filename is the store. When a song is identified later, rename the file + update its
sta ref in route.json in the same pass.
- Variants are out of scope. Two recordings of "the same song with slight differences" each get their own song-id slug. Door-chime differences across platforms/routes are not captured —
tsuga_2_gota-del-vient and monoi_1_gota-del-vient share song identity, NOT recording identity.
Japanese → ASCII slug rules
Hepburn romanization, macrons stripped, lowercase, hyphens for word boundaries — same rule already used for station name slugs throughout the repo.
| Source | Slug |
|---|
| 東京 | tokyo |
| 越中島 | etchujima |
| 高輪ゲートウェイ | takanawa-gateway |
| スイートコール | suito-koru |
| フラワーショップ | furawa-shoppu |
| チャイム3B4 | chaimu-3b4 |
For non-Japanese names: lowercase, spaces → hyphens, strip apostrophes/most punctuation, spell out & as and. Don't Stop → dont-stop, Rock & Roll → rock-and-roll.
sta_cut field
Seconds from the START of the file where melody stops and door chime begins. Computed at split time as cut_timestamp - start_timestamp. Old docs may have said "from end" — that was wrong.
route.json field rules at split output
pa: list of basenames in playback order. Mid-route 1-PA stops typically use [{prev}-dep]; longer-gap stops use [{prev}-dep, {this}-arr].
sta: list of basenames. Often one entry; can have multiple variants.
sta_cut: integer, seconds from start.
- Terminus: omit
sta and sta_cut (no departure from end-of-line). Keep time.
- Passing stations (
pa: []): omit sta, sta_cut, AND time — train doesn't stop, countdown comes from the next PA station's time.
Gotchas
- Discussion-first. Don't generate the splitter or update route.json until the user confirms the timestamp parse. Especially for PA, where per-station segment count varies (1 vs 2) and surprises are common.
- CWD persists across Bash calls in this harness. Use absolute paths in verification scripts, not relative ones —
Path('audio/...') will resolve from wherever the last cd left you.
- First N minutes of a PA source mp3 may be non-PA filler (silence, intro). The splitter starts at the first real timestamp; everything before is discarded.
- 6-second gaps between two PA segments are real. Sometimes JR back-to-backs two announcements. Don't "fix" by merging without asking.
- Unused-platform STA recordings (other-platform takes for the train you're routing) belong in
audio/_archive/<line>/<diagram>/sta/, NOT in the operational sta/. Tag them "archive" in the splitter's SEGMENTS so the script routes them automatically — don't mv them after the fact. The audio file is the atomic unit; route.json refs are the operational subset for THIS train.
- Trailing-chime gap < ~5s is suspicious — the door chime portion of an STA (between
cut and end) usually runs 5–20s. If you see only 2–3s of chime in the timestamps, double-check end against the source — likely a typo / off-by-a-few-seconds. Exception: known recurring tight-loop pattern at that station (rare, document if confirmed).
- Don't assume per-station PA count. The timestamps file is ground truth. Count actual entries before sizing route.json
pa arrays.
- Splitter scripts stay with their source folder (
audio_src_<line>_workflow/), not in a shared workflow folder. They document exactly how that batch was split.
- Formats vary between sources — even within the same line/diagram, two STA recordings may use different timestamp conventions (3 timestamps vs 2 timestamps per line, etc.). Don't try to unify into "the splitter"; each source gets its own script reflecting its own format. Multiple sources in the same folder → name them
split_{type}_{describer}.py (e.g., split_sta_tokyo.py, split_sta_higashichiba.py).
- Trailing digits in station romanization (e.g.,
airport-terminal-2) are part of the station name, not platform. Filename position-parsing has no parser, so this is human-readable ambiguity only — not a bug to fix.
- Don't create a metadata JSON sidecar for STA. The filename IS the metadata store. If
sta_meta.json shows up, that's a previous experiment that should be removed.
- Front-half placeholders are fine. When STA source covers only part of a route (e.g., from-某-station-onward), the unsplit stops keep their placeholder
sta refs (e.g., JO19_TYO, JO20) until the rest arrives. They'll fail validate_data.py's file-existence check until then — that's expected.
Documentation hook
After split + route.json update lands: if this work surfaced anything line-specific not already in audio/README.md — new diagram for existing line, IRL service quirk, filename-convention deviation, schema-corner-case usage — propose an entry. Decline if work was routine split-and-go.
Out of scope
- Variant / arrangement modeling — current scheme captures song identity, not recording identity. If/when fidelity matters, extend the convention or add a sidecar then. Don't pre-build for it.
- Cross-route audio sharing (e.g., 東京 STA used on Sobu AND Tokaido) — for now, duplicate the file. If it becomes painful, factor out an
audio/_shared/ later.
- Renaming legacy numbered routes (
audio/keiyo/, audio/chuo/ use 1.mp3, 2.mp3, etc.) — the renderer treats both conventions identically. Don't migrate.
- Audio normalization / loudness leveling — out of this skill. The simulator does -15 LUFS at runtime.
Related
audio/README.md — per-line IRL + sim quirks catalog (write-gate target above)
DATA_FORMAT.md — route.json schema reference (field meanings, validation rules)
validate_data.py — checks audio files referenced by routes exist on disk