From e271da634e53b5cc075cc557319164a262dc1a9c Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Wed, 16 Sep 2026 18:28:37 +0900 Subject: [PATCH] 274: lyric index for the visualiser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 274-lyrics-index.py and its output 274-lyrics-index.csv: every sung line resolved to its roster entry, with coordinates and timing. 98 lyric lines -> 99 rows. Columns are start/stop as mm:ss.xx and as seconds, the line as sung, the roster's full name for it, kind, km, lat/lon, and km_from/km_to for district headers. stop is the next timestamp in the file, spacer lines included, so every line has one. Four aliases cover the gap between sung and registered names: the three power stations are sung without their operator prefix, and モトツ橋にモトツシェルター names two facilities, so it emits two rows sharing one timing and one coordinate. 東に続く is typed as closing, no object. District headers resolve from the run of objects they introduce, not from 274-districts.json. Those bands are sampled along the road and so cannot describe a district the road does not enter: 宮下町 has no band at all, 本町東 exists only as 本町東一丁目, and 千栄's band sits at 107.25-107.75 while the station it introduces is at km 104.52, 666 m off the carriageway inside 千栄. Object names in the roster are unique, so no other disambiguation is needed. The script exits non-zero and names any line it cannot resolve. Co-Authored-By: Claude Opus 5 --- wip/274/274-lyrics-index.csv | 100 +++++++++++++++++++++++ wip/274/274-lyrics-index.py | 151 +++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 wip/274/274-lyrics-index.csv create mode 100644 wip/274/274-lyrics-index.py diff --git a/wip/274/274-lyrics-index.csv b/wip/274/274-lyrics-index.csv new file mode 100644 index 0000000..028fd21 --- /dev/null +++ b/wip/274/274-lyrics-index.csv @@ -0,0 +1,100 @@ +start,stop,start_s,stop_s,sung,object,kind,km,lat,lon,km_from,km_to +00:14.54,00:17.89,14.54,17.89,夕張郡由仁町川端,夕張郡由仁町川端,district,38.41,42.91715,141.90896,38.41,38.41 +00:17.89,00:19.67,17.89,19.67,川端橋,川端橋,bridge,38.41,42.91715,141.90896,, +00:25.04,00:28.41,25.04,28.41,夕張郡栗山町滝下,夕張郡栗山町滝下,district,39.80,42.91197,141.91642,39.80,41.86 +00:28.41,00:32.06,28.41,32.06,川端発電所,北海道企業局 川端発電所,power station,39.80,42.91197,141.91642,, +00:32.06,00:35.44,32.06,35.44,滝下八幡神社,滝下八幡神社,Shinto shrine,40.16,42.91183,141.92472,, +00:35.44,00:39.84,35.44,39.84,滝ノ下信号場,滝ノ下信号場,place (locality),40.57,42.90630,141.92320,, +00:39.84,00:42.01,39.84,42.01,雨霧橋,雨霧橋,bridge,41.86,42.91003,141.94197,, +00:42.45,00:45.15,42.45,45.15,夕張郡由仁町川端,夕張郡由仁町川端,district,43.03,42.91604,141.95220,43.03,43.03 +00:45.15,00:46.69,45.15,46.69,竜仙橋,竜仙橋,bridge,43.03,42.91604,141.95220,, +00:47.63,00:50.08,47.63,50.08,夕張郡栗山町滝下,夕張郡栗山町滝下,district,44.00,42.91449,141.96321,44.00,44.00 +00:50.08,00:52.88,50.08,52.88,草木橋,草木橋,bridge,44.00,42.91449,141.96321,, +00:52.88,00:54.73,52.88,54.73,滝の上こ線橋,滝の上こ線橋,bridge,44.00,42.91449,141.96321,, +00:56.44,00:59.95,56.44,59.95,夕張市滝ノ上,夕張市滝ノ上,district,44.62,42.91152,141.97021,44.62,45.29 +00:59.95,01:03.47,59.95,63.47,滝の上発電所,北海道企業局 滝の上発電所,power station,44.62,42.91152,141.97021,, +01:03.47,01:06.94,63.47,66.94,滝ノ上信号場,滝ノ上信号場,railway signal post,45.21,42.91000,141.97642,, +01:06.94,01:08.78,66.94,68.78,滝ノ上神社,滝ノ上神社,Shinto shrine,45.29,42.90979,141.97873,, +01:10.50,01:12.05,70.50,72.05,夕張市紅葉山,夕張市紅葉山,district,49.71,42.91482,142.02439,49.71,55.60 +01:12.05,01:13.86,72.05,73.86,十三里信号場,十三里信号場,place (locality),49.71,42.91482,142.02439,, +01:13.86,01:16.44,73.86,76.44,十三里橋,十三里橋,bridge,50.28,42.91772,142.03043,, +01:16.44,01:18.78,76.44,78.78,紅葉山駐車帯,紅葉山駐車帯,parking,50.52,42.91840,142.03331,, +01:18.78,01:21.06,78.78,81.06,瑞穂橋,瑞穂橋,bridge,52.11,42.93244,142.03548,, +01:21.06,01:24.44,81.06,84.44,セブンイレブン夕張紅葉山店,セブンイレブン夕張紅葉山店,convenience store,52.47,42.93507,142.03683,, +01:24.44,01:27.06,84.44,87.06,新夕張駅,新夕張駅,railway station,52.54,42.93670,142.03648,, +01:27.06,01:30.02,87.06,90.02,紅葉山郵便局,紅葉山郵便局,post office,52.61,42.93642,142.03925,, +01:30.02,01:32.30,90.02,92.30,紅葉山神社,紅葉山神社,Shinto shrine,52.85,42.93788,142.03956,, +01:32.30,01:35.42,92.30,95.42,セイコーマートもみじやま店,セイコーマートもみじやま店,convenience store,52.99,42.93910,142.04167,, +01:35.42,01:37.92,95.42,97.92,紅葉山橋,紅葉山橋,bridge (structure),53.01,42.94353,142.04306,, +01:37.92,01:39.46,97.92,99.46,紅楓橋,紅楓橋,bridge,53.13,42.93855,142.04317,, +01:39.46,01:41.54,99.46,101.54,ポン久留喜橋,ポン久留喜橋,bridge,53.69,42.93697,142.04976,, +01:41.54,01:43.84,101.54,103.84,久留喜橋,久留喜橋,bridge,53.69,42.93697,142.04976,, +01:43.84,01:45.05,103.84,105.05,楓橋,楓橋,bridge,55.41,42.92957,142.06562,, +01:45.05,01:46.47,105.05,106.47,桂橋,桂橋,bridge,55.60,42.93026,142.06775,, +01:46.84,01:48.90,106.84,108.90,夕張市楓,夕張市楓,district,56.60,42.93414,142.07860,56.60,57.84 +01:48.90,01:50.84,108.90,110.84,千紫橋,千紫橋,bridge,56.60,42.93414,142.07860,, +01:50.84,01:52.07,110.84,112.07,楓駐車帯,楓駐車帯,parking,57.08,42.93352,142.08412,, +01:52.07,01:53.93,112.07,113.93,万紅橋,万紅橋,bridge,57.25,42.93274,142.08627,, +01:53.93,01:55.61,113.93,115.61,登川神社,登川神社,Shinto shrine,57.44,42.93395,142.08819,, +01:55.61,01:57.32,115.61,117.32,楓信号場,楓信号場,railway signal post,57.76,42.93157,142.09230,, +01:57.32,01:59.00,117.32,119.00,登川橋,登川橋,bridge,57.84,42.93167,142.09327,, +01:59.00,02:00.74,119.00,120.74,登川郵便局,登川郵便局,post office,57.84,42.93185,142.09335,, +02:01.26,02:02.93,121.26,122.93,夕張市登川,夕張市登川,district,58.23,42.92922,142.09647,58.23,60.29 +02:02.93,02:04.87,122.93,124.87,花園橋,花園橋,bridge,58.23,42.92922,142.09647,, +02:04.87,02:06.63,124.87,126.63,登川トンネル,登川トンネル,tunnel,60.29,42.92478,142.11599,, +03:30.80,03:34.55,210.80,214.55,勇払郡むかわ町穂別長和,勇払郡むかわ町穂別長和,district,62.36,42.90892,142.12482,62.36,62.94 +03:34.55,03:36.77,214.55,216.77,オマナイ橋,オマナイ橋,bridge,62.36,42.90892,142.12482,, +03:36.77,03:39.41,216.77,219.41,桂木橋,桂木橋,bridge,62.94,42.90434,142.12726,, +03:41.13,03:44.62,221.13,224.62,勇払郡むかわ町穂別稲里,勇払郡むかわ町穂別稲里,district,64.44,42.89157,142.12305,64.44,71.86 +03:44.62,03:46.67,224.62,226.67,大和橋,大和橋,bridge,64.44,42.89157,142.12305,, +03:46.67,03:48.38,226.67,228.38,豊進橋,豊進橋,bridge,66.69,42.87371,142.13048,, +03:48.38,03:51.61,228.38,231.61,豊進神社,豊進神社,Shinto shrine,66.78,42.87318,142.13135,, +03:51.61,03:53.14,231.61,233.14,豊里橋,豊里橋,bridge,67.48,42.87311,142.13854,, +03:53.14,03:55.52,233.14,235.52,稲豊橋,稲豊橋,bridge,67.60,42.87313,142.14000,, +03:55.52,03:57.78,235.52,237.78,稲里橋,稲里橋,bridge,68.63,42.87144,142.15164,, +03:57.78,04:01.08,237.78,241.08,稲里簡易郵便局,稲里簡易郵便局,post office,69.29,42.86734,142.15774,, +04:01.08,04:03.50,241.08,243.50,佐主橋,佐主橋,bridge,69.48,42.86894,142.16085,, +04:03.50,04:05.59,243.50,245.59,穂別ダム神社,穂別ダム神社,Shinto shrine,71.86,42.88475,142.17737,, +04:05.96,04:10.08,245.96,250.08,勇払郡むかわ町穂別長和,勇払郡むかわ町穂別長和,district,72.02,42.88619,142.17748,72.02,75.28 +04:10.08,04:13.02,250.08,253.02,長和トンネル,長和トンネル,tunnel,72.02,42.88619,142.17748,, +04:13.02,04:15.39,253.02,255.39,穂別大橋,穂別大橋,bridge,72.29,42.88820,142.17918,, +04:15.39,04:18.81,255.39,258.81,山水橋,山水橋,bridge,72.90,42.89187,142.18476,, +04:18.81,04:23.20,258.81,263.20,沼の沢橋,沼の沢橋,bridge,74.31,42.88830,142.20015,, +04:23.20,04:24.97,263.20,264.97,稲里トンネル,稲里トンネル,tunnel,75.28,42.88343,142.20951,, +04:26.00,04:29.74,266.00,269.74,勇払郡むかわ町穂別福山,勇払郡むかわ町穂別福山,district,76.97,42.87802,142.22782,76.97,88.25 +04:29.74,04:31.50,269.74,271.50,ペンケ橋,ペンケ橋,bridge,76.97,42.87802,142.22782,, +04:31.50,04:33.62,271.50,273.62,下福山橋,下福山橋,bridge,77.05,42.88572,142.23487,, +04:33.62,04:35.02,273.62,275.02,オロロップ橋,オロロップ橋,bridge,77.55,42.87850,142.22868,, +04:35.02,04:37.07,275.02,277.07,福山橋,福山橋,bridge,79.11,42.89249,142.24390,, +04:37.07,04:38.70,277.07,278.70,福山神社,福山神社,Shinto shrine,79.67,42.89114,142.25175,, +04:38.70,04:41.02,278.70,281.02,福山大橋,福山大橋,bridge,80.24,42.88678,142.25416,, +04:41.02,04:43.29,281.02,283.29,千坂橋,千坂橋,bridge,85.40,42.88901,142.29725,, +04:43.29,04:45.12,283.29,285.12,オコタン橋,オコタン橋,bridge,85.87,42.89179,142.30059,, +04:45.12,04:46.22,285.12,286.22,モトツトンネル,モトツトンネル,tunnel,86.24,42.89321,142.30463,, +04:46.22,04:48.98,286.22,288.98,モトツ橋にモトツシェルター,モトツ橋,bridge,87.12,42.89931,142.31156,, +04:46.22,04:48.98,286.22,288.98,モトツ橋にモトツシェルター,モトツシェルター,shed,87.12,42.89931,142.31156,, +04:48.98,04:51.39,288.98,291.39,福山トンネル,福山トンネル,tunnel,87.21,42.89988,142.31229,, +04:51.39,04:53.61,291.39,293.61,福穂橋,福穂橋,bridge,88.04,42.90322,142.32132,, +04:53.61,04:55.52,293.61,295.52,穂高トンネル,穂高トンネル,tunnel,88.25,42.90398,142.32368,, +05:22.60,05:25.76,322.60,325.76,勇払郡占冠村中央,勇払郡占冠村中央,district,90.58,42.90958,142.35128,90.58,96.43 +05:25.76,05:28.88,325.76,328.88,穂高橋,穂高橋,bridge,90.58,42.90958,142.35128,, +05:28.88,05:31.52,328.88,331.52,高翠橋,高翠橋,bridge,90.90,42.90889,142.35485,, +05:31.52,05:34.95,331.52,334.95,占翠橋,占翠橋,bridge,91.14,42.90869,142.35768,, +05:34.95,05:38.08,334.95,338.08,名石駐車場,名石駐車場,parking,93.58,42.90980,142.36986,, +05:38.08,05:40.99,338.08,340.99,名石橋,名石橋,bridge,93.67,42.91029,142.37052,, +05:40.99,05:43.96,340.99,343.96,赤岩大橋,赤岩大橋,bridge,94.35,42.91317,142.37731,, +05:43.96,05:48.62,343.96,348.62,渓流大橋,渓流大橋,bridge,95.37,42.91502,142.38677,, +05:48.62,05:50.73,348.62,350.73,日高トンネル,日高トンネル,tunnel,96.43,42.91588,142.39911,, +07:11.39,07:14.14,431.39,434.14,沙流郡日高町日高,沙流郡日高町日高,district,100.60,42.89710,142.42716,100.60,100.60 +07:14.14,07:16.38,434.14,436.38,日夕橋,日夕橋,bridge,100.60,42.89710,142.42716,, +07:28.85,07:31.68,448.85,451.68,沙流郡日高町宮下町,沙流郡日高町宮下町,district,102.36,42.88427,142.43553,102.36,102.36 +07:31.68,07:34.09,451.68,454.09,日高神社,日高神社,Shinto shrine,102.36,42.88427,142.43553,, +07:42.90,07:45.51,462.90,465.51,沙流郡日高町本町東,沙流郡日高町本町東,district,103.12,42.87791,142.44001,103.12,103.29 +07:45.51,07:49.88,465.51,469.88,新川橋,新川橋,bridge,103.12,42.87791,142.44001,, +07:49.88,07:52.76,469.88,472.76,セイコーマート日高道の駅店,セイコーマート日高道の駅店,convenience store,103.29,42.87734,142.44216,, +07:52.76,07:55.11,472.76,475.11,日高郵便局,日高郵便局,post office,103.29,42.87748,142.44195,, +07:56.69,07:59.36,476.69,479.36,沙流郡日高町千栄,沙流郡日高町千栄,district,104.52,42.87724,142.45788,104.52,104.52 +07:59.36,08:02.15,479.36,482.15,北電日高発電所,北海道電力 日高発電所,power station,104.52,42.87724,142.45788,, +08:03.90,08:06.38,483.90,486.38,沙流郡日高町日高,沙流郡日高町日高,district,106.22,42.89314,142.46755,106.22,106.22 +08:06.38,08:09.44,486.38,489.44,右左府発電所,右左府発電所,power station,106.22,42.89314,142.46755,, +08:10.93,08:12.66,490.93,492.66,東に続く,,closing,,,,, diff --git a/wip/274/274-lyrics-index.py b/wip/274/274-lyrics-index.py new file mode 100644 index 0000000..abced6d --- /dev/null +++ b/wip/274/274-lyrics-index.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Build the lyric index: every sung line resolved to its roster entry and timing. + +Reads 274.lrc and 274-lookup.csv and writes 274-lyrics-index.csv, one row per +resolved object per lyric line, in song order. + + start / stop the line's own timestamp and the next timestamp in the file, + blank spacer lines included — so every line has a stop. + sung the text exactly as it appears in the lyric + object the roster's full name for it (the power stations carry their + operator prefix there but are sung without it) + kind, lat, lon from the roster + +Two lines need more than a name lookup: + + * モトツ橋にモトツシェルター is one structure driven through and two facilities + in the registers, so it emits TWO rows sharing one timing. + * The district headers (勇払郡むかわ町穂別福山 and the rest) are addresses, not + points, and three of them repeat because the road re-enters the district — + so they cannot resolve by name alone. Each is resolved from the run of + objects it introduces: the header takes the first object's coordinates, and + km_from / km_to span the run. + + Not from 274-districts.json, which is sampled along the road and therefore + cannot describe a district the road does not physically enter. 宮下町 has no + band at all, 本町東 exists only as 本町東一丁目, and 千栄's band sits at + 107.25-107.75 while the station it introduces is at km 104.52, 666 m off the + carriageway inside 千栄. + + python3 274-lyrics-index.py +""" +import csv +import io +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +p = lambda n: os.path.join(HERE, n) + +# lyric text -> roster object name(s). The sung form drops operator prefixes, +# and one line names two facilities. +ALIAS = { + '川端発電所': ['北海道企業局 川端発電所'], + '滝の上発電所': ['北海道企業局 滝の上発電所'], + '北電日高発電所': ['北海道電力 日高発電所'], + 'モトツ橋にモトツシェルター': ['モトツ橋', 'モトツシェルター'], +} +# sung lines that are deliberately not objects +NON_OBJECT = {'東に続く'} + +TS = re.compile(r'^\[(\d+):(\d+(?:\.\d+)?)\](.*)$') + + +def mmss(t): + return '%02d:%05.2f' % (int(t // 60), t % 60) + + +def read_lrc(path): + out = [] + for line in io.open(path, encoding='utf-8'): + m = TS.match(line.rstrip('\n')) + if m: + out.append((int(m.group(1)) * 60 + float(m.group(2)), m.group(3).strip())) + return out + + +def main(): + rows = list(csv.DictReader(io.open(p('274-lookup.csv'), encoding='utf-8'))) + by_obj = {r['object']: r for r in rows} + addresses = {r['address'] for r in rows} + + lrc = read_lrc(p('274.lrc')) + texts = [(i, t, s) for i, (t, s) in enumerate(lrc) if s] + + def targets(sung): + if sung in ALIAS: + return [by_obj[n] for n in ALIAS[sung] if n in by_obj] + if sung in by_obj: + return [by_obj[sung]] + return [] + + def run_after(i, sung): + """The objects a district header introduces, up to the next header.""" + got = [] + for j, _t, s in texts: + if j <= i: + continue + if s in addresses and not targets(s): + break + got.extend(targets(s)) + return [r for r in got if r['address'] == sung] or got + + out, unresolved = [], [] + for i, t, sung in texts: + stop = lrc[i + 1][0] if i + 1 < len(lrc) else None + got = targets(sung) + + if got: + for r in got: + out.append(dict( + start=t, stop=stop, sung=sung, object=r['object'], + kind=r['kind'], km=float(r['km']), + lat=float(r['lat']), lon=float(r['lon']), + km_from='', km_to='')) + continue + + # district header: resolve from the run of objects it introduces + run = run_after(i, sung) if sung in addresses else [] + if run: + kms = [float(r['km']) for r in run] + first = run[0] + out.append(dict( + start=t, stop=stop, sung=sung, object=sung, kind='district', + km=float(first['km']), lat=float(first['lat']), lon=float(first['lon']), + km_from='%.2f' % min(kms), km_to='%.2f' % max(kms))) + elif sung in NON_OBJECT: + out.append(dict(start=t, stop=stop, sung=sung, object='', kind='closing', + km='', lat='', lon='', km_from='', km_to='')) + else: + unresolved.append((t, sung)) + + cols = ['start', 'stop', 'start_s', 'stop_s', 'sung', 'object', 'kind', + 'km', 'lat', 'lon', 'km_from', 'km_to'] + with io.open(p('274-lyrics-index.csv'), 'w', encoding='utf-8', newline='') as f: + w = csv.writer(f) + w.writerow(cols) + for r in out: + w.writerow([ + mmss(r['start']), mmss(r['stop']) if r['stop'] is not None else '', + '%.2f' % r['start'], '%.2f' % r['stop'] if r['stop'] is not None else '', + r['sung'], r['object'], r['kind'], + ('%.2f' % r['km']) if r['km'] != '' else '', + ('%.5f' % r['lat']) if r['lat'] != '' else '', + ('%.5f' % r['lon']) if r['lon'] != '' else '', + r['km_from'], r['km_to']]) + + kinds = {} + for r in out: + kinds[r['kind']] = kinds.get(r['kind'], 0) + 1 + print('%d lyric lines -> %d index rows' % (len(texts), len(out))) + print(' ' + ', '.join('%s %d' % kv for kv in sorted(kinds.items()))) + if unresolved: + print('UNRESOLVED:', file=sys.stderr) + for t, s in unresolved: + print(' %s %s' % (mmss(t), s), file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main()