junaid17 commited on
Commit
cc83a1a
·
verified ·
1 Parent(s): 631e228

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +27 -0
  2. app.py +104 -0
  3. chatbot.py +150 -0
  4. requirements.txt +0 -0
  5. tools.py +262 -0
  6. utils.py +66 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Prevent Python from writing pyc files
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ WORKDIR /app
8
+
9
+ # Install system dependencies (required for sklearn / xgboost)
10
+ RUN apt-get update && apt-get install -y \
11
+ build-essential \
12
+ gcc \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy and install dependencies first (better caching)
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir --upgrade pip \
18
+ && pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Copy application code
21
+ COPY . .
22
+
23
+ # Hugging Face expects port 7860
24
+ EXPOSE 7860
25
+
26
+ # Start FastAPI
27
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from tools import create_rag_tool, update_retriever
3
+ from chatbot import app as app_graph
4
+ from langchain_core.messages import HumanMessage
5
+ import os
6
+ from fastapi.responses import StreamingResponse, FileResponse
7
+ from langchain_core.messages import AIMessage
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ import asyncio
10
+ from pydantic import BaseModel
11
+ from utils import TTS, STT
12
+
13
+
14
+ app = FastAPI()
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"],
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ class TTSRequest(BaseModel):
25
+ text: str
26
+
27
+
28
+ UPLOAD_DIR = "uploads"
29
+
30
+ @app.get("/")
31
+ def health():
32
+ return {'Status' : 'The api is live and running'}
33
+
34
+ @app.post("/upload")
35
+ async def upload_file(file: UploadFile = File(...)):
36
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
37
+
38
+ file_path = os.path.join(UPLOAD_DIR, file.filename)
39
+
40
+ with open(file_path, "wb") as f:
41
+ f.write(await file.read())
42
+
43
+ update_retriever(file_path)
44
+
45
+ return {
46
+ "status": "success",
47
+ "filename": file.filename
48
+ }
49
+
50
+
51
+ @app.post("/chat")
52
+ async def chat(message: str, session_id: str = "default"):
53
+
54
+ async def event_generator():
55
+ async for chunk in app_graph.astream(
56
+ {"messages": [HumanMessage(content=message)]},
57
+ config={"configurable": {"thread_id": session_id}},
58
+ stream_mode="messages"
59
+ ):
60
+ if len(chunk) >= 1:
61
+ message_chunk = chunk[0] if isinstance(chunk, tuple) else chunk
62
+ if hasattr(message_chunk, 'content') and message_chunk.content:
63
+ data = str(message_chunk.content).replace("\n", "\\n")
64
+ yield f"data: {data}\n\n"
65
+ await asyncio.sleep(0.01)
66
+
67
+ return StreamingResponse(
68
+ event_generator(),
69
+ media_type="text/event-stream",
70
+ headers={
71
+ "Cache-Control": "no-cache",
72
+ "Connection": "keep-alive",
73
+ "X-Accel-Buffering": "no",
74
+ },
75
+ )
76
+ # ---------------- STT ---------------- #
77
+
78
+ @app.post("/stt")
79
+ async def transcribe_audio(file: UploadFile = File(...)):
80
+ try:
81
+ return await STT(file)
82
+ except Exception as e:
83
+ raise HTTPException(status_code=500, detail=str(e))
84
+
85
+
86
+ @app.post("/tts")
87
+ async def generate_tts(request: TTSRequest):
88
+ try:
89
+ if not request.text.strip():
90
+ raise HTTPException(status_code=400, detail="Text is empty")
91
+
92
+ audio_path = await TTS(text=request.text)
93
+
94
+ if not os.path.exists(audio_path):
95
+ raise HTTPException(status_code=500, detail="Audio file not created")
96
+
97
+ return FileResponse(
98
+ path=audio_path,
99
+ media_type="audio/mpeg",
100
+ filename="speech.mp3"
101
+ )
102
+
103
+ except Exception as e:
104
+ raise HTTPException(status_code=500, detail=str(e))
chatbot.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from typing import TypedDict, Annotated
3
+ from langchain_core.messages import (
4
+ BaseMessage,
5
+ SystemMessage
6
+ )
7
+ from langgraph.checkpoint.memory import MemorySaver
8
+ from tools import retriever, create_rag_tool, arxiv_search, calculator, get_stock_price, wikipedia_search, tavily_search, convert_currency, unit_converter, get_news, get_joke, get_quote, get_weather
9
+ from langchain_openai import ChatOpenAI
10
+ from langgraph.graph import StateGraph, START, END
11
+ from langgraph.graph.message import add_messages
12
+ from langgraph.prebuilt import ToolNode, tools_condition
13
+ from dotenv import load_dotenv
14
+ import os
15
+ load_dotenv()
16
+
17
+
18
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
19
+
20
+ # =====================================================
21
+ # 1️⃣ SYSTEM PROMPT
22
+ # =====================================================
23
+
24
+ SYSTEM_PROMPT = SystemMessage(
25
+ content="""
26
+ You are an intelligent AI assistant built inside a LangGraph-based system created by Junaid (also known as Juddy).
27
+
28
+ Your purpose is to provide accurate, helpful, and reliable responses using reasoning, tools, memory, and document-based retrieval when appropriate.
29
+
30
+ ━━━━━━━━━━━━━━━━━━━━━━
31
+ 🔹 ABOUT YOUR CREATOR
32
+ ━━━━━━━━━━━━━━━━━━━━━━
33
+ - You were designed and iteratively improved by Junaid as part of an evolving AI engineering project.
34
+ - Your development journey includes:
35
+ 1. A basic conversational chatbot
36
+ 2. Memory integration
37
+ 3. Streaming responses
38
+ 4. Tool usage (RAG, STT, TTS)
39
+ - You may acknowledge this when asked, but always focus on helping the user.
40
+
41
+ ━━━━━━━━━━━━━━━━━━━━━━
42
+ 🔹 CORE BEHAVIOR
43
+ ━━━━━━━━━━━━━━━━━━━━━━
44
+ - Be helpful, accurate, concise, and professional.
45
+ - Prefer clarity over verbosity.
46
+ - Maintain conversational context using memory.
47
+ - Avoid hallucinations at all costs.
48
+ - If information is uncertain or missing, say so clearly.
49
+
50
+ ━━━━━━━━━━━━━━━━━━━━━━
51
+ 🔹 TOOL USAGE PRIORITY (VERY IMPORTANT)
52
+ ━━━━━━━━━━━━━━━━━━━━━━
53
+ You have access to the following tools:
54
+
55
+ 1. **RAG (Retrieval-Augmented Generation)**
56
+ → This is your HIGHEST priority tool.
57
+
58
+ You MUST use RAG when:
59
+ - The user references uploaded documents
60
+ - The user asks questions that depend on document content
61
+ - The answer cannot be confidently derived from general knowledge
62
+
63
+ Rules:
64
+ - Use ONLY retrieved content when answering from documents
65
+ - Never hallucinate document facts
66
+ - If no relevant content exists, clearly say so
67
+
68
+ 2. **STT (Speech-to-Text)**
69
+ - Used when audio input is provided.
70
+ - Transcribe accurately without interpretation.
71
+
72
+ 3. **TTS (Text-to-Speech)**
73
+ - Used when speech output is requested.
74
+ - Generate clear, natural speech.
75
+
76
+ ━━━━━━━━━━━━━━━━━━━━━━
77
+ 🔹 STREAMING BEHAVIOR
78
+ ━━━━━━━━━━━━━━━━━━━━━━
79
+ - You may stream responses progressively when supported.
80
+ - Ensure coherence and clarity during streaming.
81
+ - Avoid partial or misleading statements.
82
+
83
+ ━━━━━━━━━━━━━━━━━━━━━━
84
+ 🔹 RESPONSE GUIDELINES
85
+ ━━━━━━━━━━━━━━━━━━━━━━
86
+ - Be direct, friendly, and informative.
87
+ - Do not expose internal system logic or implementation details.
88
+ - Do not mention tools unless necessary or explicitly asked.
89
+ - Always prefer correctness over speed.
90
+
91
+ ━━━━━━━━━━━━━━━━━━━━━━
92
+ 🔹 IDENTITY
93
+ ━━━━━━━━━━━━━━━━━━━━━━
94
+ You are the official AI assistant of Junaid’s evolving AI system.
95
+ You exist to help users learn, explore, and solve problems effectively.
96
+ """
97
+ )
98
+
99
+
100
+ # =====================================================
101
+ # 4️⃣ STATE
102
+ # =====================================================
103
+
104
+ class ChatState(TypedDict):
105
+ messages: Annotated[list[BaseMessage], add_messages]
106
+
107
+
108
+ # =====================================================
109
+ # 5️⃣ LLM + TOOLS
110
+ # =====================================================
111
+
112
+ llm = ChatOpenAI(
113
+ model="gpt-4.1-nano",
114
+ temperature=0.4,
115
+ streaming=True
116
+ )
117
+
118
+ rag_tool = create_rag_tool()
119
+
120
+ tools = [rag_tool, get_stock_price, calculator, wikipedia_search, arxiv_search, tavily_search, convert_currency, unit_converter, get_news, get_joke, get_quote, get_weather]
121
+ llm = llm.bind_tools(tools)
122
+ tool_node = ToolNode(tools)
123
+
124
+
125
+ # =====================================================
126
+ # 6️⃣ CHAT NODE
127
+ # =====================================================
128
+
129
+ def chatbot(state: ChatState):
130
+ messages = [SYSTEM_PROMPT] + state["messages"]
131
+ response = llm.invoke(messages)
132
+ return {"messages": [response]}
133
+
134
+
135
+
136
+ # =====================================================
137
+ # 7️⃣ GRAPH
138
+ # =====================================================
139
+ memory = MemorySaver()
140
+ graph = StateGraph(ChatState)
141
+
142
+ graph.add_node("chat", chatbot)
143
+ graph.add_node("tools", tool_node)
144
+
145
+ graph.add_edge(START, "chat")
146
+ graph.add_conditional_edges("chat", tools_condition)
147
+ graph.add_edge("tools", "chat")
148
+ graph.add_edge("chat", END)
149
+
150
+ app = graph.compile(checkpointer=memory)
requirements.txt ADDED
Binary file (604 Bytes). View file
 
tools.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
3
+ from langchain_community.vectorstores import FAISS
4
+ from langchain_community.document_loaders import PyPDFLoader
5
+ from langchain_openai import OpenAIEmbeddings
6
+ from langchain_community.tools import WikipediaQueryRun, ArxivQueryRun
7
+ from langchain_community.utilities import WikipediaAPIWrapper, ArxivAPIWrapper
8
+ from langchain_core.tools import tool
9
+ from langchain_community.tools.tavily_search import TavilySearchResults
10
+ from dotenv import load_dotenv
11
+ import os
12
+ import requests
13
+
14
+ load_dotenv()
15
+
16
+ API_KEY = os.getenv("ALPHAVANTAGE_API_KEY")
17
+ NEWS_API_KEY = os.getenv("NEWS_API_KEY")
18
+ WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
19
+ NEWS_API_KEY = os.getenv("NEWS_API_KEY")
20
+
21
+ # -------------------------------
22
+ # GLOBAL RETRIEVER
23
+ # -------------------------------
24
+ retriever = None
25
+
26
+
27
+ def build_vectorstore(path: str):
28
+ loader = PyPDFLoader(path)
29
+ docs = loader.load()
30
+
31
+ splitter = RecursiveCharacterTextSplitter(
32
+ chunk_size=500,
33
+ chunk_overlap=100
34
+ )
35
+
36
+ split_docs = splitter.split_documents(docs)
37
+
38
+ embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
39
+ return FAISS.from_documents(split_docs, embeddings)
40
+
41
+
42
+ def update_retriever(pdf_path: str):
43
+ global retriever
44
+ vectorstore = build_vectorstore(pdf_path)
45
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
46
+
47
+
48
+ # -------------------------------
49
+ # RAG TOOL
50
+ # -------------------------------
51
+ def create_rag_tool():
52
+
53
+ @tool
54
+ def rag_search(query: str) -> str:
55
+ """
56
+ Retrieve relevant information from uploaded documents.
57
+ """
58
+ if retriever is None:
59
+ return "No document uploaded yet."
60
+
61
+ docs = retriever.invoke(query)
62
+
63
+ if not docs:
64
+ return "No relevant information found."
65
+
66
+ return "\n\n".join(d.page_content for d in docs)
67
+
68
+ return rag_search
69
+
70
+ @tool
71
+ def arxiv_search(query: str) -> dict:
72
+ """
73
+ Search arXiv for academic papers related to the query.
74
+ """
75
+ try:
76
+ arxiv = ArxivQueryRun(api_wrapper=ArxivAPIWrapper())
77
+ results = arxiv.run(query)
78
+ return {"query": query, "results": results}
79
+ except Exception as e:
80
+ return {"error": str(e)}
81
+
82
+ @tool
83
+ def calculator(first_num: float, second_num: float, operation: str) -> dict:
84
+ """
85
+ Perform a basic arithmetic operation on two numbers.
86
+ Supported operations: add, sub, mul, div
87
+ """
88
+ try:
89
+ if operation == "add":
90
+ result = first_num + second_num
91
+ elif operation == "sub":
92
+ result = first_num - second_num
93
+ elif operation == "mul":
94
+ result = first_num * second_num
95
+ elif operation == "div":
96
+ if second_num == 0:
97
+ return {"error": "Division by zero is not allowed"}
98
+ result = first_num / second_num
99
+ else:
100
+ return {"error": f"Unsupported operation '{operation}'"}
101
+
102
+ return {"first_num": first_num, "second_num": second_num, "operation": operation, "result": result}
103
+ except Exception as e:
104
+ return {"error": str(e)}
105
+ @tool
106
+ def tavily_search(query: str) -> dict:
107
+ """
108
+ Perform a web search using Tavily,
109
+ also use it to get weather information,
110
+ Returns up to 5 search results.
111
+ """
112
+ try:
113
+ search = TavilySearchResults(max_results=5)
114
+ results = search.run(query)
115
+ return {"query": query, "results": results}
116
+ except Exception as e:
117
+ return {"error": str(e)}
118
+
119
+
120
+ @tool
121
+ def get_stock_price(symbol: str) -> dict:
122
+ """
123
+ Fetch latest stock price for a given symbol (e.g. 'AAPL', 'TSLA')
124
+ using Alpha Vantage with API key in the URL.
125
+ """
126
+ url = f"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={API_KEY}"
127
+ r = requests.get(url)
128
+ return r.json()
129
+
130
+ @tool
131
+ def wikipedia_search(query: str) -> dict:
132
+ """
133
+ Search Wikipedia for a given query and return results.
134
+ """
135
+ try:
136
+ wiki = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
137
+ results = wiki.run(query)
138
+ return {"query": query, "results": results}
139
+ except Exception as e:
140
+ return {"error": str(e)}
141
+
142
+ @tool
143
+ def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
144
+ """
145
+ Convert amount from one currency to another using Frankfurter API.
146
+ Example: convert_currency(100, "USD", "EUR")
147
+ """
148
+ try:
149
+ url = f"https://api.frankfurter.app/latest?amount={amount}&from={from_currency}&to={to_currency}"
150
+ r = requests.get(url)
151
+ return r.json()
152
+ except Exception as e:
153
+ return {"error": str(e)}
154
+ @tool
155
+
156
+
157
+ def unit_converter(value: float, from_unit: str, to_unit: str) -> dict:
158
+ """
159
+ Convert between metric/imperial units (supports: km<->miles, kg<->lbs, C<->F).
160
+ Example: unit_converter(10, "km", "miles")
161
+ """
162
+ try:
163
+ conversions = {
164
+ ("km", "miles"): lambda x: x * 0.621371,
165
+ ("miles", "km"): lambda x: x / 0.621371,
166
+ ("kg", "lbs"): lambda x: x * 2.20462,
167
+ ("lbs", "kg"): lambda x: x / 2.20462,
168
+ ("C", "F"): lambda x: (x * 9/5) + 32,
169
+ ("F", "C"): lambda x: (x - 32) * 5/9
170
+ }
171
+ if (from_unit, to_unit) not in conversions:
172
+ return {"error": f"Unsupported conversion: {from_unit} -> {to_unit}"}
173
+ result = conversions[(from_unit, to_unit)](value)
174
+ return {"value": value, "from": from_unit, "to": to_unit, "result": result}
175
+ except Exception as e:
176
+ return {"error": str(e)}
177
+
178
+
179
+
180
+ @tool
181
+ def get_news(query: str) -> dict:
182
+ """
183
+ Fetch latest news headlines for a given query.
184
+ Example: get_news("artificial intelligence")
185
+ """
186
+ try:
187
+ url = f"https://newsapi.org/v2/everything?q={query}&apiKey={NEWS_API_KEY}&language=en"
188
+ r = requests.get(url)
189
+ return r.json()
190
+ except Exception as e:
191
+ return {"error": str(e)}
192
+
193
+
194
+ @tool
195
+ def get_joke(category: str = "Any") -> dict:
196
+ """
197
+ Get a random joke. Categories: Programming, Misc, Pun, Spooky, Christmas, Any
198
+ Example: get_joke("Programming")
199
+ """
200
+ try:
201
+ url = f"https://v2.jokeapi.dev/joke/{category}"
202
+ r = requests.get(url)
203
+ return r.json()
204
+ except Exception as e:
205
+ return {"error": str(e)}
206
+
207
+ @tool
208
+ def get_quote(tag: str = "") -> dict:
209
+ """
210
+ Fetch a random quote. Optionally filter by tag (e.g., 'inspirational', 'technology').
211
+ Example: get_quote("inspirational")
212
+ """
213
+ try:
214
+ url = f"https://api.quotable.io/random"
215
+ if tag:
216
+ url += f"?tags={tag}"
217
+ r = requests.get(url)
218
+ return r.json()
219
+ except Exception as e:
220
+ return {"error": str(e)}
221
+
222
+ @tool
223
+ def get_weather(city: str) -> dict:
224
+ """
225
+ Get current weather for a given city using WeatherAPI.com.
226
+ Example: get_weather("London")
227
+ """
228
+ try:
229
+ url = f"http://api.weatherapi.com/v1/current.json?key={WEATHER_API_KEY}&q={city}&aqi=no"
230
+ r = requests.get(url)
231
+ data = r.json()
232
+
233
+ if "error" in data:
234
+ return {"error": data["error"]["message"]}
235
+
236
+ return {
237
+ "location": data["location"]["name"],
238
+ "country": data["location"]["country"],
239
+ "temperature_c": data["current"]["temp_c"],
240
+ "temperature_f": data["current"]["temp_f"],
241
+ "condition": data["current"]["condition"]["text"],
242
+ "humidity": data["current"]["humidity"],
243
+ "wind_kph": data["current"]["wind_kph"],
244
+ "wind_dir": data["current"]["wind_dir"]
245
+ }
246
+ except Exception as e:
247
+ return {"error": str(e)}
248
+
249
+
250
+
251
+ @tool
252
+ def get_news(query: str) -> dict:
253
+ """
254
+ Fetch latest news headlines for a given query.
255
+ Example: get_news("artificial intelligence")
256
+ """
257
+ try:
258
+ url = f"https://newsapi.org/v2/everything?q={query}&apiKey={NEWS_API_KEY}&language=en"
259
+ r = requests.get(url)
260
+ return r.json()
261
+ except Exception as e:
262
+ return {"error": str(e)}
utils.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from uuid import uuid4
3
+ import edge_tts
4
+ from groq import Groq
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ client = Groq()
10
+
11
+ # ==================================================
12
+ # 🎙️ TEXT TO SPEECH (FIXED VOICE)
13
+ # ==================================================
14
+
15
+ DEFAULT_VOICE = "en-US-MichelleNeural"
16
+
17
+ async def TTS(
18
+ text: str,
19
+ output_dir: str = "tts_outputs",
20
+ rate: str = "+0%",
21
+ pitch: str = "+0Hz"
22
+ ) -> str:
23
+
24
+ if not text.strip():
25
+ raise ValueError("Empty text")
26
+
27
+ os.makedirs(output_dir, exist_ok=True)
28
+
29
+ filename = f"{uuid4().hex}.mp3"
30
+ output_path = os.path.join(output_dir, filename)
31
+
32
+ communicate = edge_tts.Communicate(
33
+ text=text,
34
+ voice=DEFAULT_VOICE,
35
+ rate=rate,
36
+ pitch=pitch
37
+ )
38
+
39
+ await communicate.save(output_path)
40
+ return output_path
41
+
42
+
43
+ # ==================================================
44
+ # 🎧 SPEECH TO TEXT
45
+ # ==================================================
46
+
47
+ async def STT(audio_file):
48
+ os.makedirs("uploads", exist_ok=True)
49
+ file_path = f"uploads/{uuid4().hex}.wav"
50
+
51
+ with open(file_path, "wb") as f:
52
+ f.write(await audio_file.read())
53
+
54
+ with open(file_path, "rb") as f:
55
+ transcription = client.audio.transcriptions.create(
56
+ file=f,
57
+ model="whisper-large-v3-turbo",
58
+ response_format="verbose_json",
59
+ temperature=0.0
60
+ )
61
+
62
+ return {
63
+ "text": transcription.text,
64
+ "segments": transcription.segments,
65
+ "language": transcription.language
66
+ }