monkeyplug-enhanced 2.3.1__tar.gz → 2.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeyplug-enhanced
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Enhanced fork of monkeyplug — censors profanity in audio files using speech recognition with Groq API, AI instrumental generation, and batch processing.
5
5
  Project-URL: Homepage, https://github.com/ljbred08/monkeyplug
6
6
  Project-URL: Issues, https://github.com/ljbred08/monkeyplug/issues
7
7
  Project-URL: Repository, https://github.com/ljbred08/monkeyplug.git
8
- Author-email: Seth Grover <mero.mero.guero@gmail.com>, Lincoln Brown <link@brown.fm>
8
+ Author-email: Lincoln Brown <link@brown.fm>, Seth Grover <mero.mero.guero@gmail.com>
9
9
  License-File: LICENSE
10
10
  Classifier: License :: OSI Approved :: BSD License
11
11
  Classifier: Operating System :: OS Independent
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "monkeyplug-enhanced"
7
- version = "2.3.1"
7
+ version = "2.3.2"
8
8
  authors = [
9
- { name="Seth Grover", email="mero.mero.guero@gmail.com" },
10
9
  { name="Lincoln Brown", email="link@brown.fm" },
10
+ { name="Seth Grover", email="mero.mero.guero@gmail.com" },
11
11
  ]
12
12
  description = "Enhanced fork of monkeyplug — censors profanity in audio files using speech recognition with Groq API, AI instrumental generation, and batch processing."
13
13
  readme = "README.md"
@@ -458,7 +458,7 @@ def _unify_album_metadata(file_paths, groq_api_key, model, prompt, rename_prompt
458
458
  raise Exception("Album unification failed after maximum retries")
459
459
 
460
460
 
461
- def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, model, batch_num=1, total_batches=1, debug=False):
461
+ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, model, batch_num=1, total_batches=1, debug=False, progress_bar=None, batch_start_position=0.0, batch_slice_size=1.0, timing_log=None, operation_name='unify_batch_groq'):
462
462
  """Make a single API call to Groq for album unification.
463
463
 
464
464
  Args:
@@ -469,6 +469,11 @@ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, mode
469
469
  batch_num: Current batch number (for debug output)
470
470
  total_batches: Total expected batches (for debug output)
471
471
  debug: Enable debug output
472
+ progress_bar: Optional tqdm progress bar for progress tracking
473
+ batch_start_position: Starting position (0.0 to 1.0) for this batch in overall progress
474
+ batch_slice_size: Size of this batch's slice (0.0 to 1.0) of overall progress
475
+ timing_log: Timing log dict for estimation
476
+ operation_name: Name of operation for timing tracking ('unify_batch_groq' or 'unify_batch_spotify')
472
477
 
473
478
  Returns:
474
479
  dict: Parsed JSON response with 'unified_album' and 'tracks'
@@ -482,9 +487,16 @@ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, mode
482
487
  # Build input for AI
483
488
  input_text = json.dumps(metadata_list, indent=2, ensure_ascii=False)
484
489
 
490
+ # Estimate tokens for this batch
491
+ batch_tokens = _estimate_batch_tokens(metadata_list, system_prompt)
492
+
493
+ # Estimate duration based on historical data
494
+ batch_estimated = estimate_step_duration_tokens(timing_log, operation_name, batch_tokens) or batch_tokens * 0.1
495
+
485
496
  # API call with retry logic (more retries for transient 400 JSON validation errors)
486
497
  max_retries = 5
487
498
  retry_delay = 1
499
+ smooth_ticker = None
488
500
 
489
501
  for attempt in range(max_retries):
490
502
  try:
@@ -493,6 +505,22 @@ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, mode
493
505
  mmguero.eprint(f"Calling Groq API{batch_info} (attempt {attempt + 1}/{max_retries})...")
494
506
  mmguero.eprint(f"Sending {len(metadata_list)} files to AI for unification")
495
507
 
508
+ # Start smooth progress ticker for this batch
509
+ if progress_bar:
510
+ # Reset to batch start position on retry
511
+ if attempt > 0:
512
+ progress_bar.n = batch_start_position * progress_bar.total
513
+ progress_bar.refresh()
514
+
515
+ if smooth_ticker is None:
516
+ smooth_ticker = _SmoothProgressTicker(progress_bar)
517
+
518
+ smooth_ticker.start(
519
+ cumulative=batch_start_position * progress_bar.total,
520
+ step_estimated_seconds=batch_estimated
521
+ )
522
+
523
+ api_start = time.time()
496
524
  response = requests.post(
497
525
  "https://api.groq.com/openai/v1/chat/completions",
498
526
  headers={
@@ -516,6 +544,19 @@ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, mode
516
544
  },
517
545
  timeout=120,
518
546
  )
547
+ api_elapsed = time.time() - api_start
548
+
549
+ # Stop ticker and get actual time
550
+ if smooth_ticker:
551
+ actual_time = smooth_ticker.stop()
552
+ # Snap to actual batch end position
553
+ progress_bar.n = (batch_start_position + batch_slice_size) * progress_bar.total
554
+ progress_bar.refresh()
555
+
556
+ # Record timing
557
+ if timing_log is not None:
558
+ update_timing_measurement_tokens(timing_log, operation_name, actual_time, batch_tokens)
559
+ save_timing_log(timing_log)
519
560
 
520
561
  # Handle rate limiting
521
562
  if response.status_code == 429:
@@ -552,6 +593,8 @@ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, mode
552
593
  return parsed
553
594
 
554
595
  except requests.exceptions.Timeout:
596
+ if smooth_ticker:
597
+ smooth_ticker.stop()
555
598
  if attempt < max_retries - 1:
556
599
  if debug:
557
600
  mmguero.eprint(f"Request timed out, retrying in {retry_delay}s...")
@@ -561,6 +604,8 @@ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, mode
561
604
  raise Exception("Album unification request timed out")
562
605
 
563
606
  except requests.exceptions.RequestException as e:
607
+ if smooth_ticker:
608
+ smooth_ticker.stop()
564
609
  if attempt < max_retries - 1:
565
610
  if debug:
566
611
  mmguero.eprint(f"Request failed: {e}, retrying in {retry_delay}s...")
@@ -572,7 +617,7 @@ def _call_groq_api_single_batch(metadata_list, system_prompt, groq_api_key, mode
572
617
  raise Exception("Album unification failed after maximum retries")
573
618
 
574
619
 
575
- def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt, rename_prompt=None, spotify_tracks=None, batch_size=10, batch_size_spotify=5, debug=False):
620
+ def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt, rename_prompt=None, spotify_tracks=None, batch_size=10, batch_size_spotify=5, debug=False, verbose=False):
576
621
  """Use Groq AI to unify album metadata with automatic batching for large file lists.
577
622
 
578
623
  Implements automatic batching to handle Groq's output token limits.
@@ -585,7 +630,10 @@ def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt,
585
630
  prompt: System prompt for the AI
586
631
  rename_prompt: Optional prompt for renaming (if provided, adds suggested_name to response)
587
632
  spotify_tracks: Optional list of track names from Spotify for accurate ordering
633
+ batch_size: Default batch size (without Spotify)
634
+ batch_size_spotify: Batch size when using Spotify (smaller due to larger prompts)
588
635
  debug: Enable debug output
636
+ verbose: Disable progress bar if True
589
637
 
590
638
  Returns:
591
639
  Dict with 'unified_album' (str) and 'tracks' (list of dicts with
@@ -608,21 +656,58 @@ def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt,
608
656
  if rename_prompt:
609
657
  system_prompt = f"{prompt}\n\n{rename_prompt}"
610
658
 
611
- unified_album = None # Will be set from first batch and reused
612
- all_tracks = [] # Accumulates results across batches
613
- processed_files = set() # Tracks which files we've gotten results for
659
+ # Determine operation name for timing
660
+ operation_name = 'unify_batch_spotify' if spotify_tracks else 'unify_batch_groq'
614
661
 
615
662
  # Proactive batching: limit batch size to avoid overwhelming Groq
616
663
  # With Spotify tracks, use smaller batches since the prompt is larger
617
664
  max_batch_size = batch_size_spotify if spotify_tracks else batch_size
618
665
 
666
+ # Calculate expected batch count
667
+ expected_batches = (len(metadata_list) + max_batch_size - 1) // max_batch_size
668
+
669
+ # Guard against empty metadata list
670
+ if expected_batches == 0:
671
+ return {'unified_album': '', 'tracks': []}
672
+
673
+ # Estimate total tokens for all batches
674
+ total_tokens = 0
675
+ for i in range(expected_batches):
676
+ batch_metadata = metadata_list[i * max_batch_size : (i + 1) * max_batch_size]
677
+ batch_system_prompt = system_prompt
678
+ if spotify_tracks:
679
+ tracks_json = json.dumps(spotify_tracks, ensure_ascii=False)
680
+ batch_system_prompt = f"{system_prompt}\n\nOfficial track listing from Spotify: {tracks_json}"
681
+ total_tokens += _estimate_batch_tokens(batch_metadata, batch_system_prompt)
682
+
683
+ # Load timing log and estimate total duration
684
+ timing_log = load_timing_log()
685
+ total_estimated = estimate_step_duration_tokens(timing_log, operation_name, total_tokens) or total_tokens * 0.1
686
+
687
+ # Create progress bar
688
+ progress = None
689
+ if not verbose:
690
+ progress = tqdm(
691
+ total=total_estimated,
692
+ desc=f"Unifying Album ({expected_batches} batches)",
693
+ unit="s",
694
+ disable=False,
695
+ bar_format='{l_bar}{bar}| {n:.0f}/{total:.0f}s [{elapsed}<{remaining}]',
696
+ )
697
+
698
+ unified_album = None # Will be set from first batch and reused
699
+ all_tracks = [] # Accumulates results across batches
700
+ processed_files = set() # Tracks which files we've gotten results for
701
+
619
702
  # Start with first batch
620
703
  batch_metadata = metadata_list[:max_batch_size]
621
704
  remaining_metadata = metadata_list[max_batch_size:]
622
705
  batch_num = 0
706
+ actual_batch_num = 0 # Track actual batch attempts (including retries)
623
707
 
624
708
  while batch_metadata:
625
709
  batch_num += 1
710
+ actual_batch_num += 1
626
711
 
627
712
  # Build system prompt for this batch
628
713
  # ALWAYS pass full Spotify list - don't slice it!
@@ -634,18 +719,35 @@ def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt,
634
719
  if debug and batch_num == 1:
635
720
  mmguero.eprint(f"Providing full Spotify track list ({len(spotify_tracks)} tracks) - AI will match by name")
636
721
 
722
+ # Calculate batch slice size and start position
723
+ batch_slice_size = 1.0 / expected_batches
724
+ # Clamp batch number for progress calculation to handle retries
725
+ progress_batch_num = min(actual_batch_num - 1, expected_batches - 1)
726
+ batch_start_position = progress_batch_num * batch_slice_size
727
+
728
+ # Update progress bar description
729
+ if progress:
730
+ display_batch_num = min(actual_batch_num, expected_batches)
731
+ progress.set_description(f"Processing Batch {display_batch_num}/{expected_batches}")
732
+
637
733
  # Call API with current batch
638
734
  try:
639
735
  parsed = _call_groq_api_single_batch(
640
736
  batch_metadata, batch_system_prompt, groq_api_key, model,
641
- batch_num, total_batches="?", debug=debug
737
+ actual_batch_num, expected_batches, debug=debug,
738
+ progress_bar=progress, batch_start_position=batch_start_position,
739
+ batch_slice_size=batch_slice_size, timing_log=timing_log,
740
+ operation_name=operation_name
642
741
  )
643
742
  except Exception as e:
644
743
  # If this isn't the first batch, we have partial results - fail gracefully
645
744
  if all_tracks:
646
- mmguero.eprint(f"Batch {batch_num} failed after {len(all_tracks)} tracks were processed: {e}")
745
+ mmguero.eprint(f"Batch {actual_batch_num} failed after {len(all_tracks)} tracks were processed: {e}")
647
746
  mmguero.eprint("Proceeding with partial results...")
648
747
  break
748
+ # Close progress bar before raising
749
+ if progress:
750
+ progress.close()
649
751
  raise
650
752
 
651
753
  # On first successful call, capture unified_album name
@@ -661,7 +763,7 @@ def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt,
661
763
 
662
764
  # Guard against empty response to avoid infinite loop
663
765
  if not returned_tracks:
664
- mmguero.eprint(f"WARNING: Batch {batch_num} returned no tracks. Stopping to avoid infinite loop.")
766
+ mmguero.eprint(f"WARNING: Batch {actual_batch_num} returned no tracks. Stopping to avoid infinite loop.")
665
767
  break
666
768
 
667
769
  for track in returned_tracks:
@@ -671,7 +773,7 @@ def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt,
671
773
  processed_files.add(filename)
672
774
 
673
775
  if debug:
674
- mmguero.eprint(f"Batch {batch_num} complete: {len(returned_tracks)} tracks returned, {len(all_tracks)} total processed")
776
+ mmguero.eprint(f"Batch {actual_batch_num} complete: {len(returned_tracks)} tracks returned, {len(all_tracks)} total processed")
675
777
 
676
778
  # Determine what's missing from this batch
677
779
  returned_filenames = {t['filename'] for t in returned_tracks}
@@ -701,10 +803,14 @@ def _unify_album_metadata_with_batching(file_paths, groq_api_key, model, prompt,
701
803
  new_files = len(next_batch) - len(missing_metadata)
702
804
  mmguero.eprint(f"Partial response: {len(missing_metadata)} files from this batch need retry. Next batch: {len(missing_metadata)} retries + {new_files} new files = {len(next_batch)} total")
703
805
  else:
704
- mmguero.eprint(f"Starting batch {batch_num + 1} with {len(next_batch)} files...")
806
+ mmguero.eprint(f"Starting batch {actual_batch_num + 1} with {len(next_batch)} files...")
705
807
 
706
808
  batch_metadata = next_batch
707
809
 
810
+ # Close progress bar
811
+ if progress:
812
+ progress.close()
813
+
708
814
  return {
709
815
  'unified_album': unified_album or '',
710
816
  'tracks': all_tracks
@@ -1061,7 +1167,7 @@ def _apply_cover_art_to_files(file_paths, image_data, debug=False):
1061
1167
 
1062
1168
  ###################################################################################################
1063
1169
  # Run album unification process
1064
- def _run_album_unification(input_path, output_path, config, rename_prompt=None, use_spotify=None, debug=False):
1170
+ def _run_album_unification(input_path, output_path, config, rename_prompt=None, use_spotify=None, debug=False, verbose=False):
1065
1171
  """Run the album unification process on a folder of files.
1066
1172
 
1067
1173
  Args:
@@ -1071,6 +1177,7 @@ def _run_album_unification(input_path, output_path, config, rename_prompt=None,
1071
1177
  rename_prompt: Optional prompt for smart renaming (None = no renaming)
1072
1178
  use_spotify: Spotify URL if provided, True to search for album, None/False to disable
1073
1179
  debug: Enable debug output
1180
+ verbose: Disable progress bar if True
1074
1181
 
1075
1182
  Returns:
1076
1183
  str: Status message
@@ -1137,7 +1244,7 @@ def _run_album_unification(input_path, output_path, config, rename_prompt=None,
1137
1244
  # Call AI to unify album metadata (first pass - gets unified album name)
1138
1245
  unified_result = _unify_album_metadata_with_batching(
1139
1246
  file_paths, groq_api_key, model, prompt, rename_prompt=rename_prompt,
1140
- batch_size=batch_size, batch_size_spotify=batch_size_spotify, debug=debug
1247
+ batch_size=batch_size, batch_size_spotify=batch_size_spotify, debug=debug, verbose=verbose
1141
1248
  )
1142
1249
 
1143
1250
  unified_album = unified_result.get('unified_album', '')
@@ -1171,7 +1278,7 @@ def _run_album_unification(input_path, output_path, config, rename_prompt=None,
1171
1278
  rename_prompt=rename_prompt,
1172
1279
  spotify_tracks=spotify_info.get('tracks', []),
1173
1280
  batch_size=batch_size, batch_size_spotify=batch_size_spotify,
1174
- debug=debug
1281
+ debug=debug, verbose=verbose
1175
1282
  )
1176
1283
  else:
1177
1284
  mmguero.eprint("Could not fetch Spotify info, using AI results only")
@@ -3837,6 +3944,80 @@ def update_timing_measurement(timing_log, operation, wall_seconds, audio_seconds
3837
3944
  entry['run_count'] += 1
3838
3945
 
3839
3946
 
3947
+ def estimate_step_duration_tokens(timing_log, operation, input_tokens):
3948
+ """Estimate wall-clock seconds for an operation based on token-based historical data.
3949
+
3950
+ Args:
3951
+ timing_log: Timing log dict
3952
+ operation: Operation name (e.g., 'unify_batch_groq')
3953
+ input_tokens: Estimated input tokens
3954
+
3955
+ Returns:
3956
+ float or None: Estimated seconds, or None if no data available.
3957
+ """
3958
+ entry = timing_log.get(operation)
3959
+ if not entry or entry.get('run_count', 0) == 0:
3960
+ return None
3961
+ total_tokens = entry.get('total_input_tokens', 0)
3962
+ if total_tokens <= 0:
3963
+ return None
3964
+ rate = entry['total_wall_seconds'] / total_tokens
3965
+ return rate * input_tokens
3966
+
3967
+
3968
+ def update_timing_measurement_tokens(timing_log, operation, wall_seconds, input_tokens):
3969
+ """Add a new token-based timing measurement to the running averages.
3970
+
3971
+ Args:
3972
+ timing_log: Timing log dict
3973
+ operation: Operation name
3974
+ wall_seconds: Actual wall-clock seconds elapsed
3975
+ input_tokens: Actual input tokens processed
3976
+ """
3977
+ if operation not in timing_log:
3978
+ timing_log[operation] = {
3979
+ 'total_input_tokens': 0,
3980
+ 'total_wall_seconds': 0.0,
3981
+ 'run_count': 0,
3982
+ }
3983
+ entry = timing_log[operation]
3984
+ entry['total_input_tokens'] += input_tokens
3985
+ entry['total_wall_seconds'] += wall_seconds
3986
+ entry['run_count'] += 1
3987
+
3988
+
3989
+ def _estimate_input_tokens(text):
3990
+ """Estimate input token count using character approximation.
3991
+
3992
+ Args:
3993
+ text: String to estimate tokens for
3994
+
3995
+ Returns:
3996
+ int: Estimated token count (approximately characters / 4)
3997
+ """
3998
+ return len(text) // 4
3999
+
4000
+
4001
+ def _estimate_batch_tokens(metadata_list, system_prompt):
4002
+ """Estimate total input tokens for a batch request.
4003
+
4004
+ Args:
4005
+ metadata_list: List of metadata dicts
4006
+ system_prompt: System prompt string
4007
+
4008
+ Returns:
4009
+ int: Estimated input tokens
4010
+ """
4011
+ # Count tokens in metadata
4012
+ metadata_json = json.dumps(metadata_list, indent=2, ensure_ascii=False)
4013
+ metadata_tokens = _estimate_input_tokens(metadata_json)
4014
+
4015
+ # Count tokens in system prompt
4016
+ prompt_tokens = _estimate_input_tokens(system_prompt)
4017
+
4018
+ return metadata_tokens + prompt_tokens
4019
+
4020
+
3840
4021
  ###################################################################################################
3841
4022
  # RunMonkeyPlug
3842
4023
  def RunMonkeyPlug():
@@ -4384,7 +4565,8 @@ def RunMonkeyPlug():
4384
4565
  config,
4385
4566
  rename_prompt=args.autoRename,
4386
4567
  use_spotify=args.useSpotify,
4387
- debug=args.debug
4568
+ debug=args.debug,
4569
+ verbose=args.debug
4388
4570
  )
4389
4571
  print(result)
4390
4572
  except Exception as e:
@@ -4681,7 +4863,8 @@ def RunMonkeyPlug():
4681
4863
  config,
4682
4864
  rename_prompt=args.autoRename,
4683
4865
  use_spotify=args.useSpotify,
4684
- debug=args.debug
4866
+ debug=args.debug,
4867
+ verbose=args.debug
4685
4868
  )
4686
4869
  mmguero.eprint(result)
4687
4870
  except Exception as e:
@@ -4991,7 +5174,8 @@ def RunMonkeyPlug():
4991
5174
  config,
4992
5175
  rename_prompt=args.autoRename,
4993
5176
  use_spotify=args.useSpotify,
4994
- debug=args.debug
5177
+ debug=args.debug,
5178
+ verbose=args.debug
4995
5179
  )
4996
5180
  mmguero.eprint(result)
4997
5181
  except Exception as e: