AndrianBalanescu commited on
Commit
e8bddb0
·
1 Parent(s): d770801

fix(cron): persist exact track order in resume state

Browse files

Resume 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(album_manifest, f, indent=2, ensure_ascii=False)
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(album_manifest, f, indent=2, ensure_ascii=False)
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