ssasio commited on
Commit
caf9e2a
·
verified ·
1 Parent(s): 53518ed

Upload 12 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ 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
+ demo1_conversation.wav filter=lfs diff=lfs merge=lfs -text
37
+ demo2_numbers.wav filter=lfs diff=lfs merge=lfs -text
38
+ demo3_expressive.wav filter=lfs diff=lfs merge=lfs -text
Ani-Voice-API.7z ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf77670c80c71675c2bdfe9404fbe7ddcd045ea3ec7f72bd34751a9a584fa033
3
+ size 140639241
README.md CHANGED
@@ -1,14 +1,66 @@
1
  ---
2
- title: Ani Voice API
3
- emoji: 😻
4
- colorFrom: pink
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 6.17.3
8
- python_version: '3.13'
9
- app_file: app.py
10
- pinned: false
11
  license: mit
 
 
 
 
 
 
12
  ---
 
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ language:
3
+ - bg
 
 
 
 
 
 
 
4
  license: mit
5
+ tags:
6
+ - text-to-speech
7
+ - tts
8
+ - bulgarian
9
+ - fastapi
10
+ pipeline_tag: text-to-speech
11
  ---
12
+ # Ani Voice API
13
 
14
+ Завършен TTS (Text-to-Speech) пакет за български език, базиран на BgTTS и Supertonic, обвит в гъвкаво FastAPI приложение.
15
+
16
+ *Проектът е създаден и разработен от **Ani-Antigravity** по идея и желание на **Наско (@beleata74)**.*
17
+
18
+ ## Инсталация
19
+
20
+ 1. Уверете се, че имате Python 3.10+
21
+ 2. Инсталирайте нужните зависимости:
22
+ ```bash
23
+ pip install -r requirements.txt
24
+ ```
25
+
26
+ ## Стартиране на API сървъра
27
+
28
+ ```bash
29
+ python api.py
30
+ ```
31
+ Сървърът ще тръгне на `http://localhost:8000`. Можете да разгледате автоматичната документация на `http://localhost:8000/docs`.
32
+
33
+ ## Използване
34
+
35
+ ### 1. Генериране на цял аудио файл
36
+ Изпраща текст и връща завършен `.wav` файл.
37
+
38
+ **Пример:**
39
+ ```bash
40
+ curl -X POST "http://localhost:8000/api/v1/synthesize" \
41
+ -H "Content-Type: application/json" \
42
+ -d '{"text": "Здравей, свят!", "voice_style": "F5", "speed": 1.6}' \
43
+ --output response.wav
44
+ ```
45
+
46
+ ### 2. Стрийминг на аудио (NDJSON)
47
+ Изпраща аудиото на малки парчета (chunks), докато се генерират, кодирани в base64. Полезно за дълги текстове, където искате да пускате аудиото веднага.
48
+
49
+ Връща редове във формат:
50
+ ```json
51
+ {"chunk_index": 0, "audio_base64": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEA..."}
52
+ ```
53
+
54
+ Вижте файла `client_example.py` за пример как да интегрирате API-то в Python код.
55
+ Вижте файла `voice_pipeline.py` за пример на работещ клиент-демон (daemon), който комуникира с API-то и пуска звука в реално време!
56
+
57
+ ## Аудио Демонстрации
58
+
59
+ В репозиторито можете да намерите няколко предварително генерирани аудио файла, за да чуете как звучи моделът:
60
+
61
+ 1. **`demo1_conversation.wav`**
62
+ - *Транскрипция:* "Здравейте! Това е тестов запис от нашия нов български TTS модел. Надявам се да ви хареса как звучи гласът ми!"
63
+ 2. **`demo2_numbers.wav`** (Демонстрира нормализацията на числа и дати)
64
+ - *Транскрипция:* "Днес е 15 май 2026 година. Температурата навън е 23.5 градуса, а вятърът духа със скорост 5.4 километра в час. Цената е 1500 лв. за м²."
65
+ 3. **`demo3_expressive.wav`**
66
+ - *Транскрипция:* "Супер! Наистина много се радвам, че всичко най-накрая работи гладко. Усилията определено си заслужаваха!"
api.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.responses import Response, StreamingResponse
3
+ from pydantic import BaseModel
4
+ import uvicorn
5
+
6
+ # Импортираме tts_engine - това автоматично ще зареди моделите в паметта при стартиране!
7
+ from tts_engine import engine
8
+
9
+ app = FastAPI(title="Ani Voice API", version="1.0.0")
10
+
11
+ class SynthesizeRequest(BaseModel):
12
+ text: str
13
+ voice_style: str = "F5"
14
+ speed: float = 1.6
15
+
16
+ @app.post("/api/v1/synthesize")
17
+ def synthesize_full_audio(request: SynthesizeRequest):
18
+ """
19
+ Генерира аудио за целия текст и го връща като един WAV файл.
20
+ Подходящо за кратки съобщения.
21
+ """
22
+ try:
23
+ audio_bytes = engine.synthesize_full(request.text, request.voice_style, request.speed)
24
+ if not audio_bytes:
25
+ raise HTTPException(status_code=400, detail="Неуспешно генериране на аудио (празен текст?).")
26
+
27
+ return Response(content=audio_bytes, media_type="audio/wav")
28
+ except Exception as e:
29
+ raise HTTPException(status_code=500, detail=str(e))
30
+
31
+ import base64
32
+ import json
33
+
34
+ @app.post("/api/v1/synthesize/stream")
35
+ def synthesize_stream_audio(request: SynthesizeRequest):
36
+ """
37
+ Стрийминг endpoint, който връща аудио на парчета (chunks).
38
+ Всеки ред е JSON обект: {"chunk_index": i, "audio_base64": "..."}
39
+ """
40
+ def generate():
41
+ try:
42
+ for i, audio_bytes in enumerate(engine.synthesize_stream(request.text, request.voice_style, request.speed)):
43
+ encoded = base64.b64encode(audio_bytes).decode("utf-8")
44
+ yield json.dumps({"chunk_index": i, "audio_base64": encoded}) + "\n"
45
+ except Exception as e:
46
+ print(f"Грешка по време на стрийминг: {e}")
47
+ yield json.dumps({"error": str(e)}) + "\n"
48
+
49
+ return StreamingResponse(generate(), media_type="application/x-ndjson")
50
+
51
+ if __name__ == "__main__":
52
+ print("Стартиране на Ani Voice API сървър на порт 8000...")
53
+ uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=False)
client_example.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+
3
+ API_URL = "http://localhost:8000/api/v1/synthesize"
4
+
5
+ def synthesize_text(text: str, output_file: str):
6
+ """
7
+ Изпраща текст към API-то и запазва резултата като WAV файл.
8
+ """
9
+ print(f"Изпращане на заявка за: '{text}'...")
10
+
11
+ response = requests.post(API_URL, json={
12
+ "text": text,
13
+ "voice_style": "F5",
14
+ "speed": 1.6
15
+ })
16
+
17
+ if response.status_code == 200:
18
+ with open(output_file, "wb") as f:
19
+ f.write(response.content)
20
+ print(f"✅ Аудиото е запазено успешно в: {output_file}")
21
+ else:
22
+ print(f"❌ Грешка: {response.status_code} - {response.text}")
23
+
24
+ if __name__ == "__main__":
25
+ text_to_say = "Здравей! Това е тестов запис, създаден чрез новото Ani Voice API."
26
+ output_filename = "test_api_output.wav"
27
+ synthesize_text(text_to_say, output_filename)
demo1_conversation.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5395b9f0d2f01685b82c1592dd236999deec3de7e2a4e1a3ab25611a8f1d01d6
3
+ size 332204
demo2_numbers.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f7cdfc164606f698053e9264117e15093f4d15d0d2d7621eee131d03141a4130
3
+ size 762284
demo3_expressive.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed299d60ff6f02ed78d7bce2322fb949e74daf0fbecfb3eae8775425943a36d7
3
+ size 318764
gitattributes ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz 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
+ demo1_conversation.wav filter=lfs diff=lfs merge=lfs -text
37
+ demo2_numbers.wav filter=lfs diff=lfs merge=lfs -text
38
+ demo3_expressive.wav filter=lfs diff=lfs merge=lfs -text
normalizer.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from bg_text_normalizer import normalize_text as bg_norm
3
+
4
+ # Допълнителни специфични замени, които bg_text_normalizer изпуска
5
+ EXTRA_ABBREVIATIONS = {
6
+ r"\bм²\b": "квадратен метър",
7
+ r"\bкв\.м\.\b": "квадратен метър",
8
+ r"\bт\.е\.\b": "тоест",
9
+ }
10
+
11
+ def normalize_text(text: str) -> str:
12
+ """
13
+ Нормализира текста, използвайки bg-text-normalizer + наши специфични правила.
14
+ """
15
+ # 0.5 Предварителна обработка на десетични дроби: заменяме точката със запетая
16
+ # bg-text-normalizer бърка '1.4' с '1 април'. За да го чете като дроб, му трябва запетая '1,4'.
17
+ text = re.sub(r'(\d)\.(\d)', r'\1,\2', text)
18
+
19
+ # 1. Първо прилагаме библиотеката bg_text_normalizer
20
+ text = bg_norm(text)
21
+
22
+ # 2. Оправяме точките след съкращения като "лв." и "гр.", които библиотеката е превърнала в "лева."
23
+ text = text.replace("лева.", "лева")
24
+ text = text.replace("стотинки.", "стотинки")
25
+
26
+ # 3. Прилагаме нашите допълнителни правила
27
+ for pattern, replacement in EXTRA_ABBREVIATIONS.items():
28
+ text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
29
+
30
+ # Допълнително хващаме м² (без \b, защото ² не е дума)
31
+ text = text.replace("м²", "квадратен метър")
32
+
33
+ # Махане на двойни интервали
34
+ text = re.sub(r"\s+", " ", text).strip()
35
+
36
+ return text
37
+
38
+ if __name__ == "__main__":
39
+ test_text = "Цената е 1500 лв. за м² в кв. Лозенец."
40
+ print("Original:", test_text)
41
+ print("Normalized:", normalize_text(test_text))
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.13.0.dev20260603+cu132
2
+ torchaudio==2.11.0.dev20260608+cu132
3
+ torchvision==0.28.0.dev20260608+cu132
4
+ numpy<2.0.0
5
+ supertonic==1.3.1
6
+ bg-text-normalizer==1.1.0
7
+ num2cyrillic==1.0.0
8
+ fastapi>=0.110.0
9
+ uvicorn>=0.29.0
10
+ pydantic>=2.7.0
11
+ requests>=2.31.0
tts_engine.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import re
4
+ import wave
5
+ import torch
6
+ import numpy as np
7
+ import tempfile
8
+ import sys
9
+ import supertonic
10
+
11
+ # Добавяме BgTTS към sys.path, за да може вътрешните му импорти да работят
12
+ sys.path.append(os.path.join(os.path.dirname(__file__), 'BgTTS'))
13
+ from inference import synthesize
14
+ from normalizer import normalize_text
15
+
16
+ class TTSEngine:
17
+ def __init__(self):
18
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
19
+ print(f"Зареждам TTS Engine на устройство: {self.device}")
20
+
21
+ # Supertonic (Референтно аудио)
22
+ from supertonic import TTS
23
+ self.engine = TTS(auto_download=True)
24
+
25
+ # BgTTS (Основен модел)
26
+ self.bgtts_checkpoint = os.path.join(os.path.dirname(__file__), "BgTTS", "checkpoint_inference.pt")
27
+ # BgTTS inference.synthesize зарежда модела всеки път, ако не му подадем модела.
28
+ # В текущия BgTTS/inference.py synthesize() вика load_for_inference(), ако се подаде път.
29
+ # За сега ще ползваме пътя, тъй като така е написан BgTTS.
30
+ # Ако искаме пълно кеширане, може да се наложи леко пренаписване на BgTTS/inference.py.
31
+ # Но засега ще ползваме оригиналната synthesize функция.
32
+
33
+ print("TTS Engine зареден успешно.")
34
+
35
+ def split_text_for_tts(self, text: str) -> list[str]:
36
+ text = text.strip()
37
+ if not text:
38
+ return []
39
+ raw = re.split(r'(?<=[\.\!\?…])\s+|\n+', text)
40
+ chunks = []
41
+ buf = ""
42
+ for part in raw:
43
+ part = part.strip()
44
+ if not part: continue
45
+
46
+ if not buf or len(buf) < 80 or len(buf) + len(part) + 1 <= 200:
47
+ buf = (buf + " " + part).strip()
48
+ else:
49
+ chunks.append(buf)
50
+ buf = part
51
+ if buf: chunks.append(buf)
52
+ return chunks
53
+
54
+ def generate_chunk(self, chunk_text: str, voice_style: str = "F5", speed: float = 1.6) -> bytes:
55
+ """
56
+ Генерира аудио за едно изречение (chunk) и го връща като WAV байтове.
57
+ """
58
+ clean_text = chunk_text.replace('"', '').replace('„', '').replace('“', '') \
59
+ .replace("’", "'").replace("–", "-").replace("—", "-") \
60
+ .replace("*", "")
61
+
62
+ if not clean_text.strip():
63
+ return b""
64
+
65
+ # 1. Генериране на референтно аудио
66
+ # Ако voice_style е стринг (напр. "F5"), взимаме съответния обект
67
+ if isinstance(voice_style, str):
68
+ v_style = self.engine.get_voice_style(voice_name=voice_style)
69
+ else:
70
+ v_style = voice_style
71
+
72
+ wav_array, _ = self.engine.synthesize(clean_text, voice_style=v_style, lang="bg", speed=speed)
73
+ wav_data = np.asarray(wav_array).flatten()
74
+ wav_max = np.max(np.abs(wav_data))
75
+ if wav_max > 0:
76
+ wav_data = wav_data / wav_max
77
+ pcm_data = (wav_data * 32767).astype(np.int16)
78
+
79
+ # Записваме временно референтното аудио (тъй като BgTTS изисква файл)
80
+ fd, ref_path = tempfile.mkstemp(suffix=".wav")
81
+ os.close(fd)
82
+ with wave.open(ref_path, "wb") as wf:
83
+ wf.setnchannels(1)
84
+ wf.setsampwidth(2)
85
+ wf.setframerate(44100)
86
+ wf.writeframes(pcm_data.tobytes())
87
+
88
+ # 2. Генериране на крайното аудио
89
+ fd, final_path = tempfile.mkstemp(suffix=".wav")
90
+ os.close(fd)
91
+
92
+ try:
93
+ synthesize(checkpoint=self.bgtts_checkpoint,
94
+ text=clean_text,
95
+ output=final_path,
96
+ speaker_wav=ref_path,
97
+ device=self.device)
98
+
99
+ # Прочитане на резултата
100
+ with open(final_path, "rb") as f:
101
+ audio_bytes = f.read()
102
+
103
+ return audio_bytes
104
+
105
+ finally:
106
+ try:
107
+ os.remove(ref_path)
108
+ os.remove(final_path)
109
+ except OSError:
110
+ pass
111
+
112
+ def synthesize_stream(self, text: str, voice_style: str = "F5", speed: float = 1.6):
113
+ """
114
+ Генератор, който нормализира текста, цепи го на парчета и връща WAV байтове за всяко парче.
115
+ """
116
+ normalized_text = normalize_text(text)
117
+ chunks = self.split_text_for_tts(normalized_text)
118
+
119
+ for chunk in chunks:
120
+ audio_bytes = self.generate_chunk(chunk, voice_style, speed)
121
+ if audio_bytes:
122
+ yield audio_bytes
123
+
124
+ def synthesize_full(self, text: str, voice_style: str = "F5", speed: float = 1.6) -> bytes:
125
+ """
126
+ Нормализира текста, цепи го, генерира всички парчета и ги слепва в един общ WAV файл.
127
+ """
128
+ normalized_text = normalize_text(text)
129
+ chunks = self.split_text_for_tts(normalized_text)
130
+
131
+ all_frames = b""
132
+ params = None
133
+
134
+ for chunk in chunks:
135
+ audio_bytes = self.generate_chunk(chunk, voice_style, speed)
136
+ if not audio_bytes:
137
+ continue
138
+
139
+ # Парсване на WAV данните, за да можем да ги слеем без да дублираме хедъри
140
+ with wave.open(io.BytesIO(audio_bytes), "rb") as wf:
141
+ if not params:
142
+ params = wf.getparams()
143
+ all_frames += wf.readframes(wf.getnframes())
144
+
145
+ if not params:
146
+ return b""
147
+
148
+ # Създаване на крайния WAV
149
+ out_io = io.BytesIO()
150
+ with wave.open(out_io, "wb") as wf:
151
+ wf.setparams(params)
152
+ wf.writeframes(all_frames)
153
+
154
+ return out_io.getvalue()
155
+
156
+ # Глобална инстанция за по-лесно преизползване
157
+ engine = TTSEngine()
voice_pipeline.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import json
4
+ import base64
5
+ import requests
6
+ import tempfile
7
+ import threading
8
+ import queue
9
+ import winsound
10
+
11
+ TRANSCRIPT_PATH = r"C:\Users\nasko\.gemini\antigravity\brain\695897cf-1c58-4886-a686-e9d8c406ebef\.system_generated\logs\transcript.jsonl"
12
+ API_URL = "http://localhost:8000/api/v1/synthesize/stream"
13
+
14
+ audio_queue = queue.Queue()
15
+
16
+ def player_worker():
17
+ """
18
+ Взима готови WAV файлове от опашката и ги пуска.
19
+ """
20
+ while True:
21
+ file_path = audio_queue.get()
22
+ if file_path is None: break
23
+
24
+ print(f"🔊 Възпроизвеждам от API...")
25
+ winsound.PlaySound(file_path, winsound.SND_FILENAME)
26
+
27
+ try:
28
+ os.remove(file_path)
29
+ except OSError:
30
+ pass
31
+
32
+ audio_queue.task_done()
33
+
34
+ def process_text(text: str):
35
+ """
36
+ Изпраща текста към API-то и чака за стрийминг на аудио парчета.
37
+ """
38
+ print(f"📡 Изпращане към API: {text[:50]}...")
39
+ try:
40
+ response = requests.post(API_URL, json={
41
+ "text": text,
42
+ "voice_style": "F5",
43
+ "speed": 1.6
44
+ }, stream=True)
45
+
46
+ if response.status_code != 200:
47
+ print(f"Грешка от API: {response.status_code} - {response.text}")
48
+ return
49
+
50
+ for line in response.iter_lines():
51
+ if line:
52
+ data = json.loads(line)
53
+ if "error" in data:
54
+ print(f"API Грешка: {data['error']}")
55
+ continue
56
+
57
+ chunk_index = data.get("chunk_index")
58
+ audio_base64 = data.get("audio_base64")
59
+
60
+ if audio_base64:
61
+ audio_bytes = base64.b64decode(audio_base64)
62
+
63
+ # Записваме временно файла и го пускаме в опашката
64
+ fd, file_path = tempfile.mkstemp(suffix=f"_chunk_{chunk_index}.wav")
65
+ os.close(fd)
66
+
67
+ with open(file_path, "wb") as f:
68
+ f.write(audio_bytes)
69
+
70
+ audio_queue.put(file_path)
71
+ except requests.exceptions.ConnectionError:
72
+ print("Не мога да се свържа с API-то! Увери се, че `api.py` работи на порт 8000.")
73
+ except Exception as e:
74
+ print(f"Грешка при комуникация с API: {e}")
75
+
76
+ def tail_file():
77
+ """
78
+ Следи чата (transcript.jsonl) за нови съобщения от модела.
79
+ """
80
+ if not os.path.exists(TRANSCRIPT_PATH):
81
+ print(f"Файлът не съществува: {TRANSCRIPT_PATH}")
82
+ return
83
+
84
+ with open(TRANSCRIPT_PATH, "r", encoding="utf-8") as f:
85
+ f.seek(0, 2)
86
+
87
+ while True:
88
+ line = f.readline()
89
+ if not line:
90
+ time.sleep(0.5)
91
+ continue
92
+
93
+ try:
94
+ data = json.loads(line)
95
+ if data.get("source") == "MODEL" and data.get("type") in ["PLANNER_RESPONSE", "GENERIC"]:
96
+ full_text = data.get("content", "")
97
+ if full_text and not full_text.startswith("Created At:"):
98
+ print("\n📝 Получен нов текст от чата.")
99
+ process_text(full_text)
100
+ except Exception as e:
101
+ pass
102
+
103
+ if __name__ == "__main__":
104
+ t_play = threading.Thread(target=player_worker, daemon=True)
105
+ t_play.start()
106
+
107
+ print("Ani Voice Client слуша за съобщения и чака API-то...")
108
+ tail_file()