Decho public Space: Persian text-to-speech demo by Dibachain (HF sign-in, three models, dialogue, podcast, pronunciation dictionary)
Browse files- .gitattributes +2 -0
- Dockerfile +10 -0
- README.md +157 -6
- app.py +477 -0
- requirements.txt +12 -0
- static/index.html +633 -0
- static/samples/dialogue.mp3 +3 -0
- static/samples/realtime.mp3 +0 -0
- static/samples/voice_ali.mp3 +0 -0
- static/samples/voice_default.mp3 +3 -0
- static/samples/voice_maryam.mp3 +0 -0
- static/samples/voice_reza.mp3 +0 -0
- static/samples/voice_sara.mp3 +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/samples/dialogue.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
static/samples/voice_default.mp3 filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
RUN apt-get update && apt-get install -y --no-install-recommends libsndfile1 ffmpeg && rm -rf /var/lib/apt/lists/*
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
USER user
|
| 5 |
+
ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH HF_HUB_DISABLE_PROGRESS_BARS=1 PYTHONUNBUFFERED=1
|
| 6 |
+
WORKDIR /home/user/app
|
| 7 |
+
COPY --chown=user requirements.txt .
|
| 8 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
COPY --chown=user . .
|
| 10 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "*"]
|
README.md
CHANGED
|
@@ -1,10 +1,161 @@
|
|
| 1 |
---
|
| 2 |
-
title: Decho
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Decho · Persian Text-to-Speech · دکو
|
| 3 |
+
emoji: 🎙️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
+
license: other
|
| 10 |
+
hf_oauth: true
|
| 11 |
+
hf_oauth_scopes:
|
| 12 |
+
- inference-api
|
| 13 |
+
hf_oauth_expiration_minutes: 480
|
| 14 |
+
short_description: Natural Persian text-to-speech, real-time on a CPU
|
| 15 |
+
tags:
|
| 16 |
+
- text-to-speech
|
| 17 |
+
- tts
|
| 18 |
+
- persian
|
| 19 |
+
- farsi
|
| 20 |
+
- persian-tts
|
| 21 |
+
- farsi-tts
|
| 22 |
+
- speech-synthesis
|
| 23 |
+
- real-time
|
| 24 |
+
- cpu
|
| 25 |
+
- onnx
|
| 26 |
+
- iranian
|
| 27 |
+
- dibachain
|
| 28 |
+
- decho
|
| 29 |
---
|
| 30 |
|
| 31 |
+
<div align="center">
|
| 32 |
+
|
| 33 |
+
<img src="https://cdn-avatars.huggingface.co/v1/production/uploads/64cb9d9186d8dc0caa4d4417/RjvRkGh5EYvG_mGUK4Uek.png" width="96" alt="Dibachain logo · لوگوی دیباچین">
|
| 34 |
+
|
| 35 |
+
# Decho · دکو — Persian Text-to-Speech · تبدیل متن به گفتار فارسی
|
| 36 |
+
|
| 37 |
+
**Natural Persian (Farsi) speech on an ordinary CPU, even in real time — by [Dibachain](https://dibachain.ir)**
|
| 38 |
+
**گفتار طبیعی فارسی روی پردازندهی معمولی، حتی بلادرنگ — ساختهی [دیباچین](https://dibachain.ir)**
|
| 39 |
+
|
| 40 |
+
[🌐 dibachain.ir](https://dibachain.ir) · [✉️ info@dibachain.ir](mailto:info@dibachain.ir) · [🤗 Dibachain on Hugging Face](https://huggingface.co/Dibachain) · [🤖 Diba Agent](https://dibaai-diba-agent.hf.space)
|
| 41 |
+
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## English
|
| 47 |
+
|
| 48 |
+
**Decho** is Dibachain's **Persian text-to-speech (TTS)** model. It turns Persian text into natural speech on an ordinary
|
| 49 |
+
CPU — no GPU — and its real-time model generates speech **faster than playback**. The voice speaks **Persian only**; this
|
| 50 |
+
page is in English and Persian.
|
| 51 |
+
|
| 52 |
+
### What you can do here
|
| 53 |
+
- **Three models:** ⚡ **Real-time** (Decho-v2-rt, faster than playback on a CPU), **Standard** (Decho-v2 int8) and
|
| 54 |
+
🎧 **Studio** (Decho-v2 fp32, the best quality and the steadiest voice).
|
| 55 |
+
- **Five built-in voices**, male and female, different ages.
|
| 56 |
+
- **Single voice, dialogue and podcast modes.** Switch speakers mid-text with `[سارا]` / `[رضا]` or one «name: text» line
|
| 57 |
+
per turn. The podcast mode can **write the whole script with AI** from a topic or an article.
|
| 58 |
+
- **Correct Persian reading:** numbers, dates, times, money and percentages («۱۵ مهر ۱۴۰۳» → «پانزدهمِ مهرِ هزار و چهارصد و
|
| 59 |
+
سه»), ezafe and homographs from the whole sentence, colloquial Persian, and English words and brands — even written in
|
| 60 |
+
Latin letters (Wi-Fi, ChatGPT, iPhone, PDF).
|
| 61 |
+
- **Emotion:** laughter («هاهاها», 😂), sighs (😮💨), crying («هقهق»), surprise, elongated words («خیلییییی»), and the
|
| 62 |
+
intonation of «!», «؟!» and «…». Pauses with `[pause:1.2]` and `[breath]`.
|
| 63 |
+
- **Smart reading (optional):** a language model tidies the spelling and writes the exact pronunciation of every sentence,
|
| 64 |
+
in a **formal** or a **casual** (polished spoken Tehrani) register.
|
| 65 |
+
- **Personal pronunciation dictionary**, with AI suggestions and an audition button; stored only in your browser.
|
| 66 |
+
- **Live reply** (short first chunk), gapless playback, **WAV and MP3 download**, and a per-chunk view of the reading.
|
| 67 |
+
|
| 68 |
+
### Sign in with Hugging Face
|
| 69 |
+
Without signing in you can listen to the samples and try short sentences. **Sign in with your Hugging Face account** for
|
| 70 |
+
long texts, dialogue, podcasts, the Studio model and MP3 download. The AI features (smart reading, the podcast script
|
| 71 |
+
writer and pronunciation suggestions) run on **your own Hugging Face account** through Hugging Face Inference Providers, so
|
| 72 |
+
they use your account's inference credit. Speech synthesis itself runs on this Space's CPU and is free.
|
| 73 |
+
|
| 74 |
+
### Measured quality (40 fixed Persian sentences)
|
| 75 |
+
| model | character error (CER) ↓ | naturalness (UTMOS) ↑ | CPU real-time factor ↓ |
|
| 76 |
+
|---|---|---|---|
|
| 77 |
+
| Decho-v2 · Standard (int8, 8 steps) | 4.2% | 3.89 | 1.1–1.8 |
|
| 78 |
+
| Decho-v2-rt · Real-time | 6.7% | 3.38 | **0.7–0.9** (real time) |
|
| 79 |
+
| Decho-v2 · Studio (fp32) | 3.5% | 3.96 | about 2.5 |
|
| 80 |
+
|
| 81 |
+
CER by Whisper-large-v3 on the generated audio; UTMOS is an automatic naturalness score for relative comparison only.
|
| 82 |
+
Real-time factor = synthesis time ÷ audio length on a 2-core machine (below 1 = faster than playback). Studio quality was
|
| 83 |
+
measured at 8 steps on the same 40 sentences; the Studio setting uses 16 steps, which kept the voice steadiest across
|
| 84 |
+
emotional sentences in our tests.
|
| 85 |
+
|
| 86 |
+
### Commercial use, API and custom voices
|
| 87 |
+
The Decho models are **proprietary** (© Dibachain). For an API, on-premise deployment, a custom voice or a partnership,
|
| 88 |
+
write to **[info@dibachain.ir](mailto:info@dibachain.ir)** or visit **[dibachain.ir](https://dibachain.ir)**.
|
| 89 |
+
|
| 90 |
+
### Responsible use
|
| 91 |
+
Always disclose that generated speech is synthetic. Using it for fraud, impersonation, harassment, political deception or
|
| 92 |
+
any unlawful purpose is prohibited. Voice cloning is not part of this public demo.
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
<div dir="rtl">
|
| 97 |
+
|
| 98 |
+
## فارسی
|
| 99 |
+
|
| 100 |
+
**دکو** مدل **تبدیل متن به گفتار فارسی** (TTS) ساختهی دیباچین است. متن فارسی را روی پردازندهی معمولی، بدون کارت گرافیک،
|
| 101 |
+
به گفتار طبیعی تبدیل میکند و مدل ریلتایم آن صدا را **سریعتر از پخش** میسازد. صدای دکو **فقط فارسی** صحبت میکند؛
|
| 102 |
+
توضیحات این صفحه فارسی و انگلیسی است.
|
| 103 |
+
|
| 104 |
+
### اینجا چه میتوانید بکنید
|
| 105 |
+
- **سه مدل:** ⚡ **ریلتایم** (Decho-v2-rt، سریعتر از پخش روی CPU)، **استاندارد** (Decho-v2 int8) و 🎧 **استودیو**
|
| 106 |
+
(Decho-v2 fp32، بهترین کیفیت و پایدارترین صدا).
|
| 107 |
+
- **پنج صدای آماده**، زن و مرد، در سنهای مختلف.
|
| 108 |
+
- **حالت یک گوینده، گفتوگو و پادکست.** با `[سارا]` و `[رضا]` وسط متن گوینده عوض کنید یا هر نوبت را یک خط «نام: متن»
|
| 109 |
+
بنویسید. حالت پادکست میتواند **کل متن پادکست را با هوش مصنوعی** از یک موضوع یا مقاله بنویسد.
|
| 110 |
+
- **خوانش درست فارسی:** عدد، تاریخ، ساعت، پول و درصد («۱۵ مهر ۱۴۰۳» ← «پانزدهمِ مهرِ هزار و چهارصد و سه»)، کسرهی اضافه و
|
| 111 |
+
همنگارهها با درک کل جمله، فارسی محاوره، و واژهها و برندهای انگلیسی، حتی با حروف لاتین (Wi-Fi، ChatGPT، iPhone، PDF).
|
| 112 |
+
- **احساس:** خنده («هاهاها»، 😂)، آه (😮💨)، گریه («هقهق»)، تعجب، کشیدن واژهها («خیلییییی») و آهنگ «!»، «؟!» و «…».
|
| 113 |
+
مکث با `[pause:1.2]` و نفس با `[breath]`.
|
| 114 |
+
- **خوانش هوشمند (اختیاری):** یک مدل زبانی املا را مرتب میکند و تلفظ دقیق هر جمله را مینویسد؛ با لحن **رسمی** یا
|
| 115 |
+
**خودمانی** (محاورهی حرفهای تهرانی).
|
| 116 |
+
- **دیکشنری تلفظ شخصی** با پیشنهاد هوش مصنوعی و دکمهی شنیدن؛ فقط در مرورگر خودتان ذخیره میشود.
|
| 117 |
+
- **پاسخ زنده** (قطعهی اول کوتاه)، پخش پیوسته، **دانلود WAV و MP3** و نمایش خوانش هر قطعه.
|
| 118 |
+
|
| 119 |
+
### ورود با Hugging Face
|
| 120 |
+
بدون ورود میتوانید نمونهها را بشنوید و جملههای کوتاه را امتحان کنید. با **حساب Hugging Face خودتان وارد شوید** تا متن بلند،
|
| 121 |
+
گفتوگو، پادکست، مدل استودیو و دانلود MP3 در دسترس باشد. قابلیتهای هوش مصنوعی (خوانش هوشمند، نویسندهی متن پادکست و
|
| 122 |
+
پیشنهاد تلفظ) با **حساب Hugging Face خود شما** و از طریق Hugging Face Inference Providers اجرا میشوند و از اعتبار Inference
|
| 123 |
+
حساب شما استفاده میکنند. خود ساخت گفتار روی پردازندهی همین Space انجام میشود و رایگان است.
|
| 124 |
+
|
| 125 |
+
### کیفیت اندازهگیریشده (۴۰ جملهی ثابت فارسی)
|
| 126 |
+
| مدل | خطای نویسه (CER) ↓ | طبیعیبودن (UTMOS) ↑ | نسبت سرعت روی CPU ↓ |
|
| 127 |
+
|---|---|---|---|
|
| 128 |
+
| Decho-v2 · استاندارد (int8، ۸ گام) | ۴٫۲٪ | ۳٫۸۹ | ۱٫۱ تا ۱٫۸ |
|
| 129 |
+
| Decho-v2-rt · ریلتایم | ۶٫۷٪ | ۳٫۳۸ | **۰٫۷ تا ۰٫۹** (بلادرنگ) |
|
| 130 |
+
| Decho-v2 · استودیو (fp32) | ۳٫۵٪ | ۳٫۹۶ | حدود ۲٫۵ |
|
| 131 |
+
|
| 132 |
+
### استفادهی تجاری، API و صدای اختصاصی
|
| 133 |
+
مدلهای دکو **اختصاصی** هستند (© دیباچین). برای API، اجرای روی سرور خودتان، صدای اختصاصی یا همکاری به
|
| 134 |
+
**[info@dibachain.ir](mailto:info@dibachain.ir)** پیام بدهید یا به **[dibachain.ir](https://dibachain.ir)** سر بزنید.
|
| 135 |
+
|
| 136 |
+
### استفادهی مسئولانه
|
| 137 |
+
همیشه اعلام کنید که صدا ساختگی است. استفاده برای کلاهبرداری، جعل هویت، آزار، فریب سیاسی یا هر کار غیرقانونی ممنوع است.
|
| 138 |
+
شبیهسازی صدا (کلون) در این نسخهی عمومی وجود ندارد.
|
| 139 |
+
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
<div align="center">
|
| 145 |
+
|
| 146 |
+
**About Dibachain · دربارهی دیباچین**
|
| 147 |
+
|
| 148 |
+
Dibachain is an Iranian AI company building localized, Persian-first artificial intelligence: the **Diba** family of
|
| 149 |
+
Iranian language models ([Diba-Base](https://huggingface.co/Dibachain/Diba-Base), [Diba-mini](https://huggingface.co/Dibachain/Diba-mini),
|
| 150 |
+
[Diba-Embed](https://huggingface.co/Dibachain/Diba-Embed), [Diba-Vision](https://huggingface.co/Dibachain/Diba-Vision),
|
| 151 |
+
[Diba-STT](https://huggingface.co/Dibachain/Diba-STT), [DibaOne X1](https://huggingface.co/Dibachain/DibaOne-X1)) and **Decho**
|
| 152 |
+
for Persian speech.
|
| 153 |
+
|
| 154 |
+
دیباچین شرکت هوش مصنوعی ایرانی برای بومیسازی هوش مصنوعی به زبان فارسی است: خانوادهی مدلهای زبانی ایرانیِ **دیبا** و **دکو**
|
| 155 |
+
برای گفتار فارسی.
|
| 156 |
+
|
| 157 |
+
[🌐 dibachain.ir](https://dibachain.ir) · [✉️ info@dibachain.ir](mailto:info@dibachain.ir) · [🤖 Diba Agent](https://dibaai-diba-agent.hf.space) · [🎙️ Persian speech-to-text](https://huggingface.co/spaces/Dibachain/diba-stt) · [🧭 DibaOne X1](https://huggingface.co/spaces/Dibachain/DibaOne-X1-demo)
|
| 158 |
+
|
| 159 |
+
Persian TTS · Farsi text to speech · Iranian AI · تبدیل متن به گفتار فارسی · تبدیل متن به صدا · هوش مصنوعی ایرانی · دیباچین
|
| 160 |
+
|
| 161 |
+
</div>
|
app.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Decho — public demo Space of Dibachain's Persian text-to-speech (CPU).
|
| 2 |
+
|
| 3 |
+
* Models (private repos, read with the Space secret HF_TOKEN; never exposed to visitors):
|
| 4 |
+
rt DibaAi/Decho-v2-rt int8 - real-time distilled model (no guidance, 8 steps)
|
| 5 |
+
int8 DibaAi/Decho-v2 int8 - standard
|
| 6 |
+
fp32 DibaAi/Decho-v2 fp32 - studio (16 steps)
|
| 7 |
+
* Sign in with Hugging Face (OAuth, scope inference-api): the language-model features (smart reading, podcast script
|
| 8 |
+
writer, pronunciation suggestions) run on the signed-in user's own Hugging Face account / Inference Providers credit.
|
| 9 |
+
Without sign-in: samples + short single-voice tries only.
|
| 10 |
+
* No voice cloning in this public version (add_voice is not exposed; the speaker encoder is not even downloaded).
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import base64
|
| 15 |
+
import hashlib
|
| 16 |
+
import io
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
import re
|
| 20 |
+
import subprocess
|
| 21 |
+
import sys
|
| 22 |
+
import tempfile
|
| 23 |
+
import threading
|
| 24 |
+
import time
|
| 25 |
+
import urllib.error
|
| 26 |
+
import urllib.request
|
| 27 |
+
import uuid
|
| 28 |
+
from collections import OrderedDict, defaultdict, deque
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
import soundfile as sf
|
| 32 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 33 |
+
from fastapi.responses import FileResponse, JSONResponse, Response
|
| 34 |
+
from fastapi.staticfiles import StaticFiles
|
| 35 |
+
from huggingface_hub import attach_huggingface_oauth, parse_huggingface_oauth, snapshot_download
|
| 36 |
+
from pydantic import BaseModel
|
| 37 |
+
|
| 38 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 39 |
+
TOKEN = os.environ.get("HF_TOKEN") # Space secret: read access to the private model repos
|
| 40 |
+
THREADS = int(os.environ.get("THREADS", "2"))
|
| 41 |
+
MAIN_REPO = os.environ.get("MAIN_REPO", "DibaAi/Decho-v2")
|
| 42 |
+
RT_REPO = os.environ.get("RT_REPO", "DibaAi/Decho-v2-rt")
|
| 43 |
+
MAIN_DIR = os.path.expanduser("~/model_main")
|
| 44 |
+
RT_DIR = os.path.expanduser("~/model_rt")
|
| 45 |
+
LLM_MODEL = os.environ.get("LLM_MODEL", "deepseek-ai/DeepSeek-V4.1-Flash")
|
| 46 |
+
ROUTER = "https://router.huggingface.co/v1/chat/completions"
|
| 47 |
+
BASE_FILES = ["config.json", "tokenizer.json", "decho/**", "g2p/**", "voices/**", "onnx/*.onnx", "onnx/*.npy"]
|
| 48 |
+
|
| 49 |
+
# limits: anonymous visitors get a short try, signed-in users the full studio
|
| 50 |
+
LIMITS = {"anon": {"chars": 220, "chars_per_hour": 2000, "streams": 1},
|
| 51 |
+
"user": {"chars": 4000, "chars_per_hour": 40000, "streams": 2}}
|
| 52 |
+
MODELS = {"rt": {"steps": 8, "cfg": None}, "int8": {"steps": 8, "cfg": 3.0}, "fp32": {"steps": 16, "cfg": 3.0}}
|
| 53 |
+
|
| 54 |
+
state = {"ready": False, "error": None, "models": {}, "loading": {}, "started": time.time()}
|
| 55 |
+
_load_lock = threading.Lock()
|
| 56 |
+
_synth_lock = threading.Lock()
|
| 57 |
+
_waiting = [0]
|
| 58 |
+
_waiting_lock = threading.Lock()
|
| 59 |
+
|
| 60 |
+
app = FastAPI(title="Decho — Persian text-to-speech", docs_url=None, redoc_url=None)
|
| 61 |
+
if os.environ.get("OAUTH_CLIENT_ID"):
|
| 62 |
+
attach_huggingface_oauth(app)
|
| 63 |
+
else: # local run without OAuth: the app still works, signed-out
|
| 64 |
+
from starlette.middleware.sessions import SessionMiddleware
|
| 65 |
+
|
| 66 |
+
app.add_middleware(SessionMiddleware, secret_key="local-dev")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ----------------------------------------------------------------------------------------------- models
|
| 70 |
+
def _download(kind: str) -> str:
|
| 71 |
+
# real files, not cache symlinks: onnxruntime refuses external data that resolves outside the model folder
|
| 72 |
+
if kind == "rt":
|
| 73 |
+
return snapshot_download(RT_REPO, token=TOKEN, allow_patterns=BASE_FILES + ["onnx/int8/*"], local_dir=RT_DIR)
|
| 74 |
+
return snapshot_download(MAIN_REPO, token=TOKEN, allow_patterns=BASE_FILES + [f"onnx/{kind}/*"], local_dir=MAIN_DIR)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_model(kind: str):
|
| 78 |
+
if kind not in MODELS:
|
| 79 |
+
raise HTTPException(400, "unknown model")
|
| 80 |
+
if kind in state["models"]:
|
| 81 |
+
return state["models"][kind]
|
| 82 |
+
with _load_lock:
|
| 83 |
+
if kind not in state["models"]:
|
| 84 |
+
t0 = time.time()
|
| 85 |
+
state["loading"][kind] = True
|
| 86 |
+
try:
|
| 87 |
+
d = _download(kind)
|
| 88 |
+
if kind != "rt" and d not in sys.path: # one runtime package per process: the main repo's (it reads the
|
| 89 |
+
sys.path.insert(0, d) # real-time model's config: film_layers, cfg_fixed, chunk sizes)
|
| 90 |
+
from decho import Decho
|
| 91 |
+
|
| 92 |
+
state["models"][kind] = Decho(d, precision="int8" if kind == "rt" else kind, threads=THREADS)
|
| 93 |
+
state.setdefault("load_s", {})[kind] = round(time.time() - t0, 1)
|
| 94 |
+
finally:
|
| 95 |
+
state["loading"].pop(kind, None)
|
| 96 |
+
return state["models"][kind]
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _warm():
|
| 100 |
+
try:
|
| 101 |
+
m = get_model("int8") # first: provides the runtime package
|
| 102 |
+
m.synth("سلام.", steps=4, reader=False)
|
| 103 |
+
r = get_model("rt")
|
| 104 |
+
r.synth("سلام.", reader=False)
|
| 105 |
+
state["ready"] = True
|
| 106 |
+
get_model("fp32") # studio model in the background (large download)
|
| 107 |
+
except Exception as e: # noqa: BLE001 - shown in the UI instead of a dead page
|
| 108 |
+
state["error"] = f"{type(e).__name__}: {str(e)[:300]}"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
threading.Thread(target=_warm, daemon=True).start()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# ----------------------------------------------------------------------------------------------- identity, limits
|
| 115 |
+
def who(request: Request) -> dict:
|
| 116 |
+
"""Signed-in Hugging Face user (OAuth) or an anonymous visitor (hashed client address)."""
|
| 117 |
+
try:
|
| 118 |
+
info = parse_huggingface_oauth(request)
|
| 119 |
+
except Exception: # noqa: BLE001
|
| 120 |
+
info = None
|
| 121 |
+
if info is not None and info.user_info is not None and info.access_token:
|
| 122 |
+
u = info.user_info
|
| 123 |
+
return {"kind": "user", "id": "u:" + (u.preferred_username or u.sub or "?"), "username": u.preferred_username,
|
| 124 |
+
"name": u.name or u.preferred_username, "avatar": u.picture, "token": info.access_token,
|
| 125 |
+
"pro": bool(u.is_pro)}
|
| 126 |
+
ip = (request.headers.get("x-forwarded-for") or (request.client.host if request.client else "?")).split(",")[0].strip()
|
| 127 |
+
return {"kind": "anon", "id": "a:" + hashlib.sha256(ip.encode()).hexdigest()[:16]}
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
_usage: dict = defaultdict(deque) # id -> deque[(t, chars)]
|
| 131 |
+
_usage_lock = threading.Lock()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def charge(ident: dict, chars: int) -> None:
|
| 135 |
+
lim = LIMITS[ident["kind"]]
|
| 136 |
+
now = time.time()
|
| 137 |
+
with _usage_lock:
|
| 138 |
+
q = _usage[ident["id"]]
|
| 139 |
+
while q and now - q[0][0] > 3600:
|
| 140 |
+
q.popleft()
|
| 141 |
+
used = sum(c for _, c in q)
|
| 142 |
+
if used + chars > lim["chars_per_hour"]:
|
| 143 |
+
if ident["kind"] == "anon":
|
| 144 |
+
raise HTTPException(429, "limit_anon")
|
| 145 |
+
raise HTTPException(429, "limit_user")
|
| 146 |
+
q.append((now, chars))
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
_readers: "OrderedDict[str, object]" = OrderedDict()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def user_reader(ident: dict):
|
| 153 |
+
"""LLM reading layer on the signed-in user's own token (their Inference Providers credit)."""
|
| 154 |
+
if ident["kind"] != "user":
|
| 155 |
+
return None
|
| 156 |
+
from decho.llm_reader import HFRouterReader
|
| 157 |
+
|
| 158 |
+
key = ident["id"] + ":" + hashlib.sha256(ident["token"].encode()).hexdigest()[:8]
|
| 159 |
+
if key not in _readers:
|
| 160 |
+
_readers[key] = HFRouterReader(token=ident["token"], timeout=60.0)
|
| 161 |
+
while len(_readers) > 200:
|
| 162 |
+
_readers.popitem(last=False)
|
| 163 |
+
_readers.move_to_end(key)
|
| 164 |
+
return _readers[key]
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def clean_lexicon(lex) -> dict:
|
| 168 |
+
"""Personal pronunciation dictionary sent by the browser: {word: Persian respelling | "r:<model text>"}."""
|
| 169 |
+
out = {}
|
| 170 |
+
if not isinstance(lex, dict):
|
| 171 |
+
return out
|
| 172 |
+
for k, v in list(lex.items())[:300]:
|
| 173 |
+
k, v = str(k).strip()[:40], str(v).strip()[:80]
|
| 174 |
+
if not k or not v or not re.search(r"[-ۿ]", k):
|
| 175 |
+
continue
|
| 176 |
+
if v.startswith("r:") and not re.fullmatch(r"r:[A-Za-z' ]{1,78}", v):
|
| 177 |
+
continue
|
| 178 |
+
out[k] = v
|
| 179 |
+
return out
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def wav_bytes(wav: np.ndarray, sr: int) -> bytes:
|
| 183 |
+
buf = io.BytesIO()
|
| 184 |
+
sf.write(buf, np.clip(np.asarray(wav, np.float32), -1, 1), sr, format="WAV", subtype="PCM_16")
|
| 185 |
+
return buf.getvalue()
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# ----------------------------------------------------------------------------------------------- status / account
|
| 189 |
+
@app.get("/api/status")
|
| 190 |
+
def status(request: Request):
|
| 191 |
+
ident = who(request)
|
| 192 |
+
m = state["models"].get("int8")
|
| 193 |
+
voices = []
|
| 194 |
+
if m is not None:
|
| 195 |
+
meta = {}
|
| 196 |
+
try:
|
| 197 |
+
meta = json.load(open(os.path.join(MAIN_DIR, "voices", "voices.json"), encoding="utf-8"))
|
| 198 |
+
except Exception: # noqa: BLE001
|
| 199 |
+
pass
|
| 200 |
+
voices = [{"name": v, "gender": (meta.get(v) or {}).get("gender"), "fa": (meta.get(v) or {}).get("description", "")}
|
| 201 |
+
for v in m.voices]
|
| 202 |
+
with _waiting_lock:
|
| 203 |
+
queue = _waiting[0]
|
| 204 |
+
return JSONResponse({"ready": state["ready"], "error": state["error"], "models": sorted(state["models"]),
|
| 205 |
+
"loading": sorted(state["loading"]), "voices": voices, "queue": queue,
|
| 206 |
+
"limits": LIMITS[ident["kind"]], "signed_in": ident["kind"] == "user",
|
| 207 |
+
"oauth": bool(os.environ.get("OAUTH_CLIENT_ID")),
|
| 208 |
+
"space_host": os.environ.get("SPACE_HOST", "")})
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@app.get("/api/me")
|
| 212 |
+
def me(request: Request):
|
| 213 |
+
ident = who(request)
|
| 214 |
+
if ident["kind"] != "user":
|
| 215 |
+
return {"signed_in": False}
|
| 216 |
+
return {"signed_in": True, "username": ident["username"], "name": ident["name"], "avatar": ident["avatar"],
|
| 217 |
+
"pro": ident["pro"]}
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# ----------------------------------------------------------------------------------------------- synthesis (streamed)
|
| 221 |
+
class StreamReq(BaseModel):
|
| 222 |
+
script: str | list
|
| 223 |
+
model: str = "rt" # rt | int8 | fp32
|
| 224 |
+
voice: str | None = "default"
|
| 225 |
+
speed: float = 1.0
|
| 226 |
+
seed: int | None = 0
|
| 227 |
+
smart: bool = False # LLM reading layer (signed-in users, their own credit)
|
| 228 |
+
register: str = "formal" # formal | casual
|
| 229 |
+
live: bool = False # short first chunk = earliest first audio
|
| 230 |
+
pause_scale: float | None = None
|
| 231 |
+
lexicon: dict | None = None
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
_streams: dict = {}
|
| 235 |
+
_streams_lock = threading.Lock()
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@app.post("/api/stream/start")
|
| 239 |
+
def stream_start(r: StreamReq, request: Request):
|
| 240 |
+
ident = who(request)
|
| 241 |
+
if not state["ready"]:
|
| 242 |
+
raise HTTPException(503, state["error"] or "loading")
|
| 243 |
+
lim = LIMITS[ident["kind"]]
|
| 244 |
+
script = r.script
|
| 245 |
+
if isinstance(script, list):
|
| 246 |
+
script = [{"text": str(x.get("text", ""))[:lim["chars"]], "voice": x.get("voice")} for x in script[:200]
|
| 247 |
+
if isinstance(x, dict)]
|
| 248 |
+
n = sum(len(x["text"]) for x in script)
|
| 249 |
+
else:
|
| 250 |
+
script = str(script).strip()
|
| 251 |
+
n = len(script)
|
| 252 |
+
if n == 0:
|
| 253 |
+
raise HTTPException(422, "empty")
|
| 254 |
+
if n > lim["chars"]:
|
| 255 |
+
raise HTTPException(413, "too_long_anon" if ident["kind"] == "anon" else "too_long")
|
| 256 |
+
if ident["kind"] == "anon" and (isinstance(script, list) or r.model == "fp32" or r.smart):
|
| 257 |
+
raise HTTPException(401, "sign_in")
|
| 258 |
+
with _streams_lock:
|
| 259 |
+
mine = [k for k, v in _streams.items() if v["owner"] == ident["id"] and not v["done"]]
|
| 260 |
+
for k in mine[: max(0, len(mine) - lim["streams"] + 1)]: # a new request replaces the oldest open one
|
| 261 |
+
_streams.pop(k, None)
|
| 262 |
+
charge(ident, n)
|
| 263 |
+
m = get_model(r.model)
|
| 264 |
+
kw = dict(steps=MODELS[r.model]["steps"], speed=max(0.7, min(1.4, float(r.speed))), seed=r.seed, voice=r.voice or None,
|
| 265 |
+
reader=False, register="casual" if r.register == "casual" else "formal",
|
| 266 |
+
lexicon=clean_lexicon(r.lexicon) or None)
|
| 267 |
+
if MODELS[r.model]["cfg"] is not None:
|
| 268 |
+
kw["cfg"] = MODELS[r.model]["cfg"]
|
| 269 |
+
if r.live:
|
| 270 |
+
kw["first_chars"] = 40
|
| 271 |
+
if r.pause_scale:
|
| 272 |
+
kw["pause_scale"] = max(0.5, min(2.0, float(r.pause_scale)))
|
| 273 |
+
if r.smart:
|
| 274 |
+
rd = user_reader(ident)
|
| 275 |
+
if rd is not None:
|
| 276 |
+
kw["reader"] = rd
|
| 277 |
+
if kw["voice"] and kw["voice"] not in m.voices:
|
| 278 |
+
raise HTTPException(422, "unknown voice")
|
| 279 |
+
sid = uuid.uuid4().hex
|
| 280 |
+
with _streams_lock:
|
| 281 |
+
for k in [k for k, v in _streams.items() if time.time() - v["t"] > 900]:
|
| 282 |
+
_streams.pop(k, None)
|
| 283 |
+
_streams[sid] = {"gen": m.stream(script, **kw), "t": time.time(), "done": False, "owner": ident["id"]}
|
| 284 |
+
return {"id": sid}
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
@app.get("/api/stream/{sid}/next")
|
| 288 |
+
def stream_next(sid: str, request: Request):
|
| 289 |
+
st = _streams.get(sid)
|
| 290 |
+
if not st or st["owner"] != who(request)["id"]:
|
| 291 |
+
raise HTTPException(404, "unknown or expired stream")
|
| 292 |
+
if st["done"]:
|
| 293 |
+
return Response(status_code=204)
|
| 294 |
+
with _waiting_lock:
|
| 295 |
+
_waiting[0] += 1
|
| 296 |
+
with _synth_lock: # one synthesis at a time: the CPU is shared
|
| 297 |
+
with _waiting_lock:
|
| 298 |
+
_waiting[0] -= 1
|
| 299 |
+
t0 = time.time()
|
| 300 |
+
try:
|
| 301 |
+
chunk = next(st["gen"])
|
| 302 |
+
except StopIteration:
|
| 303 |
+
st["done"] = True
|
| 304 |
+
_streams.pop(sid, None)
|
| 305 |
+
return Response(status_code=204)
|
| 306 |
+
dt = time.time() - t0
|
| 307 |
+
st["t"] = time.time()
|
| 308 |
+
wav, sr = np.asarray(chunk["audio"], np.float32), int(chunk["sr"])
|
| 309 |
+
dur = len(wav) / sr if sr else 0.0
|
| 310 |
+
meta = {k: chunk.get(k) for k in ("text", "voice", "model_text", "gap_after_s", "reader", "fixed_text", "uncertain")}
|
| 311 |
+
meta["model_text"] = re.sub(r"^(?:<[^>]+>\s*)+", "", meta.get("model_text") or "")
|
| 312 |
+
return Response(wav_bytes(wav, sr), media_type="audio/wav", headers={
|
| 313 |
+
"X-Synth-Seconds": f"{dt:.2f}", "X-Audio-Seconds": f"{dur:.2f}",
|
| 314 |
+
"X-Meta": base64.b64encode(json.dumps(meta, ensure_ascii=False).encode()).decode(),
|
| 315 |
+
"Access-Control-Expose-Headers": "X-Synth-Seconds, X-Audio-Seconds, X-Meta", "Cache-Control": "no-store"})
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
@app.post("/api/stream/{sid}/stop")
|
| 319 |
+
def stream_stop(sid: str, request: Request):
|
| 320 |
+
st = _streams.get(sid)
|
| 321 |
+
if st and st["owner"] == who(request)["id"]:
|
| 322 |
+
_streams.pop(sid, None)
|
| 323 |
+
return {"ok": True}
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# ----------------------------------------------------------------------------------------------- language-model features
|
| 327 |
+
def _llm(ident: dict, messages: list, max_tokens: int, temperature: float) -> str:
|
| 328 |
+
if ident["kind"] != "user":
|
| 329 |
+
raise HTTPException(401, "sign_in")
|
| 330 |
+
body = {"model": LLM_MODEL, "messages": messages, "max_tokens": max_tokens, "temperature": temperature,
|
| 331 |
+
"chat_template_kwargs": {"thinking": False, "enable_thinking": False}}
|
| 332 |
+
req = urllib.request.Request(ROUTER, data=json.dumps(body).encode(), method="POST",
|
| 333 |
+
headers={"Authorization": f"Bearer {ident['token']}", "Content-Type": "application/json"})
|
| 334 |
+
try:
|
| 335 |
+
with urllib.request.urlopen(req, timeout=120) as f:
|
| 336 |
+
return json.loads(f.read())["choices"][0]["message"]["content"] or ""
|
| 337 |
+
except urllib.error.HTTPError as e:
|
| 338 |
+
if e.code in (401, 403):
|
| 339 |
+
raise HTTPException(403, "hf_permission")
|
| 340 |
+
if e.code == 402:
|
| 341 |
+
raise HTTPException(402, "hf_credits")
|
| 342 |
+
raise HTTPException(502, "llm_failed")
|
| 343 |
+
except Exception: # noqa: BLE001
|
| 344 |
+
raise HTTPException(502, "llm_failed")
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
POD_SYS = """تو نویسندهٔ حرفهای پادکست فارسی هستی. متنی مینویسی که برای خواندهشدن با صدای انسانگونه ساخته شده.
|
| 348 |
+
قالب خروجی، بدون هیچ چیز دیگر: هر خط «نام گوینده: متن». نه عنوان، نه توضیح، نه markdown، نه شمارهگذاری.
|
| 349 |
+
قواعد:
|
| 350 |
+
- فقط از نامهای گویندهای که داده شده استفاده کن.
|
| 351 |
+
- نوبتها کوتاه و طبیعی باشد (۱ تا ۳ جمله)، مثل گفتوگوی واقعی: واکنشهای کوتاه («آره دقیقاً»، «جالبه!»، «واقعاً؟»)، سؤال، شوخی ملایم.
|
| 352 |
+
- گاهی خندهٔ کوتاه با «هاهاها» یا مکث فکر با «اممم…» بگذار؛ زیادهروی نکن.
|
| 353 |
+
- بین بخشهای اصلی یک خط جدا فقط با [pause:1.0] بگذار.
|
| 354 |
+
- شروع: خوشامد به شنوندهها و معرفی نام پادکست و موضوع. پایان: جمعبندی کوتاه و خداحافظی گرم.
|
| 355 |
+
- اعداد و تاریخ را به رقم بنویس. ایموجی نگذار. جملهها را با نقطه، ویرگول، علامت سؤال و تعجب درست تمام کن.
|
| 356 |
+
- محاوره را با املای استاندارد و رایج بنویس (مثلاً «میشه»، «اومدیم»، «نمیدونم») و نیمفاصله را رعایت کن. هیچ اِعرابی نگذار."""
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class PodReq(BaseModel):
|
| 360 |
+
source: str
|
| 361 |
+
name: str = "پادکست"
|
| 362 |
+
hosts: list
|
| 363 |
+
tone: str = "casual"
|
| 364 |
+
minutes: int = 2
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
@app.post("/api/podcast/script")
|
| 368 |
+
def podcast_script(r: PodReq, request: Request):
|
| 369 |
+
ident = who(request)
|
| 370 |
+
hosts = [h for h in (r.hosts or []) if isinstance(h, dict) and str(h.get("name", "")).strip()][:4]
|
| 371 |
+
if not hosts:
|
| 372 |
+
raise HTTPException(422, "no_hosts")
|
| 373 |
+
src = (r.source or "").strip()[:8000]
|
| 374 |
+
if not src:
|
| 375 |
+
raise HTTPException(422, "empty")
|
| 376 |
+
minutes = max(1, min(5, int(r.minutes)))
|
| 377 |
+
tone = "خودمانی و محاورهای (گفتار روزمرهٔ تهرانی، مؤدبانه)" if r.tone == "casual" else "رسمی، روان و حرفهای"
|
| 378 |
+
who_ = "، ".join(f"{h['name']} ({h.get('role') or 'میزبان'})" for h in hosts)
|
| 379 |
+
user = (f"نام پادکست: {r.name or 'پادکست'}\nگویندهها: {who_}\nلحن: {tone}\nطول تقریبی: {minutes} دقیقه "
|
| 380 |
+
f"(حدود {minutes * 140} کلمه)\n" + ("متن منبع:" if len(src) > 300 else "موضوع:") + "\n" + src)
|
| 381 |
+
text = _llm(ident, [{"role": "system", "content": POD_SYS}, {"role": "user", "content": user}],
|
| 382 |
+
min(4000, 400 + minutes * 450), 0.8)
|
| 383 |
+
names = {str(h["name"]).strip() for h in hosts}
|
| 384 |
+
lines = []
|
| 385 |
+
for ln in text.splitlines():
|
| 386 |
+
ln = ln.strip().strip("*").strip()
|
| 387 |
+
if ln.startswith("[pause"):
|
| 388 |
+
lines.append(ln)
|
| 389 |
+
elif ":" in ln or ":" in ln:
|
| 390 |
+
ln = ln.replace(":", ":")
|
| 391 |
+
if ln.split(":", 1)[0].strip() in names:
|
| 392 |
+
lines.append(ln)
|
| 393 |
+
return {"script": "\n".join(lines)}
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
class SuggestReq(BaseModel):
|
| 397 |
+
word: str
|
| 398 |
+
sentence: str | None = None
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
@app.post("/api/lexicon/suggest")
|
| 402 |
+
def lexicon_suggest(r: SuggestReq, request: Request):
|
| 403 |
+
ident = who(request)
|
| 404 |
+
if not state["ready"]:
|
| 405 |
+
raise HTTPException(503, "loading")
|
| 406 |
+
w = (r.word or "").strip()[:40]
|
| 407 |
+
if not w:
|
| 408 |
+
raise HTTPException(422, "empty")
|
| 409 |
+
m = get_model("int8")
|
| 410 |
+
rd = user_reader(ident)
|
| 411 |
+
try:
|
| 412 |
+
cands = m.suggest(w, sentence=(r.sentence or "")[:300] or None, reader=rd if rd is not None else False)
|
| 413 |
+
except Exception: # noqa: BLE001
|
| 414 |
+
cands = []
|
| 415 |
+
return {"candidates": cands, "llm": rd is not None}
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
class SayReq(BaseModel):
|
| 419 |
+
reading: str
|
| 420 |
+
word: str | None = None
|
| 421 |
+
sentence: str | None = None
|
| 422 |
+
voice: str | None = "default"
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
@app.post("/api/lexicon/say")
|
| 426 |
+
def lexicon_say(r: SayReq, request: Request):
|
| 427 |
+
ident = who(request)
|
| 428 |
+
if ident["kind"] != "user":
|
| 429 |
+
raise HTTPException(401, "sign_in")
|
| 430 |
+
if not state["ready"]:
|
| 431 |
+
raise HTTPException(503, "loading")
|
| 432 |
+
reading = (r.reading or "").strip()[:80]
|
| 433 |
+
if not reading or (not re.search(r"[-ۿ]", reading) and not re.fullmatch(r"[A-Za-z' ]+", reading)):
|
| 434 |
+
raise HTTPException(422, "bad_reading")
|
| 435 |
+
charge(ident, 60)
|
| 436 |
+
m = get_model("int8")
|
| 437 |
+
sent = (r.sentence or "")[:300] if (r.sentence and r.word and r.word in r.sentence) else None
|
| 438 |
+
with _synth_lock:
|
| 439 |
+
wav, sr = m.say_word(reading, voice=r.voice if r.voice in m.voices else "default", sentence=sent, word=r.word)
|
| 440 |
+
return Response(wav_bytes(wav, sr), media_type="audio/wav")
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
@app.post("/api/mp3")
|
| 444 |
+
async def to_mp3(request: Request):
|
| 445 |
+
"""Browser-joined WAV -> MP3 (ffmpeg) for a small, universal download."""
|
| 446 |
+
if who(request)["kind"] != "user":
|
| 447 |
+
raise HTTPException(401, "sign_in")
|
| 448 |
+
data = await request.body()
|
| 449 |
+
if not data or len(data) > 150 * 1024 * 1024:
|
| 450 |
+
raise HTTPException(413, "too_large")
|
| 451 |
+
with tempfile.TemporaryDirectory() as d:
|
| 452 |
+
src, dst = os.path.join(d, "in.wav"), os.path.join(d, "out.mp3")
|
| 453 |
+
open(src, "wb").write(data)
|
| 454 |
+
p = subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", src, "-ac", "1", "-ar", "44100", "-b:a", "128k", dst])
|
| 455 |
+
if p.returncode or not os.path.exists(dst):
|
| 456 |
+
raise HTTPException(500, "mp3_failed")
|
| 457 |
+
return Response(open(dst, "rb").read(), media_type="audio/mpeg",
|
| 458 |
+
headers={"Content-Disposition": "attachment; filename=decho.mp3"})
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
# ----------------------------------------------------------------------------------------------- pages
|
| 462 |
+
app.mount("/static", StaticFiles(directory=os.path.join(HERE, "static")), name="static")
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
@app.get("/")
|
| 466 |
+
def index():
|
| 467 |
+
return FileResponse(os.path.join(HERE, "static", "index.html"), headers={"Cache-Control": "no-cache"})
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@app.get("/robots.txt")
|
| 471 |
+
def robots():
|
| 472 |
+
return Response("User-agent: *\nAllow: /\n", media_type="text/plain")
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
@app.get("/healthz")
|
| 476 |
+
def healthz():
|
| 477 |
+
return {"ok": True, "ready": state["ready"]}
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn==0.34.0
|
| 3 |
+
huggingface_hub[oauth]>=1.0
|
| 4 |
+
itsdangerous>=2.1
|
| 5 |
+
authlib>=1.3
|
| 6 |
+
httpx>=0.27
|
| 7 |
+
onnxruntime>=1.18
|
| 8 |
+
numpy<2.3
|
| 9 |
+
scipy>=1.11
|
| 10 |
+
tokenizers>=0.19
|
| 11 |
+
soundfile>=0.12
|
| 12 |
+
rapidfuzz>=3.0
|
static/index.html
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Decho · دکو — Persian Text-to-Speech | تبدیل متن به گفتار فارسی | Dibachain</title>
|
| 7 |
+
<meta name="description" content="Decho by Dibachain: natural Persian (Farsi) text-to-speech that runs on an ordinary CPU, even in real time. Five voices, emotions, dialogue and podcasts. دکو، ساختهی دیباچین: تبدیل متن به گفتار طبیعی فارسی روی پردازندهی معمولی، حتی بلادرنگ؛ پنج صدا، احساس، گفتوگو و پادکست.">
|
| 8 |
+
<meta name="keywords" content="Persian text to speech, Farsi TTS, Persian TTS, Iranian AI, speech synthesis, real-time TTS, CPU TTS, Dibachain, Decho, تبدیل متن به گفتار, تبدیل متن به صدا فارسی, گفتار فارسی, سنتز گفتار, هوش مصنوعی ایرانی, دیباچین, دکو">
|
| 9 |
+
<meta name="author" content="Dibachain · دیباچین">
|
| 10 |
+
<meta name="robots" content="index, follow">
|
| 11 |
+
<link rel="canonical" href="https://huggingface.co/spaces/DibaAi/Decho">
|
| 12 |
+
<meta property="og:type" content="website">
|
| 13 |
+
<meta property="og:site_name" content="Dibachain · دیباچین">
|
| 14 |
+
<meta property="og:title" content="Decho · دکو — Persian Text-to-Speech by Dibachain">
|
| 15 |
+
<meta property="og:description" content="Natural Persian speech on an ordinary CPU, even in real time. گفتار طبیعی فارسی روی پردازندهی معمولی، حتی بلادرنگ.">
|
| 16 |
+
<meta property="og:url" content="https://huggingface.co/spaces/DibaAi/Decho">
|
| 17 |
+
<meta property="og:image" content="https://cdn-avatars.huggingface.co/v1/production/uploads/64cb9d9186d8dc0caa4d4417/RjvRkGh5EYvG_mGUK4Uek.png">
|
| 18 |
+
<meta property="og:locale" content="fa_IR">
|
| 19 |
+
<meta property="og:locale:alternate" content="en_US">
|
| 20 |
+
<meta name="twitter:card" content="summary">
|
| 21 |
+
<meta name="twitter:title" content="Decho · دکو — Persian Text-to-Speech by Dibachain">
|
| 22 |
+
<meta name="twitter:description" content="Natural Persian speech on an ordinary CPU, even in real time.">
|
| 23 |
+
<meta name="twitter:image" content="https://cdn-avatars.huggingface.co/v1/production/uploads/64cb9d9186d8dc0caa4d4417/RjvRkGh5EYvG_mGUK4Uek.png">
|
| 24 |
+
<link rel="icon" href="https://cdn-avatars.huggingface.co/v1/production/uploads/64cb9d9186d8dc0caa4d4417/RjvRkGh5EYvG_mGUK4Uek.png">
|
| 25 |
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 26 |
+
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap" rel="stylesheet">
|
| 27 |
+
<script type="application/ld+json">
|
| 28 |
+
{"@context":"https://schema.org","@graph":[
|
| 29 |
+
{"@type":"Organization","@id":"https://dibachain.ir/#org","name":"Dibachain","alternateName":["دیباچین","DIBACHAIN"],
|
| 30 |
+
"url":"https://dibachain.ir","email":"info@dibachain.ir",
|
| 31 |
+
"logo":"https://cdn-avatars.huggingface.co/v1/production/uploads/64cb9d9186d8dc0caa4d4417/RjvRkGh5EYvG_mGUK4Uek.png",
|
| 32 |
+
"description":"Iranian AI company building localized, Persian-first artificial intelligence. شرکت هوش مصنوعی ایرانی برای بومیسازی هوش مصنوعی به زبان فارسی.",
|
| 33 |
+
"sameAs":["https://huggingface.co/Dibachain","https://huggingface.co/DibaAi"]},
|
| 34 |
+
{"@type":"SoftwareApplication","name":"Decho · دکو","alternateName":"Decho Persian Text-to-Speech",
|
| 35 |
+
"applicationCategory":"MultimediaApplication","operatingSystem":"Web, CPU (ONNX Runtime)",
|
| 36 |
+
"url":"https://huggingface.co/spaces/DibaAi/Decho","inLanguage":["fa","en"],
|
| 37 |
+
"description":"Persian (Farsi) text-to-speech by Dibachain: natural speech on an ordinary CPU, real-time model, five voices, emotions, dialogue and podcast mode.",
|
| 38 |
+
"creator":{"@id":"https://dibachain.ir/#org"},"publisher":{"@id":"https://dibachain.ir/#org"},
|
| 39 |
+
"offers":{"@type":"Offer","price":"0","priceCurrency":"USD"}}
|
| 40 |
+
]}
|
| 41 |
+
</script>
|
| 42 |
+
<style>
|
| 43 |
+
:root{--bg:#f6f7fb;--card:#fff;--ink:#14162b;--muted:#5d6280;--line:#e3e6f0;--brand:#4f46e5;--brand2:#7c3aed;--ok:#0f9d58;--warn:#b45309;--err:#c62828;--soft:#eef0ff;--chip:#f1f2f8;--shadow:0 1px 2px rgba(20,22,43,.06),0 8px 24px rgba(20,22,43,.06)}
|
| 44 |
+
@media (prefers-color-scheme:dark){:root{--bg:#0e1020;--card:#161935;--ink:#eceefe;--muted:#a3a8c9;--line:#2a2e55;--soft:#1f2350;--chip:#20244a;--shadow:none}}
|
| 45 |
+
*{box-sizing:border-box}html{scroll-behavior:smooth}
|
| 46 |
+
body{margin:0;background:var(--bg);color:var(--ink);font-family:Vazirmatn,system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.75;font-size:15.5px}
|
| 47 |
+
a{color:var(--brand)}a:hover{opacity:.85}
|
| 48 |
+
.wrap{max-width:1080px;margin:0 auto;padding:0 18px}
|
| 49 |
+
header.top{position:sticky;top:0;z-index:20;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
|
| 50 |
+
header.top .wrap{display:flex;align-items:center;gap:12px;min-height:60px}
|
| 51 |
+
.brand{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--ink)}
|
| 52 |
+
.brand img{width:34px;height:34px;border-radius:9px}
|
| 53 |
+
.brand b{font-weight:800;font-size:17px}.brand small{color:var(--muted);font-size:12px;display:block;line-height:1.2}
|
| 54 |
+
nav.links{display:flex;gap:16px;margin-inline-start:12px}nav.links a{color:var(--muted);text-decoration:none;font-size:14px}nav.links a:hover{color:var(--ink)}
|
| 55 |
+
.spacer{flex:1}
|
| 56 |
+
.btn{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);background:var(--card);color:var(--ink);padding:8px 14px;border-radius:10px;font:inherit;font-size:14px;cursor:pointer;text-decoration:none;white-space:nowrap}
|
| 57 |
+
.btn:hover{border-color:var(--brand)}.btn:disabled{opacity:.5;cursor:not-allowed}
|
| 58 |
+
.btn.main{background:linear-gradient(135deg,var(--brand),var(--brand2));color:#fff;border:0;font-weight:700;padding:11px 22px}
|
| 59 |
+
.btn.hf{background:#ffd21e;color:#1b1b1b;border:0;font-weight:700}.btn.hf img{width:18px;height:18px}
|
| 60 |
+
.btn.small{padding:5px 10px;font-size:13px}
|
| 61 |
+
.lang{border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex}.lang button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:13px;cursor:pointer}.lang button.on{background:var(--soft);color:var(--ink);font-weight:700}
|
| 62 |
+
.user{display:flex;align-items:center;gap:8px;font-size:14px}.user img{width:28px;height:28px;border-radius:50%}
|
| 63 |
+
.hero{padding:42px 0 18px}.hero h1{font-size:34px;line-height:1.35;margin:0 0 10px;font-weight:800}
|
| 64 |
+
.hero h1 .grad{background:linear-gradient(135deg,var(--brand),var(--brand2));-webkit-background-clip:text;background-clip:text;color:transparent}
|
| 65 |
+
.hero p.lead{color:var(--muted);font-size:17px;max-width:760px;margin:0 0 18px}
|
| 66 |
+
.badges{display:flex;flex-wrap:wrap;gap:8px}.badge{background:var(--chip);border:1px solid var(--line);border-radius:999px;padding:4px 12px;font-size:13px;color:var(--muted)}
|
| 67 |
+
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);padding:20px;margin:18px 0}
|
| 68 |
+
.card h2{margin:0 0 6px;font-size:21px}.card .sub{color:var(--muted);margin:0 0 14px;font-size:14px}
|
| 69 |
+
.row{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end}
|
| 70 |
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}
|
| 71 |
+
label{display:block;font-size:13px;color:var(--muted);margin-bottom:4px}
|
| 72 |
+
select,input[type=text],input[type=number],textarea{width:100%;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:10px;padding:9px 11px;font:inherit;font-size:15px}
|
| 73 |
+
textarea{min-height:150px;resize:vertical;line-height:1.9;font-size:16.5px}
|
| 74 |
+
textarea:focus,select:focus,input:focus{outline:2px solid color-mix(in srgb,var(--brand) 40%,transparent);border-color:var(--brand)}
|
| 75 |
+
input[type=range]{width:100%}
|
| 76 |
+
.seg{display:inline-flex;background:var(--chip);border:1px solid var(--line);border-radius:12px;padding:3px;gap:3px;flex-wrap:wrap}
|
| 77 |
+
.seg button{border:0;background:transparent;color:var(--muted);font:inherit;font-size:14px;padding:7px 14px;border-radius:9px;cursor:pointer}
|
| 78 |
+
.seg button.on{background:var(--card);color:var(--ink);font-weight:700;box-shadow:0 1px 2px rgba(0,0,0,.08)}
|
| 79 |
+
.models{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin:12px 0}
|
| 80 |
+
.model{border:1.5px solid var(--line);border-radius:12px;padding:10px 12px;cursor:pointer;background:var(--bg)}
|
| 81 |
+
.model.on{border-color:var(--brand);background:var(--soft)}.model b{display:block}.model span{color:var(--muted);font-size:13px}
|
| 82 |
+
.model.locked{opacity:.6}
|
| 83 |
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}.chip{border:1px solid var(--line);background:var(--chip);color:var(--ink);border-radius:999px;padding:3px 11px;font:inherit;font-size:13px;cursor:pointer}
|
| 84 |
+
.chip:hover{border-color:var(--brand)}
|
| 85 |
+
.toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:12px}
|
| 86 |
+
.meta{color:var(--muted);font-size:13px}.err{color:var(--err)}.okc{color:var(--ok)}.warnc{color:var(--warn)}
|
| 87 |
+
details.adv{margin-top:10px;border-top:1px dashed var(--line);padding-top:8px}details.adv summary{cursor:pointer;color:var(--muted);font-size:14px}
|
| 88 |
+
.switch{display:inline-flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;color:var(--ink)}.switch input{width:18px;height:18px}
|
| 89 |
+
.note{background:var(--soft);border:1px solid var(--line);border-radius:12px;padding:10px 12px;font-size:14px;margin-top:10px}
|
| 90 |
+
.out .stats{display:flex;flex-wrap:wrap;gap:14px;font-size:13px;color:var(--muted);margin-bottom:8px}.out .stats b{color:var(--ink)}
|
| 91 |
+
.chunk{border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin:8px 0;background:var(--bg)}
|
| 92 |
+
.chunk.playing{border-color:var(--brand)}.chunk .t{font-size:15.5px}.chunk .who{font-size:12px;background:var(--chip);border-radius:6px;padding:1px 7px;margin-inline-end:6px;color:var(--muted)}
|
| 93 |
+
.chunk details{margin-top:4px}.chunk summary{cursor:pointer;font-size:12.5px;color:var(--muted)}
|
| 94 |
+
.dbg{direction:ltr;text-align:left;font-family:ui-monospace,Consolas,monospace;font-size:12.5px;background:var(--chip);border-radius:8px;padding:8px;white-space:pre-wrap;word-break:break-word;margin-top:6px}
|
| 95 |
+
.unc{display:inline-block;border-bottom:2px dotted var(--warn);cursor:pointer;margin:0 2px}
|
| 96 |
+
audio{width:100%;height:36px;margin-top:6px}
|
| 97 |
+
table{width:100%;border-collapse:collapse;font-size:14px;margin:8px 0}th,td{border-bottom:1px solid var(--line);padding:7px 8px;text-align:start}th{color:var(--muted);font-weight:500}
|
| 98 |
+
.samples{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px}.sample{border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:var(--bg)}.sample b{font-size:14px}.sample p{margin:4px 0;font-size:14px}
|
| 99 |
+
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}
|
| 100 |
+
h3{font-size:17px;margin:14px 0 6px}ul.tick{padding-inline-start:20px;margin:6px 0}ul.tick li{margin:3px 0}
|
| 101 |
+
code{background:var(--chip);padding:1px 6px;border-radius:6px;font-size:13px;direction:ltr;unicode-bidi:embed}
|
| 102 |
+
.lexrow{display:flex;gap:8px;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:6px 0;font-size:14px}
|
| 103 |
+
.cands .cand{display:flex;gap:8px;align-items:center;border:1px solid var(--line);border-radius:10px;padding:6px 10px;margin:6px 0;background:var(--bg);font-size:14px}
|
| 104 |
+
.cands .cand code{font-size:13.5px}
|
| 105 |
+
footer{border-top:1px solid var(--line);margin-top:30px;padding:26px 0;color:var(--muted);font-size:14px}
|
| 106 |
+
footer .wrap{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px}footer a{color:var(--muted)}footer b{color:var(--ink)}
|
| 107 |
+
.status-dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:#bbb;margin-inline-end:6px}.status-dot.ok{background:var(--ok)}.status-dot.err{background:var(--err)}
|
| 108 |
+
.hidden{display:none!important}
|
| 109 |
+
.toast{position:fixed;bottom:18px;inset-inline-start:50%;transform:translateX(-50%);background:var(--ink);color:var(--bg);padding:10px 16px;border-radius:10px;font-size:14px;z-index:50;max-width:90vw}
|
| 110 |
+
[dir=rtl] .toast{transform:translateX(50%)}
|
| 111 |
+
@media (max-width:720px){nav.links{display:none}.hero h1{font-size:26px}}
|
| 112 |
+
</style>
|
| 113 |
+
</head>
|
| 114 |
+
<body>
|
| 115 |
+
<header class="top"><div class="wrap">
|
| 116 |
+
<a class="brand" href="https://dibachain.ir" target="_blank" rel="noopener"><img src="https://cdn-avatars.huggingface.co/v1/production/uploads/64cb9d9186d8dc0caa4d4417/RjvRkGh5EYvG_mGUK4Uek.png" alt="Dibachain logo · لوگوی دیباچین"><span><b data-i="brand"></b><small data-i="brandSub"></small></span></a>
|
| 117 |
+
<nav class="links"><a href="#studio" data-i="navStudio"></a><a href="#samples" data-i="navSamples"></a><a href="#models" data-i="navModels"></a><a href="#dibachain" data-i="navAbout"></a></nav>
|
| 118 |
+
<span class="spacer"></span>
|
| 119 |
+
<div class="lang" role="group" aria-label="language"><button data-l="fa">فارسی</button><button data-l="en">English</button></div>
|
| 120 |
+
<div id="account"></div>
|
| 121 |
+
</div></header>
|
| 122 |
+
|
| 123 |
+
<main class="wrap">
|
| 124 |
+
<section class="hero">
|
| 125 |
+
<h1><span class="grad" data-i="heroTitle1"></span><br><span data-i="heroTitle2"></span></h1>
|
| 126 |
+
<p class="lead" data-i="heroLead"></p>
|
| 127 |
+
<div class="badges"><span class="badge" data-i="b1"></span><span class="badge" data-i="b2"></span><span class="badge" data-i="b3"></span><span class="badge" data-i="b4"></span><span class="badge" data-i="b5"></span><span class="badge" data-i="b6"></span></div>
|
| 128 |
+
</section>
|
| 129 |
+
|
| 130 |
+
<section class="card" id="studio" aria-labelledby="studioTitle">
|
| 131 |
+
<div class="row" style="justify-content:space-between;align-items:center">
|
| 132 |
+
<div><h2 id="studioTitle" data-i="studioTitle"></h2><p class="sub" style="margin:0"><span class="status-dot" id="dot"></span><span id="st" data-i="loading"></span></p></div>
|
| 133 |
+
<div class="seg" id="mode"><button data-m="single" class="on" data-i="modeSingle"></button><button data-m="dialog" data-i="modeDialog"></button><button data-m="podcast" data-i="modePodcast"></button></div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div class="models" id="modelPick">
|
| 137 |
+
<div class="model on" data-k="rt"><b data-i="mRt"></b><span data-i="mRtD"></span></div>
|
| 138 |
+
<div class="model" data-k="int8"><b data-i="mStd"></b><span data-i="mStdD"></span></div>
|
| 139 |
+
<div class="model" data-k="fp32"><b data-i="mStudio"></b><span data-i="mStudioD"></span></div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<div id="podPanel" class="note hidden">
|
| 143 |
+
<div class="grid">
|
| 144 |
+
<div><label data-i="podName"></label><input type="text" id="podName" value="دیباکست"></div>
|
| 145 |
+
<div><label data-i="podTone"></label><select id="podTone"><option value="casual" data-i="toneCasual"></option><option value="formal" data-i="toneFormal"></option></select></div>
|
| 146 |
+
<div><label data-i="podMin"></label><select id="podMin"><option>1</option><option selected>2</option><option>3</option><option>4</option><option>5</option></select></div>
|
| 147 |
+
<div><label><span data-i="podPace"></span> <span id="podPaceV">1.0</span></label><input type="range" id="podPace" min="0.7" max="1.4" step="0.05" value="1"></div>
|
| 148 |
+
</div>
|
| 149 |
+
<div class="grid" style="margin-top:10px" id="hosts"></div>
|
| 150 |
+
<label style="margin-top:10px" data-i="podSrc"></label>
|
| 151 |
+
<textarea id="podSrc" style="min-height:80px" data-ph="podSrcPh"></textarea>
|
| 152 |
+
<div class="toolbar"><button class="btn" id="podWrite" data-i="podWrite"></button><span class="meta" id="podMsg"></span></div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div class="row" style="margin-top:8px">
|
| 156 |
+
<div style="flex:1;min-width:220px" id="voiceBox"><label data-i="voice"></label><select id="voice"></select></div>
|
| 157 |
+
<div style="flex:1;min-width:200px"><label><span data-i="speed"></span> <span id="spv">1.00</span></label><input type="range" id="speed" min="0.8" max="1.25" step="0.05" value="1"></div>
|
| 158 |
+
</div>
|
| 159 |
+
<div class="chips" id="voiceChips"></div>
|
| 160 |
+
<label style="margin-top:8px" data-i="text"></label>
|
| 161 |
+
<textarea id="txt" aria-label="text"></textarea>
|
| 162 |
+
<div class="row" style="justify-content:space-between;margin-top:4px"><div class="chips" id="examples"></div><span class="meta" id="count"></span></div>
|
| 163 |
+
|
| 164 |
+
<details class="adv"><summary data-i="advanced"></summary>
|
| 165 |
+
<div class="grid" style="margin-top:10px">
|
| 166 |
+
<div><label class="switch"><input type="checkbox" id="smart"> <span data-i="smart"></span></label><div class="meta" data-i="smartD"></div></div>
|
| 167 |
+
<div><label data-i="register"></label><select id="register"><option value="formal" data-i="regFormal"></option><option value="casual" data-i="regCasual"></option></select></div>
|
| 168 |
+
<div><label class="switch"><input type="checkbox" id="live"> <span data-i="live"></span></label><div class="meta" data-i="liveD"></div></div>
|
| 169 |
+
<div><label data-i="seed"></label><input type="number" id="seed" value="0" min="0" max="999999"></div>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="note" data-i="tagsHelp"></div>
|
| 172 |
+
</details>
|
| 173 |
+
|
| 174 |
+
<div class="toolbar">
|
| 175 |
+
<button class="btn main" id="go" data-i="go"></button>
|
| 176 |
+
<button class="btn hidden" id="stop" data-i="stop"></button>
|
| 177 |
+
<button class="btn" id="dlWav" disabled data-i="dlWav"></button>
|
| 178 |
+
<button class="btn" id="dlMp3" disabled data-i="dlMp3"></button>
|
| 179 |
+
<span class="meta" id="msg"></span>
|
| 180 |
+
</div>
|
| 181 |
+
<div id="gate" class="note hidden"></div>
|
| 182 |
+
|
| 183 |
+
<div class="out hidden" id="out">
|
| 184 |
+
<div class="stats" id="stats"></div>
|
| 185 |
+
<div id="list"></div>
|
| 186 |
+
</div>
|
| 187 |
+
</section>
|
| 188 |
+
|
| 189 |
+
<section class="card" id="lexCard" aria-labelledby="lexTitle">
|
| 190 |
+
<h2 id="lexTitle" data-i="lexTitle"></h2>
|
| 191 |
+
<p class="sub" data-i="lexSub"></p>
|
| 192 |
+
<div class="grid">
|
| 193 |
+
<div><label data-i="lexWord"></label><input type="text" id="lexWord" dir="rtl"></div>
|
| 194 |
+
<div><label data-i="lexReading"></label><input type="text" id="lexReading" data-ph="lexReadingPh"></div>
|
| 195 |
+
</div>
|
| 196 |
+
<label style="margin-top:8px" data-i="lexSentence"></label><input type="text" id="lexSentence" dir="rtl">
|
| 197 |
+
<div class="toolbar"><button class="btn" id="lexSuggest" data-i="lexSuggest"></button><button class="btn" id="lexSay" data-i="lexSay"></button><button class="btn" id="lexSave" data-i="lexSave"></button><span class="meta" id="lexMsg"></span></div>
|
| 198 |
+
<div class="cands" id="cands"></div>
|
| 199 |
+
<div id="lexList" style="margin-top:10px"></div>
|
| 200 |
+
</section>
|
| 201 |
+
|
| 202 |
+
<section class="card" id="samples" aria-labelledby="sampTitle">
|
| 203 |
+
<h2 id="sampTitle" data-i="sampTitle"></h2><p class="sub" data-i="sampSub"></p>
|
| 204 |
+
<div class="samples" id="sampleList"></div>
|
| 205 |
+
</section>
|
| 206 |
+
|
| 207 |
+
<section class="card" id="models" aria-labelledby="modTitle">
|
| 208 |
+
<h2 id="modTitle" data-i="modTitle"></h2>
|
| 209 |
+
<p class="sub" data-i="modSub"></p>
|
| 210 |
+
<div id="modBody"></div>
|
| 211 |
+
</section>
|
| 212 |
+
|
| 213 |
+
<section class="card" id="dibachain" aria-labelledby="dcTitle">
|
| 214 |
+
<h2 id="dcTitle" data-i="dcTitle"></h2>
|
| 215 |
+
<div id="dcBody"></div>
|
| 216 |
+
</section>
|
| 217 |
+
</main>
|
| 218 |
+
|
| 219 |
+
<footer><div class="wrap">
|
| 220 |
+
<div><b>Dibachain · دیباچین</b><br><span data-i="ftAbout"></span><br><a href="https://dibachain.ir" target="_blank" rel="noopener">dibachain.ir</a> · <a href="mailto:info@dibachain.ir">info@dibachain.ir</a></div>
|
| 221 |
+
<div><b data-i="ftTry"></b><br><a href="https://dibaai-diba-agent.hf.space" target="_blank" rel="noopener" data-i="lAgent"></a><br><a href="https://huggingface.co/spaces/Dibachain/diba-stt" target="_blank" rel="noopener" data-i="lStt"></a><br><a href="https://huggingface.co/spaces/Dibachain/DibaOne-X1-demo" target="_blank" rel="noopener" data-i="lX1"></a></div>
|
| 222 |
+
<div><b data-i="ftModels"></b><br><a href="https://huggingface.co/Dibachain" target="_blank" rel="noopener" data-i="lOrg"></a><br><a href="https://huggingface.co/Dibachain/Diba-Base" target="_blank" rel="noopener">Diba-Base</a> · <a href="https://huggingface.co/Dibachain/Diba-mini" target="_blank" rel="noopener">Diba-mini</a> · <a href="https://huggingface.co/Dibachain/Diba-STT" target="_blank" rel="noopener">Diba-STT</a></div>
|
| 223 |
+
<div><b data-i="ftLegal"></b><br><span data-i="ftLegalD"></span></div>
|
| 224 |
+
</div></footer>
|
| 225 |
+
|
| 226 |
+
<script>
|
| 227 |
+
"use strict";
|
| 228 |
+
const $ = id => document.getElementById(id);
|
| 229 |
+
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
| 230 |
+
const faNum = s => String(s).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]).replace(/\./g, "٫");
|
| 231 |
+
const store = {get(k, d){try{const v = localStorage.getItem(k); return v === null ? d : JSON.parse(v);}catch(e){return d;}}, set(k, v){try{localStorage.setItem(k, JSON.stringify(v));}catch(e){}}};
|
| 232 |
+
|
| 233 |
+
/* ------------------------------------------------------------------ i18n */
|
| 234 |
+
const T = {
|
| 235 |
+
fa: {
|
| 236 |
+
brand:"دکو · Decho", brandSub:"گفتار فارسی · دیباچین", navStudio:"استودیو", navSamples:"نمونهها", navModels:"مدلها", navAbout:"دیباچین",
|
| 237 |
+
heroTitle1:"دکو؛ تبدیل متن به گفتار طبیعی فارسی", heroTitle2:"روی پردازندهی معمولی، حتی بلادرنگ",
|
| 238 |
+
heroLead:"دکو مدل گفتار فارسیِ ساختهی دیباچین است: متن فارسی را با تلفظ درست اعداد، تاریخها، کسرهی اضافه، کلمههای همنگاره و واژههای انگلیسی، با پنج صدای مختلف، احساس، گفتوگو و حالت پادکست میخواند؛ بدون نیاز به کارت گرافیک.",
|
| 239 |
+
b1:"فقط فارسی، از ابتدا تا انتها", b2:"ریلتایم روی CPU", b3:"۵ صدای آماده", b4:"خنده، آه، تعجب و احساس", b5:"گفتوگو و پادکست", b6:"۴۴٫۱ کیلوهرتز",
|
| 240 |
+
studioTitle:"استودیوی دکو", loading:"در حال آمادهسازی مدلها… (بار اول چند دقیقه)", ready:"آماده", queue:"در صف: {n}",
|
| 241 |
+
modeSingle:"یک گوینده", modeDialog:"گفتوگو", modePodcast:"🎙️ پادکست",
|
| 242 |
+
mRt:"⚡ ریلتایم", mRtD:"سریعتر از پخش صدا روی CPU؛ مناسب پاسخ زنده و متنهای بلند", mStd:"استاندارد", mStdD:"کیفیت بیشتر، کمی کندتر از ریلتایم", mStudio:"🎧 استودیو", mStudioD:"بهترین کیفیت و پایدارترین صدا؛ حدود دو برابر کندتر",
|
| 243 |
+
voice:"صدا", speed:"سرعت گفتار", text:"متن فارسی", advanced:"تنظیمات بیشتر",
|
| 244 |
+
smart:"خوانش هوشمند (هوش مصنوعی)", smartD:"املا را مرتب میکند و تلفظ دقیق هر جمله را با یک مدل زبانی مینویسد؛ با حساب Hugging Face خودتان.",
|
| 245 |
+
register:"لحن خوانش", regFormal:"رسمی (همانطور که نوشته شده)", regCasual:"خودمانی (محاورهی حرفهای تهرانی)",
|
| 246 |
+
live:"پاسخ زنده", liveD:"قطعهی اول کوتاه میشود تا صدا زودتر شروع شود.", seed:"بذر (برای تکرار همان خروجی)",
|
| 247 |
+
tagsHelp:"راهنما: برای عوضکردن گوینده وسط متن <code>[سارا]</code> یا <code>[رضا]</code> بنویسید؛ در گفتوگو هر خط «نام: متن». مکث دلخواه: <code>[pause:1.2]</code>، نفس: <code>[breath]</code>. خنده: «هاهاها» یا 😂، آه: 😮💨، گریه: «هقهق»، کشیدن: «خیلییییی».",
|
| 248 |
+
go:"▶ بساز و پخش کن", stop:"■ توقف", dlWav:"⬇ دانلود WAV", dlMp3:"⬇ دانلود MP3",
|
| 249 |
+
chars:"{n} از {m} نویسه",
|
| 250 |
+
gateAnon:"بدون ورود میتوانید جملههای کوتاه (تا {m} نویسه) را با یک گوینده امتحان کنید. برای متن بلند، گفتوگو، پادکست، مدل استودیو، خوانش هوشمند و دانلود MP3 با حساب Hugging Face وارد شوید.",
|
| 251 |
+
signIn:"ورود با Hugging Face", signOut:"خروج", newTab:"(در برگهی جدید)",
|
| 252 |
+
needSignIn:"برای این قابلیت با حساب Hugging Face وارد شوید.", tooLong:"متن طولانی است: حداکثر {m} نویسه.", limitAnon:"سقف استفادهی بدون ورود پر شد؛ با Hugging Face وارد شوید.", limitUser:"سقف استفادهی یک ساعته پر شد؛ کمی بعد دوباره امتحان کنید.",
|
| 253 |
+
hfCredits:"اعتبار Inference حساب Hugging Face شما تمام شده است. <a href='https://huggingface.co/settings/billing' target='_blank' rel='noopener'>شارژ حساب</a> یا <a href='https://huggingface.co/subscribe/pro' target='_blank' rel='noopener'>PRO</a>.", hfPerm:"اجازهی استفاده از Inference داده نشده است؛ یک بار خارج و دوباره وارد شوید.", llmFailed:"مدل زبانی پاسخ نداد؛ دوباره امتحان کنید.",
|
| 254 |
+
working:"در حال ساخت…", done:"تمام شد", stopped:"متوقف شد", errorX:"خطا: {e}",
|
| 255 |
+
stFirst:"زمان تا اولین صدا", stAudio:"کل صدا", stSynth:"زمان ساخت", stRtf:"نسبت سرعت (RTF)", sec:"ثانیه",
|
| 256 |
+
details:"جزئیات خوانش", readerLlm:"خوانش هوشمند", readerRules:"قواعد", uncertain:"واژههای مشکوک (برای اصلاح تلفظ کلیک کنید):", reading:"خوانش مدل",
|
| 257 |
+
voiceDesc:{default:"پیشفرض — زن، بزرگسال، آرام و کتابخوانی", sara:"سارا — زن، ۲۰ تا ۳۰ ساله، گرم و میانه", maryam:"مریم — زن، ۲۰ تا ۳۰ ساله، زیر و روشن", reza:"رضا — مرد، ۴۰ تا ۵۰ ساله، بم و پخته", ali:"علی — مرد، ۳۰ تا ۴۰ ساله، روشنتر و جوانتر"},
|
| 258 |
+
voiceName:{default:"پیشفرض", sara:"سارا", maryam:"مریم", reza:"رضا", ali:"علی"},
|
| 259 |
+
exNums:"عدد و تاریخ", exEmo:"احساس", exCasual:"محاوره", exLoan:"واژههای انگلیسی", exLong:"متن بلند", exDialog:"گفتوگو",
|
| 260 |
+
podName:"نام پادکست", podTone:"لحن", toneCasual:"خودمانی", toneFormal:"رسمی", podMin:"طول (دقیقه)", podPace:"ریتم", podSrc:"موضوع یا متن منبع", podSrcPh:"مثلاً: آیندهی هوش مصنوعی در ایران، یا یک مقاله را اینجا بچسبانید…", podWrite:"✍️ نوشتن متن پادکست با هوش مصنوعی", host:"میزبان {n}", hostName:"نام", hostVoice:"صدا", podWriting:"در حال نوشتن…", podDone:"متن آماده است؛ ویرایش کنید و بسازید.",
|
| 261 |
+
lexTitle:"📖 دیکشنری تلفظ شخصی", lexSub:"تلفظ واژههایی را که درست خوانده نمیشوند تعیین کنید. این دیکشنری فقط در مرورگر شما ذخیره میشود و برای همهی ساختهای شما اعمال میشود.",
|
| 262 |
+
lexWord:"واژه", lexReading:"تلفظ", lexReadingPh:"با اِعراب فارسی (مثلاً مُهر) یا لاتین (مثلاً r:mohr)", lexSentence:"جملهی نمونه (اختیاری، برای انتخاب بهتر)", lexSuggest:"🔎 پیشنهاد تلفظ", lexSay:"▶ بشنو", lexSave:"✓ ذخیره", lexEmpty:"هنوز واژهای ذخیره نشده است.", lexSaved:"ذخیره شد.", lexNoCand:"پیشنهادی پیدا نشد.", lexUse:"انتخاب", lexDel:"حذف", lexLlmNote:"برای پیشنهادهای هوش مصنوعی وارد شوید؛ فعلاً فقط خوانش قواعد نمایش داده میشود.",
|
| 263 |
+
sampTitle:"🎧 نمونهها", sampSub:"نمونههای آماده؛ بدون ورود و بدون صبر گوش کنید.",
|
| 264 |
+
modTitle:"مدلهای دکو", modSub:"دو مدل، یک موتور فارسی؛ هر دو روی پردازندهی معمولی (ONNX Runtime) اجرا میشوند.",
|
| 265 |
+
dcTitle:"دربارهی دیباچین",
|
| 266 |
+
ftAbout:"شرکت هوش مصنوعی ایرانی برای بومیسازی هوش مصنوعی به زبان فارسی.", ftTry:"محصولات دیگر", lAgent:"🤖 دیبا ایجنت", lStt:"🎙️ گفتار به متن فارسی", lX1:"🧭 دموی DibaOne X1", ftModels:"مدلها", lOrg:"دیباچین در Hugging Face", ftLegal:"قوانین", ftLegalD:"مدلهای دکو اختصاصیاند (© دیباچین). صدای ساختهشده را ساختگی اعلام کنید؛ استفاده برای فریب، جعل هویت یا هر کار غیرقانونی ممنوع است.",
|
| 267 |
+
copied:"کپی شد", },
|
| 268 |
+
en: {
|
| 269 |
+
brand:"Decho · دکو", brandSub:"Persian speech · Dibachain", navStudio:"Studio", navSamples:"Samples", navModels:"Models", navAbout:"Dibachain",
|
| 270 |
+
heroTitle1:"Decho: natural Persian text-to-speech", heroTitle2:"on an ordinary CPU, even in real time",
|
| 271 |
+
heroLead:"Decho is Dibachain's Persian speech model. It reads Persian text with correct numbers, dates, ezafe, homographs and English loanwords, in five voices, with emotion, dialogue and a podcast mode — no GPU needed. The voice speaks Persian only.",
|
| 272 |
+
b1:"Persian only, end to end", b2:"Real-time on CPU", b3:"5 built-in voices", b4:"Laughter, sighs, surprise, emotion", b5:"Dialogue & podcasts", b6:"44.1 kHz",
|
| 273 |
+
studioTitle:"Decho Studio", loading:"Preparing the models… (a few minutes the first time)", ready:"Ready", queue:"In queue: {n}",
|
| 274 |
+
modeSingle:"Single voice", modeDialog:"Dialogue", modePodcast:"🎙️ Podcast",
|
| 275 |
+
mRt:"⚡ Real-time", mRtD:"Faster than playback on a CPU; best for live replies and long texts", mStd:"Standard", mStdD:"Higher quality, a little slower than real-time", mStudio:"🎧 Studio", mStudioD:"Best quality and the steadiest voice; about twice as slow",
|
| 276 |
+
voice:"Voice", speed:"Speaking rate", text:"Persian text", advanced:"More settings",
|
| 277 |
+
smart:"Smart reading (AI)", smartD:"A language model tidies the spelling and writes the exact pronunciation of each sentence — on your own Hugging Face account.",
|
| 278 |
+
register:"Reading style", regFormal:"Formal (as written)", regCasual:"Casual (polished spoken Tehrani)",
|
| 279 |
+
live:"Live reply", liveD:"A short first chunk, so the audio starts sooner.", seed:"Seed (repeat the same output)",
|
| 280 |
+
tagsHelp:"Tips: switch speakers mid-text with <code>[سارا]</code> or <code>[رضا]</code>; in a dialogue, one line per turn as «name: text». Pause: <code>[pause:1.2]</code>, breath: <code>[breath]</code>. Laughter: «هاهاها» or 😂, sigh: 😮💨, crying: «هقهق», elongation: «خیلییییی».",
|
| 281 |
+
go:"▶ Generate & play", stop:"■ Stop", dlWav:"⬇ Download WAV", dlMp3:"⬇ Download MP3",
|
| 282 |
+
chars:"{n} / {m} characters",
|
| 283 |
+
gateAnon:"Without signing in you can try short sentences (up to {m} characters) with one voice. Sign in with Hugging Face for long texts, dialogue, podcasts, the Studio model, smart reading and MP3 download.",
|
| 284 |
+
signIn:"Sign in with Hugging Face", signOut:"Sign out", newTab:"(new tab)",
|
| 285 |
+
needSignIn:"Sign in with Hugging Face to use this.", tooLong:"Text too long: at most {m} characters.", limitAnon:"The signed-out limit is reached; sign in with Hugging Face.", limitUser:"Hourly limit reached; please try again a little later.",
|
| 286 |
+
hfCredits:"Your Hugging Face Inference credit is used up. <a href='https://huggingface.co/settings/billing' target='_blank' rel='noopener'>Add credit</a> or <a href='https://huggingface.co/subscribe/pro' target='_blank' rel='noopener'>go PRO</a>.", hfPerm:"Inference permission was not granted; sign out and sign in again.", llmFailed:"The language model did not answer; please try again.",
|
| 287 |
+
working:"Generating…", done:"Done", stopped:"Stopped", errorX:"Error: {e}",
|
| 288 |
+
stFirst:"Time to first audio", stAudio:"Audio", stSynth:"Synthesis time", stRtf:"Real-time factor", sec:"s",
|
| 289 |
+
details:"Reading details", readerLlm:"smart reading", readerRules:"rules", uncertain:"Uncertain words (click to fix the pronunciation):", reading:"Model reading",
|
| 290 |
+
voiceDesc:{default:"Default — female, adult, calm narration", sara:"Sara — female, 20–30, warm, mid pitch", maryam:"Maryam — female, 20–30, higher, bright", reza:"Reza — male, 40–50, deep, mature", ali:"Ali — male, 30–40, brighter, younger"},
|
| 291 |
+
voiceName:{default:"Default", sara:"Sara", maryam:"Maryam", reza:"Reza", ali:"Ali"},
|
| 292 |
+
exNums:"Numbers & dates", exEmo:"Emotion", exCasual:"Colloquial", exLoan:"English words", exLong:"Long text", exDialog:"Dialogue",
|
| 293 |
+
podName:"Podcast name", podTone:"Tone", toneCasual:"Casual", toneFormal:"Formal", podMin:"Length (minutes)", podPace:"Pace", podSrc:"Topic or source text", podSrcPh:"e.g. the future of AI in Iran — or paste an article here…", podWrite:"✍️ Write the podcast script with AI", host:"Host {n}", hostName:"Name", hostVoice:"Voice", podWriting:"Writing…", podDone:"Script ready — edit it, then generate.",
|
| 294 |
+
lexTitle:"📖 Personal pronunciation dictionary", lexSub:"Set how words that are misread should be pronounced. It is stored only in your browser and applied to everything you generate.",
|
| 295 |
+
lexWord:"Word", lexReading:"Pronunciation", lexReadingPh:"Persian with vowel marks (e.g. مُهر) or Latin (e.g. r:mohr)", lexSentence:"Example sentence (optional, for better suggestions)", lexSuggest:"🔎 Suggest", lexSay:"▶ Listen", lexSave:"✓ Save", lexEmpty:"No words saved yet.", lexSaved:"Saved.", lexNoCand:"No suggestion found.", lexUse:"Use", lexDel:"Delete", lexLlmNote:"Sign in for AI suggestions; only the rule-based reading is shown for now.",
|
| 296 |
+
sampTitle:"🎧 Samples", sampSub:"Ready-made samples — listen without signing in, no waiting.",
|
| 297 |
+
modTitle:"The Decho models", modSub:"Two models, one Persian engine; both run on an ordinary CPU (ONNX Runtime).",
|
| 298 |
+
dcTitle:"About Dibachain",
|
| 299 |
+
ftAbout:"An Iranian AI company building localized, Persian-first artificial intelligence.", ftTry:"Other products", lAgent:"🤖 Diba Agent", lStt:"🎙️ Persian speech-to-text", lX1:"🧭 DibaOne X1 demo", ftModels:"Models", lOrg:"Dibachain on Hugging Face", ftLegal:"Terms", ftLegalD:"The Decho models are proprietary (© Dibachain). Disclose that generated speech is synthetic; fraud, impersonation or any unlawful use is prohibited.",
|
| 300 |
+
copied:"Copied", }
|
| 301 |
+
};
|
| 302 |
+
let LANG = store.get("decho.lang", /^fa|^fa-/i.test(navigator.language || "") || /^fa/i.test((navigator.languages || []).join(",")) ? "fa" : "en");
|
| 303 |
+
const t = (k, v) => { let s = (T[LANG] && T[LANG][k]) ?? T.fa[k] ?? k; if (v) for (const [a, b] of Object.entries(v)) s = s.replace("{" + a + "}", LANG === "fa" ? faNum(b) : b); return s; };
|
| 304 |
+
const num = x => LANG === "fa" ? faNum(x) : String(x);
|
| 305 |
+
|
| 306 |
+
/* ------------------------------------------------------------------ content blocks (bilingual) */
|
| 307 |
+
const MODELS_HTML = {
|
| 308 |
+
fa: `<div class="cols">
|
| 309 |
+
<div><h3>⚡ Decho-v2-rt — مدل ریلتایم</h3><p>نسخهی سبکشدهی Decho-v2 که برای اجرای بلادرنگ روی پردازندهی معمولی آموزش دیده است: در هر گام فقط یک بار مدل اجرا میشود و به راهنمای جداگانه نیاز ندارد. روی یک ماشین ابری ۲ هستهای، صدا سریعتر از پخش ساخته میشود و اولین تکه در حدود ۲ تا ۳ ثانیه آماده است.</p></div>
|
| 310 |
+
<div><h3>Decho-v2 — مدل اصلی</h3><p>مدل کامل با بیشترین دقت تلفظ. در دو نسخه: int8 (استاندارد، سبک و سریع) و fp32 (استودیو، بهترین کیفیت و پایدارترین تمبر در جملههای احساسی).</p></div></div>
|
| 311 |
+
<h3>کیفیت اندازهگیریشده (۴۰ جملهی ثابت فارسی)</h3>
|
| 312 |
+
<table><thead><tr><th>مدل</th><th>خطای نویسه (CER) ↓</th><th>طبیعیبودن (UTMOS) ↑</th><th>نسبت سرعت روی CPU ↓</th></tr></thead><tbody>
|
| 313 |
+
<tr><td>Decho-v2 · استودیو (fp32)</td><td>۳٫۵٪</td><td>۳٫۹۶</td><td>حدود ۲٫۵</td></tr>
|
| 314 |
+
<tr><td>Decho-v2 · استاندارد (int8، ۸ گام)</td><td>۴٫۲٪</td><td>۳٫۸۹</td><td>۱٫۱ تا ۱٫۸</td></tr>
|
| 315 |
+
<tr><td>Decho-v2-rt · ریلتایم</td><td>۶٫۷٪</td><td>۳٫۳۸</td><td>۰٫۷ تا ۰٫۹ ✓ بلادرنگ</td></tr></tbody></table>
|
| 316 |
+
<p class="meta">CER با Whisper-large-v3 روی صدای ساختهشده؛ UTMOS امتیاز خودکار طبیعیبودن (فقط برای مقایسهی نسبی). نسبت سرعت = زمان ساخت ÷ طول صدا روی یک ماشین ۲ هستهای؛ کمتر از ۱ یعنی سریعتر از پخش. کیفیت استودیو با ۸ گام روی همین ۴۰ جمله سنجیده شده است؛ تنظیم استودیو ۱۶ گام دارد که در آزمونهای ما صدا را در جملههای احساسی پایدارتر نگه داشت.</p>
|
| 317 |
+
<h3>پیشپردازش فارسیِ اختصاصی</h3>
|
| 318 |
+
<ul class="tick"><li>عدد، تاریخ، ساعت، پول، درصد و اختصار: «۱۵ مهر ۱۴۰۳» ← «پانزدهمِ مهرِ هزار و چهارصد و سه»</li>
|
| 319 |
+
<li>کسرهی اضافه و همنگارهها با درک کل جمله: «مُهرِ نامه» / «ماهِ مهر»، «دوی ۱۰۰ متر» / «دو کتاب»</li>
|
| 320 |
+
<li>محاوره: «نمیدونم»، «رسیدیم خونه»، «بهت گفتم»</li>
|
| 321 |
+
<li>واژهها و برندهای انگلیسی، حتی با حروف لاتین وسط متن: Wi-Fi، ChatGPT، iPhone، PDF</li>
|
| 322 |
+
<li>احساس: خنده، آه، گریه، تعجب، کشیدن واژهها و علامتهای «!»، «؟!» و «…»</li>
|
| 323 |
+
<li>خوانش هوشمند اختیاری با یک مدل زبانی (رسمی یا خودمانی) و دیکشنری تلفظ شخصی</li></ul>
|
| 324 |
+
<h3>صداها</h3>
|
| 325 |
+
<table><thead><tr><th>نام</th><th>توضیح</th></tr></thead><tbody>
|
| 326 |
+
<tr><td>پیشفرض</td><td>زن، بزرگسال، لحن آرام و کتابخوانی</td></tr><tr><td>سارا</td><td>زن، ۲۰ تا ۳۰ ساله، گرم و میانه</td></tr><tr><td>مریم</td><td>زن، ۲۰ تا ۳۰ ساله، زیر و روشن</td></tr><tr><td>رضا</td><td>مرد، ۴۰ تا ۵۰ ساله، بم و پخته</td></tr><tr><td>علی</td><td>مرد، ۳۰ تا ۴۰ ساله، روشنتر و جوانتر</td></tr></tbody></table>
|
| 327 |
+
<h3>محدودیتها</h3><ul class="tick"><li>فقط فارسی صحبت میکند؛ جملههای کامل انگلیسی تقریبی خوانده میشوند.</li><li>نامها و واژههای بسیار کمیاب ممکن است اشتباه خوانده شوند؛ با دیکشنری تلفظ اصلاح کنید.</li><li>امتیازهای خودکار تقریبیاند؛ معیار اصلی گوش شماست.</li></ul>
|
| 328 |
+
<h3>استفادهی تجاری و API</h3><p>مدلهای دکو اختصاصیاند. برای API، اجرای روی سرور خودتان، صدای اختصاصی یا همکاری با ما تماس بگیرید: <a href="mailto:info@dibachain.ir">info@dibachain.ir</a> · <a href="https://dibachain.ir" target="_blank" rel="noopener">dibachain.ir</a></p>`,
|
| 329 |
+
en: `<div class="cols">
|
| 330 |
+
<div><h3>⚡ Decho-v2-rt — real-time model</h3><p>A lighter version of Decho-v2 trained to run in real time on an ordinary CPU: one model pass per step and no separate guidance pass. On a 2-core cloud machine it generates speech faster than playback, and the first chunk is ready in about 2–3 seconds.</p></div>
|
| 331 |
+
<div><h3>Decho-v2 — main model</h3><p>The full model with the most accurate pronunciation. Two builds: int8 (Standard, small and fast) and fp32 (Studio, best quality and the steadiest timbre in emotional sentences).</p></div></div>
|
| 332 |
+
<h3>Measured quality (40 fixed Persian sentences)</h3>
|
| 333 |
+
<table><thead><tr><th>Model</th><th>Character error (CER) ↓</th><th>Naturalness (UTMOS) ↑</th><th>CPU real-time factor ↓</th></tr></thead><tbody>
|
| 334 |
+
<tr><td>Decho-v2 · Studio (fp32)</td><td>3.5%</td><td>3.96</td><td>about 2.5</td></tr>
|
| 335 |
+
<tr><td>Decho-v2 · Standard (int8, 8 steps)</td><td>4.2%</td><td>3.89</td><td>1.1–1.8</td></tr>
|
| 336 |
+
<tr><td>Decho-v2-rt · Real-time</td><td>6.7%</td><td>3.38</td><td>0.7–0.9 ✓ real time</td></tr></tbody></table>
|
| 337 |
+
<p class="meta">CER by Whisper-large-v3 on the generated audio; UTMOS is an automatic naturalness score (for relative comparison only). Real-time factor = synthesis time ÷ audio length on a 2-core machine; below 1 means faster than playback. Studio quality was measured at 8 steps on the same 40 sentences; the Studio setting uses 16 steps, which kept the voice steadiest across emotional sentences in our tests.</p>
|
| 338 |
+
<h3>Our own Persian front end</h3>
|
| 339 |
+
<ul class="tick"><li>Numbers, dates, times, money, percentages and abbreviations: «۱۵ مهر ۱۴۰۳» → “the fifteenth of Mehr, fourteen hundred and three”</li>
|
| 340 |
+
<li>Ezafe and homographs from the whole sentence: «مُهرِ نامه» / «ماهِ مهر», «دوی ۱۰۰ متر» / «دو کتاب»</li>
|
| 341 |
+
<li>Colloquial Persian: «نمیدونم», «رسیدیم خونه», «بهت گفتم»</li>
|
| 342 |
+
<li>English words and brands, even in Latin letters inside Persian text: Wi-Fi, ChatGPT, iPhone, PDF</li>
|
| 343 |
+
<li>Emotion: laughter, sighs, crying, surprise, elongated words and «!», «؟!», «…»</li>
|
| 344 |
+
<li>Optional AI smart reading (formal or casual) and a personal pronunciation dictionary</li></ul>
|
| 345 |
+
<h3>Voices</h3>
|
| 346 |
+
<table><thead><tr><th>Name</th><th>Description</th></tr></thead><tbody>
|
| 347 |
+
<tr><td>Default</td><td>female, adult, calm narration</td></tr><tr><td>Sara</td><td>female, 20–30, warm, mid pitch</td></tr><tr><td>Maryam</td><td>female, 20–30, higher, bright</td></tr><tr><td>Reza</td><td>male, 40–50, deep, mature</td></tr><tr><td>Ali</td><td>male, 30–40, brighter, younger</td></tr></tbody></table>
|
| 348 |
+
<h3>Limitations</h3><ul class="tick"><li>It speaks Persian only; whole English sentences are approximated.</li><li>Very rare names and words may be misread; fix them with the pronunciation dictionary.</li><li>Automatic scores are proxies; judge by listening.</li></ul>
|
| 349 |
+
<h3>Commercial use & API</h3><p>The Decho models are proprietary. For an API, on-premise deployment, a custom voice or a partnership, contact us: <a href="mailto:info@dibachain.ir">info@dibachain.ir</a> · <a href="https://dibachain.ir" target="_blank" rel="noopener">dibachain.ir</a></p>`};
|
| 350 |
+
const DC_HTML = {
|
| 351 |
+
fa: `<p><b>دیباچین</b> یک شرکت ایرانیِ هوش مصنوعی و نرمافزار است. مأموریت ما <b>بومیسازی هوش مصنوعی برای زبان فارسی</b> است؛ ساختن هوش مصنوعیای که فارسی، ایران و نیاز کسبوکارها و کاربران ایرانی را واقعاً میفهمد. دکو، مدل گفتار فارسی دیباچین، کنار خانوادهی مدلهای زبانی <b>دیبا</b> ساخته شده است.</p>
|
| 352 |
+
<div class="cols"><div><h3>خانوادهی دیبا</h3><ul class="tick">
|
| 353 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-Base" target="_blank" rel="noopener">Diba-Base</a>: مدل زبانی بزرگ ایرانی، گفتگو و کدنویسی</li>
|
| 354 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-mini" target="_blank" rel="noopener">Diba-mini</a>: مدل زبانی کوچک و آفلاین برای CPU</li>
|
| 355 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-Embed" target="_blank" rel="noopener">Diba-Embed</a>: بردارسازی متن فارسی برای جستوجو و RAG</li>
|
| 356 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-Vision" target="_blank" rel="noopener">Diba-Vision</a>: بینایی و پاسخ فارسی</li>
|
| 357 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-STT" target="_blank" rel="noopener">Diba-STT</a>: تبدیل گفتار فارسی به متن، بلادرنگ</li>
|
| 358 |
+
<li><a href="https://huggingface.co/Dibachain/DibaOne-X1" target="_blank" rel="noopener">DibaOne X1</a>: مدل تصمیمگیری فارسیمحور برای عاملها</li>
|
| 359 |
+
<li><b>Decho</b>: تبدیل متن به گفتار فارسی (همین صفحه)</li></ul></div>
|
| 360 |
+
<div><h3>خدمات</h3><ul class="tick"><li>مشاورهی هوش مصنوعی و استراتژی کسبوکار</li><li>نرمافزار اختصاصی، CRM و ERP هوشمند، چتبات و دستیار</li><li>تحول دیجیتال و اتوماسیون فرآیندها</li><li>تحقیق و توسعهی مدلهای زبانی و گفتاری فارسی</li></ul>
|
| 361 |
+
<h3>تماس</h3><p>🌐 <a href="https://dibachain.ir" target="_blank" rel="noopener">dibachain.ir</a><br>✉️ <a href="mailto:info@dibachain.ir">info@dibachain.ir</a><br>🤗 <a href="https://huggingface.co/Dibachain" target="_blank" rel="noopener">huggingface.co/Dibachain</a><br>🤖 <a href="https://dibaai-diba-agent.hf.space" target="_blank" rel="noopener">دیبا ایجنت</a></p></div></div>`,
|
| 362 |
+
en: `<p><b>Dibachain</b> is an Iranian artificial-intelligence and software company. Our mission is the <b>localization of AI for Persian</b>: AI that genuinely understands the Persian language, Iran, and the needs of Iranian businesses and users. Decho, Dibachain's Persian speech model, is built alongside the <b>Diba</b> family of language models.</p>
|
| 363 |
+
<div class="cols"><div><h3>The Diba family</h3><ul class="tick">
|
| 364 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-Base" target="_blank" rel="noopener">Diba-Base</a>: Iranian large language model for chat and code</li>
|
| 365 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-mini" target="_blank" rel="noopener">Diba-mini</a>: small, offline Persian model for CPUs</li>
|
| 366 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-Embed" target="_blank" rel="noopener">Diba-Embed</a>: Persian text embeddings for search and RAG</li>
|
| 367 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-Vision" target="_blank" rel="noopener">Diba-Vision</a>: sees images, answers in Persian</li>
|
| 368 |
+
<li><a href="https://huggingface.co/Dibachain/Diba-STT" target="_blank" rel="noopener">Diba-STT</a>: real-time Persian speech-to-text</li>
|
| 369 |
+
<li><a href="https://huggingface.co/Dibachain/DibaOne-X1" target="_blank" rel="noopener">DibaOne X1</a>: Persian-first decision model for agents</li>
|
| 370 |
+
<li><b>Decho</b>: Persian text-to-speech (this page)</li></ul></div>
|
| 371 |
+
<div><h3>What we do</h3><ul class="tick"><li>AI consulting and business strategy</li><li>Custom software, smart CRM/ERP, chatbots and assistants</li><li>Digital transformation and process automation</li><li>R&D on Persian language and speech models</li></ul>
|
| 372 |
+
<h3>Contact</h3><p>🌐 <a href="https://dibachain.ir" target="_blank" rel="noopener">dibachain.ir</a><br>✉️ <a href="mailto:info@dibachain.ir">info@dibachain.ir</a><br>🤗 <a href="https://huggingface.co/Dibachain" target="_blank" rel="noopener">huggingface.co/Dibachain</a><br>🤖 <a href="https://dibaai-diba-agent.hf.space" target="_blank" rel="noopener">Diba Agent</a></p></div></div>`};
|
| 373 |
+
const SAMPLES = [
|
| 374 |
+
{f:"voice_default.mp3", fa:"صدای پیشفرض · تاریخ", en:"Default voice · dates", text:"سلام! من دکو هستم، صدای فارسی ساختهی دیباچین. امروز ۱۵ مهر ۱۴۰۳ است."},
|
| 375 |
+
{f:"voice_sara.mp3", fa:"سارا · احساس", en:"Sara · emotion", text:"وای، باورم نمیشه! بالاخره قبول شدم! هاهاها"},
|
| 376 |
+
{f:"voice_reza.mp3", fa:"رضا · عدد و پول", en:"Reza · numbers & money", text:"قیمت این گوشی ۲۴,۵۰۰,۰۰۰ تومان است و ۱۵٪ تخفیف دارد."},
|
| 377 |
+
{f:"voice_maryam.mp3", fa:"مریم · محاوره", en:"Maryam · colloquial", text:"سلام بچهها! امروز میخوایم دربارهی هوش مصنوعی و آیندهی کار حرف بزنیم."},
|
| 378 |
+
{f:"voice_ali.mp3", fa:"علی · واژههای انگلیسی", en:"Ali · English words", text:"رمز Wi-Fi را بفرست تا آپدیت جدید iPhone را دانلود کنم."},
|
| 379 |
+
{f:"dialogue.mp3", fa:"گفتوگو (سارا و رضا)", en:"Dialogue (Sara & Reza)", text:"سارا: سلام! کجا بودی؟ خیلی وقته ندیدمت.\nرضا: سلام! تازه از سفر برگشتم، خیلی خوش گذشت."},
|
| 380 |
+
{f:"realtime.mp3", fa:"⚡ مدل ریلتایم", en:"⚡ Real-time model", text:"این نمونه با مدل ریلتایم ساخته شده است؛ روی پردازندهی معمولی، سریعتر از پخش صدا."},
|
| 381 |
+
];
|
| 382 |
+
const EXAMPLES = {
|
| 383 |
+
exNums:"جلسهی بعدی در تاریخ ۱۴۰۳/۰۸/۱۵ ساعت ۱۰:۳۰ برگزار میشود و قیمت بلیت ۲۴۵,۰۰۰ تومان است.",
|
| 384 |
+
exEmo:"وای! باورم نمیشه، بالاخره قبول شدم!! هاهاها. آه… ولی خیلی خستهام. 😮💨",
|
| 385 |
+
exCasual:"راستش نمیدونم از کجا شروع کنم، ولی بذارید از یه خاطره بگم؛ دیروز با دوستم رفتیم کافه.",
|
| 386 |
+
exLoan:"در Google جستوجو کردم و آپدیت جدید را روی لپتاپ نصب کردم؛ رمز Wi-Fi را هم برایت فرستادم.",
|
| 387 |
+
exLong:"پژوهشگران دانشگاه در گزارشی که هفتهی گذشته منتشر شد، اعلام کردند که استفاده از انرژیهای تجدیدپذیر در ده سال آینده میتواند هزینهی برق خانوارها را به میزان قابل توجهی کاهش دهد. آنها همچنین تأکید کردند که آلودگی هوای شهرهای بزرگ نیز کمتر خواهد شد.",
|
| 388 |
+
exDialog:"سارا: سلام! حالت چطوره؟ خیلی وقته ندیدمت.\nرضا: سلام! خوبم، ممنون. تو چطوری؟ این مدت کجا بودی؟\nسارا: یه سفر کاری رفته بودم، دیروز برگشتم. [pause:0.6] هاهاها، کلی ماجرا دارم!"
|
| 389 |
+
};
|
| 390 |
+
|
| 391 |
+
/* ------------------------------------------------------------------ state */
|
| 392 |
+
let ME = {signed_in: false}, STATUS = null, MODE = "single", MODEL = store.get("decho.model", "rt"), VOICES = [];
|
| 393 |
+
let LEX = store.get("decho.lexicon", {});
|
| 394 |
+
let run = 0, sid = null, ctx = null, nextT = 0, pcm = [], sr = 44100, t0 = 0, firstAt = null, tSynth = 0, tAudio = 0;
|
| 395 |
+
|
| 396 |
+
function applyLang() {
|
| 397 |
+
document.documentElement.lang = LANG; document.documentElement.dir = LANG === "fa" ? "rtl" : "ltr";
|
| 398 |
+
document.querySelectorAll("[data-i]").forEach(el => { const k = el.dataset.i; const v = t(k); if (/<[a-z]/i.test(v)) el.innerHTML = v; else el.textContent = v; });
|
| 399 |
+
document.querySelectorAll("[data-ph]").forEach(el => el.placeholder = t(el.dataset.ph));
|
| 400 |
+
document.querySelectorAll(".lang button").forEach(b => b.classList.toggle("on", b.dataset.l === LANG));
|
| 401 |
+
$("modBody").innerHTML = MODELS_HTML[LANG]; $("dcBody").innerHTML = DC_HTML[LANG];
|
| 402 |
+
document.title = LANG === "fa" ? "دکو · Decho — تبدیل متن به گفتار فارسی | دیباچین" : "Decho · دکو — Persian Text-to-Speech | Dibachain";
|
| 403 |
+
renderAccount(); renderVoices(); renderExamples(); renderSamples(); renderLex(); renderHosts(); updateCount(); renderGate(); renderStatus();
|
| 404 |
+
}
|
| 405 |
+
document.querySelectorAll(".lang button").forEach(b => b.onclick = () => { LANG = b.dataset.l; store.set("decho.lang", LANG); applyLang(); });
|
| 406 |
+
|
| 407 |
+
function toast(msg) { const d = document.createElement("div"); d.className = "toast"; d.innerHTML = msg; document.body.append(d); setTimeout(() => d.remove(), 4200); }
|
| 408 |
+
function errMsg(code, lim) {
|
| 409 |
+
const m = {sign_in: t("needSignIn"), too_long: t("tooLong", {m: lim}), too_long_anon: t("tooLong", {m: lim}), limit_anon: t("limitAnon"),
|
| 410 |
+
limit_user: t("limitUser"), hf_credits: t("hfCredits"), hf_permission: t("hfPerm"), llm_failed: t("llmFailed")};
|
| 411 |
+
return m[code] || t("errorX", {e: esc(code)});
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* ------------------------------------------------------------------ account */
|
| 415 |
+
const inIframe = (() => { try { return window.self !== window.top; } catch (e) { return true; } })();
|
| 416 |
+
function loginHref() {
|
| 417 |
+
const host = (STATUS && STATUS.space_host) || location.host;
|
| 418 |
+
return inIframe ? `https://${host}/oauth/huggingface/login?_target_url=%2F` : `/oauth/huggingface/login?_target_url=%2F`;
|
| 419 |
+
}
|
| 420 |
+
function renderAccount() {
|
| 421 |
+
const a = $("account");
|
| 422 |
+
if (!STATUS || !STATUS.oauth) { a.innerHTML = ""; return; }
|
| 423 |
+
if (ME.signed_in) {
|
| 424 |
+
a.innerHTML = `<div class="user">${ME.avatar ? `<img src="${esc(ME.avatar)}" alt="">` : ""}<span>${esc(ME.name || ME.username)}</span><a class="btn small" href="/oauth/huggingface/logout">${t("signOut")}</a></div>`;
|
| 425 |
+
} else {
|
| 426 |
+
a.innerHTML = `<a class="btn hf" href="${loginHref()}" ${inIframe ? 'target="_blank" rel="noopener"' : ""}><img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="">${t("signIn")}${inIframe ? " <small>" + t("newTab") + "</small>" : ""}</a>`;
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
async function loadMe() { try { ME = await (await fetch("api/me", {credentials: "include"})).json(); } catch (e) { ME = {signed_in: false}; } renderAccount(); renderGate(); renderModels(); updateCount(); }
|
| 430 |
+
|
| 431 |
+
/* ------------------------------------------------------------------ status */
|
| 432 |
+
function renderStatus() {
|
| 433 |
+
if (!STATUS) return;
|
| 434 |
+
$("dot").className = "status-dot" + (STATUS.ready ? " ok" : STATUS.error ? " err" : "");
|
| 435 |
+
$("st").textContent = STATUS.error ? t("errorX", {e: STATUS.error}) : STATUS.ready ? (t("ready") + (STATUS.queue ? " · " + t("queue", {n: STATUS.queue}) : "")) : t("loading");
|
| 436 |
+
}
|
| 437 |
+
async function poll() {
|
| 438 |
+
try { STATUS = await (await fetch("api/status", {credentials: "include"})).json(); } catch (e) { setTimeout(poll, 5000); return; }
|
| 439 |
+
VOICES = STATUS.voices || []; renderStatus(); renderVoices(); renderAccount(); renderGate(); renderModels(); updateCount(); renderHosts();
|
| 440 |
+
setTimeout(poll, STATUS.ready ? 20000 : 4000);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/* ------------------------------------------------------------------ studio UI */
|
| 444 |
+
const vName = v => (T[LANG].voiceName || {})[v] || v;
|
| 445 |
+
function renderVoices() {
|
| 446 |
+
const sel = $("voice"), cur = sel.value || store.get("decho.voice", "default"); sel.innerHTML = "";
|
| 447 |
+
(VOICES.length ? VOICES : [{name: "default"}]).forEach(v => sel.append(new Option(((T[LANG].voiceDesc || {})[v.name]) || v.name, v.name)));
|
| 448 |
+
sel.value = VOICES.some(v => v.name === cur) ? cur : "default";
|
| 449 |
+
$("voiceChips").innerHTML = MODE === "single" ? "" : VOICES.map(v => `<button class="chip" data-ins="${esc(T.fa.voiceName[v.name] || v.name)}">${esc(T.fa.voiceName[v.name] || v.name)}:</button>`).join("");
|
| 450 |
+
$("voiceChips").querySelectorAll("[data-ins]").forEach(b => b.onclick = () => { const x = $("txt"); x.value = (x.value && !x.value.endsWith("\n") ? x.value + "\n" : x.value) + b.dataset.ins + ": "; x.focus(); updateCount(); });
|
| 451 |
+
}
|
| 452 |
+
$("voice").onchange = () => store.set("decho.voice", $("voice").value);
|
| 453 |
+
function renderExamples() {
|
| 454 |
+
const keys = MODE === "single" ? ["exNums", "exEmo", "exCasual", "exLoan", "exLong"] : ["exDialog"];
|
| 455 |
+
$("examples").innerHTML = keys.map(k => `<button class="chip" data-ex="${k}">${t(k)}</button>`).join("");
|
| 456 |
+
$("examples").querySelectorAll("[data-ex]").forEach(b => b.onclick = () => { $("txt").value = EXAMPLES[b.dataset.ex]; updateCount(); });
|
| 457 |
+
}
|
| 458 |
+
function limit() { return (STATUS && STATUS.limits && STATUS.limits.chars) || 220; }
|
| 459 |
+
function updateCount() { const n = $("txt").value.length; $("count").textContent = t("chars", {n, m: limit()}); $("count").className = "meta" + (n > limit() ? " err" : ""); }
|
| 460 |
+
$("txt").addEventListener("input", updateCount);
|
| 461 |
+
function renderGate() {
|
| 462 |
+
const g = $("gate");
|
| 463 |
+
if (STATUS && STATUS.oauth && !ME.signed_in) { g.classList.remove("hidden"); g.innerHTML = t("gateAnon", {m: limit()}) + ` <a href="${loginHref()}" ${inIframe ? 'target="_blank" rel="noopener"' : ""}>${t("signIn")}</a>`; }
|
| 464 |
+
else g.classList.add("hidden");
|
| 465 |
+
}
|
| 466 |
+
function renderModels() {
|
| 467 |
+
document.querySelectorAll("#modelPick .model").forEach(d => {
|
| 468 |
+
d.classList.toggle("on", d.dataset.k === MODEL);
|
| 469 |
+
d.classList.toggle("locked", d.dataset.k === "fp32" && STATUS && STATUS.oauth && !ME.signed_in);
|
| 470 |
+
});
|
| 471 |
+
}
|
| 472 |
+
document.querySelectorAll("#modelPick .model").forEach(d => d.onclick = () => {
|
| 473 |
+
if (d.dataset.k === "fp32" && STATUS && STATUS.oauth && !ME.signed_in) { toast(t("needSignIn")); return; }
|
| 474 |
+
MODEL = d.dataset.k; store.set("decho.model", MODEL); renderModels();
|
| 475 |
+
});
|
| 476 |
+
$("mode").onclick = e => {
|
| 477 |
+
const m = e.target.dataset.m; if (!m) return; MODE = m;
|
| 478 |
+
[...$("mode").children].forEach(b => b.classList.toggle("on", b.dataset.m === m));
|
| 479 |
+
$("podPanel").classList.toggle("hidden", m !== "podcast"); $("voiceBox").classList.toggle("hidden", m !== "single");
|
| 480 |
+
if (m !== "single" && !/^[^\n::]{1,24}[::]/m.test($("txt").value)) $("txt").value = m === "dialog" ? EXAMPLES.exDialog : "";
|
| 481 |
+
renderVoices(); renderExamples(); renderHosts(); updateCount();
|
| 482 |
+
};
|
| 483 |
+
$("speed").oninput = e => $("spv").textContent = num((+e.target.value).toFixed(2));
|
| 484 |
+
$("podPace").oninput = e => $("podPaceV").textContent = num((+e.target.value).toFixed(2));
|
| 485 |
+
["smart", "register", "live"].forEach(k => { const el = $(k); const v = store.get("decho." + k, null); if (v !== null) { if (el.type === "checkbox") el.checked = v; else el.value = v; } el.onchange = () => store.set("decho." + k, el.type === "checkbox" ? el.checked : el.value); });
|
| 486 |
+
|
| 487 |
+
/* ------------------------------------------------------------------ podcast */
|
| 488 |
+
let HOSTS = store.get("decho.hosts", [{name: "سارا", voice: "sara"}, {name: "رضا", voice: "reza"}]);
|
| 489 |
+
function renderHosts() {
|
| 490 |
+
if (MODE !== "podcast") { $("hosts").innerHTML = ""; return; }
|
| 491 |
+
const vs = VOICES.length ? VOICES.map(v => v.name) : ["default", "sara", "maryam", "reza", "ali"];
|
| 492 |
+
$("hosts").innerHTML = HOSTS.map((h, i) => `<div><label>${t("host", {n: i + 1})}</label><div class="row" style="gap:6px;flex-wrap:nowrap"><input type="text" data-hn="${i}" value="${esc(h.name)}" dir="rtl"><select data-hv="${i}">${vs.map(v => `<option value="${v}" ${v === h.voice ? "selected" : ""}>${esc(vName(v))}</option>`).join("")}</select></div></div>`).join("")
|
| 493 |
+
+ `<div style="align-self:end"><button class="btn small" id="addHost">+</button> <button class="btn small" id="delHost">-</button></div>`;
|
| 494 |
+
$("hosts").querySelectorAll("[data-hn]").forEach(el => el.oninput = () => { HOSTS[+el.dataset.hn].name = el.value.trim(); store.set("decho.hosts", HOSTS); });
|
| 495 |
+
$("hosts").querySelectorAll("[data-hv]").forEach(el => el.onchange = () => { HOSTS[+el.dataset.hv].voice = el.value; store.set("decho.hosts", HOSTS); });
|
| 496 |
+
$("addHost").onclick = () => { if (HOSTS.length < 4) { HOSTS.push({name: vName(vs[HOSTS.length % vs.length]), voice: vs[HOSTS.length % vs.length]}); store.set("decho.hosts", HOSTS); renderHosts(); } };
|
| 497 |
+
$("delHost").onclick = () => { if (HOSTS.length > 1) { HOSTS.pop(); store.set("decho.hosts", HOSTS); renderHosts(); } };
|
| 498 |
+
}
|
| 499 |
+
$("podWrite").onclick = async () => {
|
| 500 |
+
if (!ME.signed_in) { toast(t("needSignIn")); return; }
|
| 501 |
+
$("podWrite").disabled = true; $("podMsg").textContent = t("podWriting");
|
| 502 |
+
try {
|
| 503 |
+
const r = await fetch("api/podcast/script", {method: "POST", credentials: "include", headers: {"Content-Type": "application/json"},
|
| 504 |
+
body: JSON.stringify({source: $("podSrc").value, name: $("podName").value, hosts: HOSTS.map(h => ({name: h.name, role: "میزبان"})), tone: $("podTone").value, minutes: +$("podMin").value})});
|
| 505 |
+
const j = await r.json(); if (!r.ok) throw new Error(j.detail || r.status);
|
| 506 |
+
$("txt").value = j.script; updateCount(); $("podMsg").textContent = t("podDone");
|
| 507 |
+
} catch (e) { $("podMsg").innerHTML = `<span class="err">${errMsg(e.message, limit())}</span>`; }
|
| 508 |
+
$("podWrite").disabled = false;
|
| 509 |
+
};
|
| 510 |
+
function podcastScript() { // host names -> voices: a list of {text, voice}
|
| 511 |
+
const map = new Map(HOSTS.map(h => [h.name.trim(), h.voice])); const out = []; let last = HOSTS[0] ? HOSTS[0].voice : "default";
|
| 512 |
+
for (const line of $("txt").value.split("\n")) {
|
| 513 |
+
const s = line.trim(); if (!s) continue;
|
| 514 |
+
const m = s.match(/^([^::\[\]]{1,24})[::]\s*(.*)$/);
|
| 515 |
+
if (m && map.has(m[1].trim())) { last = map.get(m[1].trim()); if (m[2].trim()) out.push({text: m[2].trim(), voice: last}); }
|
| 516 |
+
else out.push({text: s, voice: last});
|
| 517 |
+
}
|
| 518 |
+
return out;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* ------------------------------------------------------------------ generation + gapless playback */
|
| 522 |
+
function stopAll() { run++; if (sid) fetch(`api/stream/${sid}/stop`, {method: "POST", credentials: "include"}).catch(() => {}); sid = null; if (ctx) { try { ctx.close(); } catch (e) {} ctx = null; } $("stop").classList.add("hidden"); $("go").disabled = false; }
|
| 523 |
+
$("stop").onclick = () => { stopAll(); $("msg").textContent = t("stopped"); };
|
| 524 |
+
$("go").onclick = async () => {
|
| 525 |
+
stopAll(); const my = ++run;
|
| 526 |
+
const text = $("txt").value.trim(); if (!text) return;
|
| 527 |
+
if (text.length > limit()) { $("msg").innerHTML = `<span class="err">${t("tooLong", {m: limit()})}</span>`; return; }
|
| 528 |
+
ctx = new (window.AudioContext || window.webkitAudioContext)(); nextT = 0; pcm = []; tSynth = 0; tAudio = 0; firstAt = null; t0 = performance.now();
|
| 529 |
+
$("go").disabled = true; $("stop").classList.remove("hidden"); $("dlWav").disabled = true; $("dlMp3").disabled = true;
|
| 530 |
+
$("out").classList.remove("hidden"); $("list").innerHTML = ""; $("stats").innerHTML = ""; $("msg").textContent = t("working");
|
| 531 |
+
const body = {script: MODE === "podcast" ? podcastScript() : text, model: MODEL, voice: MODE === "single" ? $("voice").value : null,
|
| 532 |
+
speed: +$("speed").value, seed: +$("seed").value || 0, smart: $("smart").checked && ME.signed_in, register: MODE === "podcast" ? $("podTone").value : $("register").value,
|
| 533 |
+
live: $("live").checked, pause_scale: MODE === "podcast" ? +$("podPace").value : null, lexicon: LEX};
|
| 534 |
+
try {
|
| 535 |
+
const r = await fetch("api/stream/start", {method: "POST", credentials: "include", headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)});
|
| 536 |
+
const j = await r.json(); if (!r.ok) throw new Error(j.detail || r.status); sid = j.id;
|
| 537 |
+
for (let i = 0; ; i++) {
|
| 538 |
+
if (my !== run) return;
|
| 539 |
+
const rr = await fetch(`api/stream/${sid}/next`, {credentials: "include"});
|
| 540 |
+
if (rr.status === 204) break;
|
| 541 |
+
if (!rr.ok) { let d = rr.status; try { d = (await rr.json()).detail; } catch (e) {} throw new Error(d); }
|
| 542 |
+
const meta = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(rr.headers.get("X-Meta") || "e30="), c => c.charCodeAt(0))));
|
| 543 |
+
const ss = +rr.headers.get("X-Synth-Seconds"), as = +rr.headers.get("X-Audio-Seconds"); tSynth += ss; tAudio += as;
|
| 544 |
+
const buf = await rr.arrayBuffer(); if (my !== run) return;
|
| 545 |
+
const audio = await ctx.decodeAudioData(buf.slice(0)); sr = audio.sampleRate;
|
| 546 |
+
const start = Math.max(ctx.currentTime + 0.06, nextT); const src = ctx.createBufferSource(); src.buffer = audio; src.connect(ctx.destination); src.start(start);
|
| 547 |
+
if (firstAt === null) firstAt = (performance.now() - t0) / 1000;
|
| 548 |
+
const gap = +meta.gap_after_s || 0; nextT = start + audio.duration + gap;
|
| 549 |
+
pcm.push(audio.getChannelData(0).slice(0)); if (gap > 0) pcm.push(new Float32Array(Math.round(gap * sr)));
|
| 550 |
+
addChunk(i, meta, buf, start, audio.duration); renderStats();
|
| 551 |
+
}
|
| 552 |
+
$("msg").innerHTML = `<span class="okc">${t("done")}</span>`; $("dlWav").disabled = false; $("dlMp3").disabled = !ME.signed_in;
|
| 553 |
+
} catch (e) { if (my === run) $("msg").innerHTML = `<span class="err">${errMsg(e.message, limit())}</span>`; }
|
| 554 |
+
if (my === run) { $("go").disabled = false; $("stop").classList.add("hidden"); sid = null; }
|
| 555 |
+
};
|
| 556 |
+
function renderStats() {
|
| 557 |
+
$("stats").innerHTML = `<span>${t("stFirst")}: <b>${num(firstAt.toFixed(1))}</b> ${t("sec")}</span><span>${t("stAudio")}: <b>${num(tAudio.toFixed(1))}</b> ${t("sec")}</span><span>${t("stSynth")}: <b>${num(tSynth.toFixed(1))}</b> ${t("sec")}</span><span>${t("stRtf")}: <b>${tAudio ? num((tSynth / tAudio).toFixed(2)) : "-"}</b></span>`;
|
| 558 |
+
}
|
| 559 |
+
function addChunk(i, meta, buf, start, dur) {
|
| 560 |
+
const d = document.createElement("div"); d.className = "chunk";
|
| 561 |
+
const unc = (meta.uncertain || []).filter(u => u.word);
|
| 562 |
+
const url = URL.createObjectURL(new Blob([buf], {type: "audio/wav"}));
|
| 563 |
+
d.innerHTML = `<div class="t">${meta.voice && MODE !== "single" ? `<span class="who">${esc(vName(meta.voice))}</span>` : ""}${esc(meta.text)}</div>
|
| 564 |
+
<audio controls preload="none" src="${url}"></audio>
|
| 565 |
+
<details><summary>${t("details")} · ${meta.reader === "llm" ? t("readerLlm") : t("readerRules")}</summary>
|
| 566 |
+
<div class="dbg">${esc(meta.model_text)}</div>
|
| 567 |
+
${unc.length ? `<div class="meta" style="margin-top:6px">${t("uncertain")} ${unc.map(u => `<span class="unc" data-w="${esc(u.word)}">${esc(u.word)}</span>`).join(" ")}</div>` : ""}</details>`;
|
| 568 |
+
d.querySelectorAll(".unc").forEach(s => s.onclick = () => openLex(s.dataset.w, meta.text));
|
| 569 |
+
$("list").append(d);
|
| 570 |
+
const on = () => d.classList.add("playing"), off = () => d.classList.remove("playing");
|
| 571 |
+
const now = ctx.currentTime; setTimeout(on, Math.max(0, (start - now) * 1000)); setTimeout(off, Math.max(0, (start + dur - now) * 1000));
|
| 572 |
+
}
|
| 573 |
+
function wavBlob() {
|
| 574 |
+
const n = pcm.reduce((a, b) => a + b.length, 0), out = new DataView(new ArrayBuffer(44 + n * 2)), w = (o, s) => [...s].forEach((c, k) => out.setUint8(o + k, c.charCodeAt(0)));
|
| 575 |
+
w(0, "RIFF"); out.setUint32(4, 36 + n * 2, true); w(8, "WAVE"); w(12, "fmt "); out.setUint32(16, 16, true); out.setUint16(20, 1, true); out.setUint16(22, 1, true);
|
| 576 |
+
out.setUint32(24, sr, true); out.setUint32(28, sr * 2, true); out.setUint16(32, 2, true); out.setUint16(34, 16, true); w(36, "data"); out.setUint32(40, n * 2, true);
|
| 577 |
+
let o = 44; for (const a of pcm) for (let i = 0; i < a.length; i++, o += 2) out.setInt16(o, Math.max(-1, Math.min(1, a[i])) * 32767, true);
|
| 578 |
+
return new Blob([out.buffer], {type: "audio/wav"});
|
| 579 |
+
}
|
| 580 |
+
function save(blob, name) { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000); }
|
| 581 |
+
$("dlWav").onclick = () => save(wavBlob(), "decho.wav");
|
| 582 |
+
$("dlMp3").onclick = async () => {
|
| 583 |
+
if (!ME.signed_in) { toast(t("needSignIn")); return; }
|
| 584 |
+
$("dlMp3").disabled = true;
|
| 585 |
+
try { const r = await fetch("api/mp3", {method: "POST", credentials: "include", body: wavBlob()}); if (!r.ok) throw new Error((await r.json()).detail); save(await r.blob(), "decho.mp3"); }
|
| 586 |
+
catch (e) { toast(errMsg(e.message, limit())); }
|
| 587 |
+
$("dlMp3").disabled = false;
|
| 588 |
+
};
|
| 589 |
+
|
| 590 |
+
/* ------------------------------------------------------------------ pronunciation dictionary (browser only) */
|
| 591 |
+
function renderLex() {
|
| 592 |
+
const ks = Object.keys(LEX);
|
| 593 |
+
$("lexList").innerHTML = ks.length ? ks.map(k => `<div class="lexrow"><span><b>${esc(k)}</b> → <code>${esc(LEX[k])}</code></span><button class="btn small" data-del="${esc(k)}">${t("lexDel")}</button></div>`).join("") : `<p class="meta">${t("lexEmpty")}</p>`;
|
| 594 |
+
$("lexList").querySelectorAll("[data-del]").forEach(b => b.onclick = () => { delete LEX[b.dataset.del]; store.set("decho.lexicon", LEX); renderLex(); });
|
| 595 |
+
}
|
| 596 |
+
function openLex(word, sentence) { $("lexWord").value = word; $("lexSentence").value = sentence || ""; $("lexReading").value = ""; $("lexCard").scrollIntoView({behavior: "smooth"}); $("lexSuggest").click(); }
|
| 597 |
+
function normReading(v) { v = v.trim(); if (!v) return ""; if (/[\u0600-\u06FF]/.test(v)) return v; return v.startsWith("r:") ? v : "r:" + v; }
|
| 598 |
+
$("lexSave").onclick = () => { const w = $("lexWord").value.trim(), r = normReading($("lexReading").value); if (!w || !r) return; LEX[w] = r; store.set("decho.lexicon", LEX); renderLex(); $("lexMsg").innerHTML = `<span class="okc">${t("lexSaved")}</span>`; };
|
| 599 |
+
$("lexSuggest").onclick = async () => {
|
| 600 |
+
const w = $("lexWord").value.trim(); if (!w) return; $("cands").innerHTML = ""; $("lexMsg").textContent = "…";
|
| 601 |
+
try {
|
| 602 |
+
const r = await fetch("api/lexicon/suggest", {method: "POST", credentials: "include", headers: {"Content-Type": "application/json"}, body: JSON.stringify({word: w, sentence: $("lexSentence").value || null})});
|
| 603 |
+
const j = await r.json(); if (!r.ok) throw new Error(j.detail || r.status);
|
| 604 |
+
$("lexMsg").innerHTML = j.llm ? "" : `<span class="meta">${t("lexLlmNote")}</span>`;
|
| 605 |
+
if (!j.candidates.length) { $("cands").innerHTML = `<p class="meta">${t("lexNoCand")}</p>`; return; }
|
| 606 |
+
$("cands").innerHTML = j.candidates.map((c, i) => `<div class="cand"><code>${esc(c.model_text)}</code>${c.respelling ? `<span>${esc(c.respelling)}</span>` : ""}<span class="meta">${esc(c.note || "")}</span><span class="spacer"></span><button class="btn small" data-say="${i}">▶</button><button class="btn small" data-use="${i}">${t("lexUse")}</button></div>`).join("");
|
| 607 |
+
$("cands").querySelectorAll("[data-use]").forEach(b => b.onclick = () => { $("lexReading").value = "r:" + j.candidates[+b.dataset.use].model_text; $("lexSave").click(); });
|
| 608 |
+
$("cands").querySelectorAll("[data-say]").forEach(b => b.onclick = () => say("r:" + j.candidates[+b.dataset.say].model_text));
|
| 609 |
+
} catch (e) { $("lexMsg").innerHTML = `<span class="err">${errMsg(e.message, limit())}</span>`; }
|
| 610 |
+
};
|
| 611 |
+
async function say(reading) {
|
| 612 |
+
if (!ME.signed_in) { toast(t("needSignIn")); return; }
|
| 613 |
+
const w = $("lexWord").value.trim(), s = $("lexSentence").value.trim();
|
| 614 |
+
try {
|
| 615 |
+
const r = await fetch("api/lexicon/say", {method: "POST", credentials: "include", headers: {"Content-Type": "application/json"},
|
| 616 |
+
body: JSON.stringify({reading: reading.replace(/^r:/, ""), word: w || null, sentence: s || null, voice: $("voice").value || "default"})});
|
| 617 |
+
if (!r.ok) throw new Error((await r.json()).detail); new Audio(URL.createObjectURL(await r.blob())).play();
|
| 618 |
+
} catch (e) { toast(errMsg(e.message, limit())); }
|
| 619 |
+
}
|
| 620 |
+
$("lexSay").onclick = () => { const r = normReading($("lexReading").value); if (r) say(r); };
|
| 621 |
+
|
| 622 |
+
/* ------------------------------------------------------------------ samples */
|
| 623 |
+
function renderSamples() {
|
| 624 |
+
$("sampleList").innerHTML = SAMPLES.map(s => `<div class="sample"><b>${esc(s[LANG])}</b><p dir="rtl">${esc(s.text).replace(/\n/g, "<br>")}</p><audio controls preload="none" src="static/samples/${s.f}"></audio><button class="chip" data-try="${esc(s.f)}">${LANG === "fa" ? "در استودیو امتحان کن" : "Try in the studio"}</button></div>`).join("");
|
| 625 |
+
$("sampleList").querySelectorAll("[data-try]").forEach(b => b.onclick = () => { const s = SAMPLES.find(x => x.f === b.dataset.try); $("txt").value = s.text; updateCount(); $("studio").scrollIntoView({behavior: "smooth"}); });
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/* ------------------------------------------------------------------ boot */
|
| 629 |
+
$("txt").value = "سلام! من دکو هستم، صدای فارسی ساختهی دیباچین. امروز ۱۵ مهر ۱۴۰۳ است و هوا آفتابی است.";
|
| 630 |
+
applyLang(); poll(); loadMe();
|
| 631 |
+
</script>
|
| 632 |
+
</body>
|
| 633 |
+
</html>
|
static/samples/dialogue.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:34f0e029be63413cd1c48892bffe417a262cc4b691a2ad76b658b187be8ecba4
|
| 3 |
+
size 123758
|
static/samples/realtime.mp3
ADDED
|
Binary file (94.5 kB). View file
|
|
|
static/samples/voice_ali.mp3
ADDED
|
Binary file (64 kB). View file
|
|
|
static/samples/voice_default.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7b42b5fc6089f762a7ccbf391e4fa1c78367563842a5f6340c719d0e5f614bbd
|
| 3 |
+
size 130446
|
static/samples/voice_maryam.mp3
ADDED
|
Binary file (87.4 kB). View file
|
|
|
static/samples/voice_reza.mp3
ADDED
|
Binary file (92.8 kB). View file
|
|
|
static/samples/voice_sara.mp3
ADDED
|
Binary file (70.7 kB). View file
|
|
|