Skip to content

Commit b2740de

Browse files
committed
Fix project.yaml overwrite when forcing a re-separate.
1 parent 92bcac6 commit b2740de

3 files changed

Lines changed: 122 additions & 11 deletions

File tree

cleave/project.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,44 @@ def write_manifest(
109109
original_path: Path,
110110
demucs_model: str,
111111
separated_at: datetime | None = None,
112-
song_markers: Sequence[float] = (),
112+
song_markers: Sequence[float] | None = None,
113113
) -> Path:
114+
"""Create or update ``project.yaml`` mix and ingest fields.
115+
116+
When the file already exists, only ``slug``, ``mix.filename``, and
117+
``ingest`` are updated. ``song-markers``, ``restored-from``, and other
118+
fields are preserved unless ``song_markers`` is passed explicitly.
119+
"""
114120
when = separated_at or datetime.now(timezone.utc)
115-
manifest = ProjectManifest(
116-
version=1,
117-
slug=slug,
118-
mix_filename=mix_filename,
119-
original_path=str(original_path.resolve()),
120-
separated_at=when.isoformat(),
121-
demucs_model=demucs_model,
122-
song_markers=tuple(float(t) for t in song_markers),
123-
)
124121
path = manifest_path(project_dir)
122+
original = str(original_path.resolve())
123+
separated = when.isoformat()
124+
if path.is_file():
125+
existing = load_manifest(project_dir)
126+
markers = (
127+
tuple(float(t) for t in song_markers)
128+
if song_markers is not None
129+
else existing.song_markers
130+
)
131+
manifest = replace(
132+
existing,
133+
slug=slug,
134+
mix_filename=mix_filename,
135+
original_path=original,
136+
separated_at=separated,
137+
demucs_model=demucs_model,
138+
song_markers=markers,
139+
)
140+
else:
141+
manifest = ProjectManifest(
142+
version=1,
143+
slug=slug,
144+
mix_filename=mix_filename,
145+
original_path=original,
146+
separated_at=separated,
147+
demucs_model=demucs_model,
148+
song_markers=tuple(float(t) for t in (song_markers or ())),
149+
)
125150
with path.open("w", encoding="utf-8") as handle:
126151
yaml.safe_dump(manifest.to_dict(), handle, sort_keys=False)
127152
return path

tests/cleave/test_project.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,47 @@ def test_manifest_round_trip_with_song_markers(tmp_path: Path) -> None:
241241
assert data["ingest"]["demucs_model"] == "htdemucs"
242242

243243

244+
def test_write_manifest_update_preserves_song_markers_and_restored_from(
245+
tmp_path: Path,
246+
) -> None:
247+
project = tmp_path / "song"
248+
project.mkdir()
249+
original = tmp_path / "source.flac"
250+
original.write_bytes(b"audio")
251+
when = datetime(2026, 6, 8, 20, 15, tzinfo=timezone.utc)
252+
write_manifest(
253+
project,
254+
slug="song",
255+
mix_filename="song.flac",
256+
original_path=original,
257+
demucs_model="htdemucs",
258+
separated_at=when,
259+
song_markers=(10.0, 42.5),
260+
)
261+
rewrite_manifest_slug(project, "song", restored_from="archived-slug")
262+
263+
new_original = tmp_path / "new-source.wav"
264+
new_original.write_bytes(b"new")
265+
later = datetime(2026, 7, 22, 12, 0, tzinfo=timezone.utc)
266+
write_manifest(
267+
project,
268+
slug="song",
269+
mix_filename="song.wav",
270+
original_path=new_original,
271+
demucs_model="htdemucs_ft",
272+
separated_at=later,
273+
)
274+
275+
manifest = load_manifest(project)
276+
assert manifest.mix_filename == "song.wav"
277+
assert manifest.original_path == str(new_original.resolve())
278+
assert manifest.separated_at == "2026-07-22T12:00:00+00:00"
279+
assert manifest.demucs_model == "htdemucs_ft"
280+
assert manifest.song_markers == (10.0, 42.5)
281+
assert manifest.restored_from == "archived-slug"
282+
assert manifest.version == 1
283+
284+
244285
def test_save_song_markers_preserves_ingest(tmp_path: Path) -> None:
245286
project = tmp_path / "song"
246287
project.mkdir()

tests/cleave/test_separate.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010

1111
from cleave.config import VIZ_CONFIG_FILENAME
1212
from cleave.extract import STEM_NAMES, stems_dir
13-
from cleave.project import PROJECT_FILENAME, load_manifest, write_manifest
13+
from cleave.project import (
14+
PROJECT_FILENAME,
15+
load_manifest,
16+
rewrite_manifest_slug,
17+
write_manifest,
18+
)
1419
from cleave.separate import (
1520
_run_demucs,
1621
project_stems_complete,
@@ -513,3 +518,43 @@ def fake_run(cmd: list[str], *, check: bool) -> None:
513518
assert (project / "song.wav").read_bytes() == b"new"
514519
manifest = load_manifest(project)
515520
assert manifest.mix_filename == "song.wav"
521+
522+
523+
def test_run_separate_force_preserves_song_markers(
524+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
525+
) -> None:
526+
monkeypatch.setenv("CLEAVE_DATA", str(tmp_path))
527+
project = tmp_path / "projects" / "song"
528+
project.mkdir(parents=True)
529+
(project / "renders").mkdir()
530+
_write_stub_stems(project)
531+
mix = project / "song.flac"
532+
mix.write_bytes(b"mix")
533+
write_manifest(
534+
project,
535+
slug="song",
536+
mix_filename="song.flac",
537+
original_path=tmp_path / "elsewhere.flac",
538+
demucs_model="htdemucs",
539+
song_markers=(12.5, 64.0, 120.0),
540+
)
541+
rewrite_manifest_slug(project, "song", restored_from="archived-slug")
542+
543+
def fake_run(cmd: list[str], *, check: bool) -> None:
544+
out_flag = cmd.index("-o")
545+
out_root = Path(cmd[out_flag + 1])
546+
target = out_root / "htdemucs" / "song"
547+
target.mkdir(parents=True, exist_ok=True)
548+
for name in STEM_NAMES:
549+
(target / f"{name}.wav").write_bytes(b"wav")
550+
551+
with patch("cleave.separate.subprocess.run", side_effect=fake_run), patch(
552+
"cleave.separate.run_analyse", return_value=project / "signals.json"
553+
):
554+
run_separate("song", force=True)
555+
556+
manifest = load_manifest(project)
557+
assert manifest.song_markers == (12.5, 64.0, 120.0)
558+
assert manifest.restored_from == "archived-slug"
559+
assert manifest.demucs_model == "htdemucs"
560+
assert manifest.mix_filename == "song.flac"

0 commit comments

Comments
 (0)