Yuvvan Talreja commited on
Commit
c31d9d3
·
1 Parent(s): 12089c6

Add application files

Browse files
Files changed (5) hide show
  1. .dockerignore +17 -0
  2. Dockerfile +9 -4
  3. app.py +9 -1
  4. app_fastapi.py +243 -1
  5. requirements.txt +2 -4
.dockerignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .vscode
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+ .env
8
+ .venv
9
+ env/
10
+ venv/
11
+ ENV/
12
+ *.log
13
+ .DS_Store
14
+ *.swp
15
+ *.swo
16
+ .ipynb_checkpoints
17
+ .pytest_cache
Dockerfile CHANGED
@@ -6,9 +6,14 @@ WORKDIR /app
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
  ffmpeg \
8
  build-essential \
 
9
  && apt-get clean \
10
  && rm -rf /var/lib/apt/lists/*
11
 
 
 
 
 
12
  # Copy requirements first for better caching
13
  COPY requirements.txt .
14
  RUN pip install --no-cache-dir -r requirements.txt
@@ -16,8 +21,8 @@ RUN pip install --no-cache-dir -r requirements.txt
16
  # Copy the rest of the application
17
  COPY . .
18
 
19
- # Expose the port
20
- EXPOSE 5000
21
 
22
- # Run using Gunicorn
23
- CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
 
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
  ffmpeg \
8
  build-essential \
9
+ git \
10
  && apt-get clean \
11
  && rm -rf /var/lib/apt/lists/*
12
 
13
+ # Download NLTK data
14
+ RUN python -m pip install --no-cache-dir nltk
15
+ RUN python -c "import nltk; nltk.download('punkt'); nltk.download('wordnet')"
16
+
17
  # Copy requirements first for better caching
18
  COPY requirements.txt .
19
  RUN pip install --no-cache-dir -r requirements.txt
 
21
  # Copy the rest of the application
22
  COPY . .
23
 
24
+ # Port for Hugging Face Spaces
25
+ EXPOSE 7860
26
 
27
+ # Run using Uvicorn
28
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,7 +1,15 @@
 
 
1
  from app_fastapi import app
2
 
3
  # This file is used for Hugging Face Spaces deployment
4
  # Spaces will automatically detect this app.py file and serve the FastAPI app
5
 
6
  # For local development, you can run:
7
- # uvicorn app_fastapi:app --host 0.0.0.0 --port 8000
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import uvicorn
3
  from app_fastapi import app
4
 
5
  # This file is used for Hugging Face Spaces deployment
6
  # Spaces will automatically detect this app.py file and serve the FastAPI app
7
 
8
  # For local development, you can run:
9
+ # uvicorn app_fastapi:app --host 0.0.0.0 --port 8000
10
+
11
+ # Hugging Face Spaces expects this variable to be named 'app'
12
+ # Already imported from app_fastapi.py
13
+
14
+ if __name__ == "__main__":
15
+ uvicorn.run(app, host="0.0.0.0", port=7860)
app_fastapi.py CHANGED
@@ -1 +1,243 @@
1
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Response, HTTPException
2
+ from fastapi.responses import JSONResponse, FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+ from pydantic import BaseModel
6
+ import os
7
+ import base64
8
+ import numpy as np
9
+ import torch
10
+ import time
11
+ from transformers import pipeline
12
+ import threading
13
+ import json
14
+ import logging
15
+ from topic_segmenter import TopicSegmenter
16
+ import uvicorn
17
+ from typing import Optional, Dict, Any, List
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+
20
+ app = FastAPI()
21
+ logging.basicConfig(level=logging.INFO)
22
+
23
+ # Add CORS middleware
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ # Templates for HTML rendering
33
+ templates = Jinja2Templates(directory="templates")
34
+
35
+ # Serve static files - mount this after creating the app but before routes
36
+ app.mount("/static", StaticFiles(directory="static"), name="static")
37
+
38
+ # Models cache
39
+ models = {}
40
+ model_lock = threading.Lock()
41
+
42
+ # Route to serve the main page
43
+ @app.get("/")
44
+ async def index(request: Request):
45
+ return templates.TemplateResponse("index.html", {"request": request})
46
+
47
+ # Defining Pydantic models for request/response data
48
+ class AudioRequest(BaseModel):
49
+ audio: str
50
+ model: Optional[str] = "openai/whisper-base"
51
+
52
+ class TranscriptRequest(BaseModel):
53
+ transcript: str
54
+
55
+ class ModelCheckRequest(BaseModel):
56
+ model: str
57
+
58
+ def get_model(model_name):
59
+ """Load and cache the model"""
60
+ with model_lock:
61
+ if model_name not in models:
62
+ logging.info(f"Loading model: {model_name}")
63
+
64
+ hf_model_name = model_name
65
+ if model_name.startswith('Xenova/'):
66
+ base_name = model_name.split('/')[-1]
67
+ if '.en' in base_name:
68
+ size = base_name.split('.')[0].replace('whisper-', '')
69
+ hf_model_name = f"openai/whisper-{size}"
70
+ else:
71
+ size = base_name.replace('whisper-', '')
72
+ hf_model_name = f"openai/whisper-{size}"
73
+
74
+ logging.info(f"Converting Xenova model {model_name} to {hf_model_name}")
75
+
76
+ models[model_name] = pipeline(
77
+ "automatic-speech-recognition",
78
+ hf_model_name,
79
+ chunk_length_s=30,
80
+ stride_length_s=5
81
+ )
82
+ logging.info(f"Model {model_name} loaded successfully")
83
+ return models[model_name]
84
+
85
+ def process_audio(audio_data, sample_rate=16000, model_name="openai/whisper-base"):
86
+ """Process audio data with the Whisper model"""
87
+ try:
88
+ model = get_model(model_name)
89
+
90
+ chunk_duration = 30
91
+ samples_per_chunk = chunk_duration * sample_rate
92
+ total_chunks = (len(audio_data) + samples_per_chunk - 1) // samples_per_chunk
93
+
94
+ full_transcript = ""
95
+
96
+ for i in range(total_chunks):
97
+ start = i * samples_per_chunk
98
+ end = min(start + samples_per_chunk, len(audio_data))
99
+ chunk = audio_data[start:end]
100
+
101
+ logging.info(f"Processing chunk {i+1}/{total_chunks}")
102
+
103
+ result = model(
104
+ chunk,
105
+ return_timestamps=True,
106
+ generate_kwargs={"language": "english", "task": "transcribe"}
107
+ )
108
+
109
+ if "chunks" in result and len(result["chunks"]) > 0:
110
+ for chunk_with_time in result["chunks"]:
111
+ if "timestamp" in chunk_with_time and len(chunk_with_time["timestamp"]) == 2:
112
+ chunk_start_seconds = chunk_with_time["timestamp"][0]
113
+ absolute_start_time = (start / sample_rate) + chunk_start_seconds
114
+ timestamp = f"[{int(absolute_start_time)}]"
115
+ full_transcript += f"{timestamp} {chunk_with_time['text'].strip()}\n"
116
+ else:
117
+ timestamp = f"[{int(start / sample_rate)}]"
118
+ full_transcript += f"{timestamp} {result['text'].strip()}\n"
119
+
120
+ return full_transcript
121
+ except Exception as e:
122
+ logging.error(f"Error processing audio: {str(e)}")
123
+ raise
124
+
125
+ @app.post("/transcribe")
126
+ async def transcribe(request: AudioRequest):
127
+ try:
128
+ if not request.audio:
129
+ raise HTTPException(status_code=400, detail="No audio data provided")
130
+
131
+ audio_bytes = base64.b64decode(request.audio)
132
+ audio_np = np.frombuffer(audio_bytes, dtype=np.float32)
133
+
134
+ start_time = time.time()
135
+ transcript = process_audio(audio_np, model_name=request.model)
136
+ elapsed_time = time.time() - start_time
137
+
138
+ logging.info(f"Transcription completed in {elapsed_time:.2f} seconds")
139
+
140
+ return {"transcript": transcript}
141
+ except Exception as e:
142
+ logging.error(f"Transcription error: {str(e)}")
143
+ raise HTTPException(status_code=500, detail=str(e))
144
+
145
+ @app.post("/analyze")
146
+ async def analyze_topics(request: TranscriptRequest):
147
+ try:
148
+ if not request.transcript:
149
+ raise HTTPException(status_code=400, detail="No transcript provided")
150
+
151
+ transcript = request.transcript
152
+
153
+ segmenter = TopicSegmenter(
154
+ window_size=2,
155
+ similarity_threshold=0.15,
156
+ context_size=1,
157
+ min_segment_size=2,
158
+ topic_similarity_threshold=0.25,
159
+ max_topics=6,
160
+ hierarchical_threshold=0.6
161
+ )
162
+
163
+ segments, topic_mappings, topic_history, topic_hierarchies = segmenter.segment_transcript(transcript)
164
+
165
+ results = []
166
+
167
+ parent_topics = set()
168
+ for parent_id in topic_hierarchies.keys():
169
+ parent_topics.add(parent_id)
170
+
171
+ added_segments = set()
172
+
173
+ for i, (segment, topic_id) in enumerate(zip(segments, topic_mappings)):
174
+ if i in added_segments:
175
+ continue
176
+
177
+ if topic_id in parent_topics:
178
+ parent_data = {
179
+ 'segment_id': i,
180
+ 'topic_name': topic_history[topic_id][1],
181
+ 'content': segment,
182
+ 'timestamp': topic_history[topic_id][3],
183
+ 'is_parent': True,
184
+ 'children': []
185
+ }
186
+
187
+ for j, (child_segment, child_topic_id) in enumerate(zip(segments, topic_mappings)):
188
+ if j != i and child_topic_id in topic_hierarchies.get(topic_id, []):
189
+ parent_data['children'].append({
190
+ 'segment_id': j,
191
+ 'topic_name': topic_history[child_topic_id][1],
192
+ 'content': child_segment,
193
+ 'timestamp': topic_history[child_topic_id][3]
194
+ })
195
+ added_segments.add(j)
196
+
197
+ results.append(parent_data)
198
+ added_segments.add(i)
199
+ elif topic_id not in [child for children in topic_hierarchies.values() for child in children]:
200
+ results.append({
201
+ 'segment_id': i,
202
+ 'topic_name': topic_history[topic_id][1],
203
+ 'content': segment,
204
+ 'timestamp': topic_history[topic_id][3],
205
+ 'is_parent': False,
206
+ 'children': []
207
+ })
208
+ added_segments.add(i)
209
+
210
+ return {"segments": results}
211
+ except Exception as e:
212
+ logging.error(f"Analysis error: {str(e)}")
213
+ raise HTTPException(status_code=500, detail=str(e))
214
+
215
+ @app.post("/check_model")
216
+ async def check_model(request: ModelCheckRequest):
217
+ try:
218
+ if not request.model:
219
+ raise HTTPException(status_code=400, detail="No model specified")
220
+
221
+ model_name = request.model
222
+ logging.info(f"Checking model availability: {model_name}")
223
+
224
+ if model_name in models:
225
+ logging.info(f"Model {model_name} is already loaded")
226
+ return {"available": True}
227
+
228
+ supported_prefixes = [
229
+ "openai/whisper",
230
+ "Xenova/whisper",
231
+ "whisper"
232
+ ]
233
+
234
+ available = any(model_name.startswith(prefix) or model_name.lower().startswith(prefix.lower()) for prefix in supported_prefixes)
235
+
236
+ logging.info(f"Model {model_name} availability: {available}")
237
+ return {"available": available}
238
+ except Exception as e:
239
+ logging.error(f"Model check error: {str(e)}")
240
+ raise HTTPException(status_code=500, detail=str(e))
241
+
242
+ if __name__ == '__main__':
243
+ uvicorn.run("app_fastapi:app", host="0.0.0.0", port=5000, reload=True)
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
- flask>=2.0.0
2
- gunicorn>=20.0.0
3
  numpy>=1.19.0
4
  scikit-learn>=1.0.0
5
  nltk>=3.6.0
@@ -10,8 +10,6 @@ spacy>=3.0.0
10
  librosa>=0.8.0
11
  soundfile>=0.10.0
12
  thinc>=8.0.0
13
- fastapi>=0.68.0
14
- uvicorn>=0.15.0
15
  jinja2>=3.0.0
16
  python-multipart>=0.0.5
17
  pydantic>=1.8.2
 
1
+ fastapi>=0.68.0
2
+ uvicorn>=0.15.0
3
  numpy>=1.19.0
4
  scikit-learn>=1.0.0
5
  nltk>=3.6.0
 
10
  librosa>=0.8.0
11
  soundfile>=0.10.0
12
  thinc>=8.0.0
 
 
13
  jinja2>=3.0.0
14
  python-multipart>=0.0.5
15
  pydantic>=1.8.2