walker11 commited on
Commit
dc41b87
·
verified ·
1 Parent(s): ba84f70

Upload 11 files

Browse files
Files changed (11) hide show
  1. .gitignore +39 -0
  2. Dockerfile +30 -0
  3. README.md +61 -11
  4. __init__.py +0 -0
  5. ai_service.py +508 -0
  6. app.py +8 -0
  7. main.py +112 -0
  8. models.py +107 -0
  9. prompts.py +189 -0
  10. requirements.txt +9 -0
  11. tts_service.py +132 -0
.gitignore ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+ .env
27
+
28
+ # Audio files
29
+ audio_files/
30
+
31
+ # IDE specific files
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+
37
+ # OS specific files
38
+ .DS_Store
39
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /code
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ && apt-get clean \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements and install Python dependencies
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY . .
17
+
18
+ # Create directories for audio files
19
+ RUN mkdir -p audio_files
20
+
21
+ # Expose port
22
+ EXPOSE 7860
23
+
24
+ # Set environment variables
25
+ ENV BACKEND_HOST=0.0.0.0
26
+ ENV BACKEND_PORT=7860
27
+ ENV BASE_URL=https://${SPACE_ID}.hf.space
28
+
29
+ # Run the application
30
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,61 @@
1
- ---
2
- title: RAWI Story Generator
3
- emoji: 👁
4
- colorFrom: yellow
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # رَاوي (Rawi) - منصة القص العربي بالذكاء الاصطناعي
2
+
3
+ ## نظرة عامة
4
+ خدمة API لتوليد قصص عربية تفاعلية باستخدام الذكاء الاصطناعي. تتيح المنصة للمستخدمين إنشاء قصص مخصصة بناءً على معلمات محددة، والتفاعل مع القصة من خلال خيارات متعددة، والاستماع إلى القصص المنشأة بتقنية تحويل النص إلى كلام.
5
+
6
+ ## المميزات
7
+ - إنشاء قصص عربية أصلية باستخدام نماذج الذكاء الاصطناعي من DeepSeek
8
+ - تخصيص معلمات القصة مثل الطول والنوع والشخصيات
9
+ - القص التفاعلي مع خيارات متعددة للمسارات
10
+ - تحويل النص إلى صوت عربي
11
+ - واجهة برمجة تطبيقات RESTful متكاملة
12
+
13
+ ## واجهة برمجة التطبيقات (API)
14
+
15
+ ### تهيئة قصة جديدة
16
+ ```
17
+ POST /api/stories/initialize
18
+ ```
19
+
20
+ ### متابعة القصة
21
+ ```
22
+ POST /api/stories/continue
23
+ ```
24
+
25
+ ### تحويل القصة إلى صوت
26
+ ```
27
+ POST /api/stories/tts
28
+ ```
29
+
30
+ ### تعديل القصة
31
+ ```
32
+ POST /api/stories/edit
33
+ ```
34
+
35
+ ### الحصول على القصة كاملة
36
+ ```
37
+ GET /api/stories/story/{story_id}
38
+ ```
39
+
40
+ ### الوصول إلى الملفات الصوتية
41
+ ```
42
+ GET /audio/{filename}
43
+ ```
44
+
45
+ ## المتطلبات البيئية
46
+ - `DEEPSEEK_API_KEY`: مفتاح API للوصول إلى نماذج DeepSeek
47
+ - `BACKEND_HOST`: مضيف الخادم (افتراضي: 0.0.0.0)
48
+ - `BACKEND_PORT`: منفذ الخادم (افتراضي: 7860)
49
+ - `BASE_URL`: عنوان URL الأساسي للخدمة
50
+ - `AUDIO_STORAGE_PATH`: مسار تخزين الملفات الصوتية (افتراضي: ./audio_files)
51
+
52
+ ## التكامل مع التطبيقات
53
+ تم تصميم هذه الخدمة للعمل مع:
54
+ 1. واجهة المستخدم الويب لـراوي
55
+ 2. تطبيق Flutter للأجهزة المحمولة
56
+
57
+ ## توثيق واجهة برمجة التطبيقات
58
+ يمكن الوصول إلى وثائق API التفاعلية الكاملة عبر:
59
+ ```
60
+ https://[your-space-name].hf.space/docs
61
+ ```
__init__.py ADDED
File without changes
ai_service.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import time
5
+ import asyncio
6
+ from typing import Dict, List, Tuple, Optional
7
+ import httpx
8
+ from dotenv import load_dotenv
9
+
10
+ from models import StoryConfig, StoryParagraph, StoryChoice, StoryResponse
11
+ from prompts import (
12
+ get_system_prompt,
13
+ create_story_init_prompt,
14
+ create_continuation_prompt,
15
+ create_title_prompt,
16
+ get_story_length_instructions
17
+ )
18
+
19
+ # تحميل المتغيرات البيئية
20
+ load_dotenv()
21
+
22
+ # الحصول على مفتاح API من المتغيرات البيئية
23
+ DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
24
+ DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
25
+
26
+ # تخزين سياق القصص
27
+ # في نظام حقيقي، يجب استخدام قاعدة بيانات بدلاً من التخزين في الذاكرة
28
+ stories_context = {}
29
+ stories_metadata = {}
30
+
31
+
32
+ async def generate_response(messages: List[Dict], retries=3, backoff_factor=1.5) -> str:
33
+ """
34
+ استدعاء DeepSeek API وتوليد استجابة بناءً على سلسلة الرسائل مع محاولة إعادة المحاولة في حالة الفشل
35
+
36
+ Args:
37
+ messages: قائمة برسائل المحادثة
38
+ retries: عدد محاولات إعادة المحاولة في حالة فشل الاتصال (افتراضي: 3)
39
+ backoff_factor: معامل التأخير للمحاولات المتتالية (افتراضي: 1.5)
40
+
41
+ Returns:
42
+ str: محتوى الاستجابة من API
43
+
44
+ Raises:
45
+ Exception: في حالة فشل جميع المحاولات
46
+ """
47
+ if not DEEPSEEK_API_KEY:
48
+ raise Exception("مفتاح API غير متوفر. يرجى التحقق من إعداد المتغيرات البيئية.")
49
+
50
+ headers = {
51
+ "Content-Type": "application/json",
52
+ "Authorization": f"Bearer {DEEPSEEK_API_KEY}"
53
+ }
54
+
55
+ payload = {
56
+ "model": "deepseek-chat", # استبدل باسم النموذج المناسب من DeepSeek API
57
+ "messages": messages,
58
+ "temperature": 0.7,
59
+ "max_tokens": 1000
60
+ }
61
+
62
+ last_exception = None
63
+
64
+ # محاولات إعادة الاتصال في حالة فشل API
65
+ for attempt in range(retries):
66
+ try:
67
+ async with httpx.AsyncClient() as client:
68
+ response = await client.post(
69
+ DEEPSEEK_API_URL,
70
+ headers=headers,
71
+ json=payload,
72
+ timeout=60.0
73
+ )
74
+
75
+ if response.status_code == 429: # Rate Limit
76
+ # انتظار فترة قبل المحاولة مرة أخرى
77
+ wait_time = backoff_factor * (2 ** attempt)
78
+ print(f"تجاوز حد معدل الطلبات، انتظار {wait_time} ثانية قبل المحاولة مرة أخرى.")
79
+ await asyncio.sleep(wait_time)
80
+ continue
81
+
82
+ if response.status_code != 200:
83
+ error_msg = f"فشل طلب DeepSeek API: {response.status_code} - {response.text}"
84
+ print(error_msg)
85
+ last_exception = Exception(error_msg)
86
+
87
+ # انتظار فترة قبل المحاولة مرة أخرى
88
+ wait_time = backoff_factor * (2 ** attempt)
89
+ await asyncio.sleep(wait_time)
90
+ continue
91
+
92
+ result = response.json()
93
+ if "choices" not in result or not result["choices"]:
94
+ raise Exception("تنسيق استجابة DeepSeek API غير صالح")
95
+
96
+ return result["choices"][0]["message"]["content"]
97
+
98
+ except httpx.HTTPError as e:
99
+ error_msg = f"خطأ في اتصال HTTP: {str(e)}"
100
+ print(error_msg)
101
+ last_exception = Exception(error_msg)
102
+
103
+ # انتظار فترة قبل المحاولة مرة أخرى
104
+ wait_time = backoff_factor * (2 ** attempt)
105
+ await asyncio.sleep(wait_time)
106
+ continue
107
+
108
+ except Exception as e:
109
+ error_msg = f"خطأ غير متوقع: {str(e)}"
110
+ print(error_msg)
111
+ last_exception = Exception(error_msg)
112
+
113
+ # انتظار فترة قبل المحاولة مرة أخرى
114
+ wait_time = backoff_factor * (2 ** attempt)
115
+ await asyncio.sleep(wait_time)
116
+ continue
117
+
118
+ # إذا وصلنا إلى هنا، فقد فشلت جميع المحاولات
119
+ raise last_exception or Exception("فشل الاتصال بـ DeepSeek API بعد عدة محاولات")
120
+
121
+
122
+ def parse_paragraph_and_choices(response_text: str) -> Tuple[str, Optional[List[StoryChoice]]]:
123
+ """
124
+ تحليل استجابة النموذج واستخراج ��لفقرة والخيارات
125
+ """
126
+ # محاولة استخراج الفقرة
127
+ paragraph_match = re.search(r"الفقرة:(.*?)(?:الخيارات:|العنوان:|$)", response_text, re.DOTALL)
128
+ paragraph = paragraph_match.group(1).strip() if paragraph_match else response_text.strip()
129
+
130
+ # محاولة استخراج الخيارات
131
+ choices = None
132
+ choices_match = re.search(r"الخيارات:(.*?)(?:$)", response_text, re.DOTALL)
133
+
134
+ if choices_match:
135
+ choices_text = choices_match.group(1).strip()
136
+ choices = []
137
+
138
+ # استخراج الخيارات المرقمة
139
+ for i, choice_match in enumerate(re.findall(r"\d+\.\s*(.*?)(?=\d+\.|$)", choices_text, re.DOTALL), 1):
140
+ choice_text = choice_match.strip()
141
+ if choice_text:
142
+ choices.append(StoryChoice(id=i, text=choice_text))
143
+
144
+ # إذا لم يتم العثور على خيارات بالطريقة السابقة، نحاول طريقة أخرى
145
+ if not choices:
146
+ lines = choices_text.split("\n")
147
+ for i, line in enumerate([l for l in lines if l.strip()], 1):
148
+ if i <= 3: # نقتصر على 3 خيارات
149
+ # تجاهل الترقيم إذا كان موجوداً
150
+ choice_text = re.sub(r"^\d+\.\s*", "", line).strip()
151
+ choices.append(StoryChoice(id=i, text=choice_text))
152
+
153
+ # استخراج العنوان إذا كان موجوداً
154
+ title = None
155
+ title_match = re.search(r"العنوان:(.*?)(?:$)", response_text, re.DOTALL)
156
+ if title_match:
157
+ title = title_match.group(1).strip()
158
+
159
+ return paragraph, choices, title
160
+
161
+
162
+ async def initialize_story(config: StoryConfig) -> StoryResponse:
163
+ """
164
+ بدء قصة جديدة باستخدام DeepSeek API
165
+ """
166
+ import uuid
167
+ story_id = str(uuid.uuid4())
168
+
169
+ # إنشاء البرومبت الأولي
170
+ system_prompt = get_system_prompt()
171
+ user_prompt = create_story_init_prompt(config)
172
+
173
+ messages = [
174
+ {"role": "system", "content": system_prompt},
175
+ {"role": "user", "content": user_prompt}
176
+ ]
177
+
178
+ # استدعاء DeepSeek API
179
+ response_text = await generate_response(messages)
180
+
181
+ # تحليل الاستجابة
182
+ paragraph_text, choices, _ = parse_paragraph_and_choices(response_text)
183
+
184
+ # إنشاء فقرة القصة
185
+ paragraph = StoryParagraph(
186
+ content=paragraph_text,
187
+ choices=choices
188
+ )
189
+
190
+ # حفظ سياق القصة
191
+ stories_context[story_id] = {
192
+ "paragraphs": [paragraph_text],
193
+ "current_paragraph": 1
194
+ }
195
+
196
+ # حفظ معلومات القصة
197
+ length_info = get_story_length_instructions(config.length)
198
+ stories_metadata[story_id] = {
199
+ "config": config.dict(),
200
+ "max_paragraphs": length_info["paragraphs"],
201
+ "messages": messages + [{"role": "assistant", "content": response_text}],
202
+ "title": None
203
+ }
204
+
205
+ # إنشاء استجابة القصة
206
+ story_response = StoryResponse(
207
+ story_id=story_id,
208
+ paragraph=paragraph,
209
+ is_complete=False
210
+ )
211
+
212
+ return story_response
213
+
214
+
215
+ async def continue_story(story_id: str, choice_id: int) -> StoryResponse:
216
+ """
217
+ متابعة القصة بناءً على اختيار المستخدم
218
+ """
219
+ if story_id not in stories_context or story_id not in stories_metadata:
220
+ raise ValueError("معرف القصة غير صالح")
221
+
222
+ context = stories_context[story_id]
223
+ metadata = stories_metadata[story_id]
224
+
225
+ # الحصول على سياق القصة والاختيار
226
+ paragraphs = context["paragraphs"]
227
+ current_paragraph = context["current_paragraph"]
228
+ max_paragraphs = metadata["max_paragraphs"]
229
+
230
+ # الحصول على الرسائل السابقة
231
+ messages = metadata["messages"]
232
+
233
+ # الحصول على نص الاختيار
234
+ last_message = messages[-1]["content"]
235
+ _, choices, _ = parse_paragraph_and_choices(last_message)
236
+
237
+ if not choices or choice_id < 1 or choice_id > len(choices):
238
+ raise ValueError("معرف الاختيار غير صالح")
239
+
240
+ choice_text = next((c.text for c in choices if c.id == choice_id), "")
241
+
242
+ # إنشاء برومبت المتابعة
243
+ story_context_text = "\n".join(paragraphs)
244
+ continuation_prompt = create_continuation_prompt(
245
+ story_context_text,
246
+ choice_id,
247
+ choice_text,
248
+ current_paragraph,
249
+ max_paragraphs
250
+ )
251
+
252
+ # إضافة رسالة المستخدم
253
+ messages.append({"role": "user", "content": continuation_prompt})
254
+
255
+ # استدعاء DeepSeek API
256
+ response_text = await generate_response(messages)
257
+
258
+ # تحليل الاستجابة
259
+ paragraph_text, choices, title = parse_paragraph_and_choices(response_text)
260
+
261
+ # تحديث سياق القصة
262
+ paragraphs.append(paragraph_text)
263
+ context["current_paragraph"] += 1
264
+ context["paragraphs"] = paragraphs
265
+
266
+ # تحديد ما إذا كانت القصة مكتملة
267
+ is_complete = current_paragraph >= max_paragraphs - 1
268
+
269
+ # إذا كانت القصة مكتملة، احفظ العنوان
270
+ if is_complete and title:
271
+ metadata["title"] = title
272
+
273
+ # تحديث الرسائل
274
+ messages.append({"role": "assistant", "content": response_text})
275
+ metadata["messages"] = messages
276
+
277
+ # إنشاء فقرة القصة
278
+ paragraph = StoryParagraph(
279
+ content=paragraph_text,
280
+ choices=None if is_complete else choices
281
+ )
282
+
283
+ # إنشاء استجابة القصة
284
+ story_response = StoryResponse(
285
+ story_id=story_id,
286
+ paragraph=paragraph,
287
+ is_complete=is_complete,
288
+ title=metadata["title"] if is_complete else None
289
+ )
290
+
291
+ return story_response
292
+
293
+
294
+ async def continue_story_with_text(story_id: str, custom_text: str) -> StoryResponse:
295
+ """
296
+ متابعة القصة بناءً على النص المخصص الذي أدخله المستخدم
297
+ """
298
+ print(f"continue_story_with_text called with story_id={story_id}, custom_text={custom_text}")
299
+
300
+ if story_id not in stories_context or story_id not in stories_metadata:
301
+ print(f"Story ID {story_id} not found in context or metadata")
302
+ raise ValueError("معرف القصة غير صالح")
303
+
304
+ context = stories_context[story_id]
305
+ metadata = stories_metadata[story_id]
306
+
307
+ # الحصول على سياق القصة
308
+ paragraphs = context["paragraphs"]
309
+ current_paragraph = context["current_paragraph"]
310
+ max_paragraphs = metadata["max_paragraphs"]
311
+
312
+ print(f"Story context: current_paragraph={current_paragraph}, max_paragraphs={max_paragraphs}")
313
+
314
+ # الحصول على الرسائل السابقة
315
+ messages = metadata["messages"]
316
+
317
+ # إنشاء برومبت المتابعة بالنص المخصص
318
+ story_context_text = "\n".join(paragraphs)
319
+
320
+ # Create a prompt for continuing with custom text
321
+ custom_prompt = f"""
322
+ لقد وصلنا إلى هذه النقطة في القصة:
323
+
324
+ {story_context_text}
325
+
326
+ المستخدم اختار أن يكتب رداً مخصصاً بدلاً من اختيار أحد الخيارات المقدمة. الرد المخصص للمستخدم هو:
327
+
328
+ "{custom_text}"
329
+
330
+ بناءً على هذا المدخل من المستخدم، استمر في القصة واكتب فقرة جديدة تأخذ بعين الاعتبار ما كتبه المستخدم.
331
+ ثم قدم 3 خيارات جديدة للمستخدم ليختار منها للاستمرار في القصة.
332
+
333
+ تذكر أن القصة الآن في:
334
+ - الفقرة رقم: {current_paragraph} من {max_paragraphs}
335
+ - إذا كانت هذه الفقرة الأخيرة أو قبل الأخيرة، قم بختم القصة بشكل مناسب.
336
+
337
+ يجب أن يكون تنسيق ردك كما يلي:
338
+
339
+ الفقرة: [نص الفقرة الجديدة من القصة]
340
+
341
+ الخيارات:
342
+ 1. [الخيار الأول]
343
+ 2. [الخيار الثاني]
344
+ 3. [الخيار الثالث]
345
+
346
+ إذا كانت هذه الفقرة الأخيرة، أضف عنواناً للقصة:
347
+
348
+ العنوان: [عنوان مناسب للقصة كاملة]
349
+ """
350
+
351
+ print(f"Custom prompt created, length: {len(custom_prompt)}")
352
+
353
+ # إضافة رسالة المستخدم
354
+ messages.append({"role": "user", "content": custom_prompt})
355
+
356
+ # استدعاء DeepSeek API
357
+ print("Calling DeepSeek API...")
358
+ response_text = await generate_response(messages)
359
+ print(f"DeepSeek API response received, length: {len(response_text)}")
360
+
361
+ # تحليل الاستجابة
362
+ paragraph_text, choices, title = parse_paragraph_and_choices(response_text)
363
+ print(f"Parsed response: paragraph length={len(paragraph_text)}, choices={choices is not None}, title={title is not None}")
364
+
365
+ # تحديث سياق القصة
366
+ paragraphs.append(paragraph_text)
367
+ context["current_paragraph"] += 1
368
+ context["paragraphs"] = paragraphs
369
+
370
+ # تحديد ما إذا كانت القصة مكتملة
371
+ is_complete = current_paragraph >= max_paragraphs - 1
372
+ print(f"Is story complete: {is_complete}")
373
+
374
+ # إذا كانت القصة مكتملة، احفظ العنوان
375
+ if is_complete and title:
376
+ metadata["title"] = title
377
+
378
+ # تحديث الرسائل
379
+ messages.append({"role": "assistant", "content": response_text})
380
+ metadata["messages"] = messages
381
+
382
+ # إنشاء فقرة القصة
383
+ paragraph = StoryParagraph(
384
+ content=paragraph_text,
385
+ choices=None if is_complete else choices
386
+ )
387
+
388
+ # إنشاء استجابة القصة
389
+ story_response = StoryResponse(
390
+ story_id=story_id,
391
+ paragraph=paragraph,
392
+ is_complete=is_complete,
393
+ title=metadata["title"] if is_complete else None
394
+ )
395
+
396
+ print(f"Story response created successfully")
397
+ return story_response
398
+
399
+
400
+ async def get_complete_story(story_id: str) -> str:
401
+ """
402
+ الحصول على النص الكامل للقصة
403
+ """
404
+ if story_id not in stories_context:
405
+ raise ValueError("معرف القصة غير صالح")
406
+
407
+ context = stories_context[story_id]
408
+ metadata = stories_metadata.get(story_id, {})
409
+
410
+ story_text = "\n\n".join(context["paragraphs"])
411
+
412
+ if metadata.get("title"):
413
+ # إضافة العنوان بصيغة صديقة لخدمة تحويل النص إلى كلام
414
+ story_text = f"قصة بعنوان {metadata['title']}.\n\n{story_text}"
415
+
416
+ return story_text
417
+
418
+
419
+ async def generate_title_if_missing(story_id: str) -> str:
420
+ """
421
+ توليد عنوان للقصة إذا لم يكن موجوداً
422
+ """
423
+ if story_id not in stories_metadata:
424
+ raise ValueError("معرف القصة غير صالح")
425
+
426
+ metadata = stories_metadata[story_id]
427
+
428
+ # إذا كان العنوان موجوداً بالفعل، أعده
429
+ if metadata.get("title"):
430
+ return metadata["title"]
431
+
432
+ # توليد العنوان
433
+ complete_story = await get_complete_story(story_id)
434
+
435
+ system_prompt = get_system_prompt()
436
+ title_prompt = create_title_prompt(complete_story)
437
+
438
+ messages = [
439
+ {"role": "system", "content": system_prompt},
440
+ {"role": "user", "content": title_prompt}
441
+ ]
442
+
443
+ title = await generate_response(messages)
444
+
445
+ # تنظيف العنوان
446
+ title = title.strip().replace("العنوان:", "").strip()
447
+
448
+ # حفظ العنوان
449
+ metadata["title"] = title
450
+
451
+ return title
452
+
453
+ async def edit_story(story_id: str, edit_instructions: str) -> dict:
454
+ """
455
+ تعديل القصة بناءً على تعليمات المستخدم
456
+ """
457
+ if story_id not in stories_context or story_id not in stories_metadata:
458
+ raise ValueError("معرف القصة غير صالح")
459
+
460
+ # الحصول على نص القصة الكامل
461
+ paragraphs = stories_context[story_id]["paragraphs"]
462
+ complete_story = "\n".join(paragraphs)
463
+
464
+ # إنشاء برومبت للتعديل
465
+ system_prompt = """أنت مساعد ذكي متخصص في تحرير وتعديل القصص العربية. مهمتك هي تعديل القصة بناءً على تعليمات المستخدم مع الحفاظ على الأسلوب والنبرة الأصلية.
466
+ قم بإرجاع القصة المعدلة بالكامل وليس فقط الأجزاء المعدلة. تأكد من تقسيم القصة إلى فقرات واضحة كما في النص الأصلي.
467
+ إذا تطلبت التعليمات تغيير العنوان، قم بإضافة سطر "العنوان الجديد: <العنوان المعدل>" في بداية استجابتك."""
468
+
469
+ user_prompt = f"""فيما يلي قصة كاملة:
470
+
471
+ {complete_story}
472
+
473
+ تعليمات التعديل:
474
+ {edit_instructions}
475
+
476
+ قم بتعديل القصة وفقًا للتعليمات المذكورة أعلاه وأرجع القصة المعدلة بالكامل مقسمة إلى فقرات.
477
+ """
478
+
479
+ messages = [
480
+ {"role": "system", "content": system_prompt},
481
+ {"role": "user", "content": user_prompt}
482
+ ]
483
+
484
+ # استدعاء DeepSeek API
485
+ response_text = await generate_response(messages)
486
+
487
+ # تحليل النص واستخراج العنوان الجديد إذا وجد
488
+ new_title = None
489
+ title_match = re.search(r"العنوان الجديد:\s*(.*?)(?:\n|$)", response_text, re.IGNORECASE)
490
+ if title_match:
491
+ new_title = title_match.group(1).strip()
492
+ response_text = re.sub(r"العنوان الجديد:\s*.*?(?:\n|$)", "", response_text, flags=re.IGNORECASE)
493
+
494
+ # تقسيم النص المعدل إلى فقرات
495
+ edited_paragraphs = [p.strip() for p in response_text.split("\n\n") if p.strip()]
496
+
497
+ # تحديث القصة في التخزين
498
+ stories_context[story_id]["paragraphs"] = edited_paragraphs
499
+
500
+ # تحديث العنوان إذا كان هناك عنوان جديد
501
+ if new_title:
502
+ stories_metadata[story_id]["title"] = new_title
503
+
504
+ # إرجاع البيانات المعدلة
505
+ return {
506
+ "paragraphs": edited_paragraphs,
507
+ "title": new_title or stories_metadata[story_id].get("title")
508
+ }
app.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Entry point for Hugging Face Spaces deployment of Rawi API
3
+ """
4
+
5
+ from main import app
6
+
7
+ # This file serves as the entry point for Hugging Face Spaces
8
+ # It imports and exposes the FastAPI app object from main.py
main.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ راوي (Rawi) - Arabic AI Storytelling Platform
3
+ Main application file for FastAPI server
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import uvicorn
9
+ import logging
10
+ from fastapi import FastAPI, HTTPException
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from dotenv import load_dotenv
13
+ from fastapi.responses import FileResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+ from pathlib import Path
16
+
17
+ # Add the current directory to Python path to enable imports
18
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
19
+
20
+ # Import application routers
21
+ from routers import story
22
+
23
+ # ======== Configure Logging ========
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # ======== Load Environment Variables ========
28
+ load_dotenv()
29
+
30
+ # ======== Server Configuration ========
31
+ HOST = os.getenv("BACKEND_HOST", "0.0.0.0")
32
+ PORT = int(os.getenv("BACKEND_PORT", "7860")) # Updated default port for Hugging Face
33
+ BASE_URL = os.getenv("BASE_URL", f"http://{HOST}:{PORT}")
34
+ AUDIO_STORAGE_PATH = os.path.abspath(os.getenv("AUDIO_STORAGE_PATH", "./audio_files"))
35
+
36
+ # DeepSeek API Key check
37
+ DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
38
+
39
+ # Log server settings
40
+ logger.info(f"Server settings:")
41
+ logger.info(f"Host: {HOST}")
42
+ logger.info(f"Port: {PORT}")
43
+ logger.info(f"Base URL: {BASE_URL}")
44
+ logger.info(f"Audio storage path: {AUDIO_STORAGE_PATH}")
45
+ logger.info(f"DeepSeek API Key set: {bool(DEEPSEEK_API_KEY)}")
46
+
47
+ # ======== Ensure Audio Storage Directory Exists ========
48
+ try:
49
+ Path(AUDIO_STORAGE_PATH).mkdir(parents=True, exist_ok=True)
50
+ logger.info(f"Created/verified audio storage directory at: {AUDIO_STORAGE_PATH}")
51
+ except Exception as e:
52
+ logger.error(f"Error creating audio storage directory: {str(e)}")
53
+ raise
54
+
55
+ # ======== Initialize FastAPI Application ========
56
+ app = FastAPI(
57
+ title="راوي API",
58
+ description="واجهة برمجة تطبيقات لمنصة راوي لتوليد القصص العربية باستخدام الذكاء الاصطناعي",
59
+ version="1.0.0"
60
+ )
61
+
62
+ # ======== Configure CORS ========
63
+ app.add_middleware(
64
+ CORSMiddleware,
65
+ allow_origins=["*"], # In production, specify allowed origins instead of "*"
66
+ allow_credentials=True,
67
+ allow_methods=["*"],
68
+ allow_headers=["*"],
69
+ )
70
+
71
+ # ======== Mount Static Files ========
72
+ app.mount("/audio", StaticFiles(directory=AUDIO_STORAGE_PATH), name="audio")
73
+
74
+ # ======== Register Routers ========
75
+ app.include_router(story.router, prefix="/api/stories", tags=["قصص"])
76
+
77
+ # ======== API Endpoints ========
78
+ @app.get("/", tags=["الرئيسية"])
79
+ async def root():
80
+ """
81
+ Root endpoint for the API
82
+ """
83
+ return {
84
+ "message": "مرحباً بك في واجهة برمجة تطبيقات راوي",
85
+ "docs": f"{BASE_URL}/docs"
86
+ }
87
+
88
+ @app.get("/health", tags=["الحالة"])
89
+ async def health_check():
90
+ """
91
+ Health check endpoint to verify API configuration
92
+ """
93
+ health_status = {
94
+ "status": "healthy",
95
+ "deepseek_api": bool(DEEPSEEK_API_KEY),
96
+ "audio_storage": os.path.exists(AUDIO_STORAGE_PATH),
97
+ }
98
+
99
+ if not DEEPSEEK_API_KEY:
100
+ health_status["status"] = "degraded"
101
+ health_status["message"] = "DeepSeek API key is not set. Story generation will not work."
102
+
103
+ return health_status
104
+
105
+ # ======== Run Application ========
106
+ if __name__ == "__main__":
107
+ uvicorn.run(
108
+ "main:app",
109
+ host=HOST,
110
+ port=PORT,
111
+ reload=True # Disable in production for better performance
112
+ )
models.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ راوي (Rawi) - Arabic AI Storytelling Platform
3
+ Data models for API requests and responses
4
+ """
5
+
6
+ from enum import Enum
7
+ from typing import List, Optional
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ # ======== Enum Types ========
12
+
13
+ class StoryLength(str, Enum):
14
+ """Story length options"""
15
+ SHORT = "short"
16
+ MEDIUM = "medium"
17
+ LONG = "long"
18
+
19
+
20
+ class StoryType(str, Enum):
21
+ """Story genre options in Arabic"""
22
+ ROMANCE = "رومانسي"
23
+ HORROR = "رعب"
24
+ COMEDY = "كوميدي"
25
+ ACTION = "أكشن"
26
+ ADVENTURE = "مغامرة"
27
+ DRAMA = "دراما"
28
+ FANTASY = "خيال"
29
+ HISTORICAL = "تاريخي"
30
+ MYSTERY = "غموض"
31
+ NONE = "لا"
32
+
33
+
34
+ class CharacterGender(str, Enum):
35
+ """Character gender options in Arabic"""
36
+ MALE = "ذكر"
37
+ FEMALE = "أنثى"
38
+
39
+
40
+ # ======== Request Models ========
41
+
42
+ class Character(BaseModel):
43
+ """Character information for story generation"""
44
+ name: str = Field(..., description="اسم الشخصية")
45
+ gender: CharacterGender = Field(..., description="جنس الشخصية")
46
+ description: str = Field(..., description="وصف الشخصية")
47
+
48
+
49
+ class StoryConfig(BaseModel):
50
+ """Configuration for initial story generation"""
51
+ length: StoryLength = Field(..., description="طول القصة")
52
+ primary_type: StoryType = Field(..., description="النوع الأساسي للقصة")
53
+ secondary_type: StoryType = Field(default=StoryType.NONE, description="النوع الثانوي للقصة")
54
+ characters: List[Character] = Field(default=[], description="الشخصيات في القصة")
55
+
56
+
57
+ class ChoiceRequest(BaseModel):
58
+ """Request to continue a story with a choice or custom text"""
59
+ story_id: str = Field(..., description="معرف القصة")
60
+ choice_id: Optional[int] = Field(None, description="معرف الاختيار الذي تم اختياره")
61
+ custom_text: Optional[str] = Field(None, description="النص المخصص الذي أدخله المستخدم")
62
+
63
+
64
+ class TTSRequest(BaseModel):
65
+ """Request to generate text-to-speech for a story"""
66
+ story_id: str = Field(..., description="معرف القصة")
67
+ speed: float = Field(1.0, description="سرعة الصوت (0.5 للبطيء، 1.0 للعادي، 2.0 للسريع)", ge=0.5, le=2.0)
68
+
69
+
70
+ class EditRequest(BaseModel):
71
+ """Request to edit a story based on user instructions"""
72
+ story_id: str = Field(..., description="معرف القصة")
73
+ edit_instructions: str = Field(..., description="تعليمات لتعديل القصة")
74
+
75
+
76
+ # ======== Response Models ========
77
+
78
+ class StoryChoice(BaseModel):
79
+ """A choice option presented to the user"""
80
+ id: int = Field(..., description="معرف الاختيار")
81
+ text: str = Field(..., description="نص الاختيار")
82
+
83
+
84
+ class StoryParagraph(BaseModel):
85
+ """A paragraph of story content with optional choices"""
86
+ content: str = Field(..., description="محتوى الفقرة")
87
+ choices: Optional[List[StoryChoice]] = Field(default=None, description="الاختيارات المتاحة بعد هذه الفقرة")
88
+
89
+
90
+ class StoryResponse(BaseModel):
91
+ """Response containing story content and metadata"""
92
+ story_id: str = Field(..., description="معرف القصة")
93
+ paragraph: StoryParagraph = Field(..., description="فقرة من القصة")
94
+ is_complete: bool = Field(default=False, description="هل القصة اكتملت؟")
95
+ title: Optional[str] = Field(default=None, description="عنوان القصة (يتم إضافته عند اكتمال القصة)")
96
+
97
+
98
+ class TTSResponse(BaseModel):
99
+ """Response containing URL to audio file"""
100
+ audio_url: str = Field(..., description="رابط ملف الصوت")
101
+
102
+
103
+ class EditResponse(BaseModel):
104
+ """Response containing edited story content"""
105
+ success: bool = Field(default=True, description="نجاح عملية التعديل")
106
+ paragraphs: List[str] = Field(..., description="فقرات القصة المعدلة")
107
+ title: Optional[str] = Field(default=None, description="عنوان القصة المعدل (اختياري)")
prompts.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any
2
+ from models import StoryLength, StoryType, Character, StoryConfig
3
+
4
+
5
+ def get_system_prompt() -> str:
6
+ """
7
+ الحصول على برومبت النظام الأساسي الذي يحدد سلوك نموذج الذكاء الاصطناعي
8
+ """
9
+ return """
10
+ You are a professional and creative Arabic story writer. Your task is to write original, engaging, and cohesive Arabic stories.
11
+
12
+ Adhere to the following standards in all the stories you write:
13
+
14
+ 1. Use correct and understandable classical Arabic language, free from grammatical and spelling errors.
15
+ 2. Build a coherent and logical story that follows good dramatic structure principles (beginning, rising action, climax, resolution).
16
+ 3. Adhere to Arabic and Islamic values and ethics in the story content.
17
+ 4. Avoid inappropriate content or anything that violates public taste or religious values.
18
+ 5. Provide detailed sensory descriptions of characters, places, and events to make the story vivid and engaging.
19
+ 6. Make dialogue realistic and natural, appropriate to the story's characters and environment.
20
+ 7. Maintain consistency in character traits and behaviors throughout the story.
21
+ 8. Include positive values and useful lessons in an indirect way.
22
+ 9. Use diverse narrative techniques: description, dialogue, narration, internal monologue.
23
+ 10. Create a clear conflict that drives the story events and maintains reader interest.
24
+
25
+ Each time you are asked to write a new paragraph of the story, you must:
26
+ - Write a coherent and engaging narrative paragraph of 4-6 lines in Arabic.
27
+ - Provide 3 distinctive and interesting options to develop the story's path.
28
+
29
+ Important rules for options:
30
+ 1. Make options very practical and short (3-5 words only) in Arabic.
31
+ 2. ALWAYS start each option with the character's name followed by the action verb.
32
+ 3. Use clear format: "[Character name] + verb", for example: "أحمد يتصل بالشرطة" (Ahmed calls the police), "سارة تهرب من المكان" (Sarah escapes from the place).
33
+ 4. Make it absolutely clear WHO is performing the action in each option.
34
+ 5. Don't explain what will happen after the choice, just mention the direct action.
35
+ 6. Ensure each option will lead to a completely different path in the story.
36
+ 7. Make options logical and appropriate to the current situation in the story.
37
+
38
+ Result of user choice:
39
+ 1. Do not summarize the user's chosen option at the beginning of the next paragraph.
40
+ 2. Start directly with the reactions and consequences resulting from the user's choice.
41
+ 3. Present surprising and unexpected developments resulting from the choice.
42
+ 4. Maintain story consistency despite the change in path.
43
+
44
+ When the story is complete, choose an engaging and deep title that reflects the essence and content of the story.
45
+ """
46
+
47
+
48
+ def format_characters_info(characters: List[Character]) -> str:
49
+ """
50
+ تنسيق معلومات الشخصيات لتضمينها في البرومبت
51
+ """
52
+ if not characters:
53
+ return "لا توجد شخصيات محددة، يمكنك إنشاء شخصيات مناسبة للقصة."
54
+
55
+ characters_info = "معلومات الشخصيات:\n"
56
+ for i, character in enumerate(characters, 1):
57
+ gender_text = "ذكر" if character.gender.value == "ذكر" else "أنثى"
58
+ characters_info += f"{i}. الشخصية: {character.name}، الجنس: {gender_text}، الوصف: {character.description}\n"
59
+
60
+ return characters_info
61
+
62
+
63
+ def get_story_length_instructions(length: StoryLength) -> Dict[str, Any]:
64
+ """
65
+ الحصول على تعليمات طول القصة وعدد الفقرات
66
+ """
67
+ length_mapping = {
68
+ StoryLength.SHORT: {"paragraphs": 3, "description": "قصة قصيرة تتكون من 3 فقرات"},
69
+ StoryLength.MEDIUM: {"paragraphs": 5, "description": "قصة متوسطة الطول تتكون من 5 فقرات"},
70
+ StoryLength.LONG: {"paragraphs": 7, "description": "قصة طويلة تتكون من 7 فقرات"}
71
+ }
72
+
73
+ return length_mapping.get(length, length_mapping[StoryLength.MEDIUM])
74
+
75
+
76
+ def get_story_type_description(primary_type: StoryType, secondary_type: StoryType) -> str:
77
+ """
78
+ الحصول على وصف نوع القصة
79
+ """
80
+ if secondary_type == StoryType.NONE:
81
+ return f"قصة من نوع {primary_type.value}"
82
+ else:
83
+ return f"قصة تجمع بين نوعي {primary_type.value} و{secondary_type.value}"
84
+
85
+
86
+ def create_story_init_prompt(config: StoryConfig) -> str:
87
+ """
88
+ إنشاء البرومبت الأولي لبدء القصة
89
+ """
90
+ length_info = get_story_length_instructions(config.length)
91
+ story_type = get_story_type_description(config.primary_type, config.secondary_type)
92
+ characters_info = format_characters_info(config.characters)
93
+
94
+ prompt = f"""
95
+ Please write {length_info['description']} of {story_type}.
96
+
97
+ {characters_info}
98
+
99
+ Required from you:
100
+ 1. Write the first paragraph of the story (4-6 lines) in Arabic.
101
+ 2. Start the story with a strong and engaging beginning that captivates the reader from the first line.
102
+ 3. Present the characters and setting (place and time) clearly and interestingly.
103
+ 4. Establish a conflict, problem, or situation that drives the story events.
104
+ 5. Present 3 short, exciting, and logical options for actions the protagonist can take.
105
+ 6. Make the options very short (3-5 words only) and practical and direct.
106
+ 7. ALWAYS include the character's name in each option before the action verb.
107
+ 8. Format: "[Character name] + verb", like: "أحمد يتصل بالشرطة", "سارة تهرب من المكان".
108
+
109
+ Present the first paragraph and options in the following format:
110
+
111
+ الفقرة:
112
+ [Write the first paragraph of the story here in Arabic]
113
+
114
+ الخيارات:
115
+ 1. [Character name + action verb in Arabic, 3-5 words total]
116
+ 2. [Character name + different action verb in Arabic, 3-5 words total]
117
+ 3. [Character name + another different action verb in Arabic, 3-5 words total]
118
+ """
119
+
120
+ return prompt
121
+
122
+
123
+ def create_continuation_prompt(story_context: str, choice_id: int, choice_text: str, current_paragraph: int, max_paragraphs: int) -> str:
124
+ """
125
+ إنشاء برومبت لاستكمال القصة بناءً على اختيار المستخدم
126
+ """
127
+ is_final = current_paragraph >= max_paragraphs - 1
128
+
129
+ prompt = f"""
130
+ Story context so far:
131
+ {story_context}
132
+
133
+ The user chose path number {choice_id}: {choice_text}
134
+
135
+ Required from you:
136
+ 1. Continue writing the story with a new paragraph (4-6 lines) in Arabic that directly follows the choice made by the user.
137
+ 2. Do not summarize the choice that the user made; instead, start directly with the events that result from this choice.
138
+ 3. Add unexpected and exciting developments to engage the reader.
139
+ 4. Maintain consistency in the story's characters and world.
140
+ """
141
+
142
+ if is_final:
143
+ prompt += """
144
+ 5. This is the final paragraph of the story, so end the story in a logical and satisfying way that closes all open paths.
145
+ 6. Suggest an appropriate and deep title for the complete story.
146
+
147
+ Present the final paragraph and title in the following format:
148
+
149
+ الفقرة:
150
+ [Write the final paragraph of the story here in Arabic]
151
+
152
+ العنوان:
153
+ [Write the suggested title for the story here in Arabic]
154
+ """
155
+ else:
156
+ prompt += """
157
+ 5. Present 3 short, logical, and practical options for continuing the story.
158
+ 6. Make the options very short (3-5 words only) in Arabic.
159
+ 7. ALWAYS include the character's name in each option before the action verb.
160
+ 8. Format: "[Character name] + verb", like: "أحمد يتصل بالشرطة", "سارة تهرب من المكان".
161
+ 9. Make it absolutely clear WHO is performing the action in each option.
162
+ 10. Ensure each option will lead to a completely different path in the story.
163
+
164
+ Present the next paragraph and options in the following format:
165
+
166
+ الفقرة:
167
+ [Write the next paragraph of the story here in Arabic]
168
+
169
+ الخيارات:
170
+ 1. [Character name + action verb in Arabic, 3-5 words total]
171
+ 2. [Character name + different action verb in Arabic, 3-5 words total]
172
+ 3. [Character name + another different action verb in Arabic, 3-5 words total]
173
+ """
174
+
175
+ return prompt
176
+
177
+
178
+ def create_title_prompt(complete_story: str) -> str:
179
+ """
180
+ إنشاء برومبت لتوليد عنوان مناسب للقصة المكتملة
181
+ """
182
+ return f"""
183
+ Here is a complete story:
184
+
185
+ {complete_story}
186
+
187
+ Suggest an appropriate and engaging title for this story that reflects its essence and content.
188
+ Provide only the title without any additional explanation and without any story Characters names in Arabic.
189
+ """
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.110.0
2
+ uvicorn==0.28.0
3
+ pydantic==2.6.1
4
+ python-dotenv==1.0.1
5
+ httpx==0.26.0
6
+ gTTS==2.5.0
7
+ python-multipart==0.0.9
8
+ starlette==0.36.3
9
+ aiofiles==23.2.1
tts_service.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import re
4
+ from gtts import gTTS
5
+ import uuid
6
+ import json
7
+ from pathlib import Path
8
+ from dotenv import load_dotenv
9
+
10
+ from ai_service import get_complete_story, generate_title_if_missing
11
+
12
+ # تحميل المتغيرات البيئية
13
+ load_dotenv()
14
+
15
+ # الحصول على مسار تخزين ملفات الصوت
16
+ AUDIO_STORAGE_PATH = os.path.abspath(os.getenv("AUDIO_STORAGE_PATH", "./audio_files"))
17
+ BASE_URL = os.getenv("BASE_URL", "http://localhost:8000")
18
+
19
+ # قاموس لتخزين معرفات الملفات الصوتية للقصص
20
+ story_audio_files = {}
21
+
22
+ def clean_text_for_tts(text: str) -> str:
23
+ """
24
+ تنظيف النص من الرموز التي قد تؤثر على جودة القراءة الصوتية
25
+ """
26
+ # استبدال علامات الترقيم التي قد تسبب مشاكل بمسافات أو استبعادها
27
+ text = re.sub(r'!', ' ', text) # استبدال علامة التعجب بمسافة
28
+ text = re.sub(r'\?', ' ', text) # استبدال علامة الاستفهام بمسافة
29
+ text = re.sub(r'[،,]', ' ', text) # استبدال الفواصل بمسافات
30
+ text = re.sub(r';', ' ', text) # استبدال الفاصلة المنقوطة بمسافة
31
+ text = re.sub(r':', ' ', text) # استبدال النقطتين بمسافة
32
+ text = re.sub(r'#', ' ', text) # استبدال علامة الهاشتاغ بمسافة
33
+ text = re.sub(r'@', ' ', text) # استبدال علامة الإيميل بمسافة
34
+ text = re.sub(r'_', ' ', text) # استبدال الشرطة السفلية بمسافة
35
+ text = re.sub(r'\*', ' ', text) # استبدال علامة النجمة بمسافة
36
+ text = re.sub(r'=', ' ', text) # استبدال علامة المساواة بمسافة
37
+ text = re.sub(r'\+', ' ', text) # استبدال علامة الزائد بمسافة
38
+ text = re.sub(r'-', ' ', text) # استبدال علامة الناقص بمسافة
39
+ text = re.sub(r'/', ' ', text) # استبدال علامة القسمة بمسافة
40
+ text = re.sub(r'\\', ' ', text) # استبدال الشرطة المائلة العكسية بمسافة
41
+ text = re.sub(r'%', ' بالمئة ', text) # استبدال علامة النسبة بكلمة "بالمئة"
42
+ text = re.sub(r'&', ' و ', text) # استبدال علامة & بكلمة "و"
43
+ text = re.sub(r'\^', ' ', text) # استبدال علامة القوة بمسافة
44
+ text = re.sub(r'\$', ' ', text) # استبدال علامة الدولار بمسافة
45
+
46
+ # إزالة علامات التنصيص تماماً
47
+ text = re.sub(r'["""\'«»]', ' ', text) # إزالة كل أنواع علامات التنصيص
48
+
49
+ # إزالة الأقواس والمحتوى بداخلها
50
+ text = re.sub(r'\(.*?\)', ' ', text)
51
+ text = re.sub(r'\[.*?\]', ' ', text)
52
+ text = re.sub(r'\{.*?\}', ' ', text)
53
+ text = re.sub(r'<.*?>', ' ', text)
54
+
55
+ # تنظيف الفترات الطويلة من المسافات المتكررة الناتجة عن الإزالة
56
+ text = re.sub(r'\s+', ' ', text)
57
+
58
+ # الحفاظ على النقاط كفواصل بين الجمل مع إضافة مسافة
59
+ text = re.sub(r'\.', '. ', text)
60
+
61
+ return text.strip()
62
+
63
+ async def ensure_storage_path():
64
+ """
65
+ التأكد من وجود مجلد لتخزين ملفات الصوت
66
+ """
67
+ Path(AUDIO_STORAGE_PATH).mkdir(parents=True, exist_ok=True)
68
+
69
+ async def text_to_speech(text: str, filename: str) -> str:
70
+ """
71
+ تحويل النص إلى صوت وحفظه في ملف
72
+ """
73
+ # التأكد من وجود مجلد التخزين
74
+ await ensure_storage_path()
75
+
76
+ # مسار الملف الكامل
77
+ file_path = os.path.join(AUDIO_STORAGE_PATH, filename)
78
+
79
+ # التحقق مما إذا كان الملف موجوداً بالفعل
80
+ if os.path.exists(file_path):
81
+ return filename
82
+
83
+ # تنظيف النص من الرموز التي قد تؤثر على جودة القراءة
84
+ cleaned_text = clean_text_for_tts(text)
85
+ print(f"Original text length: {len(text)}, Cleaned text length: {len(cleaned_text)}")
86
+
87
+ # استخدام وظيفة run_in_executor لتنفيذ عملية TTS في خيط منفصل
88
+ loop = asyncio.get_event_loop()
89
+
90
+ await loop.run_in_executor(
91
+ None,
92
+ lambda: gTTS(text=cleaned_text, lang='ar', slow=False).save(file_path)
93
+ )
94
+
95
+ return filename
96
+
97
+ async def generate_audio_for_story(story_id: str, speed: float = 1.0) -> str:
98
+ """
99
+ توليد ملف صوتي للقصة الكاملة وإرجاع معرف الملف مع معلومات السرعة
100
+ """
101
+ # التحقق مما إذا كان هناك ملف صوتي موجود للقصة
102
+ if story_id not in story_audio_files:
103
+ # التأكد من وجود عنوان للقصة
104
+ await generate_title_if_missing(story_id)
105
+
106
+ # الحصول على نص القصة الكامل
107
+ story_text = await get_complete_story(story_id)
108
+
109
+ # إنشاء اسم فريد للملف الصوتي
110
+ filename = f"{story_id}_{uuid.uuid4().hex}.mp3"
111
+
112
+ # تحويل النص إلى صوت
113
+ await text_to_speech(story_text, filename)
114
+
115
+ # تخزين معرف الملف الصوتي للقصة
116
+ story_audio_files[story_id] = filename
117
+
118
+ # إضافة معلومات السرعة للملف (سيتم استخدامها في الواجهة الأمامية)
119
+ # وذلك حتى نتجنب الحاجة إلى معالجة ملفات الصوت مباشرةً
120
+ filename = story_audio_files[story_id]
121
+
122
+ return filename
123
+
124
+ def get_audio_url(filename: str, speed: float = 1.0) -> str:
125
+ """
126
+ الحصول على رابط الملف الصوتي مع معلومات السرعة
127
+ """
128
+ base_url = f"{BASE_URL}/audio/{filename}"
129
+
130
+ # إضافة معامل سرعة التشغيل كمعامل استعلام
131
+ # سيتم استخدامه في الواجهة الأمامية لضبط سرعة التشغيل
132
+ return f"{base_url}?speed={speed}"