Training Model

This commit is contained in:
khodowany
2026-10-05 19:44:33 -05:00
parent db7966a430
commit e7180e557b
6603 changed files with 8919 additions and 523063 deletions
-92
View File
@@ -25,98 +25,6 @@ def harmonic_key(measure, previous):
if generator.model.fraction(h['onset']) > 0), measure['duration'], measure['time_signature'])
def voice(source, document, measures, part, initial=None, source_offset=0):
staff = document['staves'][0]
result = generator.score.make_arranged_document(source, staff, staff_id=staff['id'],
staff_name=staff['name'], role='guitar', arrangement_part_type=part, measures=measures)
if part == 'acoustic':
generator.acoustic.prepare(result)
generator.acoustic_voicing.place(result)
generator.acoustic.combine(result)
else:
generator.guitar.place_guitar(result)
generator.model.validate_document(result)
generator.fretboard.validate_document(result)
return result['staves'][0]['measures']
def install(source, document, plans):
"""Freeze complete canonical grips; copies are never placed a second time."""
from generator.arrange.generate import section_source
part = document['staves'][0]['arrangement_part_type']
if part == 'vocal': return
authored = generator.score.source_staff(source)['measures']
sections = form.full_arrangement_sections(authored, part)
if 1 not in plans or not any(s.section_index == 1 for s in sections):
return [] # A standalone section already is its own canonical part.
body = generator.score.body_start_measure_index(authored)
body_measures = copy.deepcopy(authored[body:])
for m in body_measures:
for h in m['harmonies']: h.pop('chord_optional', None)
previous = generator.accompaniment.carried_chords(authored)
plan = plans[1]
def build(measures, prior, offset=0):
carried = generator.accompaniment.carried_chords(measures, prior)
result = []
for i, m in enumerate(measures):
j = offset + i
rhythm = (plan.acoustic_bass[j], plan.acoustic_upper[j]) if part == 'acoustic' else plan.electric[j]
result.append(generator.parts.build_measure(m, f'canonical_{part}_{j}', 'accompaniment', part, carried[i], rhythm=rhythm))
return result
canonical = voice(source, document, build(body_measures, previous[body]), part, previous[body])
canonical_previous = generator.accompaniment.carried_chords(body_measures, previous[body])
keys = [harmonic_key(m, p) for m, p in zip(body_measures, canonical_previous)]
cache, exceptions = {}, []
destination = 0
for si, section in enumerate(sections):
if section.role != 'accompaniment':
destination += section.measure_count
continue
effective = section_source(authored, section, sections[si + 1] if si + 1 < len(sections) else None)
carried = generator.accompaniment.carried_chords(effective)
for j in range(section.measure_count):
source_index = section.source_measure_index + j
index = source_index - body
old = document['staves'][0]['measures'][destination + j]
key = harmonic_key(effective[source_index], carried[source_index])
if key == keys[index]:
template = canonical[index]
else:
# Changed pickups/ending harmony reuse the rhythm and only revoice the
# affected measure. These are chart differences, never solo responses.
identity = (index, repr(key))
if identity not in cache:
cache[identity] = voice(source, document,
build([effective[source_index]], carried[source_index], index), part, carried[source_index], index)[0]
template = cache[identity]
exceptions.append({'measure': old['number'], 'source_measure': authored[source_index]['number'], 'reason': 'changed harmony or arrangement boundary'})
replacement = copy.deepcopy(old)
replacement['voices'] = copy.deepcopy(template['voices'])
replacement['id'] = 'canonical_' + old['id']
generator.score.reidentify([replacement], [])
replacement['id'] = old['id']
following = sections[si + 1] if si + 1 < len(sections) else None
if j == section.measure_count-1 and following and following.role == 'melody' and generator.arrange.pickup.enters(following, authored):
sounding = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(authored[0]) if n['pitches']]
if sounding:
cutoff = generator.arrange.pickup.offset(old, authored[0]) + min(sounding)
prefix = [n for n in generator.score.measure_notes(replacement) if generator.model.fraction(n['onset']) < cutoff]
for n in prefix: generator.arrange.pickup.truncate(n, cutoff)
# Preserve original event IDs: incoming melody ties and tuplets refer
# to these exact pickup events, not to a freshly voiced copy.
tail = copy.deepcopy([n for n in generator.score.measure_notes(old) if generator.model.fraction(n['onset']) >= cutoff])
identifier = old['voices'][0]['id']
for n in prefix: n['voice'] = identifier
replacement['voices'] = [{'id': identifier, 'notes': prefix + tail}]
exceptions.append({'measure': old['number'], 'source_measure': authored[source_index]['number'], 'reason': 'protected melody pickup'})
document['staves'][0]['measures'][destination + j] = replacement
destination += section.measure_count
# Build-only diagnostics live outside the published musical schema.
return exceptions
def audit(source, documents):
"""Compare music, not generated IDs, at every reused form position."""
authored = generator.score.source_staff(source)['measures']
+11 -40
View File
@@ -17,21 +17,18 @@ import generator.melody
import generator.model
import generator.musicxml
import generator.parts
import generator.rhythm
import generator.scales.library
import generator.scales.mapping
import generator.scales.timeline
import generator.score
import generator.solo.arrangement
import generator.solo.sequences
FINAL_SECTION_INDEX = 6
def arrange_document(document: dict, part_type: str, *, rhythms: generator.rhythm.Library, scales: generator.scales.library.Library | None = None) -> dict:
"""Place the regular form; the song pipeline inserts solos after all heads exist."""
document = generator.parts.prepare_document(document)
plans = plan_arrangement(document, rhythms) if part_type != 'vocal' else {}
return _arrange_document(document, part_type, plans, scales=scales)
def arrange_document(document: dict, part_type: str) -> dict:
"""Canonical source-only template; solos are inserted by production roles."""
from generator import comping, vocabulary
document=generator.parts.prepare_document(document)
with vocabulary.using(vocabulary.load(),part_type+'_accompaniment'):
backing=comping.Canonical(document,part_type,'canonical') if part_type!='vocal' else None
return _arrange_document(document,part_type,accompaniment=backing)
def section_source(source: list[dict], section, next_section=None) -> list[dict]:
result = copy.deepcopy(source)
@@ -44,27 +41,13 @@ def section_source(source: list[dict], section, next_section=None) -> list[dict]
generator.arrange.pickup.harmonies(destination, result[0])
return result
def plan_arrangement(document: dict, rhythms: generator.rhythm.Library) -> dict[int, generator.accompaniment.Plan]:
source = generator.score.source_staff(document)['measures']
body = generator.score.body_start_measure_index(source)
previous = generator.accompaniment.carried_chords(source)
canonical = generator.accompaniment.plan_section(source[body:], rhythms, initial=previous[body], electric=True)
result = {}
for section in generator.arrange.form.full_arrangement_sections(source, 'acoustic'):
start = section.source_measure_index - body
end = start + section.measure_count
result[section.section_index] = generator.accompaniment.Plan(canonical.acoustic_bass[start:end],
canonical.acoustic_upper[start:end], canonical.electric[start:end])
return result
def _arrange_document(document: dict, part_type: str, plans: dict[int, generator.accompaniment.Plan], *, scales: generator.scales.library.Library | None = None, reports: list[str] | None = None, source_name: str = '', owned=None, accompaniment=None) -> dict:
def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None, accompaniment=None) -> dict:
if part_type not in generator.parts.PART_TYPES:
raise ValueError(f'unknown arrangement part: {part_type}')
staff = generator.score.source_staff(document)
source = staff['measures']
sections = generator.arrange.form.full_arrangement_sections(source, part_type)
output, tuplets, octave_shifts, voicing_origins = [], [], [], []
solos = []
absolute = fractions.Fraction()
source_starts = []
source_time = fractions.Fraction()
@@ -78,22 +61,16 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
segment = []
for si in range(section.source_measure_index, section.source_measure_index + section.measure_count):
m = section_measures[si]
rhythm = None
if role == 'accompaniment' and accompaniment is None:
plan = plans[section.section_index]
rhythm = (plan.acoustic_bass[len(segment)], plan.acoustic_upper[len(segment)]) if part_type == 'acoustic' else plan.electric[len(segment)]
if role == 'accompaniment' and accompaniment is not None:
result = accompaniment(m, previous[si], si)
result['id'] = f'measure_{len(output) + len(segment) + 1}'
result['section_label'] = section.label if not segment else ''
else:
result = generator.parts.build_measure(m, f'measure_{len(output) + len(segment) + 1}', role, part_type, previous[si], section.label if not segment else '', rhythm)
result = generator.parts.build_measure(m, f'measure_{len(output) + len(segment) + 1}', role, part_type, previous[si], section.label if not segment else '')
segment.append(result)
if part_type == 'electric' and section.role in ('melody', 'accompaniment'):
voicing_origins.append(generator.electric.MeasureOrigin(source_starts[si], section.role, result))
segment_length = sum((generator.model.fraction(m['duration']) for m in segment), fractions.Fraction())
if section.role == 'slash':
solos.append(generator.scales.timeline.Solo(absolute, absolute + segment_length, section.label))
if role in ('vocal', 'melody'):
start = source_starts[section.source_measure_index]
for shift in staff.get('octave_shifts', []):
@@ -125,7 +102,7 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
generator.acoustic.prepare(result)
generator.acoustic_voicing.place(result)
generator.acoustic.combine(result)
elif part_type == 'electric' and accompaniment is None and (owned is None or 'melody' in owned or 'accompaniment' in owned):
elif part_type == 'electric' and (owned is None or 'melody' in owned):
generator.melody.harmonize(result)
generator.guitar.place_guitar(result)
if generator.accompaniment.connect_continuations(result):
@@ -134,15 +111,9 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
# the complete performance, including its lead-in and repeated choruses.
for number, measure in enumerate(output, 1):
measure['number'] = str(number)
if part_type != 'vocal' and owned is None:
generator.arrange.accompaniment.install(document, result, plans)
generator.fretboard.validate_document(result)
if scales and solos:
report = generator.scales.mapping.annotate(result, solos, scales, source_name or document['metadata']['title'])
if reports is not None:
reports.append(report)
return result
def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, rhythms=None, scales=None, sequences=None, composition_jobs=2, cache_dir=None, progress=False, timings=None) -> int:
def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, composition_jobs=2, cache_dir=None, progress=False, timings=None) -> int:
from generator.production import generate_tree
return generate_tree(source_dir, output_dir, jobs=composition_jobs, cache_dir=cache_dir, progress=progress,timings=timings)