batoon commited on
Commit
085827b
·
verified ·
1 Parent(s): f647f52

Add sampled guitar and bass with effects; make YuE2 opt-in

Browse files
Files changed (4) hide show
  1. Dockerfile +17 -2
  2. README.md +23 -9
  3. app.py +21 -8
  4. pipeline.py +80 -16
Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM python:3.12-slim
2
 
3
  ENV DEBIAN_FRONTEND=noninteractive \
4
  PYTHONUNBUFFERED=1 \
@@ -6,9 +6,24 @@ ENV DEBIAN_FRONTEND=noninteractive \
6
  GRADIO_ANALYTICS_ENABLED=False
7
 
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
9
- ffmpeg fluidsynth libsndfile1 libgomp1 && \
10
  rm -rf /var/lib/apt/lists/*
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  # SheetSage2's published code uses the older Transformers API. Separate
13
  # processes keep that runtime independent of YuE2's pinned dependencies.
14
  RUN python -m venv /opt/sheet && /opt/sheet/bin/pip install --no-cache-dir \
 
1
+ FROM python:3.12-slim-bookworm
2
 
3
  ENV DEBIAN_FRONTEND=noninteractive \
4
  PYTHONUNBUFFERED=1 \
 
6
  GRADIO_ANALYTICS_ENABLED=False
7
 
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ ffmpeg fluidsynth libsndfile1 libgomp1 curl ca-certificates p7zip-full && \
10
  rm -rf /var/lib/apt/lists/*
11
 
12
+ # Full multi-sampled CC0 electric guitar and bass. Pin release archives so a
13
+ # changed upstream download cannot silently change audible results.
14
+ RUN mkdir -p /opt/soundfonts /tmp/fonts && \
15
+ curl -fL --retry 3 -o /tmp/guitar.7z \
16
+ 'https://github.com/freepats/electric-guitar-FSBS-clean/releases/download/2026-08-07/EGuitarFSBS-clean-SF2-20260807.7z' && \
17
+ echo '6d338afefe0efb629afd9ba28a5c8c129fc28b97ef1b57a31ebceb7bf0e3bf65 /tmp/guitar.7z' | sha256sum -c - && \
18
+ 7z x -y -o/tmp/fonts /tmp/guitar.7z >/dev/null && \
19
+ mv '/tmp/fonts/EGuitarFSBS-clean SF2-20260807/EGuitarFSBS-clean bridge 20260807.sf2' /opt/soundfonts/electric-clean.sf2 && \
20
+ curl -fL --retry 3 -o /tmp/bass.7z \
21
+ 'https://github.com/freepats/electric-bass-YR/releases/download/2019-09-30/FingerBassYR-SF2-20190930.7z' && \
22
+ echo 'f5b5cfe04435a9e9037d1f97d515dc596535acf26637db9579d724cd757f179e /tmp/bass.7z' | sha256sum -c - && \
23
+ 7z x -y -o/tmp/fonts /tmp/bass.7z >/dev/null && \
24
+ mv '/tmp/fonts/FingerBassYR SF2-20190930/FingerBassYR 20190930.sf2' /opt/soundfonts/finger-bass.sf2 && \
25
+ rm -rf /tmp/fonts /tmp/guitar.7z /tmp/bass.7z
26
+
27
  # SheetSage2's published code uses the older Transformers API. Separate
28
  # processes keep that runtime independent of YuE2's pinned dependencies.
29
  RUN python -m venv /opt/sheet && /opt/sheet/bin/pip install --no-cache-dir \
README.md CHANGED
@@ -16,15 +16,21 @@ models:
16
 
17
  # Hum
18
 
19
- Upload a short hum or record one in the browser. One request generates six
20
- comparisons. SheetSage2 extracts source-timed notes and beats; FluidSynth
21
- renders an instrument alone and the same instrument with a GM drum kit placed
22
- on those beats. A continuous-F0 synth follows voice pitch and amplitude. A
23
- single YuE2 model load produces solo, melody-with-drums, and composition
24
- variants from the SheetSage2 ABC. YuE2 drums are requested in the style prompt,
25
- not imposed as a separate symbolic track. YuE2 can introduce voices or other
26
- sounds and does not preserve source timing exactly. The three deterministic
27
- previews appear first; YuE2 previews then appear as each generation finishes.
 
 
 
 
 
 
28
 
29
  Output audio is downloadable 320 kbit/s CBR MP3. This Space is public: uploads
30
  are processed on its Hugging Face server and results are served from temporary
@@ -40,5 +46,13 @@ Model licenses and source: [SheetSage2](https://huggingface.co/m-a-p/SheetSage2)
40
  [YuE2-3B](https://huggingface.co/m-a-p/YuE2-3B),
41
  [YuE2-Vae](https://huggingface.co/m-a-p/YuE2-Vae).
42
 
 
 
 
 
 
 
 
 
43
  This is a research prototype; a single owner-reviewed recording is development
44
  evidence, not a general accuracy guarantee.
 
16
 
17
  # Hum
18
 
19
+ Upload a short hum or record one in the browser. One request generates seven
20
+ source-timed comparisons. SheetSage2 extracts notes and beats; FluidSynth
21
+ renders an instrument alone and with a GM drum kit, plus sampled electric
22
+ guitar and fingered bass in clean and processed versions. The bass is shifted
23
+ down one octave, with note starts and lengths preserved. A continuous-F0 synth
24
+ follows voice pitch and amplitude. The sampler uses full-size FreePats electric
25
+ guitar and fingered bass SoundFonts, with FFmpeg compression, filtering,
26
+ oversampled soft clipping, and a short guitar room echo in processed versions.
27
+
28
+ YuE2 is off by default. Checking its option adds three slow experiments from
29
+ SheetSage2 ABC: solo, melody-with-drums, and composition. The drums in these
30
+ experiments are requested in the style prompt, not imposed as a separate
31
+ symbolic track. YuE2 can introduce voices or other sounds and does not
32
+ preserve source timing exactly. Sampler previews appear first; optional YuE2
33
+ previews then appear as each generation finishes.
34
 
35
  Output audio is downloadable 320 kbit/s CBR MP3. This Space is public: uploads
36
  are processed on its Hugging Face server and results are served from temporary
 
46
  [YuE2-3B](https://huggingface.co/m-a-p/YuE2-3B),
47
  [YuE2-Vae](https://huggingface.co/m-a-p/YuE2-Vae).
48
 
49
+ Sample library sources, both CC0 1.0: [FreePats FSBS Clean Electric Guitar](https://freepats.zenvoid.org/ElectricGuitar/clean-electric-guitar.html)
50
+ (full SF2, release 2026-08-07; archive SHA-256
51
+ `6d338afefe0efb629afd9ba28a5c8c129fc28b97ef1b57a31ebceb7bf0e3bf65`),
52
+ and [FreePats Finger Bass YR](https://freepats.zenvoid.org/ElectricGuitar/clean-electric-bass.html)
53
+ (SF2, release 2019-09-30; archive SHA-256
54
+ `f5b5cfe04435a9e9037d1f97d515dc596535acf26637db9579d724cd757f179e`).
55
+ The Docker build verifies both archives before extraction.
56
+
57
  This is a research prototype; a single owner-reviewed recording is development
58
  evidence, not a general accuracy guarantee.
app.py CHANGED
@@ -29,12 +29,12 @@ def suggest_prompts(instrument: str) -> tuple[str, str, str]:
29
 
30
 
31
  def run(audio: str | None, instrument: str, solo_style: str, drum_style: str,
32
- arrangement_style: str, seed: int,
33
  progress: gr.Progress = gr.Progress(track_tqdm=False)):
34
  try:
35
  styles = {"yue_solo": solo_style, "yue_drums": drum_style,
36
  "yue_arrangement": arrangement_style}
37
- yield from generate_all(audio, instrument, styles, int(seed), progress)
38
  except Exception as error:
39
  raise gr.Error(str(error)) from error
40
 
@@ -52,13 +52,15 @@ CSS = """
52
  with gr.Blocks(title="Hum · напев в инструмент", css=CSS, theme=gr.themes.Soft()) as demo:
53
  gr.HTML("""<div id='hero'><h1>Hum <span style='color:#6474db'>♪</span></h1>
54
  <p>Напойте фразу в микрофон или загрузите короткую запись. Один запуск
55
- создаст все варианты: инструмент, инструмент с барабанами, непрерывный
56
- синтез и три эксперимента YuE2.</p></div>""")
57
  with gr.Row():
58
  with gr.Column(scale=4):
59
  audio = gr.Audio(label="Ваш напев", sources=["microphone", "upload"],
60
  type="filepath", format="wav")
61
  instrument = gr.Dropdown(list(INSTRUMENTS), value="Флейта", label="Инструмент")
 
 
62
  with gr.Accordion("Настройки YuE2", open=False):
63
  defaults = suggest_prompts("Флейта")
64
  solo_style = gr.Textbox(label="Соло · style", lines=2, value=defaults[0])
@@ -67,10 +69,12 @@ with gr.Blocks(title="Hum · напев в инструмент", css=CSS, theme
67
  arrangement_style = gr.Textbox(label="Композиция · style", lines=2,
68
  value=defaults[2])
69
  seed = gr.Number(label="Seed", value=4096, precision=0)
70
- go = gr.Button("Создать все варианты", variant="primary", size="lg")
71
  gr.HTML("""<div class='note'><b>Текущая цель:</b> один инструмент и
72
- приблизительно исходное время — первые два варианта. Барабаны
73
- привязаны к долям, которые нашёл SheetSage2. YuE2 может изменить
 
 
74
  длительность, добавить человеческие звуки или другие инструменты;
75
  отсутствие вокала не гарантируется. Это публичный Space: записи
76
  обрабатываются на сервере Hugging Face.</div>""")
@@ -83,6 +87,14 @@ with gr.Blocks(title="Hum · напев в инструмент", css=CSS, theme
83
  interactive=False)
84
  contour = gr.Audio(label="Непрерывная высота · synth",
85
  interactive=False)
 
 
 
 
 
 
 
 
86
  with gr.Column():
87
  yue_solo = gr.Audio(label="YuE2 · соло", interactive=False)
88
  yue_drums = gr.Audio(label="YuE2 · мелодия + барабаны",
@@ -98,8 +110,9 @@ with gr.Blocks(title="Hum · напев в инструмент", css=CSS, theme
98
  instrument.change(suggest_prompts, inputs=instrument,
99
  outputs=[solo_style, drum_style, arrangement_style])
100
  go.click(run, inputs=[audio, instrument, solo_style, drum_style,
101
- arrangement_style, seed],
102
  outputs=[instrument_only, instrument_drums, contour,
 
103
  yue_solo, yue_drums, yue_arrangement,
104
  midi, drum_midi, beat_lab, abc, details], show_progress="full")
105
 
 
29
 
30
 
31
  def run(audio: str | None, instrument: str, solo_style: str, drum_style: str,
32
+ arrangement_style: str, seed: int, include_yue: bool,
33
  progress: gr.Progress = gr.Progress(track_tqdm=False)):
34
  try:
35
  styles = {"yue_solo": solo_style, "yue_drums": drum_style,
36
  "yue_arrangement": arrangement_style}
37
+ yield from generate_all(audio, instrument, styles, int(seed), include_yue, progress)
38
  except Exception as error:
39
  raise gr.Error(str(error)) from error
40
 
 
52
  with gr.Blocks(title="Hum · напев в инструмент", css=CSS, theme=gr.themes.Soft()) as demo:
53
  gr.HTML("""<div id='hero'><h1>Hum <span style='color:#6474db'>♪</span></h1>
54
  <p>Напойте фразу в микрофон или загрузите короткую запись. Один запуск
55
+ создаст все сэмплерные варианты с исходными нотами и временем: инструмент,
56
+ электрогитару, бас и эффекты. YuE2 включается по желанию.</p></div>""")
57
  with gr.Row():
58
  with gr.Column(scale=4):
59
  audio = gr.Audio(label="Ваш напев", sources=["microphone", "upload"],
60
  type="filepath", format="wav")
61
  instrument = gr.Dropdown(list(INSTRUMENTS), value="Флейта", label="Инструмент")
62
+ include_yue = gr.Checkbox(label="Дополнительно запустить YuE2 · три генерации (~3 минуты)",
63
+ value=False)
64
  with gr.Accordion("Настройки YuE2", open=False):
65
  defaults = suggest_prompts("Флейта")
66
  solo_style = gr.Textbox(label="Соло · style", lines=2, value=defaults[0])
 
69
  arrangement_style = gr.Textbox(label="Композиция · style", lines=2,
70
  value=defaults[2])
71
  seed = gr.Number(label="Seed", value=4096, precision=0)
72
+ go = gr.Button("Создать сэмплерные варианты", variant="primary", size="lg")
73
  gr.HTML("""<div class='note'><b>Текущая цель:</b> один инструмент и
74
+ приблизительно исходное время. Электрогитара и бас используют
75
+ записанные сэмплы; бас звучит октавой ниже. Обработанные версии
76
+ добавляют усиление и лёгкий эффект, но не новые ноты. Барабаны
77
+ привязаны к долям SheetSage2. YuE2 может изменить
78
  длительность, добавить человеческие звуки или другие инструменты;
79
  отсутствие вокала не гарантируется. Это публичный Space: записи
80
  обрабатываются на сервере Hugging Face.</div>""")
 
87
  interactive=False)
88
  contour = gr.Audio(label="Непрерывная высота · synth",
89
  interactive=False)
90
+ guitar_clean = gr.Audio(label="Электрогитара · чистая",
91
+ interactive=False)
92
+ guitar_amp = gr.Audio(label="Электрогитара · усилитель и эффекты",
93
+ interactive=False)
94
+ bass_clean = gr.Audio(label="Бас · чистый",
95
+ interactive=False)
96
+ bass_amp = gr.Audio(label="Бас · тёплый усилитель",
97
+ interactive=False)
98
  with gr.Column():
99
  yue_solo = gr.Audio(label="YuE2 · соло", interactive=False)
100
  yue_drums = gr.Audio(label="YuE2 · мелодия + барабаны",
 
110
  instrument.change(suggest_prompts, inputs=instrument,
111
  outputs=[solo_style, drum_style, arrangement_style])
112
  go.click(run, inputs=[audio, instrument, solo_style, drum_style,
113
+ arrangement_style, seed, include_yue],
114
  outputs=[instrument_only, instrument_drums, contour,
115
+ guitar_clean, guitar_amp, bass_clean, bass_amp,
116
  yue_solo, yue_drums, yue_arrangement,
117
  midi, drum_midi, beat_lab, abc, details], show_progress="full")
118
 
pipeline.py CHANGED
@@ -17,8 +17,10 @@ from logic import INSTRUMENTS, drum_hits, instrumental_abc, validate_duration
17
  ROOT = Path(__file__).resolve().parent
18
  SHEET_PYTHON = Path(os.environ.get("HUM_SHEET_PYTHON", "/opt/sheet/bin/python"))
19
  YUE_PYTHON = Path(os.environ.get("HUM_YUE_PYTHON", "/opt/yue/bin/python"))
 
20
  VARIANTS = (
21
  "instrument", "instrument_drums", "contour",
 
22
  "yue_solo", "yue_drums", "yue_arrangement",
23
  )
24
 
@@ -72,12 +74,13 @@ def transcribe(pcm24: Path, folder: Path, log: Path) -> tuple[Path, Path, Path,
72
  folder / "full.abc", folder / "beat.lab")
73
 
74
 
75
- def render_midi(midi_path: Path, output: Path, duration: float, log: Path) -> Path:
 
76
  import numpy as np
77
  import pretty_midi
78
  import soundfile as sf
79
 
80
- soundfont = Path(pretty_midi.__file__).parent / "TimGM6mb.sf2"
81
  if not soundfont.is_file():
82
  raise RuntimeError("SoundFont missing from pretty_midi installation")
83
  raw = output.with_name(output.stem + "-tail.wav")
@@ -94,25 +97,61 @@ def render_midi(midi_path: Path, output: Path, duration: float, log: Path) -> Pa
94
  return output
95
 
96
 
97
- def render_instrument(midi_path: Path, folder: Path, instrument: str,
98
- duration: float, log: Path) -> tuple[Path, Path]:
 
99
  import pretty_midi
100
 
101
- if instrument not in INSTRUMENTS:
102
- raise ValueError("Неизвестный инструмент")
103
  source = pretty_midi.PrettyMIDI(str(midi_path))
104
  notes = [n for track in source.instruments for n in track.notes]
105
  if not notes:
106
  raise RuntimeError("SheetSage2 не нашёл нот в напеве")
107
  midi = pretty_midi.PrettyMIDI(initial_tempo=120, resolution=10000)
108
- track = pretty_midi.Instrument(program=INSTRUMENTS[instrument][0], name="Melody")
109
  for note in notes:
110
- track.notes.append(pretty_midi.Note(velocity=90, pitch=note.pitch,
 
 
 
111
  start=note.start, end=note.end))
112
  midi.instruments.append(track)
113
- rendered_midi = folder / "instrument.mid"
114
  midi.write(str(rendered_midi))
115
- return render_midi(rendered_midi, folder / "instrument.wav", duration, log), rendered_midi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
 
118
  def render_drums(melody_midi: Path, beat_lab: Path, folder: Path,
@@ -256,7 +295,7 @@ def render_yue_batch(melody_abc: Path, full_abc: Path, folder: Path,
256
 
257
 
258
  def encode_mp3(source: Path, destination: Path, log: Path,
259
- normalize: bool = True) -> None:
260
  import numpy as np
261
  import soundfile as sf
262
 
@@ -266,7 +305,7 @@ def encode_mp3(source: Path, destination: Path, log: Path,
266
  peak = float(np.max(np.abs(audio)))
267
  if peak <= 1e-8:
268
  raise RuntimeError("Результат слишком тихий")
269
- gain = min(4., .95 / peak) if normalize else 1.
270
  safe = destination.with_suffix(".safe.wav")
271
  sf.write(safe, audio * gain, rate, subtype="PCM_24")
272
  command(["ffmpeg", "-nostdin", "-v", "error", "-y", "-i", str(safe),
@@ -277,6 +316,7 @@ def encode_mp3(source: Path, destination: Path, log: Path,
277
 
278
 
279
  def generate_all(source: str, instrument: str, styles: dict[str, str], seed: int,
 
280
  progress=None) -> Iterator[tuple]:
281
  if not source or instrument not in INSTRUMENTS:
282
  raise ValueError("Загрузите или запишите напев и выберите инструмент")
@@ -292,9 +332,10 @@ def generate_all(source: str, instrument: str, styles: dict[str, str], seed: int
292
  drum_count = 0
293
  drums_midi = None
294
 
295
- def deliver(name: str, raw: Path, *, normalize: bool = True) -> None:
 
296
  destination = folder / f"{name}.mp3"
297
- encode_mp3(raw, destination, log, normalize=normalize)
298
  outputs[name] = str(destination)
299
  durations[name] = round(__import__("soundfile").info(raw).duration, 3)
300
 
@@ -302,9 +343,10 @@ def generate_all(source: str, instrument: str, styles: dict[str, str], seed: int
302
  metadata = {"instrument": instrument, "source_sha256": digest,
303
  "source_seconds": round(duration, 3),
304
  "result_seconds": dict(durations), "drum_hits": drum_count,
305
- "yue_timing": dict(yue_timing),
306
  "errors": dict(errors), "complete": complete,
307
- "source_timing_preserved": ["instrument", "instrument_drums", "contour"],
 
308
  "yue_vocal_absence_guaranteed": False,
309
  "elapsed_seconds": round(time.monotonic() - began, 2)}
310
  return (*(outputs[name] for name in VARIANTS), str(melody_midi),
@@ -339,6 +381,28 @@ def generate_all(source: str, instrument: str, styles: dict[str, str], seed: int
339
  (folder / "instrumental-melody.abc").write_text(
340
  instrumental_abc(melody_abc.read_text()))
341
  yield snapshot()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  if progress:
343
  progress(.50, desc="YuE2 создаёт сразу три варианта; это займёт несколько минут")
344
  completed = set()
 
17
  ROOT = Path(__file__).resolve().parent
18
  SHEET_PYTHON = Path(os.environ.get("HUM_SHEET_PYTHON", "/opt/sheet/bin/python"))
19
  YUE_PYTHON = Path(os.environ.get("HUM_YUE_PYTHON", "/opt/yue/bin/python"))
20
+ SOUNDFONT_DIR = Path(os.environ.get("HUM_SOUNDFONTS", "/opt/soundfonts"))
21
  VARIANTS = (
22
  "instrument", "instrument_drums", "contour",
23
+ "guitar_clean", "guitar_amp", "bass_clean", "bass_amp",
24
  "yue_solo", "yue_drums", "yue_arrangement",
25
  )
26
 
 
74
  folder / "full.abc", folder / "beat.lab")
75
 
76
 
77
+ def render_midi(midi_path: Path, output: Path, duration: float, log: Path,
78
+ soundfont: Path | None = None) -> Path:
79
  import numpy as np
80
  import pretty_midi
81
  import soundfile as sf
82
 
83
+ soundfont = soundfont or Path(pretty_midi.__file__).parent / "TimGM6mb.sf2"
84
  if not soundfont.is_file():
85
  raise RuntimeError("SoundFont missing from pretty_midi installation")
86
  raw = output.with_name(output.stem + "-tail.wav")
 
97
  return output
98
 
99
 
100
+ def render_preset(midi_path: Path, folder: Path, name: str, program: int,
101
+ duration: float, log: Path, *, soundfont: Path | None = None,
102
+ transpose: int = 0) -> tuple[Path, Path]:
103
  import pretty_midi
104
 
 
 
105
  source = pretty_midi.PrettyMIDI(str(midi_path))
106
  notes = [n for track in source.instruments for n in track.notes]
107
  if not notes:
108
  raise RuntimeError("SheetSage2 не нашёл нот в напеве")
109
  midi = pretty_midi.PrettyMIDI(initial_tempo=120, resolution=10000)
110
+ track = pretty_midi.Instrument(program=program, name=name)
111
  for note in notes:
112
+ pitch = note.pitch + transpose
113
+ if not 0 <= pitch <= 127:
114
+ raise ValueError("Транспонированная нота вне MIDI-диапазона")
115
+ track.notes.append(pretty_midi.Note(velocity=90, pitch=pitch,
116
  start=note.start, end=note.end))
117
  midi.instruments.append(track)
118
+ rendered_midi = folder / f"{name}.mid"
119
  midi.write(str(rendered_midi))
120
+ return (render_midi(rendered_midi, folder / f"{name}.wav", duration, log,
121
+ soundfont=soundfont), rendered_midi)
122
+
123
+
124
+ def render_instrument(midi_path: Path, folder: Path, instrument: str,
125
+ duration: float, log: Path) -> tuple[Path, Path]:
126
+ if instrument not in INSTRUMENTS:
127
+ raise ValueError("Неизвестный инструмент")
128
+ return render_preset(midi_path, folder, "instrument", INSTRUMENTS[instrument][0],
129
+ duration, log)
130
+
131
+
132
+ def apply_effect(source: Path, output: Path, kind: str, duration: float,
133
+ log: Path) -> Path:
134
+ # Both chains use the same sampled notes and retain the source duration.
135
+ chains = {
136
+ "guitar": ("highpass=f=75,acompressor=threshold=0.08:ratio=2.5:"
137
+ "attack=8:release=90,volume=2.2,"
138
+ "asoftclip=type=tanh:threshold=0.55:oversample=4,"
139
+ "lowpass=f=6500,aecho=0.9:0.85:75:0.08"),
140
+ "bass": ("highpass=f=32,lowpass=f=4500,"
141
+ "acompressor=threshold=0.09:ratio=3:attack=12:release=120,"
142
+ "volume=1.65,asoftclip=type=tanh:threshold=0.72:oversample=4"),
143
+ }
144
+ if kind not in chains:
145
+ raise ValueError("Неизвестный эффект")
146
+ command(["ffmpeg", "-nostdin", "-v", "error", "-y", "-i", str(source),
147
+ "-af", chains[kind], "-t", f"{duration:.6f}", "-ar", "48000",
148
+ "-c:a", "pcm_f32le", str(output)], log, timeout=120)
149
+ import numpy as np
150
+ import soundfile as sf
151
+ audio, rate = sf.read(output, dtype="float32", always_2d=True)
152
+ if rate != 48000 or abs(len(audio) - round(duration * rate)) > 1 or not np.isfinite(audio).all():
153
+ raise RuntimeError("Эффект нарушил длительность или повредил аудио")
154
+ return output
155
 
156
 
157
  def render_drums(melody_midi: Path, beat_lab: Path, folder: Path,
 
295
 
296
 
297
  def encode_mp3(source: Path, destination: Path, log: Path,
298
+ normalize: bool = True, max_gain: float = 4.) -> None:
299
  import numpy as np
300
  import soundfile as sf
301
 
 
305
  peak = float(np.max(np.abs(audio)))
306
  if peak <= 1e-8:
307
  raise RuntimeError("Результат слишком тихий")
308
+ gain = min(max_gain, .8 / peak) if normalize else 1.
309
  safe = destination.with_suffix(".safe.wav")
310
  sf.write(safe, audio * gain, rate, subtype="PCM_24")
311
  command(["ffmpeg", "-nostdin", "-v", "error", "-y", "-i", str(safe),
 
316
 
317
 
318
  def generate_all(source: str, instrument: str, styles: dict[str, str], seed: int,
319
+ include_yue: bool = False,
320
  progress=None) -> Iterator[tuple]:
321
  if not source or instrument not in INSTRUMENTS:
322
  raise ValueError("Загрузите или запишите напев и выберите инструмент")
 
332
  drum_count = 0
333
  drums_midi = None
334
 
335
+ def deliver(name: str, raw: Path, *, normalize: bool = True,
336
+ max_gain: float = 4.) -> None:
337
  destination = folder / f"{name}.mp3"
338
+ encode_mp3(raw, destination, log, normalize=normalize, max_gain=max_gain)
339
  outputs[name] = str(destination)
340
  durations[name] = round(__import__("soundfile").info(raw).duration, 3)
341
 
 
343
  metadata = {"instrument": instrument, "source_sha256": digest,
344
  "source_seconds": round(duration, 3),
345
  "result_seconds": dict(durations), "drum_hits": drum_count,
346
+ "yue_requested": include_yue, "yue_timing": dict(yue_timing),
347
  "errors": dict(errors), "complete": complete,
348
+ "source_timing_preserved": list(VARIANTS[:7]),
349
+ "bass_transpose_semitones": -12,
350
  "yue_vocal_absence_guaranteed": False,
351
  "elapsed_seconds": round(time.monotonic() - began, 2)}
352
  return (*(outputs[name] for name in VARIANTS), str(melody_midi),
 
381
  (folder / "instrumental-melody.abc").write_text(
382
  instrumental_abc(melody_abc.read_text()))
383
  yield snapshot()
384
+ if progress:
385
+ progress(.48, desc="Сэмплирую электрогитару и бас")
386
+ for name, font_name, transpose, effect in (
387
+ ("guitar_clean", "electric-clean.sf2", 0, "guitar"),
388
+ ("bass_clean", "finger-bass.sf2", -12, "bass"),
389
+ ):
390
+ try:
391
+ raw, _ = render_preset(midi, folder, name, 0, duration, log,
392
+ soundfont=SOUNDFONT_DIR / font_name,
393
+ transpose=transpose)
394
+ deliver(name, raw, max_gain=20.)
395
+ deliver(name.replace("clean", "amp"),
396
+ apply_effect(raw, folder / f"{name}-amp.wav", effect,
397
+ duration, log), max_gain=20.)
398
+ except (ValueError, RuntimeError, OSError) as error:
399
+ errors[name] = str(error)
400
+ yield snapshot()
401
+ if not include_yue:
402
+ final = snapshot(complete=True)
403
+ (folder / "result.json").write_text(json.dumps(final[-1], ensure_ascii=False, indent=2) + "\n")
404
+ yield final
405
+ return
406
  if progress:
407
  progress(.50, desc="YuE2 создаёт сразу три варианта; это займёт несколько минут")
408
  completed = set()