Spaces:
Paused
Paused
AndrianBalanescu commited on
Commit ·
e8bddb0
1
Parent(s): d770801
fix(cron): persist exact track order in resume state
Browse filesResume rebuilds the track list via _build_album_tracks which now shuffles
— so resumed track #N could be a different song than originally planned.
Resume state now carries track_order (persisted from _planned_tracks,
stripped from album_manifest.json serialization) and the restore path
replays the exact saved sequence with a mismatch warning fallback.
Verified by integration test: save -> reload -> rebuild -> reorder
restores the exact original sequence.
moldovan-qwen/scripts/daily_album_cron.py
CHANGED
|
@@ -200,6 +200,16 @@ class DailyAlbumGenerator:
|
|
| 200 |
album_dir = Path(resume["album_dir"])
|
| 201 |
genre_key = resume.get("genre_key") or "MOLDOVAN_QWEN"
|
| 202 |
tracks = _build_album_tracks(genre_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
album_title = resume["album_title"]
|
| 204 |
start_idx = int(resume.get("next_track_index") or 1)
|
| 205 |
else:
|
|
@@ -249,6 +259,10 @@ class DailyAlbumGenerator:
|
|
| 249 |
"tracks": []
|
| 250 |
}
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
total_album_seconds = 0.0
|
| 253 |
|
| 254 |
failed_tracks: List[str] = []
|
|
@@ -300,8 +314,9 @@ class DailyAlbumGenerator:
|
|
| 300 |
})
|
| 301 |
album_manifest["interrupted_by_quota"] = True
|
| 302 |
manifest_file = album_dir / "album_manifest.json"
|
|
|
|
| 303 |
with open(manifest_file, "w", encoding="utf-8") as f:
|
| 304 |
-
json.dump(
|
| 305 |
return album_manifest
|
| 306 |
except Exception as track_err:
|
| 307 |
# One degraded/unavailable ZeroGPU call must not kill the whole
|
|
@@ -321,8 +336,9 @@ class DailyAlbumGenerator:
|
|
| 321 |
|
| 322 |
# Write Album Cover / Metadata
|
| 323 |
manifest_file = album_dir / "album_manifest.json"
|
|
|
|
| 324 |
with open(manifest_file, "w", encoding="utf-8") as f:
|
| 325 |
-
json.dump(
|
| 326 |
|
| 327 |
print("\n" + "=" * 65)
|
| 328 |
print(f"✨ ALBUM COMPLET GENERAT: {album_title}")
|
|
@@ -352,6 +368,10 @@ class DailyAlbumGenerator:
|
|
| 352 |
"next_track_index": failed_idx,
|
| 353 |
"next_track_title": failed_track.get("title"),
|
| 354 |
"tracks_done": len(album_manifest.get("tracks", [])),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
"error": error,
|
| 356 |
"saved_at": datetime.datetime.now().isoformat(),
|
| 357 |
}
|
|
|
|
| 200 |
album_dir = Path(resume["album_dir"])
|
| 201 |
genre_key = resume.get("genre_key") or "MOLDOVAN_QWEN"
|
| 202 |
tracks = _build_album_tracks(genre_key)
|
| 203 |
+
# Re-shuffle guard: replay the EXACT saved order. A shuffled rebuild
|
| 204 |
+
# would make track #N a different song than the one originally planned.
|
| 205 |
+
saved_order = resume.get("track_order") or []
|
| 206 |
+
if saved_order:
|
| 207 |
+
by_title = {t["title"]: t for t in tracks}
|
| 208 |
+
ordered = [by_title[title] for title in saved_order if title in by_title]
|
| 209 |
+
if len(ordered) == len(tracks):
|
| 210 |
+
tracks = ordered
|
| 211 |
+
else:
|
| 212 |
+
print(f"[RESUME] ⚠️ track_order mismatch ({len(ordered)}/{len(tracks)}) — folosesc ordinea nouă")
|
| 213 |
album_title = resume["album_title"]
|
| 214 |
start_idx = int(resume.get("next_track_index") or 1)
|
| 215 |
else:
|
|
|
|
| 259 |
"tracks": []
|
| 260 |
}
|
| 261 |
|
| 262 |
+
# In-memory planned order for resume-state persistence (not serialized
|
| 263 |
+
# into album_manifest.json — kept as a private key, stripped on save).
|
| 264 |
+
album_manifest["_planned_tracks"] = tracks
|
| 265 |
+
|
| 266 |
total_album_seconds = 0.0
|
| 267 |
|
| 268 |
failed_tracks: List[str] = []
|
|
|
|
| 314 |
})
|
| 315 |
album_manifest["interrupted_by_quota"] = True
|
| 316 |
manifest_file = album_dir / "album_manifest.json"
|
| 317 |
+
_save = {k: v for k, v in album_manifest.items() if not k.startswith("_")}
|
| 318 |
with open(manifest_file, "w", encoding="utf-8") as f:
|
| 319 |
+
json.dump(_save, f, indent=2, ensure_ascii=False)
|
| 320 |
return album_manifest
|
| 321 |
except Exception as track_err:
|
| 322 |
# One degraded/unavailable ZeroGPU call must not kill the whole
|
|
|
|
| 336 |
|
| 337 |
# Write Album Cover / Metadata
|
| 338 |
manifest_file = album_dir / "album_manifest.json"
|
| 339 |
+
_save = {k: v for k, v in album_manifest.items() if not k.startswith("_")}
|
| 340 |
with open(manifest_file, "w", encoding="utf-8") as f:
|
| 341 |
+
json.dump(_save, f, indent=2, ensure_ascii=False)
|
| 342 |
|
| 343 |
print("\n" + "=" * 65)
|
| 344 |
print(f"✨ ALBUM COMPLET GENERAT: {album_title}")
|
|
|
|
| 368 |
"next_track_index": failed_idx,
|
| 369 |
"next_track_title": failed_track.get("title"),
|
| 370 |
"tracks_done": len(album_manifest.get("tracks", [])),
|
| 371 |
+
# Persist the exact track order so the resume run replays the SAME
|
| 372 |
+
# shuffled sequence — otherwise _build_album_tracks re-shuffles and
|
| 373 |
+
# track #N at resume time is a different song than planned.
|
| 374 |
+
"track_order": [t.get("title") for t in album_manifest.get("_planned_tracks", [])],
|
| 375 |
"error": error,
|
| 376 |
"saved_at": datetime.datetime.now().isoformat(),
|
| 377 |
}
|
tests/unit/test_daily_album_cron.py
CHANGED
|
@@ -39,7 +39,7 @@ def test_cron_dry_run_logic(clean_env):
|
|
| 39 |
patch("daily_album_cron.LyricistEngine.compose_lyrics", return_value={"full_lyrics": "Piesa test Chișinău\nRefren moldovenesc"}):
|
| 40 |
manifest = generator.generate_full_album()
|
| 41 |
mock_dispatcher.return_value.generate_music.assert_not_called()
|
| 42 |
-
assert manifest["genre_key"] in ["MOLDOVAN_QWEN", "CHILL", "DEEP_HOUSE", "RANDOM"]
|
| 43 |
for track in manifest["tracks"]:
|
| 44 |
assert track["audio_status"] == "simulated"
|
| 45 |
assert track["segment_count"] == 0
|
|
|
|
| 39 |
patch("daily_album_cron.LyricistEngine.compose_lyrics", return_value={"full_lyrics": "Piesa test Chișinău\nRefren moldovenesc"}):
|
| 40 |
manifest = generator.generate_full_album()
|
| 41 |
mock_dispatcher.return_value.generate_music.assert_not_called()
|
| 42 |
+
assert manifest["genre_key"] in ["ALBINA_BONDAR", "MOLDOVAN_QWEN", "CHILL", "DEEP_HOUSE", "RANDOM"]
|
| 43 |
for track in manifest["tracks"]:
|
| 44 |
assert track["audio_status"] == "simulated"
|
| 45 |
assert track["segment_count"] == 0
|