File size: 10,663 Bytes
a8d4280
 
 
 
 
 
 
 
 
df9cbef
a8d4280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# app.py
import os
import mimetypes
import tempfile
from dotenv import load_dotenv
import requests
import gradio as gr

load_dotenv()
API_URL = os.getenv("ALAVAN_TTS_API", "https://service.alavan.co.ir/api/v1/Model/TTS")
API_TOKEN = os.getenv("ALAVAN_API_TOKEN")

VALID_SPEAKERS = ["nima", "ahmad", "raha", "setare"]
RECOMMENDED = ["ahmad", "setare"]


def download_audio_from_url(url):
    try:
        with requests.get(url, stream=True, timeout=30) as r:
            r.raise_for_status()
            content_type = r.headers.get("Content-Type", "")
            ext = None
            if content_type:
                ext = mimetypes.guess_extension(content_type.split(";")[0].strip())
            if not ext:
                parsed_ext = os.path.splitext(url.split("?")[0])[1]
                ext = parsed_ext or ".wav"

            tmp_fd, tmp_path = tempfile.mkstemp(suffix=ext)
            os.close(tmp_fd)
            with open(tmp_path, "wb") as f:
                for chunk in r.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
            return tmp_path
    except Exception:
        return None


def extract_audio_url_from_json(j):
    if not isinstance(j, dict):
        return None
    data = j.get("data")
    if isinstance(data, dict):
        for key in ("audioFileUrl", "audio_file_url", "audio_url", "file", "url", "result_url"):
            if key in data and isinstance(data[key], str) and data[key].strip():
                return data[key].strip()
    for key in ("audioFileUrl", "audio_file_url", "audio_url", "file", "url", "result_url"):
        if key in j and isinstance(j[key], str) and j[key].strip():
            return j[key].strip()
    return None


def pretty_server_error(resp):
    try:
        data = resp.json()
    except Exception:
        return f"کد {resp.status_code} — پاسخ غیرقابل‌خواندن"
    parts = []
    if isinstance(data, dict):
        if "message" in data:
            parts.append(str(data["message"]))
        errors = data.get("errors") or data.get("error") or {}
        if isinstance(errors, dict):
            for k, v in errors.items():
                if isinstance(v, list):
                    parts.append(f"{k}: {', '.join(map(str, v))}")
                else:
                    parts.append(f"{k}: {v}")
        elif errors:
            parts.append(str(errors))
        return " — ".join(parts) if parts else f"کد {resp.status_code}"
    return f"کد {resp.status_code}"


def tts_call_and_fetch(text, speaker_input):
    if not API_TOKEN or not API_URL:
        return "❌ توکن یا آدرس API تنظیم نشده. لطفاً فایل .env یا Secrets را بررسی کنید.", None

    if not text or not text.strip():
        return "❌ متن ورودی خالی است.", None

    speaker = (speaker_input or "").strip().lower()
    if speaker not in VALID_SPEAKERS:
        return (
            "❌ گوینده نامعتبر است. گوینده‌های معتبر:\n"
            f"{', '.join(VALID_SPEAKERS)}\n"
            f"گویندگان پیشنهادی: {', '.join(RECOMMENDED)}"
        ), None

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {API_TOKEN}"
    }

    payload = {
        "text": text,
        "modelType": speaker
    }

    try:
        resp = requests.post(API_URL, headers=headers, json=payload, timeout=30)
    except Exception as e:
        return f"❌ خطا در اتصال به API: {e}", None
    resp_json = None
    try:
        resp_json = resp.json()
    except Exception:
        resp_json = None
    data_json = None
    if resp.status_code in (200, 201) or isinstance(resp_json, dict):
        data_json = resp_json or {}
    else:
        pretty = pretty_server_error(resp)
        return f"❌ خطا از سرور API: {pretty}", None

    audio_url = extract_audio_url_from_json(data_json)
    if not audio_url:
        if isinstance(data_json.get("data"), str) and data_json.get("data").startswith("http"):
            audio_url = data_json.get("data")
    if not audio_url:
        return "❌ پاسخ API شامل آدرس فایل صوتی نبود.", None

    audio_path = download_audio_from_url(audio_url)
    if not audio_path:
        return "❌ خطا در دانلود فایل صوتی از URL دریافتی.", None

    return "✅ تولید و دانلود صدا با موفقیت انجام شد.", audio_path


with gr.Blocks(analytics_enabled=False, css="""
body { font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
.header {
  text-align: center;
  padding: 14px 18px;
  border-radius: 10px;
  background: linear-gradient(rgb(255 114 114), rgb(251, 251, 255));
  box-shadow: 0 6px 18px rgba(16,24,40,0.04);
  margin-bottom: 18px;
}
.header h2 { margin: 0 0 6px 0; color:white;}
.header p { margin: 0; color: #6b7280; }

.card {
  padding: 12px;
  border-radius: 10px;
  background: linear-gradient(180deg, #ffffff, #fcfcff);
  box-shadow: 0 6px 18px rgba(16,24,40,0.04);
}

.badge {
  display:inline-block;
  padding:6px 10px;
  border-radius:999px;
  font-size:13px;
  margin:3px 6px 3px 0;
  background:#f1f5f9;
  color:#111827;
}

.footer {
  text-align:center;
  margin-top:18px;
  color:#6b7280;
  font-size:13px;
}
""") as demo:
    with gr.Row():
        with gr.Column(scale=3):
            gr.Markdown(
                "<div class='header'>"
                "<h2>🎤 Alavan TTS — تبدیل متن به گفتار (فارسی)</h2>"
                "<p>مدل تبدیل نوشتار به گفتار آلاوان با دریافت متون فارسی، آن ها را به خروجی صوتی با صدایی واضح، قابل فهم و نزدیک به گفتار انسانی تبدیل می کند. این مدل به گونه ای طراحی شده است که لحن و آهنگ طبیعی گفتار حفظ شود و برای شنونده تجربه ای روان و دلنشین فراهم گردد. کاربرد اصلی این مدل در تولید پادکست، کتاب صوتی، راهنمای صوتی اپلیکیشن ها، سامانه های پاسخگویی خودکار و دستیارهای صوتی است.</p>"
                "</div>"
                , rtl=True
            )
        with gr.Column(scale=1, min_width=220):
            gr.HTML(
                "<div style='text-align:right;align-item:right;'>"
                "<img src='https://alavan.co.ir/wp-content/uploads/2024/12/NEW-LOGO-ALAVAN.png' alt='تبدیل متن به صوت آلاوان' width='50%' height='50%' style='margin-left:10rem;margin-bottom:20px;'/>"
                "<a href='https://alavan.co.ir' target='_blank'>🌐 وب ‌سایت آلاوان</a><br>"
                "<a href='https://portal.alavan.co.ir' target='_blank'>🔑 پرتال کاربران</a>"
                "</div>"
            )

    gr.Markdown("---")
    with gr.Row():
        # left column: inputs + results
        with gr.Column(scale=2):
            with gr.Row():
                with gr.Column():
                    gr.Markdown("### تنظیمات و تولید صدا", rtl=True)
                    # کارت ورودی
                    gr.HTML("<div class='card'>")
                    text_in = gr.Textbox(label="متن ورودی", placeholder="متن فارسی یا انگلیسی... (حداقل یک جمله)",
                                         lines=6, rtl=True)
                    speaker = gr.Dropdown(choices=VALID_SPEAKERS, label="گوینده", value="setare")
                    with gr.Row():
                        btn = gr.Button("🔊 تولید صدا", variant="primary")
                        clear_btn = gr.Button("🧹 پاک‌کردن ورودی")
                    status = gr.Textbox(label="وضعیت", interactive=False, rtl=True)
                    audio_out = gr.Audio(label="خروجی صوت", type="filepath")
                    gr.HTML("</div>")
                    gr.Markdown("#### نمونه‌های آماده", rtl=True)
                    examples = [
                        ["سلام! امروز حال شما چطور است؟", "nima"],
                        ["این یک نمونهٔ تبدیل متن به گفتار فارسی است.", "setare"],
                        ["لطفاً صدای آزمایشی را با گوینده احمد تولید کن.", "ahmad"],
                    ]
                    gr.Examples(examples=examples, inputs=[text_in, speaker], label="کلیک کن تا فیلدها پر شوند")

        with gr.Column(scale=1):
            gr.HTML("<div class='card'>")
            gr.Markdown("### گوینده‌ها", rtl=True)
            # badges گوینده‌ها
            badges_html = " ".join([f"<span class='badge'>{s}</span>" for s in VALID_SPEAKERS])
            gr.HTML(badges_html)
            gr.Markdown("---")
            gr.Markdown(
                "**نکات مهم:**\n\n"
                "- فایل صوتی بلافاصله دانلود شده و فقط نسخهٔ محلی پخش می‌شود\n"
                "- برای استفاده در اپ های خود وارد سایت آلاوان شوید\n"
                "- در صورت دریافت پیام سرور با متن خطا، پیام خلاصه‌شده به شما نمایش داده خواهد شد.\n"
                "- خیلی از مدل های تبدیل متن به صوت فارسی تاریخ هارو ساپورت نمیکنند ولی مدل ستاره ما از تمامای واژگان اعداد و تاریخ های فارسی پشتیبانی میکند\n"
                "- برای تغیر نوع خوانش از علائم نگارشی و , برای مکس استفاده کنید.\n"
                , rtl=True
            )
            gr.Markdown("---")
            gr.Markdown("### گویندگان پیشنهادی", rtl=True)
            rec_html = " ".join([f"<span class='badge'>{s}</span>" for s in RECOMMENDED])
            gr.HTML(rec_html)
            gr.HTML("</div>")
    gr.HTML("<div class='footer'>ساخته شده با ❤️ توسط تیم <a href=https://alavan.co.ir/>آلاوان</a></div>")
    clear_btn.click(lambda: ("", "setare"), inputs=None, outputs=[text_in, speaker], api_name=False,
                    show_api=False)

    btn.click(fn=tts_call_and_fetch, inputs=[text_in, speaker], outputs=[status, audio_out], api_name=False,
              show_api=False)

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860, show_api=False)