Update app.py
Browse files
app.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
import gradio as gr
|
2 |
import base64
|
3 |
import mimetypes
|
4 |
import os
|
@@ -6,10 +5,10 @@ import re
|
|
6 |
import struct
|
7 |
import time
|
8 |
import zipfile
|
9 |
-
import
|
10 |
-
from google import genai
|
11 |
-
from google.genai import types as genai_types
|
12 |
|
|
|
|
|
13 |
try:
|
14 |
from pydub import AudioSegment
|
15 |
PYDUB_AVAILABLE = True
|
@@ -17,360 +16,481 @@ except ImportError:
|
|
17 |
PYDUB_AVAILABLE = False
|
18 |
print("⚠️ pydub در دسترس نیست. فایلهای صوتی به صورت جداگانه ذخیره میشوند.")
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
# --- Helper functions (unchanged) ---
|
35 |
-
def log_message(msg, current_logs):
|
36 |
-
print(msg)
|
37 |
-
return f"{current_logs}\n{msg}".strip()
|
38 |
-
|
39 |
-
def save_binary_file(file_name, data, log_func, current_logs):
|
40 |
-
full_path = os.path.join(OUTPUT_DIR, file_name)
|
41 |
-
try:
|
42 |
-
with open(full_path, "wb") as f: f.write(data)
|
43 |
-
current_logs = log_func(f"✅ فایل: {full_path}", current_logs)
|
44 |
-
return full_path, current_logs
|
45 |
-
except Exception as e:
|
46 |
-
current_logs = log_func(f"❌ خطا ذخیره {file_name}: {e}", current_logs)
|
47 |
-
return None, current_logs
|
48 |
|
49 |
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
50 |
parameters = parse_audio_mime_type(mime_type)
|
51 |
-
bits_per_sample
|
52 |
-
|
|
|
|
|
53 |
bytes_per_sample = bits_per_sample // 8
|
54 |
block_align = num_channels * bytes_per_sample
|
55 |
byte_rate = sample_rate * block_align
|
56 |
chunk_size = 36 + data_size
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
return header + audio_data
|
59 |
|
60 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
61 |
-
bits_per_sample
|
62 |
-
|
|
|
|
|
63 |
param = param.strip()
|
64 |
if param.lower().startswith("rate="):
|
65 |
-
try:
|
66 |
-
|
|
|
|
|
|
|
67 |
elif param.startswith("audio/L"):
|
68 |
-
try:
|
69 |
-
|
|
|
|
|
70 |
return {"bits_per_sample": bits_per_sample, "rate": rate}
|
71 |
|
72 |
-
def
|
73 |
-
if file_obj is None:
|
74 |
-
|
75 |
-
current_logs = log_func(f"✅ فایل '{os.path.basename(file_path)}' دریافت.", current_logs)
|
76 |
try:
|
77 |
-
with open(
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
81 |
|
82 |
def smart_text_split(text, max_size=3800):
|
83 |
-
if len(text) <= max_size:
|
84 |
-
|
85 |
-
|
|
|
|
|
86 |
for sentence in sentences:
|
87 |
if len(current_chunk) + len(sentence) + 1 > max_size:
|
88 |
-
if current_chunk:
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
def merge_audio_files_func(file_paths,
|
107 |
-
if not PYDUB_AVAILABLE:
|
108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
try:
|
110 |
-
|
|
|
|
|
111 |
combined = AudioSegment.empty()
|
|
|
112 |
for i, file_path in enumerate(file_paths):
|
113 |
-
if os.path.exists(file_path):
|
114 |
-
current_logs = log_func(f"📎 فایل {i+1}: {file_path}", current_logs)
|
115 |
try:
|
|
|
|
|
116 |
audio = AudioSegment.from_file(file_path)
|
117 |
combined += audio
|
118 |
-
if i < len(file_paths) - 1:
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
combined.export(output_path, format="wav")
|
125 |
-
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
127 |
|
128 |
-
def create_zip_file(file_paths, zip_name_base
|
129 |
-
|
|
|
|
|
|
|
130 |
try:
|
131 |
-
with zipfile.ZipFile(
|
132 |
for file_path in file_paths:
|
133 |
-
if os.path.exists(file_path):
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
):
|
144 |
-
|
145 |
-
if not api_key_hf_secret:
|
146 |
-
return log_message("❌ کلید API جمینای در Secrets نیست.", logs), None, None, gr.update(visible=False)
|
147 |
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
except Exception as e:
|
157 |
-
return log_message(f"❌ خطا ایجاد کلاینت: {type(e).__name__} - {e}", logs), None, None, gr.update(visible=False)
|
158 |
|
159 |
-
|
160 |
-
if
|
161 |
-
|
162 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
else:
|
164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
|
166 |
-
if
|
167 |
-
|
168 |
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
|
174 |
generated_files = []
|
175 |
-
|
176 |
-
|
|
|
|
|
177 |
|
178 |
-
|
179 |
-
# Using a simple concatenation. The Colab might have had a more specific format.
|
180 |
-
# If speech_prompt_ui is "شاد و پر انرژی" and chunk_for_api is "سلام دنیا"
|
181 |
-
# final_text_for_api will be "شاد و پر انرژی\nسلام دنیا"
|
182 |
-
if speech_prompt_ui and speech_prompt_ui.strip():
|
183 |
-
final_text_for_api = f"{speech_prompt_ui.strip()}\n{chunk_for_api}"
|
184 |
-
logs = log_message(f"ℹ️ پرامپت سبک '{speech_prompt_ui.strip()}' به متن اضافه شد.", logs)
|
185 |
-
else:
|
186 |
-
final_text_for_api = chunk_for_api
|
187 |
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
|
|
|
|
|
|
|
|
192 |
),
|
193 |
-
]
|
194 |
-
|
195 |
-
genai_speech_config = genai_types.SpeechConfig(
|
196 |
-
voice_config=genai_types.VoiceConfig(
|
197 |
-
prebuilt_voice_config=genai_types.PrebuiltVoiceConfig(voice_name=speaker_voice_ui)
|
198 |
-
)
|
199 |
)
|
200 |
|
201 |
-
|
202 |
-
temperature=temperature_ui,
|
203 |
-
response_modalities=["audio"],
|
204 |
-
speech_config=genai_speech_config
|
205 |
-
)
|
206 |
|
207 |
try:
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
stream_iterator = client.models.generate_content_stream( # type: ignore
|
213 |
-
model=model_name_ui, contents=api_contents, config=stream_generation_config,
|
214 |
)
|
|
|
|
|
|
|
215 |
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
chunk_response.candidates[0].content.parts[0].inline_data):
|
223 |
inline_data = chunk_response.candidates[0].content.parts[0].inline_data
|
224 |
-
audio_data_buffer += inline_data.data
|
225 |
-
|
226 |
elif chunk_response.text:
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
logs = log_message(f"ℹ️ Mime: {mime_type_from_api}. تبدیل به WAV...", logs)
|
241 |
-
final_audio_data = convert_to_wav(audio_data_buffer, mime_type_from_api)
|
242 |
-
file_extension = ".wav"
|
243 |
-
else:
|
244 |
-
logs = log_message(f"ℹ️ Mime ناشناخته: {mime_type_from_api}. ذخیره .bin.", logs)
|
245 |
-
file_extension = ".bin"
|
246 |
-
if mime_type_from_api == "audio/wav" and (file_extension != ".wav" and file_extension != ".wave"): file_extension = ".wav"
|
247 |
-
elif mime_type_from_api == "audio/mpeg" and file_extension != ".mp3": file_extension = ".mp3"
|
248 |
-
elif mime_type_from_api == "audio/ogg" and file_extension != ".ogg": file_extension = ".ogg"
|
249 |
-
if file_extension is None: file_extension = ".audio"
|
250 |
-
|
251 |
-
saved_file_path, logs = save_binary_file(f"{chunk_filename_base}{file_extension}", final_audio_data, log_message, logs)
|
252 |
-
if saved_file_path:
|
253 |
-
generated_files.append(saved_file_path)
|
254 |
-
logs = log_message(f"✅ قطعه {i+1} تولید شد: {saved_file_path}", logs)
|
255 |
else:
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
except Exception as e:
|
260 |
-
|
261 |
-
#
|
262 |
-
if hasattr(e, '
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
elif "DeadlineExceeded" in str(e) or "504" in str(e): error_msg += f"\n⏱️ Timeout."
|
269 |
-
logs = log_message(error_msg, logs)
|
270 |
-
# logs = log_message(traceback.format_exc(), logs) # DEBUG
|
271 |
-
continue
|
272 |
-
|
273 |
-
if i < len(text_chunks) - 1 and sleep_between_requests_ui > 0:
|
274 |
-
logs = log_message(f"⏱️ انتظار {sleep_between_requests_ui} ثانیه...", logs)
|
275 |
-
time.sleep(sleep_between_requests_ui)
|
276 |
|
|
|
|
|
|
|
|
|
277 |
if not generated_files:
|
278 |
-
|
|
|
279 |
|
280 |
-
|
281 |
-
final_audio_path, zip_file_path, zip_visible = None, None, False
|
282 |
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
else:
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
try: os.remove(fp_del); logs = log_message(f"🗑️ حذف: {fp_del}", logs)
|
297 |
-
except Exception as e_del: logs = log_message(f"⚠️ خطا حذف {fp_del}: {e_del}", logs)
|
298 |
-
else:
|
299 |
-
logs = log_message("⚠️ ادغام ناموفق. ارائه ZIP.", logs)
|
300 |
-
zip_file_path, logs = create_zip_file(generated_files, f"{output_filename_base_ui}_all", log_message, logs)
|
301 |
-
if zip_file_path: zip_visible = True
|
302 |
-
if generated_files: final_audio_path = generated_files[0]
|
303 |
elif len(generated_files) == 1:
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
316 |
css = """
|
317 |
-
body {
|
318 |
-
.rtl_override { direction: rtl !important; text-align: right !important; }
|
319 |
-
.gr-input, .gr-output, .gr-radio label span { text-align: right !important; direction: rtl !important;}
|
320 |
-
.gr-checkbox label span { text-align: right !important; direction: rtl !important; margin-right: 0.5em;}
|
321 |
-
footer { display: none !important; }
|
322 |
.gradio-container { max-width: 800px !important; margin: auto !important; }
|
|
|
|
|
|
|
323 |
"""
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
gr.Markdown(f"<p style='text-align:center; color:
|
|
|
|
|
337 |
with gr.Row():
|
338 |
with gr.Column(scale=2):
|
339 |
-
gr.Markdown("
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
gr.
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
with gr.Column(scale=1):
|
350 |
-
gr.Markdown("
|
351 |
-
max_chunk_size_slider = gr.Slider(minimum=2000, maximum=4000, step=100, value=3800, label="حداکثر کاراکتر در قطعه",
|
352 |
-
sleep_between_requests_slider = gr.Slider(minimum=
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
submit_button = gr.Button("
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import base64
|
2 |
import mimetypes
|
3 |
import os
|
|
|
5 |
import struct
|
6 |
import time
|
7 |
import zipfile
|
8 |
+
import shutil # برای حذف فایلها و پوشهها
|
|
|
|
|
9 |
|
10 |
+
from google import genai
|
11 |
+
from google.genai import types
|
12 |
try:
|
13 |
from pydub import AudioSegment
|
14 |
PYDUB_AVAILABLE = True
|
|
|
16 |
PYDUB_AVAILABLE = False
|
17 |
print("⚠️ pydub در دسترس نیست. فایلهای صوتی به صورت جداگانه ذخیره میشوند.")
|
18 |
|
19 |
+
import gradio as gr
|
20 |
+
|
21 |
+
# --- Helper functions (mostly from your Colab notebook) ---
|
22 |
+
|
23 |
+
def save_binary_file(file_name, data):
|
24 |
+
# Ensure output directory exists
|
25 |
+
output_dir = "outputs"
|
26 |
+
os.makedirs(output_dir, exist_ok=True)
|
27 |
+
full_file_path = os.path.join(output_dir, file_name)
|
28 |
+
with open(full_file_path, "wb") as f:
|
29 |
+
f.write(data)
|
30 |
+
print(f"✅ فایل در مسیر زیر ذخیره شد: {full_file_path}")
|
31 |
+
return full_file_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
34 |
parameters = parse_audio_mime_type(mime_type)
|
35 |
+
bits_per_sample = parameters["bits_per_sample"]
|
36 |
+
sample_rate = parameters["rate"]
|
37 |
+
num_channels = 1
|
38 |
+
data_size = len(audio_data)
|
39 |
bytes_per_sample = bits_per_sample // 8
|
40 |
block_align = num_channels * bytes_per_sample
|
41 |
byte_rate = sample_rate * block_align
|
42 |
chunk_size = 36 + data_size
|
43 |
+
|
44 |
+
header = struct.pack(
|
45 |
+
"<4sI4s4sIHHIIHH4sI",
|
46 |
+
b"RIFF",
|
47 |
+
chunk_size,
|
48 |
+
b"WAVE",
|
49 |
+
b"fmt ",
|
50 |
+
16,
|
51 |
+
1,
|
52 |
+
num_channels,
|
53 |
+
sample_rate,
|
54 |
+
byte_rate,
|
55 |
+
block_align,
|
56 |
+
bits_per_sample,
|
57 |
+
b"data",
|
58 |
+
data_size
|
59 |
+
)
|
60 |
return header + audio_data
|
61 |
|
62 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
63 |
+
bits_per_sample = 16
|
64 |
+
rate = 24000
|
65 |
+
parts = mime_type.split(";")
|
66 |
+
for param in parts:
|
67 |
param = param.strip()
|
68 |
if param.lower().startswith("rate="):
|
69 |
+
try:
|
70 |
+
rate_str = param.split("=", 1)[1]
|
71 |
+
rate = int(rate_str)
|
72 |
+
except (ValueError, IndexError):
|
73 |
+
pass
|
74 |
elif param.startswith("audio/L"):
|
75 |
+
try:
|
76 |
+
bits_per_sample = int(param.split("L", 1)[1])
|
77 |
+
except (ValueError, IndexError):
|
78 |
+
pass
|
79 |
return {"bits_per_sample": bits_per_sample, "rate": rate}
|
80 |
|
81 |
+
def load_text_from_gradio_file(file_obj):
|
82 |
+
if file_obj is None:
|
83 |
+
return ""
|
|
|
84 |
try:
|
85 |
+
with open(file_obj.name, 'r', encoding='utf-8') as f:
|
86 |
+
content = f.read().strip()
|
87 |
+
print(f"📖 متن بارگذاری شده: {len(content)} کاراکتر")
|
88 |
+
print(f"📝 نمونه متن: '{content[:100]}{'...' if len(content) > 100 else ''}'")
|
89 |
+
return content
|
90 |
+
except Exception as e:
|
91 |
+
print(f"❌ خطا در خواندن فایل: {e}")
|
92 |
+
return ""
|
93 |
|
94 |
def smart_text_split(text, max_size=3800):
|
95 |
+
if len(text) <= max_size:
|
96 |
+
return [text]
|
97 |
+
chunks = []
|
98 |
+
current_chunk = ""
|
99 |
+
sentences = re.split(r'(?<=[.!?])\s+', text)
|
100 |
for sentence in sentences:
|
101 |
if len(current_chunk) + len(sentence) + 1 > max_size:
|
102 |
+
if current_chunk:
|
103 |
+
chunks.append(current_chunk.strip())
|
104 |
+
current_chunk = sentence
|
105 |
+
# Handle very long sentences by splitting words/characters
|
106 |
+
while len(current_chunk) > max_size:
|
107 |
+
# Find a good split point (e.g., space) within max_size
|
108 |
+
split_at = current_chunk[:max_size].rfind(' ')
|
109 |
+
if split_at == -1 or split_at < max_size // 2 : # if no space or space is too early, force split
|
110 |
+
split_at = max_size
|
111 |
+
chunks.append(current_chunk[:split_at].strip())
|
112 |
+
current_chunk = current_chunk[split_at:].strip()
|
113 |
+
else:
|
114 |
+
current_chunk += (" " if current_chunk else "") + sentence
|
115 |
+
if current_chunk:
|
116 |
+
chunks.append(current_chunk.strip())
|
117 |
+
return chunks
|
118 |
+
|
119 |
+
|
120 |
+
def merge_audio_files_func(file_paths, output_path):
|
121 |
+
if not PYDUB_AVAILABLE:
|
122 |
+
print("❌ pydub در دسترس نیست. نمیتوان فایلها را ادغام کرد.")
|
123 |
+
return None, "خطا: کتابخانه pydub برای ادغام فایلها در دسترس نیست."
|
124 |
+
if not file_paths:
|
125 |
+
return None, "خطا: هیچ فایلی برای ادغام وجود ندارد."
|
126 |
+
|
127 |
+
# Ensure output directory exists
|
128 |
+
output_dir = os.path.dirname(output_path)
|
129 |
+
if output_dir: # If output_path includes a directory
|
130 |
+
os.makedirs(output_dir, exist_ok=True)
|
131 |
+
|
132 |
try:
|
133 |
+
print(f"🔗 در حال ادغام {len(file_paths)} فایل صوتی...")
|
134 |
+
log_messages = [f"🔗 در حال ادغام {len(file_paths)} فایل صوتی..."]
|
135 |
+
|
136 |
combined = AudioSegment.empty()
|
137 |
+
valid_files_merged = 0
|
138 |
for i, file_path in enumerate(file_paths):
|
139 |
+
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
|
|
|
140 |
try:
|
141 |
+
print(f"📎 اضافه کردن فایل {i+1}: {file_path}")
|
142 |
+
log_messages.append(f"📎 اضافه کردن فایل {i+1}: {os.path.basename(file_path)}")
|
143 |
audio = AudioSegment.from_file(file_path)
|
144 |
combined += audio
|
145 |
+
if i < len(file_paths) - 1: # Add silence between segments, except for the last one
|
146 |
+
combined += AudioSegment.silent(duration=200) # 200ms silence
|
147 |
+
valid_files_merged += 1
|
148 |
+
except Exception as e:
|
149 |
+
print(f"⚠️ خطا در خواندن یا اضافه کردن فایل {file_path}: {e}")
|
150 |
+
log_messages.append(f"⚠️ خطا در خواندن یا اضافه کردن فایل {os.path.basename(file_path)}: {e}")
|
151 |
+
else:
|
152 |
+
print(f"⚠️ فایل پیدا نشد یا خالی است: {file_path}")
|
153 |
+
log_messages.append(f"⚠️ فایل پیدا نشد یا خالی است: {os.path.basename(file_path)}")
|
154 |
+
|
155 |
+
if valid_files_merged == 0:
|
156 |
+
return None, "\n".join(log_messages) + "\n❌ هیچ فایل معتبری برای ادغام پیدا نشد."
|
157 |
+
|
158 |
combined.export(output_path, format="wav")
|
159 |
+
print(f"✅ فایل ادغام شده ذخیره شد: {output_path}")
|
160 |
+
log_messages.append(f"✅ فایل ادغام شده ذخیره شد: {os.path.basename(output_path)}")
|
161 |
+
return output_path, "\n".join(log_messages)
|
162 |
+
except Exception as e:
|
163 |
+
print(f"❌ خطا در ادغام فایلها: {e}")
|
164 |
+
log_messages.append(f"❌ خطا در ادغام فایلها: {e}")
|
165 |
+
return None, "\n".join(log_messages)
|
166 |
|
167 |
+
def create_zip_file(file_paths, zip_name_base):
|
168 |
+
output_dir = "outputs"
|
169 |
+
os.makedirs(output_dir, exist_ok=True)
|
170 |
+
zip_path = os.path.join(output_dir, f"{zip_name_base}.zip")
|
171 |
+
log_messages = []
|
172 |
try:
|
173 |
+
with zipfile.ZipFile(zip_path, 'w') as zipf:
|
174 |
for file_path in file_paths:
|
175 |
+
if os.path.exists(file_path):
|
176 |
+
zipf.write(file_path, os.path.basename(file_path))
|
177 |
+
log_messages.append(f"📦 فایل ZIP ایجاد شد: {os.path.basename(zip_path)}")
|
178 |
+
return zip_path, "\n".join(log_messages)
|
179 |
+
except Exception as e:
|
180 |
+
log_messages.append(f"❌ خطا در ایجاد فایل ZIP: {e}")
|
181 |
+
return None, "\n".join(log_messages)
|
182 |
+
|
183 |
+
def cleanup_temp_files(files_to_delete):
|
184 |
+
if not files_to_delete:
|
185 |
+
return
|
186 |
+
print("🧹 در حال پاکسازی فایلهای موقت...")
|
187 |
+
for f_path in files_to_delete:
|
188 |
+
if f_path and os.path.exists(f_path):
|
189 |
+
try:
|
190 |
+
os.remove(f_path)
|
191 |
+
print(f"🗑️ فایل موقت حذف شد: {f_path}")
|
192 |
+
except Exception as e:
|
193 |
+
print(f"⚠️ خطا در حذف فایل {f_path}: {e}")
|
194 |
+
# Clean up the 'outputs' directory content as well, but not the directory itself
|
195 |
+
output_dir_content = "outputs"
|
196 |
+
if os.path.exists(output_dir_content):
|
197 |
+
for item in os.listdir(output_dir_content):
|
198 |
+
item_path = os.path.join(output_dir_content, item)
|
199 |
+
try:
|
200 |
+
if os.path.isfile(item_path) or os.path.islink(item_path):
|
201 |
+
os.unlink(item_path)
|
202 |
+
elif os.path.isdir(item_path):
|
203 |
+
shutil.rmtree(item_path) # Danger: be careful with rmtree
|
204 |
+
print(f"🗑️ آیتم حذف شده از outputs: {item_path}")
|
205 |
+
except Exception as e:
|
206 |
+
print(f"⚠️ خطا در حذف {item_path} از outputs: {e}")
|
207 |
+
|
208 |
+
|
209 |
+
# --- Main generation function for Gradio ---
|
210 |
+
def generate_audio_gradio(
|
211 |
+
use_file_input, text_file_obj, text_to_speak, speech_prompt,
|
212 |
+
selected_voice, output_filename_base, model_name, temperature,
|
213 |
+
max_chunk_size, sleep_between_requests, merge_audio_files, delete_partial_files,
|
214 |
+
progress=gr.Progress(track_tqdm=True)
|
215 |
):
|
216 |
+
log_messages = ["🚀 شروع فرآیند تبدیل متن به گفتار...\n"]
|
|
|
|
|
217 |
|
218 |
+
# Cleanup previous run files from 'outputs' if any, except for permanent ones.
|
219 |
+
# It's better to generate files in a unique temp dir per request or clean up specifically.
|
220 |
+
# For simplicity here, we'll clean up 'outputs' at the start of each run.
|
221 |
+
# Ensure 'outputs' directory exists for this run
|
222 |
+
os.makedirs("outputs", exist_ok=True)
|
223 |
+
|
224 |
+
# It's safer to delete specific files from previous runs rather than wiping the whole dir
|
225 |
+
# For now, let's skip aggressive auto-deletion of 'outputs' and rely on specific file deletion.
|
|
|
|
|
226 |
|
227 |
+
text_input = ""
|
228 |
+
if use_file_input:
|
229 |
+
log_messages.append("📁 حالت فایل فعال است. در حال خواندن فایل...")
|
230 |
+
if text_file_obj is None:
|
231 |
+
log_messages.append("❌ خطا: هیچ فایلی آپلود نشده است.")
|
232 |
+
return None, None, "\n".join(log_messages)
|
233 |
+
text_input = load_text_from_gradio_file(text_file_obj)
|
234 |
+
if not text_input:
|
235 |
+
log_messages.append("❌ خطا: متن استخراج شده از فایل خالی است.")
|
236 |
+
return None, None, "\n".join(log_messages)
|
237 |
+
log_messages.append("✅ متن از فایل با موفقیت بارگذاری شد.")
|
238 |
else:
|
239 |
+
log_messages.append("⌨️ حالت ورودی دستی فعال است.")
|
240 |
+
text_input = text_to_speak
|
241 |
+
|
242 |
+
if not text_input or text_input.strip() == "":
|
243 |
+
log_messages.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است.")
|
244 |
+
return None, None, "\n".join(log_messages)
|
245 |
+
|
246 |
+
api_key = os.environ.get("GEMINI_API_KEY")
|
247 |
+
if not api_key:
|
248 |
+
log_messages.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) پیدا نشد.")
|
249 |
+
log_messages.append("لطفاً کلید API خود را در بخش Secrets این Space تنظیم کنید.")
|
250 |
+
return None, None, "\n".join(log_messages)
|
251 |
|
252 |
+
# os.environ["GEMINI_API_KEY"] = api_key # Not needed if genai client picks it up directly
|
253 |
+
log_messages.append("🔑 کلید API از Hugging Face Secrets بارگذاری شد.")
|
254 |
|
255 |
+
try:
|
256 |
+
log_messages.append("🛠️ در حال ایجاد کلاینت جمینای...")
|
257 |
+
client = genai.Client(api_key=api_key)
|
258 |
+
log_messages.append("✅ کلاینت جمینای با موفقیت ایجاد شد.")
|
259 |
+
except Exception as e:
|
260 |
+
log_messages.append(f"❌ خطا در ایجاد کلاینت جمینای: {e}")
|
261 |
+
return None, None, "\n".join(log_messages)
|
262 |
+
|
263 |
+
text_chunks = smart_text_split(text_input, int(max_chunk_size))
|
264 |
+
log_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.")
|
265 |
+
for i, chunk in enumerate(text_chunks):
|
266 |
+
log_messages.append(f"📝 قطعه {i+1}: {len(chunk)} کاراکتر")
|
267 |
|
268 |
generated_files = []
|
269 |
+
total_chunks = len(text_chunks)
|
270 |
+
|
271 |
+
for i, chunk in enumerate(progress.tqdm(text_chunks, desc="تولید قطعات صوتی")):
|
272 |
+
current_log = [f"\n🔊 تولید صدا برای قطعه {i+1}/{total_chunks}..."]
|
273 |
|
274 |
+
final_text_for_api = f'"{speech_prompt}"\n{chunk}' if speech_prompt and speech_prompt.strip() else chunk
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
|
276 |
+
contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
|
277 |
+
generate_content_config = types.GenerateContentConfig(
|
278 |
+
temperature=float(temperature),
|
279 |
+
response_modalities=["audio"],
|
280 |
+
speech_config=types.SpeechConfig(
|
281 |
+
voice_config=types.VoiceConfig(
|
282 |
+
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)
|
283 |
+
)
|
284 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
285 |
)
|
286 |
|
287 |
+
chunk_filename_base = f"{output_filename_base}_part_{i+1:03d}"
|
|
|
|
|
|
|
|
|
288 |
|
289 |
try:
|
290 |
+
response_stream = client.models.generate_content_stream(
|
291 |
+
model=model_name,
|
292 |
+
contents=contents,
|
293 |
+
config=generate_content_config,
|
|
|
|
|
294 |
)
|
295 |
+
|
296 |
+
audio_data_buffer = b""
|
297 |
+
final_mime_type = None
|
298 |
|
299 |
+
for chunk_response in response_stream:
|
300 |
+
if (
|
301 |
+
chunk_response.candidates and chunk_response.candidates[0].content and
|
302 |
+
chunk_response.candidates[0].content.parts and
|
303 |
+
chunk_response.candidates[0].content.parts[0].inline_data
|
304 |
+
):
|
|
|
305 |
inline_data = chunk_response.candidates[0].content.parts[0].inline_data
|
306 |
+
audio_data_buffer += inline_data.data # Accumulate data if streamed in parts
|
307 |
+
final_mime_type = inline_data.mime_type # Mime type should be consistent
|
308 |
elif chunk_response.text:
|
309 |
+
current_log.append(f"ℹ️ پیام متنی از API: {chunk_response.text}")
|
310 |
+
|
311 |
+
if audio_data_buffer and final_mime_type:
|
312 |
+
file_extension = mimetypes.guess_extension(final_mime_type)
|
313 |
+
if file_extension is None or file_extension == ".bin": # .bin is often default for unknown binary
|
314 |
+
file_extension = ".wav" # Force wav if unknown
|
315 |
+
processed_data_buffer = convert_to_wav(audio_data_buffer, final_mime_type)
|
316 |
+
else:
|
317 |
+
processed_data_buffer = audio_data_buffer
|
318 |
+
|
319 |
+
generated_file_path = save_binary_file(f"{chunk_filename_base}{file_extension}", processed_data_buffer)
|
320 |
+
generated_files.append(generated_file_path)
|
321 |
+
current_log.append(f"✅ قطعه {i+1} تولید شد: {os.path.basename(generated_file_path)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
322 |
else:
|
323 |
+
current_log.append(f"❌ داده صوتی برای قطعه {i+1} دریافت نشد.")
|
324 |
+
|
325 |
+
|
326 |
except Exception as e:
|
327 |
+
current_log.append(f"❌ خطا در تولید قطعه {i+1}: {e}")
|
328 |
+
# Attempt to get more details from the error if it's a GenAI specific one
|
329 |
+
if hasattr(e, 'message'):
|
330 |
+
current_log.append(f" جزئیات خطا: {e.message}")
|
331 |
+
if "API key not valid" in str(e):
|
332 |
+
current_log.append(" 🔑 به نظر میرسد کلید API نامعتبر است. لطفاً آن را در بخش Secrets بررسی کنید.")
|
333 |
+
|
334 |
+
log_messages.extend(current_log)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
|
336 |
+
if i < total_chunks - 1:
|
337 |
+
log_messages.append(f"⏱️ انتظار {sleep_between_requests} ثانیه...")
|
338 |
+
time.sleep(float(sleep_between_requests))
|
339 |
+
|
340 |
if not generated_files:
|
341 |
+
log_messages.append("\n❌ هیچ فایل صوتی تولید نشد!")
|
342 |
+
return None, None, "\n".join(log_messages)
|
343 |
|
344 |
+
log_messages.append(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!")
|
|
|
345 |
|
346 |
+
final_audio_output_path = None
|
347 |
+
zip_file_output_path = None
|
348 |
+
|
349 |
+
if merge_audio_files and len(generated_files) > 1:
|
350 |
+
merged_filename_path = os.path.join("outputs", f"{output_filename_base}_merged.wav")
|
351 |
+
merged_path, merge_log = merge_audio_files_func(generated_files, merged_filename_path)
|
352 |
+
log_messages.append(merge_log)
|
353 |
+
if merged_path:
|
354 |
+
final_audio_output_path = merged_path
|
355 |
+
log_messages.append(f"🎵 فایل نهایی ادغام شده: {os.path.basename(final_audio_output_path)}")
|
356 |
+
if delete_partial_files:
|
357 |
+
log_messages.append("🗑️ حذف فایلهای جزئی فعال است...")
|
358 |
+
files_to_remove = [f for f in generated_files if f != final_audio_output_path]
|
359 |
+
cleanup_temp_files(files_to_remove)
|
360 |
else:
|
361 |
+
log_messages.append("⚠️ ادغام ممکن نبود یا موفقیت آمیز نبود. فایلهای جداگانه در صورت وجود حفظ شدند.")
|
362 |
+
# If merge fails, offer zip of parts
|
363 |
+
zip_path, zip_log = create_zip_file(generated_files, f"{output_filename_base}_all_parts")
|
364 |
+
log_messages.append(zip_log)
|
365 |
+
zip_file_output_path = zip_path
|
366 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
367 |
elif len(generated_files) == 1:
|
368 |
+
final_audio_output_path = generated_files[0]
|
369 |
+
log_messages.append(f"🎵 فقط یک فایل تولید شد: {os.path.basename(final_audio_output_path)}")
|
370 |
+
|
371 |
+
else: # Multiple files but merge_audio_files is False
|
372 |
+
log_messages.append("📦 چون ادغام فایلها انتخاب نشده، فایلها به صورت ZIP ارائه میشوند.")
|
373 |
+
zip_path, zip_log = create_zip_file(generated_files, f"{output_filename_base}_all_parts")
|
374 |
+
log_messages.append(zip_log)
|
375 |
+
zip_file_output_path = zip_path
|
376 |
+
# Optionally, provide the first audio file for direct listening if no zip
|
377 |
+
if not zip_file_output_path and generated_files:
|
378 |
+
final_audio_output_path = generated_files[0]
|
379 |
+
|
380 |
+
|
381 |
+
# Determine what to return for audio and file download components
|
382 |
+
# Priority: Merged Audio > First Chunk Audio (if no merge/zip)
|
383 |
+
# Zip file is always offered if multiple parts exist and not merged, or if merge fails
|
384 |
+
|
385 |
+
primary_audio_to_play = final_audio_output_path
|
386 |
+
downloadable_file = None
|
387 |
+
|
388 |
+
if final_audio_output_path: # Merged or single file
|
389 |
+
downloadable_file = final_audio_output_path
|
390 |
+
elif zip_file_output_path: # Zip of multiple parts
|
391 |
+
downloadable_file = zip_file_output_path
|
392 |
+
# If we have a zip, maybe don't auto-play anything or play the first part?
|
393 |
+
# For now, let's not auto-play if only zip is available.
|
394 |
+
# primary_audio_to_play = None
|
395 |
+
if generated_files: # Still offer first part for playing
|
396 |
+
primary_audio_to_play = generated_files[0]
|
397 |
+
|
398 |
+
|
399 |
+
if not primary_audio_to_play and not downloadable_file:
|
400 |
+
log_messages.append("🛑 هیچ خروجی صوتی یا فایل دانلودی برای ارائه وجود ندارد.")
|
401 |
+
|
402 |
+
# Cleanup non-primary generated files if delete_partial_files is True AND merge was successful
|
403 |
+
# This part is tricky because `generated_files` might be what `downloadable_file` or `primary_audio_to_play` points to.
|
404 |
+
# The `merge_audio_files_func` already handles deletion if `delete_partial_files` is true AND merge is successful.
|
405 |
+
# If not merging, and delete_partial_files is true, it means we shouldn't delete anything as they are the final product (in a zip).
|
406 |
+
# So, the logic inside merge_audio_files_func for deletion is probably sufficient.
|
407 |
+
|
408 |
+
return primary_audio_to_play, downloadable_file, "\n".join(log_messages)
|
409 |
+
|
410 |
+
|
411 |
+
# --- Gradio Interface ---
|
412 |
css = """
|
413 |
+
body { font-family: 'Tahoma', sans-serif; }
|
|
|
|
|
|
|
|
|
414 |
.gradio-container { max-width: 800px !important; margin: auto !important; }
|
415 |
+
footer { display: none !important; }
|
416 |
+
.gr-button { background-color: #4CAF50 !important; color: white !important; }
|
417 |
+
.gr-button:hover { background-color: #45a049 !important; }
|
418 |
"""
|
419 |
+
|
420 |
+
# استخراج نام گویندگان از کد شما
|
421 |
+
speaker_choices = ["Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"]
|
422 |
+
model_choices = ["gemini-1.5-flash-latest", "gemini-1.5-pro-latest"] # Updated model names
|
423 |
+
|
424 |
+
# لوگوی Base64 (اختیاری)
|
425 |
+
aigolden_logo_encoded = "Q3JlYXRlIGJ5IDogYWlnb2xkZW4="
|
426 |
+
aigolden_logo_decoded = base64.b64decode(aigolden_logo_encoded.encode()).decode()
|
427 |
+
|
428 |
+
|
429 |
+
with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="green", secondary_hue="lightGreen")) as demo:
|
430 |
+
gr.Markdown(f"<h1 style='text-align: center; color: #2E7D32;'>تبدیل متن به گفتار با Gemini گوگل</h1>")
|
431 |
+
gr.Markdown(f"<p style='text-align: center; color: gray;'>{aigolden_logo_decoded}</p>")
|
432 |
+
gr.Markdown("⚠️ **توجه:** برای استفاده از این ابزار، نیاز به یک کلید API از Google AI Studio دارید. لطفاً کلید خود را در بخش `Settings` -> `Secrets` این Space با نام `GEMINI_API_KEY` ذخیره کنید.")
|
433 |
+
|
434 |
with gr.Row():
|
435 |
with gr.Column(scale=2):
|
436 |
+
gr.Markdown("### ⚙️ تنظیمات اصلی")
|
437 |
+
use_file_input_cb = gr.Checkbox(label="استفاده از فایل متنی ورودی (.txt)", value=False, info="اگر فعال شود، متن از فایل آپلود شده خوانده میشود.")
|
438 |
+
|
439 |
+
# نمایش شرطی برای آپلود فایل یا ورود متن دستی
|
440 |
+
text_file_upload = gr.File(label="آپلو�� فایل متنی (.txt)", file_types=[".txt"], visible=False)
|
441 |
+
text_to_speak_input = gr.Textbox(lines=7, label="متن برای تبدیل به گفتار (اگر فایل آپلود نشده باشد)", placeholder="متن خود را اینجا وارد کنید...", visible=True)
|
442 |
+
|
443 |
+
def toggle_input_method(use_file):
|
444 |
+
return {
|
445 |
+
text_file_upload: gr.update(visible=use_file),
|
446 |
+
text_to_speak_input: gr.update(visible=not use_file)
|
447 |
+
}
|
448 |
+
use_file_input_cb.change(toggle_input_method, inputs=use_file_input_cb, outputs=[text_file_upload, text_to_speak_input])
|
449 |
+
|
450 |
+
speech_prompt_input = gr.Textbox(label="پرامپت راهنمای سبک گفتار (اختیاری)", placeholder="مثال: از زبان یک یوتوبر پر انرژی و حرفه ای", info="این پرامپت به مدل کمک میکند تا سبک و لحن گفتار را تنظیم کند.")
|
451 |
+
output_filename_base_input = gr.Textbox(label="نام پایه فایل خروجی (بدون پسوند)", value="gemini_tts_output", info="برای نامگذاری فایلهای صوتی تولید شده استفاده میشود.")
|
452 |
+
|
453 |
+
gr.Markdown("### 🗣️ تنظیمات مدل و گوینده")
|
454 |
+
model_name_dd = gr.Dropdown(model_choices, label="انتخاب مدل Gemini", value=model_choices[0], info="مدلهای جدیدتر ممکن است کیفیت بهتری داشته باشند.")
|
455 |
+
speaker_voice_dd = gr.Dropdown(speaker_choices, label="انتخاب گوینده", value="Charon", info="گوینده مورد نظر را برای تولید صدا انتخاب کنید.")
|
456 |
+
temperature_slider = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, value=0.7, label="دمای مدل (Temperature)", info="مقادیر بالاتر منجر به خروجی خلاقانهتر و متنوعتر میشود، مقادیر پایینتر خروجی قابل پیشبینیتری دارد.")
|
457 |
+
|
458 |
with gr.Column(scale=1):
|
459 |
+
gr.Markdown("### 分割 و خروجی") # Corrected title
|
460 |
+
max_chunk_size_slider = gr.Slider(minimum=2000, maximum=4000, step=100, value=3800, label="حداکثر کاراکتر در هر قطعه", info="متنهای طولانی به قطعات کوچکتر تقسیم میشوند.")
|
461 |
+
sleep_between_requests_slider = gr.Slider(minimum=5, maximum=20, step=0.5, value=12, label="فاصله زمانی بین درخواستها (ثانیه)", info="برای جلوگیری از خطاهای مربوط به محدودیت تعداد درخواست به API.")
|
462 |
+
|
463 |
+
merge_audio_files_cb = gr.Checkbox(label="ادغام فایلهای صوتی جزئی", value=True, info="اگر متن طولانی باشد و به چند بخش تقسیم شود، فایلهای صوتی این بخشها ادغام میشوند.")
|
464 |
+
delete_partial_files_cb = gr.Checkbox(label="حذف فایلهای جزئی پس از ادغام (در صورت انتخاب ادغام)", value=False, info="فقط در صورتی اعمال میشود که ادغام فایلها فعال باشد.")
|
465 |
+
|
466 |
+
submit_button = gr.Button("🎧 تبدیل متن به گفتار 🎧", variant="primary")
|
467 |
+
|
468 |
+
gr.Markdown("### 🔊 خروجی و گزارش")
|
469 |
+
audio_output = gr.Audio(label="فایل صوتی نهایی", type="filepath")
|
470 |
+
file_download_output = gr.File(label="دانلود فایل (ادغام شده یا ZIP)", type="filepath")
|
471 |
+
status_output = gr.Textbox(label="وضعیت و گزارشها", lines=10, interactive=False)
|
472 |
+
|
473 |
+
# اتصال دکمه به تابع اصلی
|
474 |
+
submit_button.click(
|
475 |
+
generate_audio_gradio,
|
476 |
+
inputs=[
|
477 |
+
use_file_input_cb, text_file_upload, text_to_speak_input, speech_prompt_input,
|
478 |
+
speaker_voice_dd, output_filename_base_input, model_name_dd, temperature_slider,
|
479 |
+
max_chunk_size_slider, sleep_between_requests_slider, merge_audio_files_cb, delete_partial_files_cb
|
480 |
+
],
|
481 |
+
outputs=[audio_output, file_download_output, status_output]
|
482 |
+
)
|
483 |
+
|
484 |
+
gr.Markdown("---")
|
485 |
+
gr.Markdown("ℹ️ **نکات مهم:**")
|
486 |
+
gr.Markdown("- **کلید API:** مطمئن شوید که کلید API جمینای شما معتبر است و در بخش Secrets این Space به درستی تنظیم شده است.")
|
487 |
+
gr.Markdown("- **محدودیتهای API:** گوگل ممکن است محدودیتهایی برای تعداد درخواستها در دقیقه اعمال کند. اگر با خطا مواجه شدید، کمی صبر کنید و دوباره تلاش کنید یا فاصله زمانی بین درخواستها را افزایش دهید.")
|
488 |
+
gr.Markdown("- **فایلهای حجیم:** پردازش متنهای بسیار طولانی ممکن است زمانبر باشد و به دلیل محدودیتهای منابع در Spaces با مشکل مواجه شود.")
|
489 |
+
gr.Markdown("- **پاکسازی:** فایلهای تولید شده در هر اجرا در پوشه `outputs` در سرور ذخیره میشوند. برای جلوگیری از پر شدن فضا، بهتر است فایلها را پس از دانلود، از بخش Files and versions اسپیس خود مدیریت کنید یا از قابلیت حذف فایلهای جزئی استفاده کنید.")
|
490 |
+
|
491 |
+
|
492 |
+
if __name__ == "__main__":
|
493 |
+
# ایجاد پوشه outputs اگر وجود ندارد (برای اجرای محلی)
|
494 |
+
if not os.path.exists("outputs"):
|
495 |
+
os.makedirs("outputs")
|
496 |
+
demo.launch(debug=True) # share=True برای اشتراک گذاری لینک عمومی در اجرای محلی
|