152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
|
|
#!/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()
|