monkeyplug-enhanced 2.2.3__tar.gz → 2.2.4__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeyplug-enhanced
3
- Version: 2.2.3
3
+ Version: 2.2.4
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "monkeyplug-enhanced"
7
- version = "2.2.3"
7
+ version = "2.2.4"
8
8
  authors = [
9
9
  { name="Seth Grover", email="mero.mero.guero@gmail.com" },
10
10
  { name="Lincoln Brown", email="link@brown.fm" },
@@ -16,6 +16,8 @@ import requests
16
16
  import shutil
17
17
  import string
18
18
  import sys
19
+ import threading
20
+ import time
19
21
  import wave
20
22
  from tqdm import tqdm
21
23
 
@@ -221,6 +223,63 @@ def GetCodecs(local_filename, debug=False):
221
223
  return result
222
224
 
223
225
 
226
+ ###################################################################################################
227
+ class _SmoothProgressTicker:
228
+ """Background thread that smoothly advances a tqdm bar based on elapsed time.
229
+
230
+ Used when historical timing data allows estimating step durations.
231
+ The bar advances linearly within each step's estimated range, clamped
232
+ so it never overshoots. When the step completes, stop() snaps to actual.
233
+ """
234
+
235
+ def __init__(self, bar):
236
+ self._bar = bar
237
+ self._cumulative = 0.0 # Position where current step begins
238
+ self._step_estimate = 0.0 # Estimated seconds for current step
239
+ self._step_start = 0.0 # time.time() when step started
240
+ self._stop_event = threading.Event()
241
+ self._thread = None
242
+
243
+ def start(self, cumulative, step_estimated_seconds):
244
+ """Begin ticking for a new step."""
245
+ self.stop() # Stop any previous tick
246
+ self._cumulative = cumulative
247
+ self._step_estimate = step_estimated_seconds
248
+ self._step_start = time.time()
249
+ self._stop_event.clear()
250
+ self._thread = threading.Thread(target=self._tick, daemon=True)
251
+ self._thread.start()
252
+
253
+ def _tick(self):
254
+ while not self._stop_event.is_set():
255
+ try:
256
+ elapsed = time.time() - self._step_start
257
+ position = self._cumulative + min(elapsed, self._step_estimate)
258
+ # Never exceed the bar's total
259
+ if self._bar.total is not None:
260
+ position = min(position, self._bar.total)
261
+ self._bar.n = position
262
+ self._bar.refresh()
263
+ except (TypeError, ValueError, AttributeError):
264
+ break # Bar was closed externally
265
+ self._stop_event.wait(0.25)
266
+
267
+ def stop(self):
268
+ """Stop the ticker and return actual elapsed seconds for this step."""
269
+ self._stop_event.set()
270
+ if self._thread and self._thread.is_alive():
271
+ self._thread.join(timeout=1.0)
272
+ self._thread = None
273
+ if self._step_start > 0:
274
+ return time.time() - self._step_start
275
+ return 0.0
276
+
277
+ def adjust_total(self, delta):
278
+ """Adjust the bar's total by delta (e.g., remove an unused step estimate)."""
279
+ if self._bar.total is not None:
280
+ self._bar.total = max(self._bar.total + delta, self._bar.n)
281
+
282
+
224
283
  #################################################################################
225
284
  class Plugger(object):
226
285
  debug = False
@@ -670,10 +729,30 @@ class Plugger(object):
670
729
 
671
730
  ######## CreateCleanMuteList #################################################
672
731
  def CreateCleanMuteList(self):
673
- # Try to load existing transcript first, otherwise perform speech recognition
732
+ smooth = hasattr(self, '_smooth_ticker') and self._smooth_ticker is not None
733
+ cumulative = getattr(self, '_smooth_cumulative', 0.0)
734
+ will_transcribe = getattr(self, '_will_transcribe', False)
735
+
736
+ # Start ticker for transcribe step (if applicable)
737
+ if smooth and will_transcribe:
738
+ est = getattr(self, '_smooth_transcribe_est', 0)
739
+ if hasattr(self, '_progress') and self._progress:
740
+ self._progress.set_description("Transcribing")
741
+ self._smooth_ticker.start(cumulative, est)
742
+
743
+ transcribe_start = time.time() if will_transcribe else 0
674
744
  if not self.LoadTranscriptFromFile():
675
745
  self.RecognizeSpeech()
676
746
 
747
+ if will_transcribe:
748
+ actual_transcribe = time.time() - transcribe_start
749
+ if smooth:
750
+ self._smooth_ticker.stop()
751
+ cumulative += actual_transcribe
752
+ self._smooth_cumulative = cumulative
753
+ if hasattr(self, '_step_timings') and self._step_timings is not None:
754
+ self._step_timings['transcribe'] = (actual_transcribe, getattr(self, '_timing_file_duration', 0))
755
+
677
756
  self.naughtyWordList = [word for word in self.wordList if word["scrub"] is True]
678
757
 
679
758
  # Handle auto-generation mode
@@ -684,30 +763,58 @@ class Plugger(object):
684
763
  # Extract, separate, and get instrumental file
685
764
  if self.instrumentalSegments:
686
765
  try:
687
- # Update progress bar to show extraction starting
766
+ # Update progress bar for extraction step
688
767
  if hasattr(self, '_progress') and self._progress and not self.debug:
689
- self._progress.update(1)
690
- self._progress.total = 3
691
- self._progress.set_description("Extracting instrumental")
768
+ if smooth:
769
+ extract_est = getattr(self, '_smooth_extract_est', 0)
770
+ self._progress.set_description("Extracting instrumental")
771
+ self._smooth_ticker.start(cumulative, extract_est)
772
+ else:
773
+ self._progress.update(1)
774
+ self._progress.total = 3
775
+ self._progress.set_description("Extracting instrumental")
692
776
 
777
+ extract_start = time.time()
693
778
  self.instrumentalFileSpec = self._create_combined_profanity_file()
694
779
 
695
- # Update progress after extraction completes
696
- if hasattr(self, '_progress') and self._progress and not self.debug:
780
+ actual_extract = time.time() - extract_start
781
+ if smooth:
782
+ self._smooth_ticker.stop()
783
+ cumulative += actual_extract
784
+ self._smooth_cumulative = cumulative
785
+ if hasattr(self, '_step_timings') and self._step_timings is not None:
786
+ self._step_timings['extract'] = (actual_extract, getattr(self, '_timing_file_duration', 0))
787
+
788
+ # Update progress after extraction completes (step-based mode)
789
+ if not smooth and hasattr(self, '_progress') and self._progress and not self.debug:
697
790
  self._progress.update(1)
791
+
698
792
  if self.instrumentalFileSpec:
699
793
  self.instrumentalMode = True
700
794
  self._build_instrumental_filters()
701
795
  return [] # Return empty list for muteTimeList
702
796
  except Exception as e:
703
797
  # Fallback to mute if generation fails
798
+ if smooth:
799
+ self._smooth_ticker.stop()
704
800
  if self.debug:
705
801
  mmguero.eprint(f"Generation failed: {e}, falling back to mute mode")
706
802
  self.instrumentalMode = False
707
803
  return self._create_mute_beep_list()
708
804
  else:
805
+ # No instrumental segments — remove extract estimate from smooth bar
806
+ if smooth and hasattr(self, '_progress') and self._progress:
807
+ extract_est = getattr(self, '_smooth_extract_est', 0)
808
+ self._smooth_ticker.adjust_total(-extract_est)
709
809
  return []
710
810
 
811
+ else:
812
+ # No profanity found in auto mode — remove extract estimate if applicable
813
+ if smooth and hasattr(self, 'autoGenerateMode') and self.autoGenerateMode:
814
+ extract_est = getattr(self, '_smooth_extract_est', 0)
815
+ if extract_est > 0 and hasattr(self, '_progress') and self._progress:
816
+ self._smooth_ticker.adjust_total(-extract_est)
817
+
711
818
  # Handle traditional instrumental file mode or mute/beep mode
712
819
  if self.instrumentalMode:
713
820
  return self._create_instrumental_splice_list()
@@ -912,28 +1019,84 @@ class Plugger(object):
912
1019
  if (self.forceDespiteTag is True) or (GetMonkeyplugTagged(self.inputFileSpec, debug=self.debug) is False):
913
1020
  # Initialize progress (only when not in debug mode)
914
1021
  progress = None
1022
+ smooth_ticker = None
1023
+ step_timings = None
1024
+ timing_log = None
1025
+ file_duration = 0.0
1026
+
915
1027
  if not self.debug:
916
- # Determine first action
917
- if not self.inputTranscript:
918
- initial_desc = "Transcribing"
919
- else:
920
- initial_desc = "Processing"
921
-
922
- progress = tqdm(
923
- total=1, # Will be updated based on actual steps
924
- desc=initial_desc,
925
- unit="step",
926
- disable=False,
927
- bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]',
1028
+ # Load timing log and file duration for progress estimation
1029
+ timing_log = load_timing_log()
1030
+ file_duration = self._get_file_duration(self.inputFileSpec)
1031
+ step_timings = {}
1032
+
1033
+ # Determine which steps will run
1034
+ will_transcribe = not self.inputTranscript
1035
+ will_extract = hasattr(self, 'autoGenerateMode') and self.autoGenerateMode
1036
+ # encode always runs
1037
+
1038
+ # Check if we have estimates for all needed steps
1039
+ est_transcribe = estimate_step_duration(timing_log, 'transcribe', file_duration) if will_transcribe else None
1040
+ est_extract = estimate_step_duration(timing_log, 'extract', file_duration) if will_extract else None
1041
+ est_encode = estimate_step_duration(timing_log, 'encode', file_duration)
1042
+
1043
+ can_smooth = (
1044
+ file_duration > 0
1045
+ and est_encode is not None
1046
+ and (not will_transcribe or est_transcribe is not None)
1047
+ and (not will_extract or est_extract is not None)
928
1048
  )
929
1049
 
1050
+ if can_smooth:
1051
+ # Smooth mode: single bar with total in seconds
1052
+ est_transcribe_val = est_transcribe or 0
1053
+ est_extract_val = est_extract or 0
1054
+ total_est = est_transcribe_val + est_extract_val + est_encode
1055
+
1056
+ initial_desc = "Transcribing" if will_transcribe else "Processing"
1057
+ progress = tqdm(
1058
+ total=total_est,
1059
+ desc=initial_desc,
1060
+ unit="s",
1061
+ disable=False,
1062
+ bar_format='{l_bar}{bar}| {n:.0f}/{total:.0f}s [{elapsed}<{remaining}]',
1063
+ )
1064
+
1065
+ smooth_ticker = _SmoothProgressTicker(progress)
1066
+ # Ticker will be started inside CreateCleanMuteList for each step
1067
+
1068
+ # Pass context to CreateCleanMuteList
1069
+ self._smooth_ticker = smooth_ticker
1070
+ self._smooth_cumulative = 0.0
1071
+ self._smooth_transcribe_est = est_transcribe_val
1072
+ self._smooth_extract_est = est_extract_val
1073
+ self._step_timings = {}
1074
+ self._timing_log = timing_log
1075
+ self._timing_file_duration = file_duration
1076
+ self._will_transcribe = will_transcribe
1077
+ else:
1078
+ # Fallback: step-based bar (existing behavior)
1079
+ initial_desc = "Transcribing" if not self.inputTranscript else "Processing"
1080
+ progress = tqdm(
1081
+ total=1,
1082
+ desc=initial_desc,
1083
+ unit="step",
1084
+ disable=False,
1085
+ bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]',
1086
+ )
1087
+
1088
+ # Always pass timing context (even in step-based mode, for data collection)
1089
+ self._step_timings = step_timings
1090
+ self._timing_file_duration = file_duration
1091
+ self._will_transcribe = not self.inputTranscript
1092
+
930
1093
  # Store progress reference for use in CreateCleanMuteList
931
1094
  self._progress = progress
932
1095
 
933
1096
  self.CreateCleanMuteList()
934
1097
 
935
- # Update progress after CreateCleanMuteList
936
- if progress:
1098
+ # Update progress after CreateCleanMuteList (step-based mode only)
1099
+ if progress and not smooth_ticker:
937
1100
  did_extraction = (
938
1101
  hasattr(self, 'autoGenerateMode') and
939
1102
  self.autoGenerateMode and
@@ -942,23 +1105,21 @@ class Plugger(object):
942
1105
  )
943
1106
 
944
1107
  if not self.inputTranscript and not did_extraction:
945
- # Transcription done inside CreateCleanMuteList, no extraction
946
1108
  progress.update(1)
947
1109
  progress.total = 2
948
1110
  progress.set_description("Encoding")
949
1111
  elif not self.inputTranscript and did_extraction:
950
- # Both transcription and extraction handled inside CreateCleanMuteList
951
- # Just set description to encoding
952
1112
  progress.set_description("Encoding")
953
1113
  elif self.inputTranscript and did_extraction:
954
- # Extraction handled inside CreateCleanMuteList (no transcription update needed)
955
1114
  progress.total = 2
956
1115
  progress.set_description("Encoding")
957
1116
  else:
958
- # No transcription, no extraction - just encoding
959
1117
  progress.total = 1
960
1118
  progress.set_description("Encoding")
961
1119
 
1120
+ # Get cumulative position after CreateCleanMuteList (smooth mode)
1121
+ cumulative = getattr(self, '_smooth_cumulative', 0.0) if smooth_ticker else 0
1122
+
962
1123
  # Handle instrumental mode differently
963
1124
  if self.instrumentalMode:
964
1125
  # Use instrumental splicing
@@ -1030,6 +1191,15 @@ class Plugger(object):
1030
1191
  ffmpegCmd.extend(self.aParams)
1031
1192
  ffmpegCmd.append(self.outputFileSpec)
1032
1193
 
1194
+ # Start encode step with timing
1195
+ if progress and smooth_ticker:
1196
+ est_encode = estimate_step_duration(timing_log, 'encode', file_duration) or 0
1197
+ progress.set_description("Encoding")
1198
+ smooth_ticker.start(cumulative, est_encode)
1199
+ elif progress:
1200
+ progress.set_description("Encoding")
1201
+ encode_start = time.time()
1202
+
1033
1203
  ffmpegResult, ffmpegOutput = mmguero.run_process(ffmpegCmd, stdout=True, stderr=True, debug=self.debug)
1034
1204
  if (ffmpegResult != 0) or (not os.path.isfile(self.outputFileSpec)):
1035
1205
  mmguero.eprint(' '.join(mmguero.flatten(ffmpegCmd)))
@@ -1037,21 +1207,43 @@ class Plugger(object):
1037
1207
  mmguero.eprint(ffmpegOutput)
1038
1208
  raise ValueError(f"Could not process {self.inputFileSpec}")
1039
1209
 
1210
+ # Record encode timing and finalize
1211
+ actual_encode = time.time() - encode_start
1212
+ if smooth_ticker:
1213
+ smooth_ticker.stop()
1214
+ step_timings['encode'] = (actual_encode, file_duration)
1215
+
1040
1216
  SetMonkeyplugTag(self.outputFileSpec, debug=self.debug)
1041
1217
 
1042
- # Complete progress
1218
+ # Complete progress and save timing data
1043
1219
  if progress:
1044
- progress.update(1)
1220
+ if smooth_ticker:
1221
+ # Snap bar to total
1222
+ progress.n = progress.total
1223
+ progress.refresh()
1224
+ else:
1225
+ progress.update(1)
1045
1226
  progress.close()
1046
1227
 
1228
+ # Update timing log with actual measurements (only on success)
1229
+ if timing_log is not None and file_duration > 0:
1230
+ for op, (wall_secs, audio_secs) in step_timings.items():
1231
+ update_timing_measurement(timing_log, op, wall_secs, audio_secs)
1232
+ save_timing_log(timing_log)
1233
+
1047
1234
  else:
1048
1235
  shutil.copyfile(self.inputFileSpec, self.outputFileSpec)
1049
1236
  if progress:
1050
1237
  progress.close()
1051
1238
 
1052
- # Clean up progress reference
1239
+ # Clean up progress references
1053
1240
  if hasattr(self, '_progress'):
1054
1241
  delattr(self, '_progress')
1242
+ for attr in ('_smooth_ticker', '_smooth_cumulative', '_smooth_extract_est',
1243
+ '_smooth_transcribe_est', '_will_transcribe',
1244
+ '_step_timings', '_timing_log', '_timing_file_duration'):
1245
+ if hasattr(self, attr):
1246
+ delattr(self, attr)
1055
1247
 
1056
1248
  return self.outputFileSpec
1057
1249
 
@@ -1970,6 +2162,7 @@ def expand_and_detect_vocals(input_pattern, output_pattern, args, skip_detection
1970
2162
  # Config file loading
1971
2163
  MONKEYPLUG_CACHE_DIR = os.path.join(os.path.expanduser('~'), '.cache', 'monkeyplug')
1972
2164
  MONKEYPLUG_CONFIG_PATH = os.path.join(MONKEYPLUG_CACHE_DIR, 'config.json')
2165
+ MONKEYPLUG_TIMING_LOG_PATH = os.path.join(MONKEYPLUG_CACHE_DIR, 'timing_log.json')
1973
2166
 
1974
2167
  DEFAULT_CONFIG = {
1975
2168
  "pad_milliseconds": 10,
@@ -2029,6 +2222,69 @@ def load_config_settings(debug=False):
2029
2222
  return dict(DEFAULT_CONFIG)
2030
2223
 
2031
2224
 
2225
+ ###################################################################################################
2226
+ # Timing log for progress estimation
2227
+ def load_timing_log():
2228
+ """Load historical timing data for progress bar estimation.
2229
+
2230
+ Returns:
2231
+ dict: Timing log with per-operation running averages, or {} if unavailable.
2232
+ """
2233
+ if not os.path.isfile(MONKEYPLUG_TIMING_LOG_PATH):
2234
+ return {}
2235
+ try:
2236
+ with open(MONKEYPLUG_TIMING_LOG_PATH, 'r') as f:
2237
+ data = json.load(f)
2238
+ if isinstance(data, dict):
2239
+ return data
2240
+ except (json.JSONDecodeError, IOError, ValueError):
2241
+ pass
2242
+ return {}
2243
+
2244
+
2245
+ def save_timing_log(timing_log):
2246
+ """Save timing log atomically to disk."""
2247
+ try:
2248
+ os.makedirs(os.path.dirname(MONKEYPLUG_TIMING_LOG_PATH), exist_ok=True)
2249
+ tmp_path = MONKEYPLUG_TIMING_LOG_PATH + '.tmp'
2250
+ with open(tmp_path, 'w') as f:
2251
+ json.dump(timing_log, f, indent=2)
2252
+ f.write('\n')
2253
+ os.replace(tmp_path, MONKEYPLUG_TIMING_LOG_PATH)
2254
+ except (IOError, OSError):
2255
+ pass # Best-effort
2256
+
2257
+
2258
+ def estimate_step_duration(timing_log, operation, audio_seconds):
2259
+ """Estimate wall-clock seconds for an operation based on historical data.
2260
+
2261
+ Returns:
2262
+ float or None: Estimated seconds, or None if no data available.
2263
+ """
2264
+ entry = timing_log.get(operation)
2265
+ if not entry or entry.get('run_count', 0) == 0:
2266
+ return None
2267
+ total_audio = entry.get('total_audio_seconds', 0)
2268
+ if total_audio <= 0:
2269
+ return None
2270
+ rate = entry['total_wall_seconds'] / total_audio
2271
+ return rate * audio_seconds
2272
+
2273
+
2274
+ def update_timing_measurement(timing_log, operation, wall_seconds, audio_seconds):
2275
+ """Add a new timing measurement to the running averages."""
2276
+ if operation not in timing_log:
2277
+ timing_log[operation] = {
2278
+ 'total_audio_seconds': 0.0,
2279
+ 'total_wall_seconds': 0.0,
2280
+ 'run_count': 0,
2281
+ }
2282
+ entry = timing_log[operation]
2283
+ entry['total_audio_seconds'] += audio_seconds
2284
+ entry['total_wall_seconds'] += wall_seconds
2285
+ entry['run_count'] += 1
2286
+
2287
+
2032
2288
  ###################################################################################################
2033
2289
  # RunMonkeyPlug
2034
2290
  def RunMonkeyPlug():