Spaces:
Sleeping
Sleeping
initial add app.py
Browse files- .gitignore +12 -0
- README.md +1 -1
- app.py +305 -0
- requirements.txt +3 -0
- webui.bat +162 -0
.gitignore
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.vs
|
2 |
+
.vscode
|
3 |
+
# Byte-compiled / optimized / DLL files
|
4 |
+
__pycache__/
|
5 |
+
|
6 |
+
venv/
|
7 |
+
tmp/
|
8 |
+
sf2/
|
9 |
+
models/
|
10 |
+
output/
|
11 |
+
rendered_midi/
|
12 |
+
transcribed_/
|
README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
---
|
2 |
title: Audio To CUE Generator
|
3 |
-
emoji:
|
4 |
colorFrom: green
|
5 |
colorTo: blue
|
6 |
sdk: gradio
|
|
|
1 |
---
|
2 |
title: Audio To CUE Generator
|
3 |
+
emoji: π
|
4 |
colorFrom: green
|
5 |
colorTo: blue
|
6 |
sdk: gradio
|
app.py
ADDED
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import re
|
3 |
+
import librosa
|
4 |
+
import gradio as gr
|
5 |
+
|
6 |
+
# --- Helper Functions ---
|
7 |
+
|
8 |
+
def seconds_to_cue_time(t):
|
9 |
+
"""Converts a time in seconds to the CUE sheet format (MM:SS:FF)."""
|
10 |
+
t = max(0, t)
|
11 |
+
minutes = int(t // 60)
|
12 |
+
seconds = int(t % 60)
|
13 |
+
frames = int((t - minutes * 60 - seconds) * 75)
|
14 |
+
return f'{minutes:02d}:{seconds:02d}:{frames:02d}'
|
15 |
+
|
16 |
+
def parse_cue_time_to_seconds(time_str):
|
17 |
+
"""Parses MM:SS:FF into seconds. Returns None on failure."""
|
18 |
+
if not time_str:
|
19 |
+
return None
|
20 |
+
match = re.match(r'(\d+):(\d{1,2}):(\d{1,2})', time_str)
|
21 |
+
if match:
|
22 |
+
m, s, f = map(int, match.groups())
|
23 |
+
return m * 60 + s + f / 75.0
|
24 |
+
return None
|
25 |
+
|
26 |
+
def format_cue_text(times, audio_filename="CDImage.wav"):
|
27 |
+
"""Generates the final CUE sheet string from a list of times."""
|
28 |
+
if not times:
|
29 |
+
return ""
|
30 |
+
filename_no_ext = os.path.splitext(audio_filename)[0]
|
31 |
+
cue_text = f'PERFORMER "Unknown Artist"\n'
|
32 |
+
cue_text += f'TITLE "{filename_no_ext}"\n'
|
33 |
+
cue_text += f'FILE "{audio_filename}" WAVE\n'
|
34 |
+
|
35 |
+
# Always sort times before formatting to handle out-of-order additions from splitting
|
36 |
+
sorted_times = sorted(list(set(times)))
|
37 |
+
for idx, t in enumerate(sorted_times):
|
38 |
+
cue_time_str = seconds_to_cue_time(t)
|
39 |
+
cue_text += f' TRACK {idx+1:02d} AUDIO\n'
|
40 |
+
cue_text += f' TITLE "Track {idx+1:02d}"\n'
|
41 |
+
cue_text += f' INDEX 01 {cue_time_str}\n'
|
42 |
+
return cue_text
|
43 |
+
|
44 |
+
def generate_track_labels(times, audio_duration):
|
45 |
+
"""Creates descriptive labels for the checklist, including track length."""
|
46 |
+
if not times:
|
47 |
+
return []
|
48 |
+
sorted_times = sorted(list(set(times)))
|
49 |
+
track_choices = []
|
50 |
+
for i, t in enumerate(sorted_times):
|
51 |
+
track_length = (sorted_times[i+1] - t) if i < len(sorted_times) - 1 else (audio_duration - t)
|
52 |
+
label = f"Track {i+1:02d} (Starts: {seconds_to_cue_time(t)}) [Length: {seconds_to_cue_time(track_length)}]"
|
53 |
+
track_choices.append(label)
|
54 |
+
return track_choices
|
55 |
+
|
56 |
+
# --- Core Gradio Functions ---
|
57 |
+
def analyze_audio_to_cue(audio_file, top_db, min_segment_len, merge_threshold, merge_protection_len):
|
58 |
+
"""Workflow 1: Analyzes an uploaded audio file to generate the initial CUE text."""
|
59 |
+
if not audio_file:
|
60 |
+
raise gr.Error("Please upload an audio file first.")
|
61 |
+
|
62 |
+
# --- 1. Load Audio File ---
|
63 |
+
try:
|
64 |
+
y, sr = librosa.load(audio_file, sr=None)
|
65 |
+
audio_duration = librosa.get_duration(y=y, sr=sr)
|
66 |
+
except Exception as e:
|
67 |
+
raise gr.Error(f"Could not load audio file: {e}")
|
68 |
+
|
69 |
+
# --- 2. Detect Segments using Silence Detection ---
|
70 |
+
intervals = librosa.effects.split(y, top_db=top_db)
|
71 |
+
|
72 |
+
# Corrected way to check if NumPy array is empty
|
73 |
+
times = [iv[0] / sr for iv in intervals if (iv[1] - iv[0]) / sr >= min_segment_len] if intervals.size > 0 else []
|
74 |
+
|
75 |
+
# --- 3. Post-process Tracks (Add Start, Auto-Merge) ---
|
76 |
+
if not times or times[0] > 0.5:
|
77 |
+
times.insert(0, 0.0)
|
78 |
+
|
79 |
+
# Auto-merging logic
|
80 |
+
if len(times) > 1:
|
81 |
+
final_times = [times[0]]
|
82 |
+
i = 0
|
83 |
+
while i < len(times) - 1:
|
84 |
+
track_length = times[i+1] - times[i]
|
85 |
+
|
86 |
+
# Merge if track is shorter than threshold AND not longer than protection length
|
87 |
+
if (track_length < merge_threshold) and (track_length <= merge_protection_len):
|
88 |
+
# Condition to MERGE is met. Skip adding the next timestamp.
|
89 |
+
pass
|
90 |
+
else:
|
91 |
+
# Condition to KEEP is met.
|
92 |
+
final_times.append(times[i+1])
|
93 |
+
|
94 |
+
i += 1
|
95 |
+
|
96 |
+
if len(final_times) > 1 and (audio_duration - final_times[-1]) < merge_threshold:
|
97 |
+
final_times.pop()
|
98 |
+
times = final_times
|
99 |
+
|
100 |
+
# --- 4. Prepare Outputs for Gradio ---
|
101 |
+
times = sorted(list(set(times)))
|
102 |
+
audio_filename = os.path.basename(audio_file)
|
103 |
+
initial_cue_text = format_cue_text(times, audio_filename)
|
104 |
+
track_labels = generate_track_labels(times, audio_duration)
|
105 |
+
|
106 |
+
# This function now returns everything needed to update the entire UI in one step.
|
107 |
+
return (
|
108 |
+
initial_cue_text, audio_filename, times, audio_duration,
|
109 |
+
gr.update(choices=track_labels, value=[]), gr.update(visible=True)
|
110 |
+
)
|
111 |
+
|
112 |
+
def parse_cue_and_update_ui(cue_text):
|
113 |
+
"""Workflow 2: Parses pasted CUE text. NOW returns the text itself to populate the output box."""
|
114 |
+
if not cue_text or "INDEX 01" not in cue_text:
|
115 |
+
return cue_text, "CDImage.wav", None, 0, gr.update(choices=[], value=[]), gr.update(visible=False)
|
116 |
+
|
117 |
+
file_match = re.search(r'FILE\s+"([^"]+)"', cue_text, re.IGNORECASE)
|
118 |
+
audio_filename = file_match.group(1) if file_match else "CDImage.wav"
|
119 |
+
|
120 |
+
index_matches = re.findall(r'INDEX\s+\d+\s+([\d:]{7,8})', cue_text)
|
121 |
+
times = [parse_cue_time_to_seconds(t) for t in index_matches if parse_cue_time_to_seconds(t) is not None]
|
122 |
+
|
123 |
+
if not times:
|
124 |
+
return cue_text, audio_filename, None, 0, gr.update(choices=[], value=[]), gr.update(visible=False)
|
125 |
+
|
126 |
+
times = sorted(list(set(times)))
|
127 |
+
# Estimate duration for UI labels. It's the last track's start time.
|
128 |
+
# This is a limitation of text-only mode, but makes the tool usable.
|
129 |
+
audio_duration = times[-1] if times else 0
|
130 |
+
track_labels = generate_track_labels(times, audio_duration)
|
131 |
+
|
132 |
+
return cue_text, audio_filename, times, audio_duration, gr.update(choices=track_labels, value=[]), gr.update(visible=True)
|
133 |
+
|
134 |
+
def update_editing_tools(selected_tracks, current_times, audio_duration):
|
135 |
+
"""Dynamically shows/hides Merge or Split tools based on selection count."""
|
136 |
+
num_selected = len(selected_tracks)
|
137 |
+
|
138 |
+
if num_selected == 1:
|
139 |
+
# Configure and show Split UI
|
140 |
+
# --- 1. Get track boundaries ---
|
141 |
+
track_idx = int(selected_tracks[0].split(' ')[1]) - 1
|
142 |
+
start_time = current_times[track_idx]
|
143 |
+
end_time = audio_duration if (track_idx + 1) >= len(current_times) else current_times[track_idx + 1]
|
144 |
+
|
145 |
+
# --- 2. [CORRECTION] Add padding to prevent splitting at the exact edges ---
|
146 |
+
# A CUE sheet frame is 1/75s (~0.013s). We use a slightly larger padding.
|
147 |
+
padding = 0.02
|
148 |
+
|
149 |
+
new_min_time = start_time + padding
|
150 |
+
new_max_time = end_time
|
151 |
+
|
152 |
+
# --- 3. [CORRECTION] Check if the track is too short to be split ---
|
153 |
+
if new_min_time >= new_max_time:
|
154 |
+
# If the track is too short, splitting is not possible. Hide the tools.
|
155 |
+
return (
|
156 |
+
gr.update(visible=False), # Hide Merge button
|
157 |
+
gr.update(visible=False), # Hide Split Group
|
158 |
+
None,
|
159 |
+
None
|
160 |
+
)
|
161 |
+
|
162 |
+
# --- 4. Configure and show the Split UI with the corrected range ---
|
163 |
+
mid_point = start_time + (end_time - start_time) / 2
|
164 |
+
|
165 |
+
return (
|
166 |
+
gr.update(visible=False), # Hide Merge button
|
167 |
+
gr.update(visible=True), # Show Split Group
|
168 |
+
# Use the new padded min/max values for the slider
|
169 |
+
gr.update(minimum=new_min_time, maximum=new_max_time, value=mid_point), # Configure Slider
|
170 |
+
gr.update(value=f"Split at: {seconds_to_cue_time(mid_point)}") # Update slider label
|
171 |
+
)
|
172 |
+
|
173 |
+
elif num_selected > 1:
|
174 |
+
# Show Merge UI
|
175 |
+
return gr.update(visible=True), gr.update(visible=False), None, None
|
176 |
+
else:
|
177 |
+
# Hide everything
|
178 |
+
return gr.update(visible=False), gr.update(visible=False), None, None
|
179 |
+
|
180 |
+
def perform_manual_merge(selected_tracks, original_times, audio_duration, audio_filename):
|
181 |
+
"""Merges selected tracks. The internal logic is robust and unchanged."""
|
182 |
+
|
183 |
+
# --- 1. Identify which track start times to remove ---
|
184 |
+
indices_to_merge = {int(label.split(' ')[1]) - 1 for label in selected_tracks}
|
185 |
+
|
186 |
+
# --- 2. Create the new list of times ---
|
187 |
+
# --- This logic correctly handles all merge cases. ---
|
188 |
+
new_times = []
|
189 |
+
# We iterate through the original times and decide which ones to KEEP.
|
190 |
+
for i, time in enumerate(original_times):
|
191 |
+
is_selected = i in indices_to_merge
|
192 |
+
|
193 |
+
# Condition to KEEP a track's start time:
|
194 |
+
# 1. It was NOT selected.
|
195 |
+
# OR
|
196 |
+
# 2. It WAS selected, BUT it's the start of a merge block.
|
197 |
+
# (This means it's the very first track, OR the track before it was NOT selected).
|
198 |
+
if not is_selected or (i == 0) or ((i - 1) not in indices_to_merge):
|
199 |
+
new_times.append(time)
|
200 |
+
|
201 |
+
# --- 3. Prepare all the outputs to update the UI ---
|
202 |
+
# The new CUE text for the textbox
|
203 |
+
final_cue_text = format_cue_text(new_times, audio_filename)
|
204 |
+
new_track_labels = generate_track_labels(new_times, audio_duration)
|
205 |
+
|
206 |
+
# Return a tuple that will update the textbox, the state, and the checklist
|
207 |
+
return final_cue_text, new_times, gr.update(choices=new_track_labels, value=[])
|
208 |
+
|
209 |
+
|
210 |
+
def perform_manual_split(split_time_sec, original_times, audio_duration, audio_filename):
|
211 |
+
"""Splits a track at the time specified by the slider."""
|
212 |
+
if split_time_sec in original_times:
|
213 |
+
raise gr.Error("This exact timestamp already exists.")
|
214 |
+
|
215 |
+
new_times = sorted(original_times + [split_time_sec])
|
216 |
+
final_cue_text = format_cue_text(new_times, audio_filename)
|
217 |
+
new_track_labels = generate_track_labels(new_times, audio_duration)
|
218 |
+
return final_cue_text, new_times, gr.update(choices=new_track_labels, value=[])
|
219 |
+
|
220 |
+
|
221 |
+
# --- Gradio User Interface Definition ---
|
222 |
+
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
223 |
+
gr.Markdown("# π΅ Advanced CUE Sheet Generator")
|
224 |
+
|
225 |
+
# --- Hidden State Variables ---
|
226 |
+
track_times_state = gr.State([])
|
227 |
+
audio_duration_state = gr.State(0)
|
228 |
+
audio_filename_state = gr.State("CDImage.wav")
|
229 |
+
|
230 |
+
with gr.Tabs():
|
231 |
+
with gr.TabItem("Start with Audio File"):
|
232 |
+
gr.Markdown("Upload an audio file to automatically detect track points.")
|
233 |
+
audio_input = gr.Audio(type="filepath", label="Upload Audio File")
|
234 |
+
with gr.Accordion("Analysis Parameters", open=False):
|
235 |
+
threshold_slider = gr.Slider(10, 80, 40, step=1, label="Silence Threshold (dB)")
|
236 |
+
min_length_slider = gr.Slider(0.5, 30, 2, step=0.1, label="Min. Segment Length (s)")
|
237 |
+
merge_length_slider = gr.Slider(1, 60, 15, step=1, label="Auto-Merge Threshold (s)")
|
238 |
+
min_silence_length_slider = gr.Slider(0.5, 60, 5, step=0.1, label="Merge Protection Length (s)")
|
239 |
+
generate_button = gr.Button("Analyze Audio", variant="primary")
|
240 |
+
|
241 |
+
with gr.TabItem("Start with CUE Text"):
|
242 |
+
gr.Markdown("Or paste CUE text below and click outside the box. The editing tools will appear automatically.")
|
243 |
+
cue_text_input_for_paste = gr.Textbox(label="Paste CUE Text Here", lines=8)
|
244 |
+
|
245 |
+
# The main output textbox is now outside the tabs, serving as a central display.
|
246 |
+
output_text = gr.Textbox(label="CUE Sheet Output", lines=15, show_copy_button=True, interactive=True)
|
247 |
+
|
248 |
+
with gr.Group(visible=False) as manual_editing_group:
|
249 |
+
gr.Markdown("### Manual Editing Tools")
|
250 |
+
track_checkboxes = gr.CheckboxGroup(label="Select Tracks to Edit")
|
251 |
+
|
252 |
+
with gr.Row(visible=False) as merge_tools:
|
253 |
+
merge_button = gr.Button("Merge Selected Tracks", variant="secondary", size="lg")
|
254 |
+
|
255 |
+
with gr.Group(visible=False) as split_tools:
|
256 |
+
split_slider_label = gr.Textbox(label="Current Split Time", interactive=False)
|
257 |
+
split_slider = gr.Slider(label="Drag to select split point")
|
258 |
+
split_button = gr.Button("Split Track at Selected Time", variant="secondary")
|
259 |
+
|
260 |
+
# --- Event Wiring ---
|
261 |
+
|
262 |
+
# Workflow 1: Audio analysis button now updates everything, including the editing tools.
|
263 |
+
generate_button.click(
|
264 |
+
fn=analyze_audio_to_cue,
|
265 |
+
inputs=[audio_input, threshold_slider, min_length_slider, merge_length_slider, min_silence_length_slider],
|
266 |
+
outputs=[output_text, audio_filename_state, track_times_state, audio_duration_state, track_checkboxes, manual_editing_group]
|
267 |
+
)
|
268 |
+
|
269 |
+
# Workflow 2: Pasting text in the dedicated input box populates the main output and enables tools.
|
270 |
+
# The `.change` event now updates all necessary outputs in a single, direct step.
|
271 |
+
cue_text_input_for_paste.change(
|
272 |
+
fn=parse_cue_and_update_ui,
|
273 |
+
inputs=[cue_text_input_for_paste],
|
274 |
+
outputs=[output_text, audio_filename_state, track_times_state, audio_duration_state, track_checkboxes, manual_editing_group]
|
275 |
+
)
|
276 |
+
|
277 |
+
# Dynamic UI controller for showing/hiding Merge/Split tools
|
278 |
+
track_checkboxes.change(
|
279 |
+
fn=update_editing_tools,
|
280 |
+
inputs=[track_checkboxes, track_times_state, audio_duration_state],
|
281 |
+
outputs=[merge_tools, split_tools, split_slider, split_slider_label]
|
282 |
+
)
|
283 |
+
|
284 |
+
# Live update for the split slider's time display
|
285 |
+
split_slider.input(
|
286 |
+
fn=lambda t: f"Split at: {seconds_to_cue_time(t)}",
|
287 |
+
inputs=[split_slider],
|
288 |
+
outputs=[split_slider_label]
|
289 |
+
)
|
290 |
+
|
291 |
+
# Action buttons
|
292 |
+
merge_button.click(
|
293 |
+
fn=perform_manual_merge,
|
294 |
+
inputs=[track_checkboxes, track_times_state, audio_duration_state, audio_filename_state],
|
295 |
+
outputs=[output_text, track_times_state, track_checkboxes]
|
296 |
+
)
|
297 |
+
|
298 |
+
split_button.click(
|
299 |
+
fn=perform_manual_split,
|
300 |
+
inputs=[split_slider, track_times_state, audio_duration_state, audio_filename_state],
|
301 |
+
outputs=[output_text, track_times_state, track_checkboxes]
|
302 |
+
)
|
303 |
+
|
304 |
+
if __name__ == "__main__":
|
305 |
+
demo.launch(inbrowser=True)
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
gradio
|
2 |
+
librosa
|
3 |
+
soundfile
|
webui.bat
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
|
3 |
+
:: The original source of the webui.bat file is stable-diffusion-webui
|
4 |
+
:: Modified and enhanced by Gemini with features for venv management and requirements handling.
|
5 |
+
|
6 |
+
:: --------- Configuration ---------
|
7 |
+
set COMMANDLINE_ARGS=
|
8 |
+
:: Define the name of the Launch application
|
9 |
+
set APPLICATION_NAME=app.py
|
10 |
+
:: Define the name of the virtual environment directory
|
11 |
+
set VENV_NAME=venv
|
12 |
+
:: Set to 1 to always attempt to update packages from requirements.txt on every launch
|
13 |
+
set ALWAYS_UPDATE_REQS=0
|
14 |
+
:: ---------------------------------
|
15 |
+
|
16 |
+
|
17 |
+
:: Set PYTHON executable if not already defined
|
18 |
+
if not defined PYTHON (set PYTHON=python)
|
19 |
+
:: Set VENV_DIR using VENV_NAME if not already defined
|
20 |
+
if not defined VENV_DIR (set "VENV_DIR=%~dp0%VENV_NAME%")
|
21 |
+
|
22 |
+
mkdir tmp 2>NUL
|
23 |
+
|
24 |
+
:: Check if Python is callable
|
25 |
+
%PYTHON% -c "" >tmp/stdout.txt 2>tmp/stderr.txt
|
26 |
+
if %ERRORLEVEL% == 0 goto :check_pip
|
27 |
+
echo Couldn't launch python
|
28 |
+
goto :show_stdout_stderr
|
29 |
+
|
30 |
+
:check_pip
|
31 |
+
:: Check if pip is available
|
32 |
+
%PYTHON% -mpip --help >tmp/stdout.txt 2>tmp/stderr.txt
|
33 |
+
if %ERRORLEVEL% == 0 goto :start_venv
|
34 |
+
:: If pip is not available and PIP_INSTALLER_LOCATION is set, try to install pip
|
35 |
+
if "%PIP_INSTALLER_LOCATION%" == "" goto :show_stdout_stderr
|
36 |
+
%PYTHON% "%PIP_INSTALLER_LOCATION%" >tmp/stdout.txt 2>tmp/stderr.txt
|
37 |
+
if %ERRORLEVEL% == 0 goto :start_venv
|
38 |
+
echo Couldn't install pip
|
39 |
+
goto :show_stdout_stderr
|
40 |
+
|
41 |
+
:start_venv
|
42 |
+
:: Skip venv creation/activation if VENV_DIR is explicitly set to "-"
|
43 |
+
if ["%VENV_DIR%"] == ["-"] goto :skip_venv_entirely
|
44 |
+
:: Skip venv creation/activation if SKIP_VENV is set to "1"
|
45 |
+
if ["%SKIP_VENV%"] == ["1"] goto :skip_venv_entirely
|
46 |
+
|
47 |
+
:: Check if the venv already exists by looking for Python.exe in its Scripts directory
|
48 |
+
dir "%VENV_DIR%\Scripts\Python.exe" >tmp/stdout.txt 2>tmp/stderr.txt
|
49 |
+
if %ERRORLEVEL% == 0 goto :activate_venv_and_maybe_update
|
50 |
+
|
51 |
+
:: Venv does not exist, create it
|
52 |
+
echo Virtual environment not found in "%VENV_DIR%". Creating a new one.
|
53 |
+
for /f "delims=" %%i in ('CALL %PYTHON% -c "import sys; print(sys.executable)"') do set PYTHON_FULLNAME="%%i"
|
54 |
+
echo Creating venv in directory %VENV_DIR% using python %PYTHON_FULLNAME%
|
55 |
+
%PYTHON_FULLNAME% -m venv "%VENV_DIR%" >tmp/stdout.txt 2>tmp/stderr.txt
|
56 |
+
if %ERRORLEVEL% NEQ 0 (
|
57 |
+
echo Unable to create venv in directory "%VENV_DIR%"
|
58 |
+
goto :show_stdout_stderr
|
59 |
+
)
|
60 |
+
echo Venv created.
|
61 |
+
|
62 |
+
:: Install requirements for the first time if venv was just created
|
63 |
+
:: This section handles the initial installation of packages from requirements.txt
|
64 |
+
:: immediately after a new virtual environment is created.
|
65 |
+
echo Checking for requirements.txt for initial setup in %~dp0
|
66 |
+
if exist "%~dp0requirements.txt" (
|
67 |
+
echo Found requirements.txt, attempting to install for initial setup...
|
68 |
+
call "%VENV_DIR%\Scripts\activate.bat"
|
69 |
+
echo Installing packages from requirements.txt ^(initial setup^)...
|
70 |
+
"%VENV_DIR%\Scripts\python.exe" -m pip install -r "%~dp0requirements.txt"
|
71 |
+
if %ERRORLEVEL% NEQ 0 (
|
72 |
+
echo Failed to install requirements during initial setup. Please check the output above.
|
73 |
+
pause
|
74 |
+
goto :show_stdout_stderr_custom_pip_initial
|
75 |
+
)
|
76 |
+
echo Initial requirements installed successfully.
|
77 |
+
call "%VENV_DIR%\Scripts\deactivate.bat"
|
78 |
+
) else (
|
79 |
+
echo No requirements.txt found for initial setup, skipping package installation.
|
80 |
+
)
|
81 |
+
goto :activate_venv_and_maybe_update
|
82 |
+
|
83 |
+
|
84 |
+
:activate_venv_and_maybe_update
|
85 |
+
:: This label is reached if the venv exists or was just created.
|
86 |
+
:: Set PYTHON to point to the venv's Python interpreter.
|
87 |
+
set PYTHON="%VENV_DIR%\Scripts\Python.exe"
|
88 |
+
echo Activating venv: %PYTHON%
|
89 |
+
|
90 |
+
:: Always update requirements if ALWAYS_UPDATE_REQS is 1
|
91 |
+
:: This section allows for updating packages from requirements.txt on every launch
|
92 |
+
:: if the ALWAYS_UPDATE_REQS variable is set to 1.
|
93 |
+
if defined ALWAYS_UPDATE_REQS (
|
94 |
+
if "%ALWAYS_UPDATE_REQS%"=="1" (
|
95 |
+
echo ALWAYS_UPDATE_REQS is enabled.
|
96 |
+
if exist "%~dp0requirements.txt" (
|
97 |
+
echo Attempting to update packages from requirements.txt...
|
98 |
+
REM No need to call activate.bat here again, PYTHON is already set to the venv's python
|
99 |
+
%PYTHON% -m pip install -r "%~dp0requirements.txt"
|
100 |
+
if %ERRORLEVEL% NEQ 0 (
|
101 |
+
echo Failed to update requirements. Please check the output above.
|
102 |
+
pause
|
103 |
+
goto :endofscript
|
104 |
+
)
|
105 |
+
echo Requirements updated successfully.
|
106 |
+
) else (
|
107 |
+
echo ALWAYS_UPDATE_REQS is enabled, but no requirements.txt found. Skipping update.
|
108 |
+
)
|
109 |
+
) else (
|
110 |
+
echo ALWAYS_UPDATE_REQS is not enabled or not set to 1. Skipping routine update.
|
111 |
+
)
|
112 |
+
)
|
113 |
+
|
114 |
+
goto :launch
|
115 |
+
|
116 |
+
:skip_venv_entirely
|
117 |
+
:: This label is reached if venv usage is explicitly skipped.
|
118 |
+
echo Skipping venv.
|
119 |
+
goto :launch
|
120 |
+
|
121 |
+
:launch
|
122 |
+
:: Launch the main application
|
123 |
+
echo Launching Web UI with arguments: %COMMANDLINE_ARGS% %*
|
124 |
+
%PYTHON% %APPLICATION_NAME% %COMMANDLINE_ARGS% %*
|
125 |
+
echo Launch finished.
|
126 |
+
pause
|
127 |
+
exit /b
|
128 |
+
|
129 |
+
:show_stdout_stderr_custom_pip_initial
|
130 |
+
:: Custom error handler for failures during the initial pip install process.
|
131 |
+
echo.
|
132 |
+
echo exit code ^(pip initial install^): %errorlevel%
|
133 |
+
echo Errors during initial pip install. See output above.
|
134 |
+
echo.
|
135 |
+
echo Launch unsuccessful. Exiting.
|
136 |
+
pause
|
137 |
+
exit /b
|
138 |
+
|
139 |
+
|
140 |
+
:show_stdout_stderr
|
141 |
+
:: General error handler: displays stdout and stderr from the tmp directory.
|
142 |
+
echo.
|
143 |
+
echo exit code: %errorlevel%
|
144 |
+
|
145 |
+
for /f %%i in ("tmp\stdout.txt") do set size=%%~zi
|
146 |
+
if %size% equ 0 goto :show_stderr
|
147 |
+
echo.
|
148 |
+
echo stdout:
|
149 |
+
type tmp\stdout.txt
|
150 |
+
|
151 |
+
:show_stderr
|
152 |
+
for /f %%i in ("tmp\stderr.txt") do set size=%%~zi
|
153 |
+
if %size% equ 0 goto :endofscript
|
154 |
+
echo.
|
155 |
+
echo stderr:
|
156 |
+
type tmp\stderr.txt
|
157 |
+
|
158 |
+
:endofscript
|
159 |
+
echo.
|
160 |
+
echo Launch unsuccessful. Exiting.
|
161 |
+
pause
|
162 |
+
exit /b
|