Chirapath commited on
Commit
dc88e08
Β·
verified Β·
1 Parent(s): 2233c1f

Upload projects

Browse files
Files changed (6) hide show
  1. .env +17 -0
  2. Developer.md +1904 -0
  3. USER.md +459 -0
  4. app_core.py +1133 -0
  5. gradio_app.py +1471 -0
  6. requirements.txt +3 -0
.env ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ AZURE_SPEECH_KEY=3HsgmegEhUwONKDChLmFlSWXTb4aiwTOjXuBBAOxWjURmt9sOowXJQQJ99BFACqBBLyXJ3w3AAAYACOGoq5r
2
+ AZURE_SPEECH_KEY_ENDPOINT=https://southeastasia.api.cognitive.microsoft.com
3
+ AZURE_REGION=southeastasia
4
+ AZURE_BLOB_CONNECTION=DefaultEndpointsProtocol=https;AccountName=speechtotextservice01;AccountKey=GAFbqMBvIHkRXLIx9173jFVb7W96lQ02t7bGgKwq6LbpU2gqaUeU+pWAKcbdn38rQYfKnOFVy5ar+AStxXjAJA==;EndpointSuffix=core.windows.net
5
+ AZURE_CONTAINER=history
6
+ AZURE_BLOB_SAS_TOKEN=sp=racwdl&st=2025-06-23T07:18:08Z&se=2025-06-30T15:18:08Z&sv=2024-11-04&sr=c&sig=GFwuSzZqOe51QIGDYq%2B2JVLAWoy4UhKs2GaArjyEKEk%3D
7
+ ALLOWED_LANGS={"en-US": "English (United States)", "th-TH": "Thai", "zh-CN": "Chinese (Mandarin, Simplified)"}
8
+ API_VERSION=v3.2
9
+
10
+ # Application Configuration
11
+ MAX_CONCURRENT_JOBS=5
12
+ DATABASE_PATH=database/transcriptions.db
13
+ CLEANUP_OLDER_THAN_DAYS=30
14
+
15
+ # Development Settings
16
+ DEBUG=false
17
+ LOG_LEVEL=INFO
Developer.md ADDED
@@ -0,0 +1,1904 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ› οΈ Azure Speech Transcription - Developer Guide
2
+
3
+ ## πŸ“‹ Table of Contents
4
+
5
+ - [System Architecture](#-system-architecture)
6
+ - [Development Environment](#-development-environment)
7
+ - [Deployment Guide](#-deployment-guide)
8
+ - [API Documentation](#-api-documentation)
9
+ - [Database Schema](#-database-schema)
10
+ - [Security Implementation](#-security-implementation)
11
+ - [Monitoring & Maintenance](#-monitoring--maintenance)
12
+ - [Contributing Guidelines](#-contributing-guidelines)
13
+ - [Advanced Configuration](#-advanced-configuration)
14
+ - [Troubleshooting](#-troubleshooting)
15
+
16
+ ---
17
+
18
+ ## πŸ—οΈ System Architecture
19
+
20
+ ### Overview
21
+
22
+ The Azure Speech Transcription service is built with a modern, secure architecture focusing on user privacy, PDPA compliance, and scalability.
23
+
24
+ ```
25
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
26
+ β”‚ Frontend UI β”‚ β”‚ Backend API β”‚ β”‚ Azure Services β”‚
27
+ β”‚ (Gradio) │◄──►│ (Python) │◄──►│ Speech & Blob β”‚
28
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
29
+ β”‚ β”‚ β”‚
30
+ β”‚ β”‚ β”‚
31
+ β–Ό β–Ό β–Ό
32
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
33
+ β”‚ User Session β”‚ β”‚ SQLite Database β”‚ β”‚ User Storage β”‚
34
+ β”‚ Management β”‚ β”‚ (Metadata) β”‚ β”‚ (Isolated) β”‚
35
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
36
+ ```
37
+
38
+ ### Core Components
39
+
40
+ #### 1. Frontend Layer (`gradio_app.py`)
41
+ - **Technology**: Gradio with custom CSS
42
+ - **Purpose**: User interface and session management
43
+ - **Features**: Authentication, file upload, real-time status, history management
44
+
45
+ #### 2. Backend Layer (`app_core.py`)
46
+ - **Technology**: Python with threading and async processing
47
+ - **Purpose**: Business logic, authentication, and Azure integration
48
+ - **Features**: User management, transcription processing, PDPA compliance
49
+
50
+ #### 3. Data Layer
51
+ - **Database**: SQLite with Azure Blob backup
52
+ - **Storage**: Azure Blob Storage with user separation
53
+ - **Security**: User-isolated folders and encrypted connections
54
+
55
+ #### 4. External Services
56
+ - **Azure Speech Services**: Transcription processing
57
+ - **Azure Blob Storage**: File and database storage
58
+ - **FFmpeg**: Audio/video conversion
59
+
60
+ ### Data Flow
61
+
62
+ ```
63
+ 1. User uploads file β†’ 2. Authentication check β†’ 3. File validation
64
+ ↓ ↓ ↓
65
+ 8. Download results ← 7. Store transcript ← 6. Process with Azure
66
+ ↑ ↑ ↑
67
+ 9. Update UI status ← 4. Save to user folder ← 5. Background processing
68
+ ```
69
+
70
+ ---
71
+
72
+ ## πŸ’» Development Environment
73
+
74
+ ### Prerequisites
75
+
76
+ - **Python**: 3.8 or higher
77
+ - **Azure Account**: With Speech Services and Blob Storage
78
+ - **FFmpeg**: For audio/video processing
79
+ - **Git**: For version control
80
+
81
+ ### Environment Setup
82
+
83
+ #### 1. Clone Repository
84
+ ```bash
85
+ git clone <repository-url>
86
+ cd azure-speech-transcription
87
+ ```
88
+
89
+ #### 2. Virtual Environment
90
+ ```bash
91
+ # Create virtual environment
92
+ python -m venv venv
93
+
94
+ # Activate (Windows)
95
+ venv\Scripts\activate
96
+
97
+ # Activate (macOS/Linux)
98
+ source venv/bin/activate
99
+ ```
100
+
101
+ #### 3. Install Dependencies
102
+ ```bash
103
+ pip install -r requirements.txt
104
+ ```
105
+
106
+ #### 4. Environment Configuration
107
+ ```bash
108
+ # Copy environment template
109
+ cp .env.example .env
110
+
111
+ # Edit with your Azure credentials
112
+ nano .env
113
+ ```
114
+
115
+ #### 5. Install FFmpeg
116
+
117
+ **Windows (Chocolatey):**
118
+ ```bash
119
+ choco install ffmpeg
120
+ ```
121
+
122
+ **macOS (Homebrew):**
123
+ ```bash
124
+ brew install ffmpeg
125
+ ```
126
+
127
+ **Ubuntu/Debian:**
128
+ ```bash
129
+ sudo apt update
130
+ sudo apt install ffmpeg
131
+ ```
132
+
133
+ #### 6. Verify Installation
134
+ ```python
135
+ python -c "
136
+ import gradio as gr
137
+ from azure.storage.blob import BlobServiceClient
138
+ import subprocess
139
+ print('Gradio:', gr.__version__)
140
+ print('FFmpeg:', subprocess.run(['ffmpeg', '-version'], capture_output=True).returncode == 0)
141
+ print('Azure Blob:', 'OK')
142
+ "
143
+ ```
144
+
145
+ ### Development Server
146
+
147
+ ```bash
148
+ # Start development server
149
+ python gradio_app.py
150
+
151
+ # Server will be available at:
152
+ # http://localhost:7860
153
+ ```
154
+
155
+ ### Development Tools
156
+
157
+ #### Recommended IDE Setup
158
+ - **VS Code**: With Python, Azure, and Git extensions
159
+ - **PyCharm**: Professional edition with Azure toolkit
160
+ - **Vim/Emacs**: With appropriate Python plugins
161
+
162
+ #### Useful Extensions
163
+ ```json
164
+ {
165
+ "recommendations": [
166
+ "ms-python.python",
167
+ "ms-vscode.azure-cli",
168
+ "ms-azuretools.azure-cli-tools",
169
+ "ms-python.black-formatter",
170
+ "ms-python.flake8"
171
+ ]
172
+ }
173
+ ```
174
+
175
+ #### Code Quality Tools
176
+ ```bash
177
+ # Install development tools
178
+ pip install black flake8 pytest mypy
179
+
180
+ # Format code
181
+ black .
182
+
183
+ # Lint code
184
+ flake8 .
185
+
186
+ # Type checking
187
+ mypy app_core.py gradio_app.py
188
+ ```
189
+
190
+ ---
191
+
192
+ ## πŸš€ Deployment Guide
193
+
194
+ ### Production Deployment Options
195
+
196
+ #### Option 1: Traditional Server Deployment
197
+
198
+ **1. Server Preparation**
199
+ ```bash
200
+ # Update system
201
+ sudo apt update && sudo apt upgrade -y
202
+
203
+ # Install Python and dependencies
204
+ sudo apt install python3 python3-pip python3-venv nginx ffmpeg -y
205
+
206
+ # Create application user
207
+ sudo useradd -m -s /bin/bash transcription
208
+ sudo su - transcription
209
+ ```
210
+
211
+ **2. Application Setup**
212
+ ```bash
213
+ # Clone repository
214
+ git clone <repository-url> /home/transcription/app
215
+ cd /home/transcription/app
216
+
217
+ # Setup virtual environment
218
+ python3 -m venv venv
219
+ source venv/bin/activate
220
+ pip install -r requirements.txt
221
+
222
+ # Configure environment
223
+ cp .env.example .env
224
+ # Edit .env with production values
225
+ ```
226
+
227
+ **3. Systemd Service**
228
+ ```ini
229
+ # /etc/systemd/system/transcription.service
230
+ [Unit]
231
+ Description=Azure Speech Transcription Service
232
+ After=network.target
233
+
234
+ [Service]
235
+ Type=simple
236
+ User=transcription
237
+ Group=transcription
238
+ WorkingDirectory=/home/transcription/app
239
+ Environment=PATH=/home/transcription/app/venv/bin
240
+ ExecStart=/home/transcription/app/venv/bin/python gradio_app.py
241
+ Restart=always
242
+ RestartSec=10
243
+
244
+ [Install]
245
+ WantedBy=multi-user.target
246
+ ```
247
+
248
+ **4. Nginx Configuration**
249
+ ```nginx
250
+ # /etc/nginx/sites-available/transcription
251
+ server {
252
+ listen 80;
253
+ server_name your-domain.com;
254
+ client_max_body_size 500M;
255
+
256
+ location / {
257
+ proxy_pass http://127.0.0.1:7860;
258
+ proxy_set_header Host $host;
259
+ proxy_set_header X-Real-IP $remote_addr;
260
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
261
+ proxy_set_header X-Forwarded-Proto $scheme;
262
+ proxy_read_timeout 300s;
263
+ proxy_connect_timeout 75s;
264
+ }
265
+ }
266
+ ```
267
+
268
+ **5. SSL Certificate**
269
+ ```bash
270
+ # Install Certbot
271
+ sudo apt install certbot python3-certbot-nginx -y
272
+
273
+ # Get SSL certificate
274
+ sudo certbot --nginx -d your-domain.com
275
+
276
+ # Verify auto-renewal
277
+ sudo certbot renew --dry-run
278
+ ```
279
+
280
+ **6. Start Services**
281
+ ```bash
282
+ # Enable and start application
283
+ sudo systemctl enable transcription
284
+ sudo systemctl start transcription
285
+
286
+ # Enable and restart nginx
287
+ sudo systemctl enable nginx
288
+ sudo systemctl restart nginx
289
+
290
+ # Check status
291
+ sudo systemctl status transcription
292
+ sudo systemctl status nginx
293
+ ```
294
+
295
+ #### Option 2: Docker Deployment
296
+
297
+ **1. Dockerfile**
298
+ ```dockerfile
299
+ FROM python:3.9-slim
300
+
301
+ # Install system dependencies
302
+ RUN apt-get update && apt-get install -y \
303
+ ffmpeg \
304
+ && rm -rf /var/lib/apt/lists/*
305
+
306
+ # Set working directory
307
+ WORKDIR /app
308
+
309
+ # Copy requirements and install Python dependencies
310
+ COPY requirements.txt .
311
+ RUN pip install --no-cache-dir -r requirements.txt
312
+
313
+ # Copy application code
314
+ COPY . .
315
+
316
+ # Create necessary directories
317
+ RUN mkdir -p uploads database temp
318
+
319
+ # Expose port
320
+ EXPOSE 7860
321
+
322
+ # Run application
323
+ CMD ["python", "gradio_app.py"]
324
+ ```
325
+
326
+ **2. Docker Compose**
327
+ ```yaml
328
+ # docker-compose.yml
329
+ version: '3.8'
330
+
331
+ services:
332
+ transcription:
333
+ build: .
334
+ ports:
335
+ - "7860:7860"
336
+ environment:
337
+ - AZURE_SPEECH_KEY=${AZURE_SPEECH_KEY}
338
+ - AZURE_SPEECH_KEY_ENDPOINT=${AZURE_SPEECH_KEY_ENDPOINT}
339
+ - AZURE_REGION=${AZURE_REGION}
340
+ - AZURE_BLOB_CONNECTION=${AZURE_BLOB_CONNECTION}
341
+ - AZURE_CONTAINER=${AZURE_CONTAINER}
342
+ - AZURE_BLOB_SAS_TOKEN=${AZURE_BLOB_SAS_TOKEN}
343
+ - ALLOWED_LANGS=${ALLOWED_LANGS}
344
+ volumes:
345
+ - ./uploads:/app/uploads
346
+ - ./database:/app/database
347
+ - ./temp:/app/temp
348
+ restart: unless-stopped
349
+
350
+ nginx:
351
+ image: nginx:alpine
352
+ ports:
353
+ - "80:80"
354
+ - "443:443"
355
+ volumes:
356
+ - ./nginx.conf:/etc/nginx/nginx.conf
357
+ - ./ssl:/etc/ssl/certs
358
+ depends_on:
359
+ - transcription
360
+ restart: unless-stopped
361
+ ```
362
+
363
+ **3. Deploy with Docker**
364
+ ```bash
365
+ # Build and start
366
+ docker-compose up -d
367
+
368
+ # View logs
369
+ docker-compose logs -f transcription
370
+
371
+ # Update application
372
+ git pull
373
+ docker-compose build transcription
374
+ docker-compose up -d transcription
375
+ ```
376
+
377
+ #### Option 3: Cloud Deployment (Azure Container Instances)
378
+
379
+ **1. Create Container Registry**
380
+ ```bash
381
+ # Create ACR
382
+ az acr create --resource-group myResourceGroup \
383
+ --name myregistry --sku Basic
384
+
385
+ # Login to ACR
386
+ az acr login --name myregistry
387
+
388
+ # Build and push image
389
+ docker build -t myregistry.azurecr.io/transcription:latest .
390
+ docker push myregistry.azurecr.io/transcription:latest
391
+ ```
392
+
393
+ **2. Deploy Container Instance**
394
+ ```bash
395
+ # Create container instance
396
+ az container create \
397
+ --resource-group myResourceGroup \
398
+ --name transcription-app \
399
+ --image myregistry.azurecr.io/transcription:latest \
400
+ --cpu 2 --memory 4 \
401
+ --port 7860 \
402
+ --environment-variables \
403
+ AZURE_SPEECH_KEY=$AZURE_SPEECH_KEY \
404
+ AZURE_SPEECH_KEY_ENDPOINT=$AZURE_SPEECH_KEY_ENDPOINT \
405
+ AZURE_REGION=$AZURE_REGION \
406
+ AZURE_BLOB_CONNECTION="$AZURE_BLOB_CONNECTION" \
407
+ AZURE_CONTAINER=$AZURE_CONTAINER \
408
+ AZURE_BLOB_SAS_TOKEN="$AZURE_BLOB_SAS_TOKEN"
409
+ ```
410
+
411
+ ---
412
+
413
+ ## πŸ“‘ API Documentation
414
+
415
+ ### Core Classes and Methods
416
+
417
+ #### TranscriptionManager Class
418
+
419
+ **Purpose**: Main service class handling all transcription operations
420
+
421
+ ```python
422
+ class TranscriptionManager:
423
+ def __init__(self)
424
+
425
+ # User Authentication
426
+ def register_user(email: str, username: str, password: str,
427
+ gdpr_consent: bool, data_retention_agreed: bool,
428
+ marketing_consent: bool) -> Tuple[bool, str, Optional[str]]
429
+
430
+ def login_user(login: str, password: str) -> Tuple[bool, str, Optional[User]]
431
+
432
+ # Transcription Operations
433
+ def submit_transcription(file_bytes: bytes, original_filename: str,
434
+ user_id: str, language: str,
435
+ settings: Dict) -> str
436
+
437
+ def get_job_status(job_id: str) -> Optional[TranscriptionJob]
438
+
439
+ # Data Management
440
+ def get_user_history(user_id: str, limit: int) -> List[TranscriptionJob]
441
+ def get_user_stats(user_id: str) -> Dict
442
+ def export_user_data(user_id: str) -> Dict
443
+ def delete_user_account(user_id: str) -> bool
444
+ ```
445
+
446
+ #### DatabaseManager Class
447
+
448
+ **Purpose**: Handle database operations and Azure blob synchronization
449
+
450
+ ```python
451
+ class DatabaseManager:
452
+ def __init__(db_path: str = None)
453
+
454
+ # User Operations
455
+ def create_user(...) -> Tuple[bool, str, Optional[str]]
456
+ def authenticate_user(login: str, password: str) -> Tuple[bool, str, Optional[User]]
457
+ def get_user_by_id(user_id: str) -> Optional[User]
458
+
459
+ # Job Operations
460
+ def save_job(job: TranscriptionJob)
461
+ def get_job(job_id: str) -> Optional[TranscriptionJob]
462
+ def get_user_jobs(user_id: str, limit: int) -> List[TranscriptionJob]
463
+ def get_pending_jobs() -> List[TranscriptionJob]
464
+ ```
465
+
466
+ #### AuthManager Class
467
+
468
+ **Purpose**: Authentication utilities and validation
469
+
470
+ ```python
471
+ class AuthManager:
472
+ @staticmethod
473
+ def hash_password(password: str) -> str
474
+ def verify_password(password: str, password_hash: str) -> bool
475
+ def validate_email(email: str) -> bool
476
+ def validate_username(username: str) -> bool
477
+ def validate_password(password: str) -> Tuple[bool, str]
478
+ ```
479
+
480
+ ### Data Models
481
+
482
+ #### User Model
483
+ ```python
484
+ @dataclass
485
+ class User:
486
+ user_id: str
487
+ email: str
488
+ username: str
489
+ password_hash: str
490
+ created_at: str
491
+ last_login: Optional[str] = None
492
+ is_active: bool = True
493
+ gdpr_consent: bool = False
494
+ data_retention_agreed: bool = False
495
+ marketing_consent: bool = False
496
+ ```
497
+
498
+ #### TranscriptionJob Model
499
+ ```python
500
+ @dataclass
501
+ class TranscriptionJob:
502
+ job_id: str
503
+ user_id: str
504
+ original_filename: str
505
+ audio_url: str
506
+ language: str
507
+ status: str # pending, processing, completed, failed
508
+ created_at: str
509
+ completed_at: Optional[str] = None
510
+ transcript_text: Optional[str] = None
511
+ transcript_url: Optional[str] = None
512
+ error_message: Optional[str] = None
513
+ azure_trans_id: Optional[str] = None
514
+ settings: Optional[Dict] = None
515
+ ```
516
+
517
+ ### Configuration Parameters
518
+
519
+ #### Environment Variables
520
+ ```python
521
+ # Required
522
+ AZURE_SPEECH_KEY: str
523
+ AZURE_SPEECH_KEY_ENDPOINT: str
524
+ AZURE_REGION: str
525
+ AZURE_BLOB_CONNECTION: str
526
+ AZURE_CONTAINER: str
527
+ AZURE_BLOB_SAS_TOKEN: str
528
+
529
+ # Optional
530
+ ALLOWED_LANGS: str # JSON string
531
+ API_VERSION: str = "v3.2"
532
+ PASSWORD_SALT: str = "default_salt"
533
+ MAX_FILE_SIZE_MB: int = 500
534
+ ```
535
+
536
+ #### Transcription Settings
537
+ ```python
538
+ settings = {
539
+ 'audio_format': str, # wav, mp3, etc.
540
+ 'diarization_enabled': bool, # Speaker identification
541
+ 'speakers': int, # Max speakers (1-10)
542
+ 'profanity': str, # masked, removed, raw
543
+ 'punctuation': str, # automatic, dictated, none
544
+ 'timestamps': bool, # Include timestamps
545
+ 'lexical': bool, # Include lexical forms
546
+ 'language_id_enabled': bool, # Auto language detection
547
+ 'candidate_locales': List[str] # Language candidates
548
+ }
549
+ ```
550
+
551
+ ---
552
+
553
+ ## πŸ—„οΈ Database Schema
554
+
555
+ ### SQLite Database Structure
556
+
557
+ #### Users Table
558
+ ```sql
559
+ CREATE TABLE users (
560
+ user_id TEXT PRIMARY KEY,
561
+ email TEXT UNIQUE NOT NULL,
562
+ username TEXT UNIQUE NOT NULL,
563
+ password_hash TEXT NOT NULL,
564
+ created_at TEXT NOT NULL,
565
+ last_login TEXT,
566
+ is_active BOOLEAN DEFAULT 1,
567
+ gdpr_consent BOOLEAN DEFAULT 0,
568
+ data_retention_agreed BOOLEAN DEFAULT 0,
569
+ marketing_consent BOOLEAN DEFAULT 0
570
+ );
571
+
572
+ -- Indexes
573
+ CREATE INDEX idx_users_email ON users(email);
574
+ CREATE INDEX idx_users_username ON users(username);
575
+ ```
576
+
577
+ #### Transcriptions Table
578
+ ```sql
579
+ CREATE TABLE transcriptions (
580
+ job_id TEXT PRIMARY KEY,
581
+ user_id TEXT NOT NULL,
582
+ original_filename TEXT NOT NULL,
583
+ audio_url TEXT,
584
+ language TEXT NOT NULL,
585
+ status TEXT NOT NULL,
586
+ created_at TEXT NOT NULL,
587
+ completed_at TEXT,
588
+ transcript_text TEXT,
589
+ transcript_url TEXT,
590
+ error_message TEXT,
591
+ azure_trans_id TEXT,
592
+ settings TEXT,
593
+ FOREIGN KEY (user_id) REFERENCES users (user_id)
594
+ );
595
+
596
+ -- Indexes
597
+ CREATE INDEX idx_transcriptions_user_id ON transcriptions(user_id);
598
+ CREATE INDEX idx_transcriptions_status ON transcriptions(status);
599
+ CREATE INDEX idx_transcriptions_created_at ON transcriptions(created_at DESC);
600
+ CREATE INDEX idx_transcriptions_user_created ON transcriptions(user_id, created_at DESC);
601
+ ```
602
+
603
+ ### Azure Blob Storage Structure
604
+
605
+ ```
606
+ Container: {AZURE_CONTAINER}/
607
+ β”œβ”€β”€ shared/
608
+ β”‚ └── database/
609
+ β”‚ └── transcriptions.db # Shared database backup
610
+ β”œβ”€β”€ users/
611
+ β”‚ β”œβ”€β”€ {user-id-1}/
612
+ β”‚ β”‚ β”œβ”€β”€ audio/ # Processed audio files
613
+ β”‚ β”‚ β”‚ β”œβ”€β”€ {job-id-1}.wav
614
+ β”‚ β”‚ β”‚ └── {job-id-2}.wav
615
+ β”‚ β”‚ β”œβ”€β”€ transcripts/ # Transcript files
616
+ β”‚ β”‚ β”‚ β”œβ”€β”€ {job-id-1}.txt
617
+ β”‚ β”‚ β”‚ └── {job-id-2}.txt
618
+ β”‚ β”‚ └── originals/ # Original uploaded files
619
+ β”‚ β”‚ β”œβ”€β”€ {job-id-1}_{filename}.mp4
620
+ β”‚ β”‚ └── {job-id-2}_{filename}.wav
621
+ β”‚ └── {user-id-2}/
622
+ β”‚ β”œβ”€β”€ audio/
623
+ β”‚ β”œβ”€β”€ transcripts/
624
+ β”‚ └── originals/
625
+ ```
626
+
627
+ ### Database Operations
628
+
629
+ #### User Management Queries
630
+ ```sql
631
+ -- Create user
632
+ INSERT INTO users (user_id, email, username, password_hash, created_at,
633
+ gdpr_consent, data_retention_agreed, marketing_consent)
634
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
635
+
636
+ -- Authenticate user
637
+ SELECT * FROM users
638
+ WHERE (email = ? OR username = ?) AND is_active = 1;
639
+
640
+ -- Update last login
641
+ UPDATE users SET last_login = ? WHERE user_id = ?;
642
+
643
+ -- Get user stats
644
+ SELECT status, COUNT(*) FROM transcriptions
645
+ WHERE user_id = ? GROUP BY status;
646
+ ```
647
+
648
+ #### Job Management Queries
649
+ ```sql
650
+ -- Create job
651
+ INSERT INTO transcriptions (job_id, user_id, original_filename, language,
652
+ status, created_at, settings)
653
+ VALUES (?, ?, ?, ?, 'pending', ?, ?);
654
+
655
+ -- Update job status
656
+ UPDATE transcriptions
657
+ SET status = ?, completed_at = ?, transcript_text = ?, transcript_url = ?
658
+ WHERE job_id = ?;
659
+
660
+ -- Get user jobs
661
+ SELECT * FROM transcriptions
662
+ WHERE user_id = ?
663
+ ORDER BY created_at DESC LIMIT ?;
664
+
665
+ -- Get pending jobs for background processor
666
+ SELECT * FROM transcriptions
667
+ WHERE status IN ('pending', 'processing');
668
+ ```
669
+
670
+ ---
671
+
672
+ ## πŸ”’ Security Implementation
673
+
674
+ ### Authentication Security
675
+
676
+ #### Password Security
677
+ ```python
678
+ # Password hashing with salt
679
+ def hash_password(password: str) -> str:
680
+ salt = os.environ.get("PASSWORD_SALT", "default_salt")
681
+ return hashlib.sha256((password + salt).encode()).hexdigest()
682
+
683
+ # Password validation
684
+ def validate_password(password: str) -> Tuple[bool, str]:
685
+ if len(password) < 8:
686
+ return False, "Password must be at least 8 characters"
687
+ if not re.search(r'[A-Z]', password):
688
+ return False, "Password must contain uppercase letter"
689
+ if not re.search(r'[a-z]', password):
690
+ return False, "Password must contain lowercase letter"
691
+ if not re.search(r'\d', password):
692
+ return False, "Password must contain number"
693
+ return True, "Valid"
694
+ ```
695
+
696
+ #### Session Management
697
+ ```python
698
+ # User session state
699
+ session_state = {
700
+ 'user_id': str,
701
+ 'username': str,
702
+ 'logged_in_at': datetime,
703
+ 'last_activity': datetime
704
+ }
705
+
706
+ # Session validation
707
+ def validate_session(session_state: dict) -> bool:
708
+ if not session_state or 'user_id' not in session_state:
709
+ return False
710
+
711
+ # Check session timeout (if implemented)
712
+ last_activity = session_state.get('last_activity')
713
+ if last_activity:
714
+ timeout = timedelta(hours=24) # 24-hour sessions
715
+ if datetime.now() - last_activity > timeout:
716
+ return False
717
+
718
+ return True
719
+ ```
720
+
721
+ ### Data Security
722
+
723
+ #### Access Control
724
+ ```python
725
+ # User data access verification
726
+ def verify_user_access(job_id: str, user_id: str) -> bool:
727
+ job = get_job(job_id)
728
+ return job and job.user_id == user_id
729
+
730
+ # File path security
731
+ def get_user_blob_path(user_id: str, blob_type: str, filename: str) -> str:
732
+ # Ensure user can only access their own folder
733
+ safe_filename = os.path.basename(filename) # Prevent path traversal
734
+ return f"users/{user_id}/{blob_type}/{safe_filename}"
735
+ ```
736
+
737
+ #### Data Encryption
738
+ ```python
739
+ # Azure Blob Storage encryption (configured at Azure level)
740
+ # - Encryption at rest: Enabled by default
741
+ # - Encryption in transit: HTTPS enforced
742
+ # - Customer-managed keys: Optional enhancement
743
+
744
+ # Database encryption (for sensitive fields)
745
+ from cryptography.fernet import Fernet
746
+
747
+ def encrypt_sensitive_data(data: str, key: bytes) -> str:
748
+ f = Fernet(key)
749
+ return f.encrypt(data.encode()).decode()
750
+
751
+ def decrypt_sensitive_data(encrypted_data: str, key: bytes) -> str:
752
+ f = Fernet(key)
753
+ return f.decrypt(encrypted_data.encode()).decode()
754
+ ```
755
+
756
+ ### Azure Security
757
+
758
+ #### Blob Storage Security
759
+ ```python
760
+ # SAS token configuration for least privilege
761
+ sas_permissions = BlobSasPermissions(
762
+ read=True,
763
+ write=True,
764
+ delete=True,
765
+ list=True
766
+ )
767
+
768
+ # IP restrictions (optional)
769
+ sas_ip_range = "192.168.1.0/24" # Restrict to specific IP range
770
+
771
+ # Time-limited tokens
772
+ sas_expiry = datetime.utcnow() + timedelta(hours=1)
773
+ ```
774
+
775
+ #### Speech Service Security
776
+ ```python
777
+ # Secure API calls
778
+ headers = {
779
+ "Ocp-Apim-Subscription-Key": AZURE_SPEECH_KEY,
780
+ "Content-Type": "application/json"
781
+ }
782
+
783
+ # Request timeout and retry logic
784
+ response = requests.post(
785
+ url,
786
+ headers=headers,
787
+ json=body,
788
+ timeout=30,
789
+ verify=True # Verify SSL certificates
790
+ )
791
+ ```
792
+
793
+ ### Input Validation
794
+
795
+ #### File Upload Security
796
+ ```python
797
+ def validate_uploaded_file(file_path: str, max_size: int = 500 * 1024 * 1024) -> Tuple[bool, str]:
798
+ try:
799
+ # Check file exists
800
+ if not os.path.exists(file_path):
801
+ return False, "File not found"
802
+
803
+ # Check file size
804
+ file_size = os.path.getsize(file_path)
805
+ if file_size > max_size:
806
+ return False, f"File too large: {file_size / 1024 / 1024:.1f}MB"
807
+
808
+ # Check file type by content (not just extension)
809
+ import magic
810
+ mime_type = magic.from_file(file_path, mime=True)
811
+ allowed_types = ['audio/', 'video/']
812
+ if not any(mime_type.startswith(t) for t in allowed_types):
813
+ return False, f"Invalid file type: {mime_type}"
814
+
815
+ return True, "Valid"
816
+
817
+ except Exception as e:
818
+ return False, f"Validation error: {str(e)}"
819
+ ```
820
+
821
+ #### SQL Injection Prevention
822
+ ```python
823
+ # Use parameterized queries (already implemented)
824
+ cursor.execute(
825
+ "SELECT * FROM users WHERE email = ? AND password_hash = ?",
826
+ (email, password_hash)
827
+ )
828
+
829
+ # Input sanitization
830
+ def sanitize_input(user_input: str) -> str:
831
+ # Remove dangerous characters
832
+ import html
833
+ sanitized = html.escape(user_input)
834
+ # Limit length
835
+ return sanitized[:1000]
836
+ ```
837
+
838
+ ---
839
+
840
+ ## πŸ“Š Monitoring & Maintenance
841
+
842
+ ### Application Monitoring
843
+
844
+ #### Health Checks
845
+ ```python
846
+ def health_check() -> Dict[str, Any]:
847
+ """System health check endpoint"""
848
+ try:
849
+ # Database check
850
+ db_status = check_database_connection()
851
+
852
+ # Azure services check
853
+ blob_status = check_blob_storage()
854
+ speech_status = check_speech_service()
855
+
856
+ # FFmpeg check
857
+ ffmpeg_status = check_ffmpeg_installation()
858
+
859
+ # Disk space check
860
+ disk_status = check_disk_space()
861
+
862
+ return {
863
+ 'status': 'healthy' if all([db_status, blob_status, speech_status, ffmpeg_status]) else 'unhealthy',
864
+ 'timestamp': datetime.now().isoformat(),
865
+ 'services': {
866
+ 'database': db_status,
867
+ 'blob_storage': blob_status,
868
+ 'speech_service': speech_status,
869
+ 'ffmpeg': ffmpeg_status,
870
+ 'disk_space': disk_status
871
+ }
872
+ }
873
+
874
+ except Exception as e:
875
+ return {
876
+ 'status': 'error',
877
+ 'timestamp': datetime.now().isoformat(),
878
+ 'error': str(e)
879
+ }
880
+
881
+ def check_database_connection() -> bool:
882
+ try:
883
+ with transcription_manager.db.get_connection() as conn:
884
+ conn.execute("SELECT 1").fetchone()
885
+ return True
886
+ except:
887
+ return False
888
+
889
+ def check_blob_storage() -> bool:
890
+ try:
891
+ client = BlobServiceClient.from_connection_string(AZURE_BLOB_CONNECTION)
892
+ client.list_containers(max_results=1)
893
+ return True
894
+ except:
895
+ return False
896
+ ```
897
+
898
+ #### Logging Configuration
899
+ ```python
900
+ import logging
901
+ from logging.handlers import RotatingFileHandler
902
+
903
+ def setup_logging():
904
+ """Configure application logging"""
905
+
906
+ # Create formatter
907
+ formatter = logging.Formatter(
908
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
909
+ )
910
+
911
+ # Console handler
912
+ console_handler = logging.StreamHandler()
913
+ console_handler.setFormatter(formatter)
914
+ console_handler.setLevel(logging.INFO)
915
+
916
+ # File handler with rotation
917
+ file_handler = RotatingFileHandler(
918
+ 'logs/transcription.log',
919
+ maxBytes=10*1024*1024, # 10MB
920
+ backupCount=5
921
+ )
922
+ file_handler.setFormatter(formatter)
923
+ file_handler.setLevel(logging.DEBUG)
924
+
925
+ # Configure root logger
926
+ logger = logging.getLogger()
927
+ logger.setLevel(logging.DEBUG)
928
+ logger.addHandler(console_handler)
929
+ logger.addHandler(file_handler)
930
+
931
+ # Separate logger for sensitive operations
932
+ auth_logger = logging.getLogger('auth')
933
+ auth_handler = RotatingFileHandler(
934
+ 'logs/auth.log',
935
+ maxBytes=5*1024*1024, # 5MB
936
+ backupCount=10
937
+ )
938
+ auth_handler.setFormatter(formatter)
939
+ auth_logger.addHandler(auth_handler)
940
+ auth_logger.setLevel(logging.INFO)
941
+ ```
942
+
943
+ #### Performance Monitoring
944
+ ```python
945
+ import time
946
+ from functools import wraps
947
+
948
+ def monitor_performance(func):
949
+ """Decorator to monitor function performance"""
950
+ @wraps(func)
951
+ def wrapper(*args, **kwargs):
952
+ start_time = time.time()
953
+ try:
954
+ result = func(*args, **kwargs)
955
+ duration = time.time() - start_time
956
+ logging.info(f"{func.__name__} completed in {duration:.2f}s")
957
+ return result
958
+ except Exception as e:
959
+ duration = time.time() - start_time
960
+ logging.error(f"{func.__name__} failed after {duration:.2f}s: {str(e)}")
961
+ raise
962
+ return wrapper
963
+
964
+ # Usage
965
+ @monitor_performance
966
+ def submit_transcription(self, file_bytes, filename, user_id, language, settings):
967
+ # Implementation here
968
+ pass
969
+ ```
970
+
971
+ ### Database Maintenance
972
+
973
+ #### Backup Strategy
974
+ ```python
975
+ def backup_database():
976
+ """Backup database to Azure Blob Storage"""
977
+ try:
978
+ # Create timestamped backup
979
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
980
+ backup_name = f"shared/backups/transcriptions_backup_{timestamp}.db"
981
+
982
+ # Upload current database
983
+ blob_client = blob_service.get_blob_client(
984
+ container=AZURE_CONTAINER,
985
+ blob=backup_name
986
+ )
987
+
988
+ with open(db_path, "rb") as data:
989
+ blob_client.upload_blob(data)
990
+
991
+ logging.info(f"Database backup created: {backup_name}")
992
+
993
+ # Clean old backups (keep last 30 days)
994
+ cleanup_old_backups()
995
+
996
+ except Exception as e:
997
+ logging.error(f"Database backup failed: {str(e)}")
998
+
999
+ def cleanup_old_backups():
1000
+ """Remove backups older than 30 days"""
1001
+ try:
1002
+ cutoff_date = datetime.now() - timedelta(days=30)
1003
+ container_client = blob_service.get_container_client(AZURE_CONTAINER)
1004
+
1005
+ for blob in container_client.list_blobs(name_starts_with="shared/backups/"):
1006
+ if blob.last_modified < cutoff_date:
1007
+ blob_service.delete_blob(AZURE_CONTAINER, blob.name)
1008
+ logging.info(f"Deleted old backup: {blob.name}")
1009
+
1010
+ except Exception as e:
1011
+ logging.error(f"Backup cleanup failed: {str(e)}")
1012
+ ```
1013
+
1014
+ #### Database Optimization
1015
+ ```python
1016
+ def optimize_database():
1017
+ """Optimize database performance"""
1018
+ try:
1019
+ with transcription_manager.db.get_connection() as conn:
1020
+ # Analyze tables
1021
+ conn.execute("ANALYZE")
1022
+
1023
+ # Vacuum database (compact)
1024
+ conn.execute("VACUUM")
1025
+
1026
+ # Update statistics
1027
+ conn.execute("PRAGMA optimize")
1028
+
1029
+ logging.info("Database optimization completed")
1030
+
1031
+ except Exception as e:
1032
+ logging.error(f"Database optimization failed: {str(e)}")
1033
+
1034
+ # Schedule optimization (run weekly)
1035
+ import schedule
1036
+
1037
+ schedule.every().week.do(optimize_database)
1038
+ schedule.every().day.at("02:00").do(backup_database)
1039
+ ```
1040
+
1041
+ ### Resource Management
1042
+
1043
+ #### Cleanup Tasks
1044
+ ```python
1045
+ def cleanup_temporary_files():
1046
+ """Clean up temporary files older than 24 hours"""
1047
+ try:
1048
+ cutoff_time = time.time() - (24 * 60 * 60) # 24 hours ago
1049
+ temp_dirs = ['uploads', 'temp']
1050
+
1051
+ for temp_dir in temp_dirs:
1052
+ if os.path.exists(temp_dir):
1053
+ for filename in os.listdir(temp_dir):
1054
+ filepath = os.path.join(temp_dir, filename)
1055
+ if os.path.isfile(filepath) and os.path.getmtime(filepath) < cutoff_time:
1056
+ os.remove(filepath)
1057
+ logging.info(f"Cleaned up temporary file: {filepath}")
1058
+
1059
+ except Exception as e:
1060
+ logging.error(f"Temporary file cleanup failed: {str(e)}")
1061
+
1062
+ def monitor_disk_space():
1063
+ """Monitor and alert on disk space"""
1064
+ try:
1065
+ import shutil
1066
+ total, used, free = shutil.disk_usage("/")
1067
+
1068
+ # Convert to GB
1069
+ free_gb = free // (1024**3)
1070
+ total_gb = total // (1024**3)
1071
+ usage_percent = (used / total) * 100
1072
+
1073
+ if usage_percent > 85:
1074
+ logging.warning(f"High disk usage: {usage_percent:.1f}% ({free_gb}GB free)")
1075
+
1076
+ if free_gb < 5:
1077
+ logging.critical(f"Low disk space: {free_gb}GB remaining")
1078
+
1079
+ except Exception as e:
1080
+ logging.error(f"Disk space monitoring failed: {str(e)}")
1081
+ ```
1082
+
1083
+ ### Monitoring Alerts
1084
+
1085
+ #### Email Alerts (Optional)
1086
+ ```python
1087
+ import smtplib
1088
+ from email.mime.text import MIMEText
1089
+
1090
+ def send_alert(subject: str, message: str):
1091
+ """Send email alert for critical issues"""
1092
+ try:
1093
+ smtp_server = os.environ.get("SMTP_SERVER")
1094
+ smtp_port = int(os.environ.get("SMTP_PORT", "587"))
1095
+ smtp_user = os.environ.get("SMTP_USER")
1096
+ smtp_pass = os.environ.get("SMTP_PASS")
1097
+ alert_email = os.environ.get("ALERT_EMAIL")
1098
+
1099
+ if not all([smtp_server, smtp_user, smtp_pass, alert_email]):
1100
+ return # Email not configured
1101
+
1102
+ msg = MIMEText(message)
1103
+ msg['Subject'] = f"[Transcription Service] {subject}"
1104
+ msg['From'] = smtp_user
1105
+ msg['To'] = alert_email
1106
+
1107
+ with smtplib.SMTP(smtp_server, smtp_port) as server:
1108
+ server.starttls()
1109
+ server.login(smtp_user, smtp_pass)
1110
+ server.send_message(msg)
1111
+
1112
+ except Exception as e:
1113
+ logging.error(f"Failed to send alert: {str(e)}")
1114
+ ```
1115
+
1116
+ ---
1117
+
1118
+ ## 🀝 Contributing Guidelines
1119
+
1120
+ ### Development Workflow
1121
+
1122
+ #### 1. Setup Development Environment
1123
+ ```bash
1124
+ # Fork repository
1125
+ git clone https://github.com/your-username/azure-speech-transcription.git
1126
+ cd azure-speech-transcription
1127
+
1128
+ # Create feature branch
1129
+ git checkout -b feature/your-feature-name
1130
+
1131
+ # Setup environment
1132
+ python -m venv venv
1133
+ source venv/bin/activate # or venv\Scripts\activate on Windows
1134
+ pip install -r requirements.txt
1135
+ pip install -r requirements-dev.txt # Development dependencies
1136
+ ```
1137
+
1138
+ #### 2. Code Quality Standards
1139
+
1140
+ **Python Style Guide**
1141
+ - Follow PEP 8 style guidelines
1142
+ - Use type hints for function parameters and return values
1143
+ - Maximum line length: 88 characters (Black formatter)
1144
+ - Use meaningful variable and function names
1145
+
1146
+ **Code Formatting**
1147
+ ```bash
1148
+ # Install development tools
1149
+ pip install black flake8 mypy pytest
1150
+
1151
+ # Format code
1152
+ black .
1153
+
1154
+ # Check style
1155
+ flake8 .
1156
+
1157
+ # Type checking
1158
+ mypy app_core.py gradio_app.py
1159
+
1160
+ # Run tests
1161
+ pytest tests/
1162
+ ```
1163
+
1164
+ **Documentation Standards**
1165
+ - All functions must have docstrings
1166
+ - Include type hints
1167
+ - Document complex logic with inline comments
1168
+ - Update README.md for new features
1169
+
1170
+ ```python
1171
+ def submit_transcription(
1172
+ self,
1173
+ file_bytes: bytes,
1174
+ original_filename: str,
1175
+ user_id: str,
1176
+ language: str,
1177
+ settings: Dict[str, Any]
1178
+ ) -> str:
1179
+ """
1180
+ Submit a new transcription job for processing.
1181
+
1182
+ Args:
1183
+ file_bytes: Raw bytes of the audio/video file
1184
+ original_filename: Original name of the uploaded file
1185
+ user_id: ID of the authenticated user
1186
+ language: Language code for transcription (e.g., 'en-US')
1187
+ settings: Transcription configuration options
1188
+
1189
+ Returns:
1190
+ str: Unique job ID for tracking transcription progress
1191
+
1192
+ Raises:
1193
+ ValueError: If user_id is invalid or file is too large
1194
+ ConnectionError: If Azure services are unavailable
1195
+ """
1196
+ ```
1197
+
1198
+ #### 3. Testing Requirements
1199
+
1200
+ **Unit Tests**
1201
+ ```python
1202
+ import pytest
1203
+ from unittest.mock import Mock, patch
1204
+ from app_core import TranscriptionManager, AuthManager
1205
+
1206
+ class TestAuthManager:
1207
+ def test_password_hashing(self):
1208
+ password = "TestPassword123"
1209
+ hashed = AuthManager.hash_password(password)
1210
+
1211
+ assert hashed != password
1212
+ assert AuthManager.verify_password(password, hashed)
1213
+ assert not AuthManager.verify_password("wrong", hashed)
1214
+
1215
+ def test_email_validation(self):
1216
+ assert AuthManager.validate_email("[email protected]")
1217
+ assert not AuthManager.validate_email("invalid-email")
1218
+ assert not AuthManager.validate_email("")
1219
+
1220
+ class TestTranscriptionManager:
1221
+ @patch('app_core.BlobServiceClient')
1222
+ def test_submit_transcription(self, mock_blob):
1223
+ manager = TranscriptionManager()
1224
+
1225
+ job_id = manager.submit_transcription(
1226
+ b"fake audio data",
1227
+ "test.wav",
1228
+ "user123",
1229
+ "en-US",
1230
+ {"audio_format": "wav"}
1231
+ )
1232
+
1233
+ assert isinstance(job_id, str)
1234
+ assert len(job_id) == 36 # UUID length
1235
+ ```
1236
+
1237
+ **Integration Tests**
1238
+ ```python
1239
+ class TestIntegration:
1240
+ def test_full_transcription_workflow(self):
1241
+ # Test complete workflow from upload to download
1242
+ pass
1243
+
1244
+ def test_user_registration_and_login(self):
1245
+ # Test complete auth workflow
1246
+ pass
1247
+ ```
1248
+
1249
+ #### 4. Commit Guidelines
1250
+
1251
+ **Commit Message Format**
1252
+ ```
1253
+ type(scope): brief description
1254
+
1255
+ Detailed explanation of changes if needed
1256
+
1257
+ - List specific changes
1258
+ - Include any breaking changes
1259
+ - Reference issue numbers
1260
+
1261
+ Closes #123
1262
+ ```
1263
+
1264
+ **Commit Types**
1265
+ - `feat`: New feature
1266
+ - `fix`: Bug fix
1267
+ - `docs`: Documentation changes
1268
+ - `style`: Code style changes (formatting, etc.)
1269
+ - `refactor`: Code refactoring
1270
+ - `test`: Adding or updating tests
1271
+ - `chore`: Maintenance tasks
1272
+
1273
+ **Example Commits**
1274
+ ```bash
1275
+ git commit -m "feat(auth): add password strength validation
1276
+
1277
+ - Implement password complexity requirements
1278
+ - Add client-side validation feedback
1279
+ - Update registration form UI
1280
+
1281
+ Closes #45"
1282
+
1283
+ git commit -m "fix(transcription): handle Azure service timeouts
1284
+
1285
+ - Add retry logic for failed API calls
1286
+ - Improve error messages for users
1287
+ - Log detailed error information
1288
+
1289
+ Fixes #67"
1290
+ ```
1291
+
1292
+ #### 5. Pull Request Process
1293
+
1294
+ **PR Checklist**
1295
+ - [ ] Code follows style guidelines
1296
+ - [ ] All tests pass
1297
+ - [ ] Documentation updated
1298
+ - [ ] Security considerations reviewed
1299
+ - [ ] Performance impact assessed
1300
+ - [ ] Breaking changes documented
1301
+
1302
+ **PR Template**
1303
+ ```markdown
1304
+ ## Description
1305
+ Brief description of changes
1306
+
1307
+ ## Type of Change
1308
+ - [ ] Bug fix
1309
+ - [ ] New feature
1310
+ - [ ] Breaking change
1311
+ - [ ] Documentation update
1312
+
1313
+ ## Testing
1314
+ - [ ] Unit tests added/updated
1315
+ - [ ] Integration tests pass
1316
+ - [ ] Manual testing completed
1317
+
1318
+ ## Security
1319
+ - [ ] No sensitive data exposed
1320
+ - [ ] Input validation implemented
1321
+ - [ ] Access controls maintained
1322
+
1323
+ ## Performance
1324
+ - [ ] No performance degradation
1325
+ - [ ] Database queries optimized
1326
+ - [ ] Resource usage considered
1327
+ ```
1328
+
1329
+ ### Feature Development
1330
+
1331
+ #### Adding New Languages
1332
+ ```python
1333
+ # 1. Update environment configuration
1334
+ ALLOWED_LANGS = {
1335
+ "en-US": "English (United States)",
1336
+ "es-ES": "Spanish (Spain)",
1337
+ "new-LANG": "New Language Name"
1338
+ }
1339
+
1340
+ # 2. Test language support
1341
+ def test_new_language():
1342
+ # Verify Azure Speech Services supports the language
1343
+ # Test transcription accuracy
1344
+ # Update documentation
1345
+ ```
1346
+
1347
+ #### Adding New Audio Formats
1348
+ ```python
1349
+ # 1. Update supported formats list
1350
+ AUDIO_FORMATS = [
1351
+ "wav", "mp3", "ogg", "opus", "flac",
1352
+ "new_format" # Add new format
1353
+ ]
1354
+
1355
+ # 2. Update FFmpeg conversion logic
1356
+ def _convert_to_audio(self, input_path, output_path, audio_format="wav"):
1357
+ if audio_format == "new_format":
1358
+ # Add specific conversion parameters
1359
+ cmd = ["ffmpeg", "-i", input_path, "-codec", "new_codec", output_path]
1360
+ ```
1361
+
1362
+ #### Adding New Features
1363
+ ```python
1364
+ # 1. Database schema updates
1365
+ def upgrade_database_schema():
1366
+ with self.get_connection() as conn:
1367
+ conn.execute("""
1368
+ ALTER TABLE transcriptions
1369
+ ADD COLUMN new_feature_data TEXT
1370
+ """)
1371
+
1372
+ # 2. API endpoint updates
1373
+ def new_feature_endpoint(user_id: str, feature_data: Dict) -> Dict:
1374
+ # Implement new feature logic
1375
+ pass
1376
+
1377
+ # 3. UI updates
1378
+ def add_new_feature_ui():
1379
+ new_feature_input = gr.Textbox(label="New Feature")
1380
+ new_feature_button = gr.Button("Use New Feature")
1381
+ ```
1382
+
1383
+ ---
1384
+
1385
+ ## βš™οΈ Advanced Configuration
1386
+
1387
+ ### Performance Optimization
1388
+
1389
+ #### Concurrent Processing
1390
+ ```python
1391
+ # Adjust worker thread pool size based on server capacity
1392
+ class TranscriptionManager:
1393
+ def __init__(self, max_workers: int = None):
1394
+ if max_workers is None:
1395
+ # Auto-detect based on CPU cores
1396
+ import multiprocessing
1397
+ max_workers = min(multiprocessing.cpu_count(), 10)
1398
+
1399
+ self.executor = ThreadPoolExecutor(max_workers=max_workers)
1400
+
1401
+ # Configure based on server specs
1402
+ # Small server: max_workers=2-4
1403
+ # Medium server: max_workers=5-8
1404
+ # Large server: max_workers=10+
1405
+ ```
1406
+
1407
+ #### Database Optimization
1408
+ ```python
1409
+ # SQLite performance tuning
1410
+ def configure_database_performance(db_path: str):
1411
+ with sqlite3.connect(db_path) as conn:
1412
+ # Enable WAL mode for better concurrency
1413
+ conn.execute("PRAGMA journal_mode=WAL")
1414
+
1415
+ # Increase cache size (in KB)
1416
+ conn.execute("PRAGMA cache_size=10000")
1417
+
1418
+ # Optimize synchronization
1419
+ conn.execute("PRAGMA synchronous=NORMAL")
1420
+
1421
+ # Enable foreign keys
1422
+ conn.execute("PRAGMA foreign_keys=ON")
1423
+ ```
1424
+
1425
+ #### Memory Management
1426
+ ```python
1427
+ # Large file handling
1428
+ def process_large_file(file_path: str):
1429
+ """Process large files in chunks to manage memory"""
1430
+ chunk_size = 64 * 1024 * 1024 # 64MB chunks
1431
+
1432
+ with open(file_path, 'rb') as f:
1433
+ while chunk := f.read(chunk_size):
1434
+ # Process chunk
1435
+ yield chunk
1436
+
1437
+ # Garbage collection for long-running processes
1438
+ import gc
1439
+
1440
+ def cleanup_memory():
1441
+ """Force garbage collection"""
1442
+ gc.collect()
1443
+
1444
+ # Schedule periodic cleanup
1445
+ schedule.every(30).minutes.do(cleanup_memory)
1446
+ ```
1447
+
1448
+ ### Security Hardening
1449
+
1450
+ #### Rate Limiting
1451
+ ```python
1452
+ from collections import defaultdict
1453
+ from time import time
1454
+
1455
+ class RateLimiter:
1456
+ def __init__(self, max_requests: int = 100, window: int = 3600):
1457
+ self.max_requests = max_requests
1458
+ self.window = window
1459
+ self.requests = defaultdict(list)
1460
+
1461
+ def is_allowed(self, user_id: str) -> bool:
1462
+ now = time()
1463
+ user_requests = self.requests[user_id]
1464
+
1465
+ # Clean old requests
1466
+ user_requests[:] = [req_time for req_time in user_requests
1467
+ if now - req_time < self.window]
1468
+
1469
+ # Check limit
1470
+ if len(user_requests) >= self.max_requests:
1471
+ return False
1472
+
1473
+ user_requests.append(now)
1474
+ return True
1475
+
1476
+ # Usage in endpoints
1477
+ rate_limiter = RateLimiter(max_requests=50, window=3600) # 50 per hour
1478
+
1479
+ def submit_transcription(self, user_id: str, ...):
1480
+ if not rate_limiter.is_allowed(user_id):
1481
+ raise Exception("Rate limit exceeded")
1482
+ ```
1483
+
1484
+ #### Input Sanitization
1485
+ ```python
1486
+ import bleach
1487
+ import re
1488
+
1489
+ def sanitize_filename(filename: str) -> str:
1490
+ """Sanitize uploaded filename"""
1491
+ # Remove path traversal attempts
1492
+ filename = os.path.basename(filename)
1493
+
1494
+ # Remove dangerous characters
1495
+ filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
1496
+
1497
+ # Limit length
1498
+ if len(filename) > 255:
1499
+ name, ext = os.path.splitext(filename)
1500
+ filename = name[:250] + ext
1501
+
1502
+ return filename
1503
+
1504
+ def sanitize_user_input(text: str) -> str:
1505
+ """Sanitize user text input"""
1506
+ # Remove HTML tags
1507
+ text = bleach.clean(text, tags=[], strip=True)
1508
+
1509
+ # Limit length
1510
+ text = text[:1000]
1511
+
1512
+ return text.strip()
1513
+ ```
1514
+
1515
+ #### Audit Logging
1516
+ ```python
1517
+ class AuditLogger:
1518
+ def __init__(self):
1519
+ self.logger = logging.getLogger('audit')
1520
+
1521
+ def log_user_action(self, user_id: str, action: str, details: Dict = None):
1522
+ """Log user actions for security auditing"""
1523
+ audit_entry = {
1524
+ 'timestamp': datetime.now().isoformat(),
1525
+ 'user_id': user_id,
1526
+ 'action': action,
1527
+ 'details': details or {},
1528
+ 'ip_address': self._get_client_ip(),
1529
+ 'user_agent': self._get_user_agent()
1530
+ }
1531
+
1532
+ self.logger.info(json.dumps(audit_entry))
1533
+
1534
+ def _get_client_ip(self) -> str:
1535
+ # Implementation depends on deployment setup
1536
+ return "unknown"
1537
+
1538
+ def _get_user_agent(self) -> str:
1539
+ # Implementation depends on deployment setup
1540
+ return "unknown"
1541
+
1542
+ # Usage
1543
+ audit = AuditLogger()
1544
+ audit.log_user_action(user_id, "login", {"success": True})
1545
+ audit.log_user_action(user_id, "transcription_submit", {"filename": filename})
1546
+ ```
1547
+
1548
+ ### Custom Extensions
1549
+
1550
+ #### Plugin Architecture
1551
+ ```python
1552
+ class TranscriptionPlugin:
1553
+ """Base class for transcription plugins"""
1554
+
1555
+ def pre_process(self, file_bytes: bytes, settings: Dict) -> bytes:
1556
+ """Pre-process audio before transcription"""
1557
+ return file_bytes
1558
+
1559
+ def post_process(self, transcript: str, settings: Dict) -> str:
1560
+ """Post-process transcript text"""
1561
+ return transcript
1562
+
1563
+ def get_name(self) -> str:
1564
+ """Return plugin name"""
1565
+ raise NotImplementedError
1566
+
1567
+ class NoiseReductionPlugin(TranscriptionPlugin):
1568
+ def get_name(self) -> str:
1569
+ return "noise_reduction"
1570
+
1571
+ def pre_process(self, file_bytes: bytes, settings: Dict) -> bytes:
1572
+ # Implement noise reduction using audio processing library
1573
+ # This is a placeholder - actual implementation would use
1574
+ # libraries like librosa, scipy, or pydub
1575
+ return file_bytes
1576
+
1577
+ class LanguageDetectionPlugin(TranscriptionPlugin):
1578
+ def get_name(self) -> str:
1579
+ return "language_detection"
1580
+
1581
+ def pre_process(self, file_bytes: bytes, settings: Dict) -> bytes:
1582
+ # Detect language and update settings
1583
+ detected_language = self._detect_language(file_bytes)
1584
+ settings['detected_language'] = detected_language
1585
+ return file_bytes
1586
+
1587
+ # Plugin manager
1588
+ class PluginManager:
1589
+ def __init__(self):
1590
+ self.plugins: List[TranscriptionPlugin] = []
1591
+
1592
+ def register_plugin(self, plugin: TranscriptionPlugin):
1593
+ self.plugins.append(plugin)
1594
+
1595
+ def apply_pre_processing(self, file_bytes: bytes, settings: Dict) -> bytes:
1596
+ for plugin in self.plugins:
1597
+ file_bytes = plugin.pre_process(file_bytes, settings)
1598
+ return file_bytes
1599
+
1600
+ def apply_post_processing(self, transcript: str, settings: Dict) -> str:
1601
+ for plugin in self.plugins:
1602
+ transcript = plugin.post_process(transcript, settings)
1603
+ return transcript
1604
+ ```
1605
+
1606
+ ---
1607
+
1608
+ ## πŸ”§ Troubleshooting
1609
+
1610
+ ### Common Development Issues
1611
+
1612
+ #### Environment Setup Problems
1613
+
1614
+ **Issue**: Azure connection fails
1615
+ ```bash
1616
+ # Check environment variables
1617
+ python -c "
1618
+ import os
1619
+ print('AZURE_SPEECH_KEY:', bool(os.getenv('AZURE_SPEECH_KEY')))
1620
+ print('AZURE_BLOB_CONNECTION:', bool(os.getenv('AZURE_BLOB_CONNECTION')))
1621
+ "
1622
+
1623
+ # Test Azure connection
1624
+ python -c "
1625
+ from azure.storage.blob import BlobServiceClient
1626
+ client = BlobServiceClient.from_connection_string('$AZURE_BLOB_CONNECTION')
1627
+ print('Containers:', list(client.list_containers()))
1628
+ "
1629
+ ```
1630
+
1631
+ **Issue**: FFmpeg not found
1632
+ ```bash
1633
+ # Check FFmpeg installation
1634
+ ffmpeg -version
1635
+
1636
+ # Install FFmpeg (Ubuntu/Debian)
1637
+ sudo apt update && sudo apt install ffmpeg
1638
+
1639
+ # Install FFmpeg (Windows with Chocolatey)
1640
+ choco install ffmpeg
1641
+
1642
+ # Install FFmpeg (macOS with Homebrew)
1643
+ brew install ffmpeg
1644
+ ```
1645
+
1646
+ **Issue**: Database initialization fails
1647
+ ```python
1648
+ # Check database permissions
1649
+ import os
1650
+ db_dir = "database"
1651
+ if not os.path.exists(db_dir):
1652
+ os.makedirs(db_dir)
1653
+ print(f"Created directory: {db_dir}")
1654
+
1655
+ # Test database creation
1656
+ import sqlite3
1657
+ conn = sqlite3.connect("database/test.db")
1658
+ conn.execute("CREATE TABLE test (id INTEGER)")
1659
+ conn.close()
1660
+ print("Database test successful")
1661
+ ```
1662
+
1663
+ #### Runtime Issues
1664
+
1665
+ **Issue**: Memory errors with large files
1666
+ ```python
1667
+ # Monitor memory usage
1668
+ import psutil
1669
+
1670
+ def check_memory():
1671
+ memory = psutil.virtual_memory()
1672
+ print(f"Memory usage: {memory.percent}%")
1673
+ print(f"Available: {memory.available / 1024**3:.1f}GB")
1674
+
1675
+ # Implement file chunking for large uploads
1676
+ def process_large_file_in_chunks(file_path: str, chunk_size: int = 64*1024*1024):
1677
+ with open(file_path, 'rb') as f:
1678
+ while chunk := f.read(chunk_size):
1679
+ yield chunk
1680
+ ```
1681
+
1682
+ **Issue**: Transcription jobs stuck
1683
+ ```python
1684
+ # Check pending jobs
1685
+ def diagnose_stuck_jobs():
1686
+ pending_jobs = transcription_manager.db.get_pending_jobs()
1687
+ print(f"Pending jobs: {len(pending_jobs)}")
1688
+
1689
+ for job in pending_jobs:
1690
+ duration = datetime.now() - datetime.fromisoformat(job.created_at)
1691
+ print(f"Job {job.job_id}: {job.status} for {duration}")
1692
+
1693
+ if duration.total_seconds() > 3600: # 1 hour
1694
+ print(f"⚠️ Job {job.job_id} may be stuck")
1695
+
1696
+ # Reset stuck jobs
1697
+ def reset_stuck_jobs():
1698
+ with transcription_manager.db.get_connection() as conn:
1699
+ conn.execute("""
1700
+ UPDATE transcriptions
1701
+ SET status = 'pending', azure_trans_id = NULL
1702
+ WHERE status = 'processing'
1703
+ AND created_at < datetime('now', '-1 hour')
1704
+ """)
1705
+ ```
1706
+
1707
+ **Issue**: Azure API errors
1708
+ ```python
1709
+ # Test Azure Speech Service
1710
+ def test_azure_speech():
1711
+ try:
1712
+ url = f"{AZURE_SPEECH_KEY_ENDPOINT}/speechtotext/v3.2/transcriptions"
1713
+ headers = {"Ocp-Apim-Subscription-Key": AZURE_SPEECH_KEY}
1714
+
1715
+ response = requests.get(url, headers=headers)
1716
+ print(f"Status: {response.status_code}")
1717
+ print(f"Response: {response.text[:200]}")
1718
+
1719
+ except Exception as e:
1720
+ print(f"Azure Speech test failed: {e}")
1721
+
1722
+ # Check Azure service status
1723
+ def check_azure_status():
1724
+ # Check Azure status page
1725
+ status_url = "https://status.azure.com/en-us/status"
1726
+ print(f"Check Azure status: {status_url}")
1727
+ ```
1728
+
1729
+ ### Debugging Tools
1730
+
1731
+ #### Debug Mode Configuration
1732
+ ```python
1733
+ # Enable debug mode
1734
+ DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
1735
+
1736
+ if DEBUG:
1737
+ logging.basicConfig(level=logging.DEBUG)
1738
+
1739
+ # Enable Gradio debug mode
1740
+ demo.launch(debug=True, show_error=True)
1741
+ ```
1742
+
1743
+ #### Performance Profiling
1744
+ ```python
1745
+ import cProfile
1746
+ import pstats
1747
+
1748
+ def profile_function(func):
1749
+ """Profile function performance"""
1750
+ profiler = cProfile.Profile()
1751
+
1752
+ def wrapper(*args, **kwargs):
1753
+ profiler.enable()
1754
+ result = func(*args, **kwargs)
1755
+ profiler.disable()
1756
+
1757
+ # Print stats
1758
+ stats = pstats.Stats(profiler)
1759
+ stats.sort_stats('cumulative')
1760
+ stats.print_stats(10) # Top 10 functions
1761
+
1762
+ return result
1763
+
1764
+ return wrapper
1765
+
1766
+ # Usage
1767
+ @profile_function
1768
+ def submit_transcription(self, ...):
1769
+ # Function implementation
1770
+ pass
1771
+ ```
1772
+
1773
+ #### Log Analysis
1774
+ ```python
1775
+ def analyze_logs(log_file: str = "logs/transcription.log"):
1776
+ """Analyze application logs for issues"""
1777
+
1778
+ errors = []
1779
+ warnings = []
1780
+ performance_issues = []
1781
+
1782
+ with open(log_file, 'r') as f:
1783
+ for line in f:
1784
+ if 'ERROR' in line:
1785
+ errors.append(line.strip())
1786
+ elif 'WARNING' in line:
1787
+ warnings.append(line.strip())
1788
+ elif 'completed in' in line:
1789
+ # Extract timing information
1790
+ import re
1791
+ match = re.search(r'completed in (\d+\.\d+)s', line)
1792
+ if match and float(match.group(1)) > 30: # > 30 seconds
1793
+ performance_issues.append(line.strip())
1794
+
1795
+ print(f"Errors: {len(errors)}")
1796
+ print(f"Warnings: {len(warnings)}")
1797
+ print(f"Performance issues: {len(performance_issues)}")
1798
+
1799
+ return {
1800
+ 'errors': errors[-10:], # Last 10 errors
1801
+ 'warnings': warnings[-10:], # Last 10 warnings
1802
+ 'performance_issues': performance_issues[-10:]
1803
+ }
1804
+ ```
1805
+
1806
+ ### Production Troubleshooting
1807
+
1808
+ #### Service Health Check
1809
+ ```bash
1810
+ #!/bin/bash
1811
+ # health_check.sh
1812
+
1813
+ echo "=== System Health Check ==="
1814
+
1815
+ # Check service status
1816
+ systemctl is-active transcription
1817
+ systemctl is-active nginx
1818
+
1819
+ # Check disk space
1820
+ df -h
1821
+
1822
+ # Check memory usage
1823
+ free -h
1824
+
1825
+ # Check CPU usage
1826
+ top -b -n1 | grep "Cpu(s)"
1827
+
1828
+ # Check logs for errors
1829
+ tail -n 50 /home/transcription/app/logs/transcription.log | grep ERROR
1830
+
1831
+ # Check Azure connectivity
1832
+ curl -s -o /dev/null -w "%{http_code}" https://azure.microsoft.com/
1833
+
1834
+ echo "=== Health Check Complete ==="
1835
+ ```
1836
+
1837
+ #### Database Recovery
1838
+ ```python
1839
+ def recover_database():
1840
+ """Recover database from Azure backup"""
1841
+ try:
1842
+ # List available backups
1843
+ container_client = blob_service.get_container_client(AZURE_CONTAINER)
1844
+ backups = []
1845
+
1846
+ for blob in container_client.list_blobs(name_starts_with="shared/backups/"):
1847
+ backups.append({
1848
+ 'name': blob.name,
1849
+ 'modified': blob.last_modified
1850
+ })
1851
+
1852
+ # Sort by date (newest first)
1853
+ backups.sort(key=lambda x: x['modified'], reverse=True)
1854
+
1855
+ if not backups:
1856
+ print("No backups found")
1857
+ return
1858
+
1859
+ # Download latest backup
1860
+ latest_backup = backups[0]['name']
1861
+ print(f"Restoring from: {latest_backup}")
1862
+
1863
+ blob_client = blob_service.get_blob_client(
1864
+ container=AZURE_CONTAINER,
1865
+ blob=latest_backup
1866
+ )
1867
+
1868
+ # Download backup
1869
+ with open("database/transcriptions_restored.db", "wb") as f:
1870
+ f.write(blob_client.download_blob().readall())
1871
+
1872
+ print("Database restored successfully")
1873
+ print("Restart the application to use restored database")
1874
+
1875
+ except Exception as e:
1876
+ print(f"Database recovery failed: {str(e)}")
1877
+ ```
1878
+
1879
+ ---
1880
+
1881
+ ## πŸ“š Additional Resources
1882
+
1883
+ ### Documentation Links
1884
+ - [Azure Speech Services Documentation](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/)
1885
+ - [Azure Blob Storage Documentation](https://docs.microsoft.com/en-us/azure/storage/blobs/)
1886
+ - [Gradio Documentation](https://gradio.app/docs/)
1887
+ - [SQLite Documentation](https://www.sqlite.org/docs.html)
1888
+ - [FFmpeg Documentation](https://ffmpeg.org/documentation.html)
1889
+
1890
+ ### Useful Tools
1891
+ - **Azure Storage Explorer**: GUI for managing blob storage
1892
+ - **DB Browser for SQLite**: Visual database management
1893
+ - **Postman**: API testing and development
1894
+ - **Azure CLI**: Command-line Azure management
1895
+ - **Visual Studio Code**: Recommended IDE with Azure extensions
1896
+
1897
+ ### Community Resources
1898
+ - [Azure Speech Services Community](https://docs.microsoft.com/en-us/answers/topics/azure-speech-services.html)
1899
+ - [Gradio Community](https://github.com/gradio-app/gradio/discussions)
1900
+ - [Python Audio Processing Libraries](https://github.com/topics/audio-processing)
1901
+
1902
+ ---
1903
+
1904
+ **This developer guide provides comprehensive information for setting up, developing, deploying, and maintaining the Azure Speech Transcription service. For additional help, refer to the linked documentation and community resources.** πŸš€
USER.md ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸŽ™οΈ Azure Speech Transcription - User Guide
2
+
3
+ ## πŸ“‹ Table of Contents
4
+
5
+ - [Getting Started](#-getting-started)
6
+ - [Account Management](#-account-management)
7
+ - [Using Transcription Services](#-using-transcription-services)
8
+ - [Managing Your History](#-managing-your-history)
9
+ - [Privacy & Data Control](#-privacy--data-control)
10
+ - [Troubleshooting](#-troubleshooting)
11
+ - [FAQ](#-faq)
12
+
13
+ ---
14
+
15
+ ## πŸš€ Getting Started
16
+
17
+ ### What is Azure Speech Transcription?
18
+
19
+ Azure Speech Transcription is a secure, PDPA-compliant service that converts your audio and video files into accurate text transcripts with speaker identification and precise timestamps. Your data is stored securely in your own private folder, ensuring complete privacy and compliance with data protection regulations.
20
+
21
+ ### Key Features
22
+
23
+ - 🎯 **High Accuracy**: Powered by Microsoft Azure Speech Services
24
+ - πŸ—£οΈ **Speaker Identification**: Automatically identifies different speakers
25
+ - ⏰ **Precise Timestamps**: Every sentence includes exact timing (HH:MM:SS)
26
+ - πŸ”’ **Privacy Compliant**: GDPR, PDPA, and data protection compliant
27
+ - πŸ“ **Multiple Formats**: Supports audio and video files
28
+ - 🌍 **Multi-Language**: Support for multiple languages
29
+ - πŸ“Š **Personal Dashboard**: Track your usage and history
30
+
31
+ ---
32
+
33
+ ## πŸ” Account Management
34
+
35
+ ### Creating Your Account
36
+
37
+ 1. **Go to Registration Tab**
38
+ - Open the application in your web browser
39
+ - Click on the "πŸ“ Register" tab
40
+
41
+ 2. **Fill Your Information**
42
+ - **Email**: Enter a valid email address
43
+ - **Username**: Choose 3-30 characters (letters, numbers, underscore only)
44
+ - **Password**: Create a strong password (minimum 8 characters with uppercase, lowercase, and numbers)
45
+ - **Confirm Password**: Re-enter your password
46
+
47
+ 3. **Privacy Consents** (Required)
48
+ - βœ… **GDPR Consent**: Required for account creation
49
+ - βœ… **Data Retention**: Required for service functionality
50
+ - ☐ **Marketing**: Optional - receive updates and news
51
+
52
+ 4. **Create Account**
53
+ - Click "πŸ“ Create Account"
54
+ - You'll see a success message
55
+ - Click "πŸ”‘ Go to Login" to proceed
56
+
57
+ ### Logging In
58
+
59
+ 1. **Go to Login Tab**
60
+ - Click on the "πŸ”‘ Login" tab
61
+
62
+ 2. **Enter Credentials**
63
+ - **Email or Username**: Enter either your email or username
64
+ - **Password**: Enter your password
65
+
66
+ 3. **Access Your Account**
67
+ - Click "πŸ”‘ Login"
68
+ - You'll be taken to your personal dashboard
69
+
70
+ ### Password Requirements
71
+
72
+ Your password must include:
73
+ - βœ… At least 8 characters
74
+ - βœ… One uppercase letter (A-Z)
75
+ - βœ… One lowercase letter (a-z)
76
+ - βœ… One number (0-9)
77
+
78
+ **Strong Password Examples:**
79
+ - `MySecure123!`
80
+ - `Transcribe2024#`
81
+ - `AudioFiles456$`
82
+
83
+ ---
84
+
85
+ ## πŸŽ™οΈ Using Transcription Services
86
+
87
+ ### Supported File Formats
88
+
89
+ #### Audio Formats
90
+ - **WAV** (recommended for fastest processing)
91
+ - **MP3**, **OGG**, **OPUS**, **FLAC**
92
+ - **WMA**, **AAC**, **M4A**, **AMR**
93
+ - **WebM**, **Speex**
94
+
95
+ #### Video Formats
96
+ - **MP4**, **MOV**, **AVI**, **MKV**
97
+ - **WMV**, **FLV**, **3GP**
98
+
99
+ ### File Size Limits
100
+ - **Maximum**: 500MB per file
101
+ - **Recommended**: Under 100MB for faster processing
102
+
103
+ ### Step-by-Step Transcription
104
+
105
+ 1. **Upload Your File**
106
+ - Click "Browse" under "Audio or Video File"
107
+ - Select your file from your computer
108
+ - Wait for upload confirmation
109
+
110
+ 2. **Configure Settings**
111
+
112
+ **Basic Settings:**
113
+ - **Language**: Choose the primary language spoken
114
+ - **Output Format**: Select audio format (WAV recommended)
115
+
116
+ **Advanced Settings:**
117
+ - **Speaker Identification**: Enable to identify different speakers
118
+ - **Max Speakers**: Set expected number of speakers (1-10)
119
+ - **Timestamps**: Include precise timing information
120
+ - **Profanity Filter**: Choose how to handle profanity
121
+ - `Masked`: Replace with ***
122
+ - `Removed`: Remove completely
123
+ - `Raw`: Keep original
124
+ - **Punctuation**: Automatic punctuation insertion
125
+ - **Lexical Form**: Include alternative word forms
126
+
127
+ 3. **Start Transcription**
128
+ - Click "πŸš€ Start Transcription"
129
+ - Processing begins automatically
130
+ - Status updates every 10 seconds
131
+
132
+ 4. **Monitor Progress**
133
+ - **⏳ Queued**: Waiting to start
134
+ - **πŸ”„ Processing**: Converting and analyzing
135
+ - **βœ… Done**: Transcription complete
136
+
137
+ 5. **Download Results**
138
+ - View transcript in the text area
139
+ - Download the transcript file
140
+ - Access from your history anytime
141
+
142
+ ### Understanding Your Transcript
143
+
144
+ Your transcript includes:
145
+ - **Timestamps**: `[00:02:15]` (hours:minutes:seconds)
146
+ - **Speaker Labels**: `Speaker 0:`, `Speaker 1:`, etc.
147
+ - **Formatted Text**: Proper punctuation and capitalization
148
+
149
+ **Example Output:**
150
+ ```
151
+ [00:00:12] Speaker 0: Welcome to today's meeting. Let's start with the agenda.
152
+
153
+ [00:00:18] Speaker 1: Thank you. First item is the quarterly review.
154
+
155
+ [00:00:25] Speaker 0: Perfect. Let me share the presentation now.
156
+ ```
157
+
158
+ ---
159
+
160
+ ## πŸ“š Managing Your History
161
+
162
+ ### Viewing Your Transcriptions
163
+
164
+ 1. **Go to History Tab**
165
+ - Click "πŸ“š My History" tab
166
+ - Your transcriptions appear automatically
167
+
168
+ 2. **Understanding the Table**
169
+ - **Date**: When transcription was created
170
+ - **Filename**: Original file name
171
+ - **Language**: Language used for transcription
172
+ - **Status**: Current status (Done, Processing, etc.)
173
+ - **Duration**: How long processing took
174
+ - **Job ID**: Unique identifier
175
+ - **Download**: Availability status
176
+
177
+ 3. **View Options**
178
+ - **Recent 20**: Default view of recent transcriptions
179
+ - **Show All**: Check box to see all your transcriptions
180
+
181
+ ### Downloading Transcripts
182
+
183
+ **Method 1: From Results**
184
+ - Complete transcription β†’ Download button appears
185
+ - Click to download immediately
186
+
187
+ **Method 2: From History**
188
+ - Go to "πŸ“š My History" tab
189
+ - Click "πŸ”„ Refresh My History & Downloads"
190
+ - Download files appear below the table
191
+ - Click any available download link
192
+
193
+ **Method 3: From Downloads Section**
194
+ - Scroll to "πŸ“₯ Download Your Completed Transcripts"
195
+ - Available transcripts show as file download buttons
196
+ - Click to download specific transcripts
197
+
198
+ ### Personal Statistics
199
+
200
+ Your dashboard shows:
201
+ - **Total Jobs**: All transcriptions you've created
202
+ - **Completed**: Successfully finished transcriptions
203
+ - **Processing**: Currently in progress
204
+ - **Pending**: Waiting to start
205
+ - **Failed**: Transcriptions that encountered errors
206
+ - **Last 7 Days**: Recent activity
207
+
208
+ ---
209
+
210
+ ## πŸ”’ Privacy & Data Control
211
+
212
+ ### GDPR & Data Rights
213
+
214
+ You have complete control over your data:
215
+
216
+ #### πŸ“Š Data Export
217
+ 1. Go to "πŸ”’ Privacy & Data" tab
218
+ 2. Click "πŸ“¦ Export My Data"
219
+ 3. Download complete data archive (JSON format)
220
+
221
+ **What's Included:**
222
+ - Account information
223
+ - All transcription history
224
+ - Usage statistics
225
+ - Privacy preferences
226
+
227
+ #### πŸ“§ Marketing Preferences
228
+ 1. Go to "πŸ”’ Privacy & Data" tab
229
+ 2. Update "Marketing Consent" checkbox
230
+ 3. Click "βœ… Update Consent"
231
+
232
+ #### πŸ—‘οΈ Account Deletion
233
+ 1. Go to "πŸ”’ Privacy & Data" tab
234
+ 2. Type "DELETE MY ACCOUNT" in confirmation field
235
+ 3. Click "πŸ—‘οΈ Delete My Account"
236
+
237
+ **⚠️ Warning**: This permanently deletes:
238
+ - Your account and profile
239
+ - All transcription files
240
+ - Usage history and statistics
241
+ - Data stored in Azure
242
+
243
+ ### Data Security
244
+
245
+ Your data is protected by:
246
+ - **Encryption**: All data encrypted in transit and at rest
247
+ - **Isolation**: Your files stored in private user folder
248
+ - **Access Control**: Only you can access your data
249
+ - **Compliance**: GDPR, PDPA, and privacy regulation compliant
250
+ - **Audit Trail**: Complete logging for security
251
+
252
+ ### Where Your Data is Stored
253
+
254
+ - **User Folder**: `users/{your-user-id}/`
255
+ - `audio/`: Processed audio files
256
+ - `transcripts/`: Text transcriptions
257
+ - `originals/`: Original uploaded files
258
+ - **Location**: Secure Azure Blob Storage
259
+ - **Retention**: Until you delete your account
260
+
261
+ ---
262
+
263
+ ## πŸ› οΈ Troubleshooting
264
+
265
+ ### Common Issues
266
+
267
+ #### Authentication Problems
268
+
269
+ **Problem**: Can't log in
270
+ - βœ… Check username/email spelling
271
+ - βœ… Verify password (case-sensitive)
272
+ - βœ… Ensure caps lock is off
273
+ - βœ… Try password reset if available
274
+
275
+ **Problem**: Registration fails
276
+ - βœ… Check email format (must be valid email)
277
+ - βœ… Username requirements (3-30 chars, alphanumeric + underscore)
278
+ - βœ… Password requirements (8+ chars, mixed case, numbers)
279
+ - βœ… Required consents must be checked
280
+
281
+ #### File Upload Issues
282
+
283
+ **Problem**: File won't upload
284
+ - βœ… Check file size (max 500MB)
285
+ - βœ… Verify file format is supported
286
+ - βœ… Ensure stable internet connection
287
+ - βœ… Try smaller file first
288
+
289
+ **Problem**: Unsupported format error
290
+ - βœ… Convert to supported format (WAV, MP3, MP4)
291
+ - βœ… Check file isn't corrupted
292
+ - βœ… Try different file
293
+
294
+ #### Processing Issues
295
+
296
+ **Problem**: Transcription stuck in "Processing"
297
+ - βœ… Wait - large files take time
298
+ - βœ… Check auto-refresh is active
299
+ - βœ… Refresh browser if needed
300
+ - βœ… Check Azure service status
301
+
302
+ **Problem**: Transcription failed
303
+ - βœ… Check error message for details
304
+ - βœ… Verify audio quality is good
305
+ - βœ… Try different audio format
306
+ - βœ… Ensure speakers are clearly audible
307
+
308
+ #### Results Issues
309
+
310
+ **Problem**: Poor transcription quality
311
+ - βœ… Use clear, high-quality audio
312
+ - βœ… Minimize background noise
313
+ - βœ… Ensure speakers speak clearly
314
+ - βœ… Select correct language
315
+ - βœ… Try WAV format for best results
316
+
317
+ **Problem**: Speakers not identified correctly
318
+ - βœ… Enable "Speaker Identification"
319
+ - βœ… Set correct number of speakers
320
+ - βœ… Ensure speakers have distinct voices
321
+ - βœ… Minimize speaker overlap
322
+
323
+ ### Performance Tips
324
+
325
+ #### For Best Results
326
+ - **Audio Quality**: Use high-quality recordings
327
+ - **File Format**: WAV files process fastest
328
+ - **Speaker Separation**: Clear pauses between speakers
329
+ - **Background Noise**: Minimize environmental noise
330
+ - **Language Selection**: Choose correct primary language
331
+
332
+ #### For Faster Processing
333
+ - **File Size**: Smaller files process faster
334
+ - **Format**: WAV > MP3 > other formats
335
+ - **Settings**: Disable unused features
336
+ - **Timing**: Process during off-peak hours
337
+
338
+ ---
339
+
340
+ ## ❓ FAQ
341
+
342
+ ### General Questions
343
+
344
+ **Q: Is my data secure?**
345
+ A: Yes, your data is stored in encrypted, user-separated Azure Blob Storage with enterprise-grade security.
346
+
347
+ **Q: Can others see my transcriptions?**
348
+ A: No, your data is completely private. Only you can access your transcriptions.
349
+
350
+ **Q: How long are transcriptions stored?**
351
+ A: Indefinitely, until you delete your account or individual transcriptions.
352
+
353
+ **Q: Is there a usage limit?**
354
+ A: Check with your administrator for any usage limits or quotas.
355
+
356
+ ### Technical Questions
357
+
358
+ **Q: What languages are supported?**
359
+ A: Multiple languages including English, Thai, Chinese, Japanese, Korean, Spanish, French, German, and others.
360
+
361
+ **Q: How accurate are the transcriptions?**
362
+ A: Very high accuracy using Microsoft Azure Speech Services, typically 85-95% depending on audio quality.
363
+
364
+ **Q: Can I edit transcriptions?**
365
+ A: You can copy and edit the text after download, but the original transcript is preserved.
366
+
367
+ **Q: Do you store my original files?**
368
+ A: Yes, originals are stored in your private folder and can be downloaded anytime.
369
+
370
+ ### Privacy Questions
371
+
372
+ **Q: Can I export all my data?**
373
+ A: Yes, use the "Export My Data" feature to download everything in JSON format.
374
+
375
+ **Q: How do I delete my account?**
376
+ A: Go to Privacy & Data tab and follow the account deletion process.
377
+
378
+ **Q: What happens to my data if I delete my account?**
379
+ A: All data is permanently deleted from Azure storage within 24 hours.
380
+
381
+ **Q: Do you use my data for training?**
382
+ A: No, your data is never used for training or shared with third parties.
383
+
384
+ ### Billing Questions
385
+
386
+ **Q: How much does it cost?**
387
+ A: Contact your administrator for pricing information.
388
+
389
+ **Q: Are there free tiers available?**
390
+ A: Depends on your organization's setup and Azure subscription.
391
+
392
+ ---
393
+
394
+ ## πŸ“ž Getting Help
395
+
396
+ ### Support Resources
397
+
398
+ 1. **This User Guide**: Comprehensive information for all features
399
+ 2. **Error Messages**: Pay attention to specific error descriptions
400
+ 3. **System Status**: Check if Azure services are operational
401
+ 4. **Administrator**: Contact your system administrator for account issues
402
+
403
+ ### Reporting Issues
404
+
405
+ When reporting problems, include:
406
+ - **What you were trying to do**
407
+ - **What happened instead**
408
+ - **Error messages (exact text)**
409
+ - **File type and size**
410
+ - **Browser and operating system**
411
+ - **Steps to reproduce the issue**
412
+
413
+ ---
414
+
415
+ ## 🎯 Tips for Success
416
+
417
+ ### Getting the Best Transcriptions
418
+
419
+ 1. **Audio Quality Matters**
420
+ - Use good microphones
421
+ - Record in quiet environments
422
+ - Ensure clear speech
423
+ - Avoid speaker overlap
424
+
425
+ 2. **File Preparation**
426
+ - Convert to WAV for best results
427
+ - Trim unnecessary silence
428
+ - Normalize audio levels
429
+ - Remove background music if possible
430
+
431
+ 3. **Settings Optimization**
432
+ - Choose correct language
433
+ - Set appropriate speaker count
434
+ - Enable relevant features only
435
+ - Use appropriate profanity filter
436
+
437
+ 4. **Workflow Efficiency**
438
+ - Process multiple files in batches
439
+ - Use consistent naming conventions
440
+ - Download transcripts promptly
441
+ - Keep originals as backups
442
+
443
+ ### Privacy Best Practices
444
+
445
+ 1. **Account Security**
446
+ - Use strong, unique passwords
447
+ - Log out when finished
448
+ - Don't share login credentials
449
+ - Review account regularly
450
+
451
+ 2. **Data Management**
452
+ - Export data periodically
453
+ - Delete unnecessary transcriptions
454
+ - Review privacy settings
455
+ - Understand data retention
456
+
457
+ ---
458
+
459
+ **Welcome to Azure Speech Transcription! We hope this guide helps you make the most of our service. For the best experience, keep your audio quality high and your data organized.** πŸŽ‰
app_core.py ADDED
@@ -0,0 +1,1133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import uuid
4
+ import json
5
+ import requests
6
+ import subprocess
7
+ import asyncio
8
+ import threading
9
+ import hashlib
10
+ import re
11
+ from datetime import datetime, timedelta
12
+ from typing import Optional, Dict, List, Tuple
13
+ from dataclasses import dataclass, asdict
14
+ from concurrent.futures import ThreadPoolExecutor
15
+ import sqlite3
16
+ from contextlib import contextmanager
17
+ from dotenv import load_dotenv
18
+ from azure.storage.blob import BlobServiceClient
19
+ import tempfile
20
+ import shutil
21
+
22
+ # Load Environment
23
+ load_dotenv()
24
+
25
+ def _require_env_var(varname):
26
+ value = os.environ.get(varname)
27
+ if not value or value.strip() == "" or "your" in value.lower():
28
+ raise ValueError(f"Environment variable {varname} is missing or invalid. Check your .env file.")
29
+ return value
30
+
31
+ # Environment variables
32
+ AZURE_SPEECH_KEY = _require_env_var("AZURE_SPEECH_KEY")
33
+ AZURE_SPEECH_KEY_ENDPOINT = _require_env_var("AZURE_SPEECH_KEY_ENDPOINT").rstrip('/')
34
+ AZURE_REGION = _require_env_var("AZURE_REGION")
35
+ AZURE_BLOB_CONNECTION = _require_env_var("AZURE_BLOB_CONNECTION")
36
+ AZURE_CONTAINER = _require_env_var("AZURE_CONTAINER")
37
+ AZURE_BLOB_SAS_TOKEN = _require_env_var("AZURE_BLOB_SAS_TOKEN")
38
+ ALLOWED_LANGS = json.loads(os.environ.get("ALLOWED_LANGS", "{}"))
39
+ API_VERSION = os.environ.get("API_VERSION", "v3.2")
40
+
41
+ # Directories
42
+ UPLOAD_DIR = "uploads"
43
+ DB_DIR = "database"
44
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
45
+ os.makedirs(DB_DIR, exist_ok=True)
46
+
47
+ AUDIO_FORMATS = [
48
+ "wav", "mp3", "ogg", "opus", "flac", "wma", "aac", "alaw", "mulaw", "amr", "webm", "speex"
49
+ ]
50
+
51
+ @dataclass
52
+ class User:
53
+ user_id: str
54
+ email: str
55
+ username: str
56
+ password_hash: str
57
+ created_at: str
58
+ last_login: Optional[str] = None
59
+ is_active: bool = True
60
+ gdpr_consent: bool = False
61
+ data_retention_agreed: bool = False
62
+ marketing_consent: bool = False
63
+
64
+ @dataclass
65
+ class TranscriptionJob:
66
+ job_id: str
67
+ user_id: str # Changed from user_session to user_id for proper authentication
68
+ original_filename: str
69
+ audio_url: str
70
+ language: str
71
+ status: str # pending, processing, completed, failed
72
+ created_at: str
73
+ completed_at: Optional[str] = None
74
+ transcript_text: Optional[str] = None
75
+ transcript_url: Optional[str] = None
76
+ error_message: Optional[str] = None
77
+ azure_trans_id: Optional[str] = None
78
+ settings: Optional[Dict] = None
79
+
80
+ class AuthManager:
81
+ """Handle user authentication and PDPA compliance"""
82
+
83
+ @staticmethod
84
+ def hash_password(password: str) -> str:
85
+ """Hash password using SHA-256 with salt"""
86
+ salt = "azure_speech_transcription_salt_2024" # In production, use environment variable
87
+ return hashlib.sha256((password + salt).encode()).hexdigest()
88
+
89
+ @staticmethod
90
+ def verify_password(password: str, password_hash: str) -> bool:
91
+ """Verify password against hash"""
92
+ return AuthManager.hash_password(password) == password_hash
93
+
94
+ @staticmethod
95
+ def validate_email(email: str) -> bool:
96
+ """Validate email format"""
97
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
98
+ return re.match(pattern, email) is not None
99
+
100
+ @staticmethod
101
+ def validate_username(username: str) -> bool:
102
+ """Validate username format"""
103
+ # Username: 3-30 characters, alphanumeric and underscore only
104
+ pattern = r'^[a-zA-Z0-9_]{3,30}$'
105
+ return re.match(pattern, username) is not None
106
+
107
+ @staticmethod
108
+ def validate_password(password: str) -> Tuple[bool, str]:
109
+ """Validate password strength"""
110
+ if len(password) < 8:
111
+ return False, "Password must be at least 8 characters long"
112
+ if not re.search(r'[A-Z]', password):
113
+ return False, "Password must contain at least one uppercase letter"
114
+ if not re.search(r'[a-z]', password):
115
+ return False, "Password must contain at least one lowercase letter"
116
+ if not re.search(r'\d', password):
117
+ return False, "Password must contain at least one number"
118
+ return True, "Password is valid"
119
+
120
+ class DatabaseManager:
121
+ def __init__(self, db_path: str = None):
122
+ self.db_path = db_path or os.path.join(DB_DIR, "transcriptions.db")
123
+ self.blob_service = BlobServiceClient.from_connection_string(AZURE_BLOB_CONNECTION)
124
+ self.db_blob_name = "shared/database/transcriptions.db" # Shared database location
125
+ self._lock = threading.Lock()
126
+ self._last_backup_time = 0
127
+ self._backup_interval = 30 # Backup every 30 seconds at most
128
+
129
+ # Download existing database from blob storage or create new one
130
+ self.init_database()
131
+
132
+ def _download_db_from_blob(self):
133
+ """Download database from Azure Blob Storage if it exists"""
134
+ try:
135
+ blob_client = self.blob_service.get_blob_client(container=AZURE_CONTAINER, blob=self.db_blob_name)
136
+
137
+ # Check if blob exists
138
+ if blob_client.exists():
139
+ print("πŸ“₯ Downloading existing shared database from Azure Blob Storage...")
140
+
141
+ # Create temporary file
142
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
143
+ temp_path = temp_file.name
144
+
145
+ # Download blob to temporary file
146
+ with open(temp_path, "wb") as download_file:
147
+ download_file.write(blob_client.download_blob().readall())
148
+
149
+ # Move to final location
150
+ os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
151
+ shutil.move(temp_path, self.db_path)
152
+
153
+ print("βœ… Shared database downloaded successfully")
154
+ return True
155
+ else:
156
+ print("πŸ“ No existing shared database found in blob storage, will create new one")
157
+ return False
158
+
159
+ except Exception as e:
160
+ print(f"⚠️ Warning: Could not download shared database from blob storage: {e}")
161
+ print("πŸ“ Will create new local database")
162
+ return False
163
+
164
+ def _upload_db_to_blob(self):
165
+ """Upload database to Azure Blob Storage with rate limiting"""
166
+ try:
167
+ current_time = time.time()
168
+ if current_time - self._last_backup_time < self._backup_interval:
169
+ return # Skip backup if too recent
170
+
171
+ if not os.path.exists(self.db_path):
172
+ return
173
+
174
+ blob_client = self.blob_service.get_blob_client(container=AZURE_CONTAINER, blob=self.db_blob_name)
175
+
176
+ with open(self.db_path, "rb") as data:
177
+ blob_client.upload_blob(data, overwrite=True)
178
+
179
+ self._last_backup_time = current_time
180
+
181
+ except Exception as e:
182
+ print(f"⚠️ Warning: Could not upload shared database to blob storage: {e}")
183
+
184
+ @contextmanager
185
+ def get_connection(self):
186
+ with self._lock:
187
+ conn = sqlite3.connect(self.db_path, timeout=30.0)
188
+ conn.row_factory = sqlite3.Row
189
+ try:
190
+ yield conn
191
+ finally:
192
+ conn.close()
193
+ # Auto-backup after any database operation (rate limited)
194
+ threading.Thread(target=self._upload_db_to_blob, daemon=True).start()
195
+
196
+ def init_database(self):
197
+ # Try to download existing database first
198
+ self._download_db_from_blob()
199
+
200
+ # Initialize database structure
201
+ with self.get_connection() as conn:
202
+ # Users table
203
+ conn.execute("""
204
+ CREATE TABLE IF NOT EXISTS users (
205
+ user_id TEXT PRIMARY KEY,
206
+ email TEXT UNIQUE NOT NULL,
207
+ username TEXT UNIQUE NOT NULL,
208
+ password_hash TEXT NOT NULL,
209
+ created_at TEXT NOT NULL,
210
+ last_login TEXT,
211
+ is_active BOOLEAN DEFAULT 1,
212
+ gdpr_consent BOOLEAN DEFAULT 0,
213
+ data_retention_agreed BOOLEAN DEFAULT 0,
214
+ marketing_consent BOOLEAN DEFAULT 0
215
+ )
216
+ """)
217
+
218
+ # Transcriptions table (updated to use user_id instead of user_session)
219
+ conn.execute("""
220
+ CREATE TABLE IF NOT EXISTS transcriptions (
221
+ job_id TEXT PRIMARY KEY,
222
+ user_id TEXT NOT NULL,
223
+ original_filename TEXT NOT NULL,
224
+ audio_url TEXT,
225
+ language TEXT NOT NULL,
226
+ status TEXT NOT NULL,
227
+ created_at TEXT NOT NULL,
228
+ completed_at TEXT,
229
+ transcript_text TEXT,
230
+ transcript_url TEXT,
231
+ error_message TEXT,
232
+ azure_trans_id TEXT,
233
+ settings TEXT,
234
+ FOREIGN KEY (user_id) REFERENCES users (user_id)
235
+ )
236
+ """)
237
+
238
+ # Create indexes
239
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)")
240
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)")
241
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_transcriptions_user_id ON transcriptions(user_id)")
242
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_transcriptions_status ON transcriptions(status)")
243
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_transcriptions_created_at ON transcriptions(created_at DESC)")
244
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_transcriptions_user_created ON transcriptions(user_id, created_at DESC)")
245
+
246
+ conn.commit()
247
+
248
+ # User management methods
249
+ def create_user(self, email: str, username: str, password: str, gdpr_consent: bool = True,
250
+ data_retention_agreed: bool = True, marketing_consent: bool = False) -> Tuple[bool, str, Optional[str]]:
251
+ """Create new user account"""
252
+ try:
253
+ # Validate inputs
254
+ if not AuthManager.validate_email(email):
255
+ return False, "Invalid email format", None
256
+
257
+ if not AuthManager.validate_username(username):
258
+ return False, "Username must be 3-30 characters, alphanumeric and underscore only", None
259
+
260
+ is_valid, message = AuthManager.validate_password(password)
261
+ if not is_valid:
262
+ return False, message, None
263
+
264
+ if not gdpr_consent:
265
+ return False, "GDPR consent is required to create an account", None
266
+
267
+ if not data_retention_agreed:
268
+ return False, "Data retention agreement is required", None
269
+
270
+ user_id = str(uuid.uuid4())
271
+ password_hash = AuthManager.hash_password(password)
272
+
273
+ with self.get_connection() as conn:
274
+ # Check if email or username already exists
275
+ existing = conn.execute(
276
+ "SELECT email, username FROM users WHERE email = ? OR username = ?",
277
+ (email, username)
278
+ ).fetchone()
279
+
280
+ if existing:
281
+ if existing['email'] == email:
282
+ return False, "Email already registered", None
283
+ else:
284
+ return False, "Username already taken", None
285
+
286
+ # Create user
287
+ user = User(
288
+ user_id=user_id,
289
+ email=email,
290
+ username=username,
291
+ password_hash=password_hash,
292
+ created_at=datetime.now().isoformat(),
293
+ gdpr_consent=gdpr_consent,
294
+ data_retention_agreed=data_retention_agreed,
295
+ marketing_consent=marketing_consent
296
+ )
297
+
298
+ conn.execute("""
299
+ INSERT INTO users
300
+ (user_id, email, username, password_hash, created_at, is_active,
301
+ gdpr_consent, data_retention_agreed, marketing_consent)
302
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
303
+ """, (
304
+ user.user_id, user.email, user.username, user.password_hash,
305
+ user.created_at, user.is_active, user.gdpr_consent,
306
+ user.data_retention_agreed, user.marketing_consent
307
+ ))
308
+ conn.commit()
309
+
310
+ print(f"πŸ‘€ New user registered: {username} ({email})")
311
+ return True, "Account created successfully", user_id
312
+
313
+ except Exception as e:
314
+ print(f"❌ Error creating user: {str(e)}")
315
+ return False, f"Registration failed: {str(e)}", None
316
+
317
+ def authenticate_user(self, login: str, password: str) -> Tuple[bool, str, Optional[User]]:
318
+ """Authenticate user by email or username"""
319
+ try:
320
+ with self.get_connection() as conn:
321
+ # Find user by email or username
322
+ user_row = conn.execute("""
323
+ SELECT * FROM users
324
+ WHERE (email = ? OR username = ?) AND is_active = 1
325
+ """, (login, login)).fetchone()
326
+
327
+ if not user_row:
328
+ return False, "Invalid credentials", None
329
+
330
+ # Verify password
331
+ if not AuthManager.verify_password(password, user_row['password_hash']):
332
+ return False, "Invalid credentials", None
333
+
334
+ # Update last login
335
+ conn.execute(
336
+ "UPDATE users SET last_login = ? WHERE user_id = ?",
337
+ (datetime.now().isoformat(), user_row['user_id'])
338
+ )
339
+ conn.commit()
340
+
341
+ # Convert to User object
342
+ user = User(
343
+ user_id=user_row['user_id'],
344
+ email=user_row['email'],
345
+ username=user_row['username'],
346
+ password_hash=user_row['password_hash'],
347
+ created_at=user_row['created_at'],
348
+ last_login=datetime.now().isoformat(),
349
+ is_active=bool(user_row['is_active']),
350
+ gdpr_consent=bool(user_row['gdpr_consent']),
351
+ data_retention_agreed=bool(user_row['data_retention_agreed']),
352
+ marketing_consent=bool(user_row['marketing_consent'])
353
+ )
354
+
355
+ print(f"πŸ” User logged in: {user.username} ({user.email})")
356
+ return True, "Login successful", user
357
+
358
+ except Exception as e:
359
+ print(f"❌ Authentication error: {str(e)}")
360
+ return False, f"Login failed: {str(e)}", None
361
+
362
+ def get_user_by_id(self, user_id: str) -> Optional[User]:
363
+ """Get user by ID"""
364
+ try:
365
+ with self.get_connection() as conn:
366
+ user_row = conn.execute(
367
+ "SELECT * FROM users WHERE user_id = ? AND is_active = 1",
368
+ (user_id,)
369
+ ).fetchone()
370
+
371
+ if user_row:
372
+ return User(
373
+ user_id=user_row['user_id'],
374
+ email=user_row['email'],
375
+ username=user_row['username'],
376
+ password_hash=user_row['password_hash'],
377
+ created_at=user_row['created_at'],
378
+ last_login=user_row['last_login'],
379
+ is_active=bool(user_row['is_active']),
380
+ gdpr_consent=bool(user_row['gdpr_consent']),
381
+ data_retention_agreed=bool(user_row['data_retention_agreed']),
382
+ marketing_consent=bool(user_row['marketing_consent'])
383
+ )
384
+ except Exception as e:
385
+ print(f"❌ Error getting user: {str(e)}")
386
+ return None
387
+
388
+ def update_user_consent(self, user_id: str, marketing_consent: bool) -> bool:
389
+ """Update user marketing consent"""
390
+ try:
391
+ with self.get_connection() as conn:
392
+ conn.execute(
393
+ "UPDATE users SET marketing_consent = ? WHERE user_id = ?",
394
+ (marketing_consent, user_id)
395
+ )
396
+ conn.commit()
397
+ return True
398
+ except Exception as e:
399
+ print(f"❌ Error updating consent: {str(e)}")
400
+ return False
401
+
402
+ def delete_user_account(self, user_id: str) -> bool:
403
+ """Delete user account and all associated data (GDPR compliance)"""
404
+ try:
405
+ with self.get_connection() as conn:
406
+ # Delete all transcriptions
407
+ conn.execute("DELETE FROM transcriptions WHERE user_id = ?", (user_id,))
408
+ # Deactivate user (for audit trail) rather than delete
409
+ conn.execute(
410
+ "UPDATE users SET is_active = 0, email = ?, username = ? WHERE user_id = ?",
411
+ (f"deleted_{user_id}@deleted.com", f"deleted_{user_id}", user_id)
412
+ )
413
+ conn.commit()
414
+ print(f"πŸ—‘οΈ User account deleted: {user_id}")
415
+ return True
416
+ except Exception as e:
417
+ print(f"❌ Error deleting user account: {str(e)}")
418
+ return False
419
+
420
+ # Transcription methods (updated for authenticated users)
421
+ def save_job(self, job: TranscriptionJob):
422
+ with self.get_connection() as conn:
423
+ conn.execute("""
424
+ INSERT OR REPLACE INTO transcriptions
425
+ (job_id, user_id, original_filename, audio_url, language, status,
426
+ created_at, completed_at, transcript_text, transcript_url, error_message,
427
+ azure_trans_id, settings)
428
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
429
+ """, (
430
+ job.job_id, job.user_id, job.original_filename, job.audio_url,
431
+ job.language, job.status, job.created_at, job.completed_at,
432
+ job.transcript_text, job.transcript_url, job.error_message,
433
+ job.azure_trans_id, json.dumps(job.settings) if job.settings else None
434
+ ))
435
+ conn.commit()
436
+
437
+ def get_job(self, job_id: str) -> Optional[TranscriptionJob]:
438
+ with self.get_connection() as conn:
439
+ row = conn.execute(
440
+ "SELECT * FROM transcriptions WHERE job_id = ?", (job_id,)
441
+ ).fetchone()
442
+ if row:
443
+ return self._row_to_job(row)
444
+ return None
445
+
446
+ def get_user_jobs(self, user_id: str, limit: int = 50) -> List[TranscriptionJob]:
447
+ """Get all jobs for a specific user - PDPA compliant"""
448
+ with self.get_connection() as conn:
449
+ rows = conn.execute("""
450
+ SELECT * FROM transcriptions
451
+ WHERE user_id = ?
452
+ ORDER BY created_at DESC
453
+ LIMIT ?
454
+ """, (user_id, limit)).fetchall()
455
+ return [self._row_to_job(row) for row in rows]
456
+
457
+ def get_user_jobs_paginated(self, user_id: str, offset: int = 0, limit: int = 20) -> List[TranscriptionJob]:
458
+ """Get paginated jobs for a specific user"""
459
+ with self.get_connection() as conn:
460
+ rows = conn.execute("""
461
+ SELECT * FROM transcriptions
462
+ WHERE user_id = ?
463
+ ORDER BY created_at DESC
464
+ LIMIT ? OFFSET ?
465
+ """, (user_id, limit, offset)).fetchall()
466
+ return [self._row_to_job(row) for row in rows]
467
+
468
+ def get_user_job_count(self, user_id: str) -> int:
469
+ """Get total job count for a user"""
470
+ with self.get_connection() as conn:
471
+ result = conn.execute("""
472
+ SELECT COUNT(*) FROM transcriptions
473
+ WHERE user_id = ?
474
+ """, (user_id,)).fetchone()
475
+ return result[0] if result else 0
476
+
477
+ def get_pending_jobs(self) -> List[TranscriptionJob]:
478
+ """Get pending jobs across all users for background processing"""
479
+ with self.get_connection() as conn:
480
+ rows = conn.execute(
481
+ "SELECT * FROM transcriptions WHERE status IN ('pending', 'processing')"
482
+ ).fetchall()
483
+ return [self._row_to_job(row) for row in rows]
484
+
485
+ def get_user_stats(self, user_id: str) -> Dict:
486
+ """Get statistics for a specific user"""
487
+ with self.get_connection() as conn:
488
+ stats = {}
489
+
490
+ # Total jobs
491
+ result = conn.execute("""
492
+ SELECT COUNT(*) FROM transcriptions WHERE user_id = ?
493
+ """, (user_id,)).fetchone()
494
+ stats['total_jobs'] = result[0] if result else 0
495
+
496
+ # Jobs by status
497
+ result = conn.execute("""
498
+ SELECT status, COUNT(*) FROM transcriptions
499
+ WHERE user_id = ?
500
+ GROUP BY status
501
+ """, (user_id,)).fetchall()
502
+ stats['by_status'] = {row[0]: row[1] for row in result}
503
+
504
+ # Recent activity (last 7 days)
505
+ week_ago = (datetime.now() - timedelta(days=7)).isoformat()
506
+ result = conn.execute("""
507
+ SELECT COUNT(*) FROM transcriptions
508
+ WHERE user_id = ? AND created_at >= ?
509
+ """, (user_id, week_ago)).fetchone()
510
+ stats['recent_jobs'] = result[0] if result else 0
511
+
512
+ return stats
513
+
514
+ def export_user_data(self, user_id: str) -> Dict:
515
+ """Export all user data for GDPR compliance"""
516
+ try:
517
+ with self.get_connection() as conn:
518
+ # Get user info
519
+ user_row = conn.execute(
520
+ "SELECT * FROM users WHERE user_id = ?", (user_id,)
521
+ ).fetchone()
522
+
523
+ # Get all transcriptions
524
+ transcription_rows = conn.execute(
525
+ "SELECT * FROM transcriptions WHERE user_id = ?", (user_id,)
526
+ ).fetchall()
527
+
528
+ export_data = {
529
+ "export_date": datetime.now().isoformat(),
530
+ "user_info": dict(user_row) if user_row else {},
531
+ "transcriptions": [dict(row) for row in transcription_rows],
532
+ "statistics": self.get_user_stats(user_id)
533
+ }
534
+
535
+ return export_data
536
+
537
+ except Exception as e:
538
+ print(f"❌ Error exporting user data: {str(e)}")
539
+ return {}
540
+
541
+ def _row_to_job(self, row) -> TranscriptionJob:
542
+ settings = json.loads(row['settings']) if row['settings'] else None
543
+ return TranscriptionJob(
544
+ job_id=row['job_id'],
545
+ user_id=row['user_id'],
546
+ original_filename=row['original_filename'],
547
+ audio_url=row['audio_url'],
548
+ language=row['language'],
549
+ status=row['status'],
550
+ created_at=row['created_at'],
551
+ completed_at=row['completed_at'],
552
+ transcript_text=row['transcript_text'],
553
+ transcript_url=row['transcript_url'],
554
+ error_message=row['error_message'],
555
+ azure_trans_id=row['azure_trans_id'],
556
+ settings=settings
557
+ )
558
+
559
+ class TranscriptionManager:
560
+ def __init__(self):
561
+ self.db = DatabaseManager()
562
+ self.executor = ThreadPoolExecutor(max_workers=5)
563
+ self.blob_service = BlobServiceClient.from_connection_string(AZURE_BLOB_CONNECTION)
564
+ self._job_status_cache = {} # Cache to track status changes
565
+
566
+ # Start background worker
567
+ self.running = True
568
+ self.worker_thread = threading.Thread(target=self._background_worker, daemon=True)
569
+ self.worker_thread.start()
570
+
571
+ def _get_user_blob_path(self, user_id: str, blob_type: str, filename: str) -> str:
572
+ """Generate user-specific blob path for PDPA compliance"""
573
+ # Create user-specific folder structure: users/{user_id}/{type}/{filename}
574
+ return f"users/{user_id}/{blob_type}/{filename}"
575
+
576
+ def _log_status_change(self, job_id: str, old_status: str, new_status: str, filename: str = "", user_id: str = ""):
577
+ """Only log when status actually changes"""
578
+ cache_key = f"{job_id}_{old_status}_{new_status}"
579
+ if cache_key not in self._job_status_cache:
580
+ self._job_status_cache[cache_key] = True
581
+ user_display = f"[{user_id[:8]}...]" if user_id else ""
582
+ if filename:
583
+ print(f"πŸ”„ {user_display} Job {job_id[:8]}... ({filename}): {old_status} β†’ {new_status}")
584
+ else:
585
+ print(f"πŸ”„ {user_display} Job {job_id[:8]}...: {old_status} β†’ {new_status}")
586
+
587
+ def _background_worker(self):
588
+ """Background worker to process pending transcriptions - minimal logging"""
589
+ iteration_count = 0
590
+ while self.running:
591
+ try:
592
+ pending_jobs = self.db.get_pending_jobs()
593
+
594
+ # Only log if there are jobs to process
595
+ if pending_jobs and iteration_count % 6 == 0: # Log every minute (6 * 10 seconds)
596
+ active_jobs = len([j for j in pending_jobs if j.status == 'processing'])
597
+ queued_jobs = len([j for j in pending_jobs if j.status == 'pending'])
598
+ if active_jobs > 0 or queued_jobs > 0:
599
+ print(f"πŸ“Š Background worker: {active_jobs} processing, {queued_jobs} queued")
600
+
601
+ for job in pending_jobs:
602
+ if job.status == 'pending':
603
+ self.executor.submit(self._process_transcription_job, job.job_id)
604
+ elif job.status == 'processing' and job.azure_trans_id:
605
+ self.executor.submit(self._check_transcription_status, job.job_id)
606
+
607
+ time.sleep(10) # Check every 10 seconds
608
+ iteration_count += 1
609
+
610
+ except Exception as e:
611
+ print(f"❌ Background worker error: {e}")
612
+ time.sleep(30)
613
+
614
+ def submit_transcription(
615
+ self,
616
+ file_bytes: bytes,
617
+ original_filename: str,
618
+ user_id: str, # Now requires authenticated user_id
619
+ language: str,
620
+ settings: Dict
621
+ ) -> str:
622
+ """Submit a new transcription job for authenticated user"""
623
+ job_id = str(uuid.uuid4())
624
+
625
+ print(f"πŸš€ [{user_id[:8]}...] New transcription: {original_filename} ({len(file_bytes):,} bytes)")
626
+
627
+ # Create job record
628
+ job = TranscriptionJob(
629
+ job_id=job_id,
630
+ user_id=user_id,
631
+ original_filename=original_filename,
632
+ audio_url="", # Will be set after upload
633
+ language=language,
634
+ status="pending",
635
+ created_at=datetime.now().isoformat(),
636
+ settings=settings
637
+ )
638
+
639
+ # Save job to database
640
+ self.db.save_job(job)
641
+
642
+ # Submit file processing to thread pool
643
+ self.executor.submit(self._prepare_audio_file, job_id, file_bytes, original_filename, settings)
644
+
645
+ return job_id
646
+
647
+ def _prepare_audio_file(self, job_id: str, file_bytes: bytes, original_filename: str, settings: Dict):
648
+ """Prepare audio file and upload to user-specific blob storage path"""
649
+ try:
650
+ job = self.db.get_job(job_id)
651
+ if not job:
652
+ return
653
+
654
+ user_id = job.user_id
655
+
656
+ # Save original file
657
+ src_ext = original_filename.split('.')[-1].lower() if '.' in original_filename else "bin"
658
+ upload_path = os.path.join(UPLOAD_DIR, f"{job_id}_original.{src_ext}")
659
+
660
+ with open(upload_path, "wb") as f:
661
+ f.write(file_bytes)
662
+
663
+ # Determine if conversion is needed
664
+ audio_format = settings.get('audio_format', 'wav')
665
+
666
+ # Check if file is already in target format and specs
667
+ if src_ext == audio_format and audio_format == 'wav':
668
+ # Check if it's already 16kHz mono (Azure Speech preferred format)
669
+ try:
670
+ probe_cmd = [
671
+ 'ffprobe', '-v', 'quiet', '-print_format', 'json',
672
+ '-show_streams', upload_path
673
+ ]
674
+ result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=30)
675
+
676
+ if result.returncode == 0:
677
+ import json
678
+ probe_data = json.loads(result.stdout)
679
+ audio_stream = probe_data.get('streams', [{}])[0]
680
+
681
+ sample_rate = int(audio_stream.get('sample_rate', 0))
682
+ channels = int(audio_stream.get('channels', 0))
683
+
684
+ # If already optimal format, use as-is
685
+ if sample_rate == 16000 and channels == 1:
686
+ out_path = upload_path # Use original file
687
+ else:
688
+ print(f"πŸ”„ [{user_id[:8]}...] Converting {original_filename} to 16kHz mono")
689
+ out_path = os.path.join(UPLOAD_DIR, f"{job_id}_converted.{audio_format}")
690
+ self._convert_to_audio(upload_path, out_path, audio_format)
691
+ else:
692
+ out_path = os.path.join(UPLOAD_DIR, f"{job_id}_converted.{audio_format}")
693
+ self._convert_to_audio(upload_path, out_path, audio_format)
694
+
695
+ except Exception as e:
696
+ print(f"⚠️ [{user_id[:8]}...] Audio probing failed for {original_filename}: {e}")
697
+ out_path = os.path.join(UPLOAD_DIR, f"{job_id}_converted.{audio_format}")
698
+ self._convert_to_audio(upload_path, out_path, audio_format)
699
+ else:
700
+ # Different format, need conversion
701
+ print(f"πŸ”„ [{user_id[:8]}...] Converting {original_filename}: {src_ext} β†’ {audio_format}")
702
+ out_path = os.path.join(UPLOAD_DIR, f"{job_id}_converted.{audio_format}")
703
+
704
+ try:
705
+ self._convert_to_audio(upload_path, out_path, audio_format)
706
+ except Exception as e:
707
+ print(f"❌ [{user_id[:8]}...] Audio conversion failed for {original_filename}: {str(e)}")
708
+ job.status = "failed"
709
+ job.error_message = f"Audio conversion failed: {str(e)}"
710
+ job.completed_at = datetime.now().isoformat()
711
+ self.db.save_job(job)
712
+
713
+ # Clean up files
714
+ try:
715
+ os.remove(upload_path)
716
+ except:
717
+ pass
718
+ return
719
+
720
+ # Upload to user-specific blob storage paths
721
+ try:
722
+ # Upload the processed audio file to user's audio folder
723
+ audio_blob_name = self._get_user_blob_path(user_id, "audio", f"{job_id}.{audio_format}")
724
+ audio_url = self._upload_blob(out_path, audio_blob_name)
725
+
726
+ # Upload original file to user's originals folder
727
+ orig_blob_name = self._get_user_blob_path(user_id, "originals", f"{job_id}_{original_filename}")
728
+ self._upload_blob(upload_path if out_path == upload_path else upload_path, orig_blob_name)
729
+
730
+ print(f"☁️ [{user_id[:8]}...] {original_filename} uploaded to user-specific blob storage")
731
+
732
+ # Update job with audio URL
733
+ job.audio_url = audio_url
734
+ job.status = "pending"
735
+ self.db.save_job(job)
736
+
737
+ except Exception as e:
738
+ print(f"❌ [{user_id[:8]}...] Blob upload failed for {original_filename}: {str(e)}")
739
+ job.status = "failed"
740
+ job.error_message = f"Blob storage upload failed: {str(e)}"
741
+ job.completed_at = datetime.now().isoformat()
742
+ self.db.save_job(job)
743
+
744
+ # Clean up local files
745
+ try:
746
+ if os.path.exists(upload_path):
747
+ os.remove(upload_path)
748
+ if out_path != upload_path and os.path.exists(out_path):
749
+ os.remove(out_path)
750
+ except Exception as e:
751
+ print(f"⚠️ [{user_id[:8]}...] Warning: Could not clean up local files for {original_filename}: {e}")
752
+
753
+ except Exception as e:
754
+ print(f"❌ File preparation error for {original_filename}: {e}")
755
+ job = self.db.get_job(job_id)
756
+ if job:
757
+ job.status = "failed"
758
+ job.error_message = f"File preparation failed: {str(e)}"
759
+ job.completed_at = datetime.now().isoformat()
760
+ self.db.save_job(job)
761
+
762
+ def _process_transcription_job(self, job_id: str):
763
+ """Process a transcription job - minimal logging"""
764
+ try:
765
+ job = self.db.get_job(job_id)
766
+ if not job or job.status != 'pending' or not job.audio_url:
767
+ return
768
+
769
+ old_status = job.status
770
+ # Update status to processing
771
+ job.status = "processing"
772
+ self.db.save_job(job)
773
+
774
+ self._log_status_change(job_id, old_status, job.status, job.original_filename, job.user_id)
775
+
776
+ # Create Azure transcription
777
+ settings = job.settings or {}
778
+ azure_trans_id = self._create_transcription(
779
+ job.audio_url,
780
+ job.language,
781
+ settings.get('diarization_enabled', False),
782
+ settings.get('speakers', 2),
783
+ settings.get('profanity', 'masked'),
784
+ settings.get('punctuation', 'automatic'),
785
+ settings.get('timestamps', True),
786
+ settings.get('lexical', False),
787
+ settings.get('language_id_enabled', False),
788
+ settings.get('candidate_locales', None)
789
+ )
790
+
791
+ # Update job with Azure transcription ID
792
+ job.azure_trans_id = azure_trans_id
793
+ self.db.save_job(job)
794
+
795
+ except Exception as e:
796
+ print(f"❌ Transcription submission failed for job {job_id[:8]}...: {str(e)}")
797
+ job = self.db.get_job(job_id)
798
+ if job:
799
+ old_status = job.status
800
+ job.status = "failed"
801
+ job.error_message = f"Transcription submission failed: {str(e)}"
802
+ job.completed_at = datetime.now().isoformat()
803
+ self.db.save_job(job)
804
+ self._log_status_change(job_id, old_status, job.status, job.original_filename, job.user_id)
805
+
806
+ def _check_transcription_status(self, job_id: str):
807
+ """Check status of Azure transcription - minimal logging"""
808
+ try:
809
+ job = self.db.get_job(job_id)
810
+ if not job or job.status != 'processing' or not job.azure_trans_id:
811
+ return
812
+
813
+ # Check Azure transcription status
814
+ url = f"{AZURE_SPEECH_KEY_ENDPOINT}/speechtotext/{API_VERSION}/transcriptions/{job.azure_trans_id}"
815
+ headers = {"Ocp-Apim-Subscription-Key": AZURE_SPEECH_KEY}
816
+
817
+ r = requests.get(url, headers=headers)
818
+ data = r.json()
819
+
820
+ if data.get("status") == "Succeeded":
821
+ # Get transcription result
822
+ content_url = self._get_transcription_result_url(job.azure_trans_id)
823
+ if content_url:
824
+ transcript = self._fetch_transcript(content_url)
825
+
826
+ # Save transcript to user-specific blob storage
827
+ transcript_blob_name = self._get_user_blob_path(job.user_id, "transcripts", f"{job_id}.txt")
828
+ transcript_path = os.path.join(UPLOAD_DIR, f"{job_id}_transcript.txt")
829
+
830
+ with open(transcript_path, "w", encoding="utf-8") as f:
831
+ f.write(transcript)
832
+
833
+ transcript_url = self._upload_blob(transcript_path, transcript_blob_name)
834
+
835
+ # Update job
836
+ old_status = job.status
837
+ job.status = "completed"
838
+ job.transcript_text = transcript
839
+ job.transcript_url = transcript_url
840
+ job.completed_at = datetime.now().isoformat()
841
+ self.db.save_job(job)
842
+
843
+ self._log_status_change(job_id, old_status, job.status, job.original_filename, job.user_id)
844
+ print(f"βœ… [{job.user_id[:8]}...] Transcription completed: {job.original_filename}")
845
+
846
+ # Clean up
847
+ try:
848
+ os.remove(transcript_path)
849
+ except:
850
+ pass
851
+
852
+ elif data.get("status") in ("Failed", "FailedWithPartialResults"):
853
+ error_message = ""
854
+ if "properties" in data and "error" in data["properties"]:
855
+ error_message = data["properties"]["error"].get("message", "")
856
+ elif "error" in data:
857
+ error_message = data["error"].get("message", "")
858
+
859
+ old_status = job.status
860
+ job.status = "failed"
861
+ job.error_message = f"Azure transcription failed: {error_message}"
862
+ job.completed_at = datetime.now().isoformat()
863
+ self.db.save_job(job)
864
+
865
+ self._log_status_change(job_id, old_status, job.status, job.original_filename, job.user_id)
866
+ print(f"❌ [{job.user_id[:8]}...] Transcription failed: {job.original_filename} - {error_message}")
867
+
868
+ except Exception as e:
869
+ print(f"❌ Status check failed for job {job_id[:8]}...: {str(e)}")
870
+ job = self.db.get_job(job_id)
871
+ if job:
872
+ old_status = job.status
873
+ job.status = "failed"
874
+ job.error_message = f"Status check failed: {str(e)}"
875
+ job.completed_at = datetime.now().isoformat()
876
+ self.db.save_job(job)
877
+ self._log_status_change(job_id, old_status, job.status, job.original_filename, job.user_id)
878
+
879
+ def get_job_status(self, job_id: str) -> Optional[TranscriptionJob]:
880
+ """Get current job status"""
881
+ return self.db.get_job(job_id)
882
+
883
+ def get_user_history(self, user_id: str, limit: int = 50) -> List[TranscriptionJob]:
884
+ """Get user's transcription history - PDPA compliant"""
885
+ return self.db.get_user_jobs(user_id, limit)
886
+
887
+ def get_user_history_paginated(self, user_id: str, offset: int = 0, limit: int = 20) -> List[TranscriptionJob]:
888
+ """Get paginated user history for large datasets"""
889
+ return self.db.get_user_jobs_paginated(user_id, offset, limit)
890
+
891
+ def get_user_stats(self, user_id: str) -> Dict:
892
+ """Get user statistics"""
893
+ return self.db.get_user_stats(user_id)
894
+
895
+ def download_transcript(self, job_id: str, user_id: str) -> Optional[str]:
896
+ """Download transcript content - with user verification for PDPA compliance"""
897
+ job = self.db.get_job(job_id)
898
+ if job and job.user_id == user_id and job.transcript_text:
899
+ return job.transcript_text
900
+ return None
901
+
902
+ # Authentication methods
903
+ def register_user(self, email: str, username: str, password: str, gdpr_consent: bool = True,
904
+ data_retention_agreed: bool = True, marketing_consent: bool = False) -> Tuple[bool, str, Optional[str]]:
905
+ """Register new user"""
906
+ return self.db.create_user(email, username, password, gdpr_consent, data_retention_agreed, marketing_consent)
907
+
908
+ def login_user(self, login: str, password: str) -> Tuple[bool, str, Optional[User]]:
909
+ """Login user"""
910
+ return self.db.authenticate_user(login, password)
911
+
912
+ def get_user(self, user_id: str) -> Optional[User]:
913
+ """Get user by ID"""
914
+ return self.db.get_user_by_id(user_id)
915
+
916
+ def update_user_consent(self, user_id: str, marketing_consent: bool) -> bool:
917
+ """Update user marketing consent"""
918
+ return self.db.update_user_consent(user_id, marketing_consent)
919
+
920
+ def export_user_data(self, user_id: str) -> Dict:
921
+ """Export all user data for GDPR compliance"""
922
+ return self.db.export_user_data(user_id)
923
+
924
+ def delete_user_account(self, user_id: str) -> bool:
925
+ """Delete user account and all data"""
926
+ return self.db.delete_user_account(user_id)
927
+
928
+ # Helper methods remain the same
929
+ def _convert_to_audio(self, input_path, output_path, audio_format="wav"):
930
+ """Convert audio/video file to specified audio format - minimal logging"""
931
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
932
+
933
+ if audio_format in {"wav", "alaw", "mulaw"}:
934
+ cmd = ["ffmpeg", "-y", "-i", input_path, "-ar", "16000", "-ac", "1", output_path]
935
+ else:
936
+ cmd = ["ffmpeg", "-y", "-i", input_path, output_path]
937
+
938
+ try:
939
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300, text=True)
940
+
941
+ if result.returncode != 0:
942
+ raise Exception(f"FFmpeg conversion failed: {result.stderr}")
943
+
944
+ if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
945
+ raise Exception(f"Output file was not created or is empty: {output_path}")
946
+
947
+ except subprocess.TimeoutExpired:
948
+ raise Exception(f"FFmpeg conversion timed out after 5 minutes")
949
+ except Exception as e:
950
+ if "FFmpeg conversion failed" in str(e):
951
+ raise
952
+ else:
953
+ raise Exception(f"FFmpeg error: {str(e)}")
954
+
955
+ def _upload_blob(self, local_file, blob_name):
956
+ blob_client = self.blob_service.get_blob_client(container=AZURE_CONTAINER, blob=blob_name)
957
+ with open(local_file, "rb") as data:
958
+ blob_client.upload_blob(data, overwrite=True)
959
+ sas = AZURE_BLOB_SAS_TOKEN.lstrip("?")
960
+ return f"{blob_client.url}?{sas}"
961
+
962
+ def _create_transcription(self, audio_url, language, diarization_enabled, speakers,
963
+ profanity, punctuation, timestamps, lexical,
964
+ language_id_enabled=False, candidate_locales=None):
965
+ url = f"{AZURE_SPEECH_KEY_ENDPOINT}/speechtotext/{API_VERSION}/transcriptions"
966
+ headers = {
967
+ "Ocp-Apim-Subscription-Key": AZURE_SPEECH_KEY,
968
+ "Content-Type": "application/json"
969
+ }
970
+
971
+ properties = {
972
+ "profanityFilterMode": profanity,
973
+ "punctuationMode": punctuation,
974
+ "wordLevelTimestampsEnabled": timestamps,
975
+ "displayFormWordLevelTimestampsEnabled": timestamps,
976
+ "lexical": lexical
977
+ }
978
+ if diarization_enabled:
979
+ properties["diarizationEnabled"] = True
980
+ properties["diarization"] = {
981
+ "speakers": {
982
+ "minCount": 1,
983
+ "maxCount": int(speakers)
984
+ }
985
+ }
986
+ if language_id_enabled and candidate_locales:
987
+ properties["languageIdentification"] = {
988
+ "mode": "continuous",
989
+ "candidateLocales": candidate_locales
990
+ }
991
+
992
+ properties = {k: v for k, v in properties.items() if v is not None}
993
+ body = {
994
+ "displayName": f"Transcription_{uuid.uuid4()}",
995
+ "description": "Batch speech-to-text with advanced options",
996
+ "locale": language,
997
+ "contentUrls": [audio_url],
998
+ "properties": properties,
999
+ "customProperties": {}
1000
+ }
1001
+ r = requests.post(url, headers=headers, json=body)
1002
+ r.raise_for_status()
1003
+ trans_id = r.headers["Location"].split("/")[-1].split("?")[0]
1004
+ return trans_id
1005
+
1006
+ def _get_transcription_result_url(self, trans_id):
1007
+ url = f"{AZURE_SPEECH_KEY_ENDPOINT}/speechtotext/{API_VERSION}/transcriptions/{trans_id}"
1008
+ headers = {"Ocp-Apim-Subscription-Key": AZURE_SPEECH_KEY}
1009
+
1010
+ r = requests.get(url, headers=headers)
1011
+ data = r.json()
1012
+
1013
+ if data.get("status") == "Succeeded":
1014
+ files_url = None
1015
+ if "links" in data and "files" in data["links"]:
1016
+ files_url = data["links"]["files"]
1017
+ if files_url:
1018
+ r2 = requests.get(files_url, headers=headers)
1019
+ file_list = r2.json().get("values", [])
1020
+ for f in file_list:
1021
+ if f.get("kind", "").lower() == "transcription":
1022
+ return f["links"]["contentUrl"]
1023
+ return None
1024
+
1025
+ def _fetch_transcript(self, content_url):
1026
+ """Enhanced transcript fetching with improved timestamp handling"""
1027
+ r = requests.get(content_url)
1028
+ try:
1029
+ j = r.json()
1030
+ out = []
1031
+
1032
+ def get_text(phrase):
1033
+ if 'nBest' in phrase and phrase['nBest']:
1034
+ return phrase['nBest'][0].get('display', '') or phrase.get('display', '')
1035
+ return phrase.get('display', '')
1036
+
1037
+ def safe_offset(val):
1038
+ try:
1039
+ return int(val)
1040
+ except (ValueError, TypeError):
1041
+ return None
1042
+
1043
+ def format_time(seconds):
1044
+ """Format seconds into HH:MM:SS format"""
1045
+ try:
1046
+ td = timedelta(seconds=int(seconds))
1047
+ hours, remainder = divmod(td.total_seconds(), 3600)
1048
+ minutes, seconds = divmod(remainder, 60)
1049
+ return f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
1050
+ except:
1051
+ return "00:00:00"
1052
+
1053
+ # Check if this is a diarization result or regular transcription
1054
+ if 'recognizedPhrases' in j:
1055
+ for phrase in j['recognizedPhrases']:
1056
+ speaker_id = phrase.get('speaker', 0)
1057
+ text = get_text(phrase)
1058
+
1059
+ if not text.strip():
1060
+ continue
1061
+
1062
+ # Try to get timestamp from multiple possible locations
1063
+ timestamp_seconds = None
1064
+
1065
+ if 'offset' in phrase and phrase['offset'] is not None:
1066
+ offset_100ns = safe_offset(phrase['offset'])
1067
+ if offset_100ns is not None:
1068
+ timestamp_seconds = offset_100ns / 10_000_000
1069
+
1070
+ if timestamp_seconds is None and 'words' in phrase and phrase['words']:
1071
+ first_word = phrase['words'][0]
1072
+ if 'offset' in first_word and first_word['offset'] is not None:
1073
+ offset_100ns = safe_offset(first_word['offset'])
1074
+ if offset_100ns is not None:
1075
+ timestamp_seconds = offset_100ns / 10_000_000
1076
+
1077
+ if timestamp_seconds is None and 'offsetInTicks' in phrase:
1078
+ offset_ticks = safe_offset(phrase['offsetInTicks'])
1079
+ if offset_ticks is not None:
1080
+ timestamp_seconds = offset_ticks / 10_000_000
1081
+
1082
+ # Format output based on whether we have speaker diarization and timestamps
1083
+ if timestamp_seconds is not None:
1084
+ time_str = format_time(timestamp_seconds)
1085
+ if 'speaker' in phrase:
1086
+ out.append(f"[{time_str}] Speaker {speaker_id}: {text}")
1087
+ else:
1088
+ out.append(f"[{time_str}] {text}")
1089
+ else:
1090
+ if 'speaker' in phrase:
1091
+ out.append(f"Speaker {speaker_id}: {text}")
1092
+ else:
1093
+ out.append(text)
1094
+
1095
+ if out:
1096
+ return '\n\n'.join(out)
1097
+
1098
+ # Fallback: handle combined results or other formats
1099
+ if 'combinedRecognizedPhrases' in j:
1100
+ combined_results = []
1101
+ for combined_phrase in j['combinedRecognizedPhrases']:
1102
+ text = combined_phrase.get('display', '')
1103
+ if text.strip():
1104
+ combined_results.append(text)
1105
+
1106
+ if combined_results:
1107
+ return '\n\n'.join(combined_results)
1108
+
1109
+ # Last resort: return raw JSON for debugging
1110
+ return json.dumps(j, ensure_ascii=False, indent=2)
1111
+
1112
+ except Exception as e:
1113
+ return f"Unable to parse transcription result: {str(e)}\n\nRaw response: {r.text[:1000]}..."
1114
+
1115
+ # Global transcription manager instance
1116
+ transcription_manager = TranscriptionManager()
1117
+
1118
+ # Backward compatibility functions
1119
+ def allowed_file(filename):
1120
+ """Check if file extension is supported"""
1121
+ if not filename or filename in ["upload.unknown", ""]:
1122
+ return True # Let FFmpeg handle unknown formats
1123
+
1124
+ if '.' not in filename:
1125
+ return True # No extension, let FFmpeg try
1126
+
1127
+ ext = filename.rsplit('.', 1)[1].lower()
1128
+ supported_extensions = set(AUDIO_FORMATS) | {
1129
+ 'mp4', 'mov', 'avi', 'mkv', 'webm', 'm4a', '3gp', 'f4v',
1130
+ 'wmv', 'asf', 'rm', 'rmvb', 'flv', 'mpg', 'mpeg', 'mts', 'vob'
1131
+ }
1132
+
1133
+ return ext in supported_extensions
gradio_app.py ADDED
@@ -0,0 +1,1471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import time
3
+ import json
4
+ import os
5
+ import subprocess
6
+ from datetime import datetime, timedelta
7
+ from typing import List, Tuple, Optional
8
+ from app_core import (
9
+ ALLOWED_LANGS, AUDIO_FORMATS, transcription_manager,
10
+ allowed_file, User
11
+ )
12
+
13
+ def format_status(status):
14
+ """Convert status to user-friendly format"""
15
+ status_map = {
16
+ 'pending': '⏳ Queued',
17
+ 'processing': 'πŸ”„ Processing',
18
+ 'completed': 'βœ… Done',
19
+ 'failed': '❌ Failed'
20
+ }
21
+ return status_map.get(status, status)
22
+
23
+ def format_processing_time(created_at, completed_at=None):
24
+ """Calculate and format processing time"""
25
+ try:
26
+ start_time = datetime.fromisoformat(created_at)
27
+ if completed_at:
28
+ end_time = datetime.fromisoformat(completed_at)
29
+ duration = end_time - start_time
30
+ else:
31
+ duration = datetime.now() - start_time
32
+
33
+ total_seconds = int(duration.total_seconds())
34
+ if total_seconds < 60:
35
+ return f"{total_seconds}s"
36
+ elif total_seconds < 3600:
37
+ minutes = total_seconds // 60
38
+ seconds = total_seconds % 60
39
+ return f"{minutes}m {seconds}s"
40
+ else:
41
+ hours = total_seconds // 3600
42
+ minutes = (total_seconds % 3600) // 60
43
+ return f"{hours}h {minutes}m"
44
+ except:
45
+ return "Unknown"
46
+
47
+ def get_user_stats_display(user: User):
48
+ """Get user statistics for display"""
49
+ if not user:
50
+ return "πŸ‘€ Please log in to view statistics"
51
+
52
+ try:
53
+ stats = transcription_manager.get_user_stats(user.user_id)
54
+
55
+ total = stats.get('total_jobs', 0)
56
+ recent = stats.get('recent_jobs', 0)
57
+ by_status = stats.get('by_status', {})
58
+
59
+ completed = by_status.get('completed', 0)
60
+ processing = by_status.get('processing', 0)
61
+ pending = by_status.get('pending', 0)
62
+ failed = by_status.get('failed', 0)
63
+
64
+ stats_text = f"πŸ‘€ {user.username} | πŸ“Š Total: {total} | βœ… Completed: {completed}"
65
+ if processing > 0:
66
+ stats_text += f" | πŸ”„ Processing: {processing}"
67
+ if pending > 0:
68
+ stats_text += f" | ⏳ Pending: {pending}"
69
+ if failed > 0:
70
+ stats_text += f" | ❌ Failed: {failed}"
71
+ if recent > 0:
72
+ stats_text += f" | πŸ“… Last 7 days: {recent}"
73
+
74
+ return stats_text
75
+
76
+ except Exception as e:
77
+ return f"πŸ‘€ {user.username} | Stats error: {str(e)}"
78
+
79
+ # Authentication Functions
80
+ def register_user(email, username, password, confirm_password, gdpr_consent, data_retention_consent, marketing_consent):
81
+ """Register new user account"""
82
+ try:
83
+ print(f"πŸ“ Registration attempt for: {username} ({email})")
84
+
85
+ # Validate inputs
86
+ if not email or not username or not password:
87
+ return "❌ All fields are required", gr.update(visible=False)
88
+
89
+ if password != confirm_password:
90
+ return "❌ Passwords do not match", gr.update(visible=False)
91
+
92
+ if not gdpr_consent:
93
+ return "❌ GDPR consent is required to create an account", gr.update(visible=False)
94
+
95
+ if not data_retention_consent:
96
+ return "❌ Data retention agreement is required", gr.update(visible=False)
97
+
98
+ # Attempt registration
99
+ success, message, user_id = transcription_manager.register_user(
100
+ email, username, password, gdpr_consent, data_retention_consent, marketing_consent
101
+ )
102
+
103
+ print(f"πŸ“ Registration result: success={success}, message={message}")
104
+
105
+ if success:
106
+ print(f"βœ… User registered successfully: {username}")
107
+ return f"βœ… {message}! Please log in with your credentials.", gr.update(visible=True)
108
+ else:
109
+ print(f"❌ Registration failed: {message}")
110
+ return f"❌ {message}", gr.update(visible=False)
111
+
112
+ except Exception as e:
113
+ print(f"❌ Registration error: {str(e)}")
114
+ return f"❌ Registration error: {str(e)}", gr.update(visible=False)
115
+
116
+ def login_user(login, password):
117
+ """Login user"""
118
+ try:
119
+ print(f"πŸ” Login attempt for: {login}")
120
+
121
+ if not login or not password:
122
+ return "❌ Please enter both username/email and password", None, gr.update(visible=True), gr.update(visible=False), "πŸ‘€ Please log in to view your statistics..."
123
+
124
+ success, message, user = transcription_manager.login_user(login, password)
125
+ print(f"πŸ” Login result: success={success}, message={message}")
126
+
127
+ if success and user:
128
+ print(f"βœ… User logged in successfully: {user.username}")
129
+ stats_display = get_user_stats_display(user)
130
+ return f"βœ… Welcome back, {user.username}!", user, gr.update(visible=False), gr.update(visible=True), stats_display
131
+ else:
132
+ print(f"❌ Login failed: {message}")
133
+ return f"❌ {message}", None, gr.update(visible=True), gr.update(visible=False), "πŸ‘€ Please log in to view your statistics..."
134
+
135
+ except Exception as e:
136
+ print(f"❌ Login error: {str(e)}")
137
+ return f"❌ Login error: {str(e)}", None, gr.update(visible=True), gr.update(visible=False), "πŸ‘€ Please log in to view your statistics..."
138
+
139
+ def logout_user():
140
+ """Logout user"""
141
+ print("πŸ‘‹ User logged out")
142
+ return None, "πŸ‘‹ You have been logged out. Please log in to continue.", gr.update(visible=True), gr.update(visible=False), "πŸ‘€ Please log in to view your statistics..."
143
+
144
+ # Transcription Functions (require authentication)
145
+ def submit_transcription(file, language, audio_format, diarization_enabled, speakers,
146
+ profanity, punctuation, timestamps, lexical, user):
147
+ """Submit transcription job - requires authenticated user"""
148
+ if not user:
149
+ return (
150
+ "❌ Please log in to submit transcriptions",
151
+ "",
152
+ gr.update(visible=False),
153
+ "",
154
+ {},
155
+ gr.update(visible=False),
156
+ gr.update()
157
+ )
158
+
159
+ if file is None:
160
+ return (
161
+ "Please upload an audio or video file first.",
162
+ "",
163
+ gr.update(visible=False),
164
+ "",
165
+ {},
166
+ gr.update(visible=False),
167
+ gr.update()
168
+ )
169
+
170
+ try:
171
+ # Get file data
172
+ try:
173
+ if isinstance(file, str):
174
+ if os.path.exists(file):
175
+ with open(file, 'rb') as f:
176
+ file_bytes = f.read()
177
+ original_filename = os.path.basename(file)
178
+ else:
179
+ return (
180
+ "File not found. Please try uploading again.",
181
+ "",
182
+ gr.update(visible=False),
183
+ "",
184
+ {},
185
+ gr.update(visible=False),
186
+ gr.update()
187
+ )
188
+ else:
189
+ file_path = str(file)
190
+ if os.path.exists(file_path):
191
+ with open(file_path, 'rb') as f:
192
+ file_bytes = f.read()
193
+ original_filename = os.path.basename(file_path)
194
+ else:
195
+ return (
196
+ "Unable to process file. Please try again.",
197
+ "",
198
+ gr.update(visible=False),
199
+ "",
200
+ {},
201
+ gr.update(visible=False),
202
+ gr.update()
203
+ )
204
+ except Exception as e:
205
+ return (
206
+ f"Error reading file: {str(e)}",
207
+ "",
208
+ gr.update(visible=False),
209
+ "",
210
+ {},
211
+ gr.update(visible=False),
212
+ gr.update()
213
+ )
214
+
215
+ # Validate file
216
+ file_extension = original_filename.split('.')[-1].lower() if '.' in original_filename else ""
217
+ supported_extensions = set(AUDIO_FORMATS) | {
218
+ 'mp4', 'mov', 'avi', 'mkv', 'webm', 'm4a', '3gp', 'f4v',
219
+ 'wmv', 'asf', 'rm', 'rmvb', 'flv', 'mpg', 'mpeg', 'mts', 'vob'
220
+ }
221
+
222
+ if file_extension not in supported_extensions and file_extension != "":
223
+ return (
224
+ f"Unsupported file format: .{file_extension}",
225
+ "",
226
+ gr.update(visible=False),
227
+ "",
228
+ {},
229
+ gr.update(visible=False),
230
+ gr.update()
231
+ )
232
+
233
+ # Basic file size check
234
+ if len(file_bytes) > 500 * 1024 * 1024: # 500MB limit
235
+ return (
236
+ "File too large. Please upload files smaller than 500MB.",
237
+ "",
238
+ gr.update(visible=False),
239
+ "",
240
+ {},
241
+ gr.update(visible=False),
242
+ gr.update()
243
+ )
244
+
245
+ # Prepare settings
246
+ settings = {
247
+ 'audio_format': audio_format,
248
+ 'diarization_enabled': diarization_enabled,
249
+ 'speakers': speakers,
250
+ 'profanity': profanity,
251
+ 'punctuation': punctuation,
252
+ 'timestamps': timestamps,
253
+ 'lexical': lexical
254
+ }
255
+
256
+ # Submit job (logging happens in app_core)
257
+ job_id = transcription_manager.submit_transcription(
258
+ file_bytes, original_filename, user.user_id, language, settings
259
+ )
260
+
261
+ # Update job state
262
+ job_state = {
263
+ 'current_job_id': job_id,
264
+ 'start_time': datetime.now().isoformat(),
265
+ 'auto_refresh_active': True,
266
+ 'last_status': 'pending'
267
+ }
268
+
269
+ # Get updated user stats
270
+ stats_display = get_user_stats_display(user)
271
+
272
+ return (
273
+ f"πŸš€ Transcription started for: {original_filename}\nπŸ“‘ Auto-refreshing every 10 seconds...",
274
+ "",
275
+ gr.update(visible=False),
276
+ f"Job ID: {job_id}",
277
+ job_state,
278
+ gr.update(visible=True, value="πŸ”„ Auto-refresh active"),
279
+ stats_display
280
+ )
281
+
282
+ except Exception as e:
283
+ print(f"❌ Error submitting transcription: {str(e)}")
284
+ return (
285
+ f"Error: {str(e)}",
286
+ "",
287
+ gr.update(visible=False),
288
+ "",
289
+ {},
290
+ gr.update(visible=False),
291
+ gr.update()
292
+ )
293
+
294
+ def check_current_job_status(job_state, user):
295
+ """Check status of current job"""
296
+ if not user:
297
+ return (
298
+ "❌ Please log in to check status",
299
+ "",
300
+ gr.update(visible=False),
301
+ "",
302
+ gr.update(visible=False),
303
+ gr.update()
304
+ )
305
+
306
+ if not job_state or 'current_job_id' not in job_state:
307
+ return (
308
+ "No active job",
309
+ "",
310
+ gr.update(visible=False),
311
+ "",
312
+ gr.update(visible=False),
313
+ gr.update()
314
+ )
315
+
316
+ job_id = job_state['current_job_id']
317
+
318
+ try:
319
+ job = transcription_manager.get_job_status(job_id)
320
+ if not job or job.user_id != user.user_id:
321
+ return (
322
+ "Job not found or access denied",
323
+ "",
324
+ gr.update(visible=False),
325
+ "",
326
+ gr.update(visible=False),
327
+ gr.update()
328
+ )
329
+
330
+ # Calculate processing time
331
+ processing_time = format_processing_time(job.created_at, job.completed_at)
332
+
333
+ # Only log status changes to reduce spam
334
+ last_status = job_state.get('last_status', '')
335
+ if job.status != last_status:
336
+ print(f"πŸ”„ [{user.username}] Job status change: {last_status} β†’ {job.status} ({job.original_filename})")
337
+ job_state['last_status'] = job.status
338
+
339
+ # Get updated user stats
340
+ stats_display = get_user_stats_display(user)
341
+
342
+ # Check for completed status AND transcript availability
343
+ if job.status == 'completed' and job.transcript_text and job.transcript_text.strip():
344
+ # Job is complete and transcript is available, stop auto-refresh
345
+ job_state['auto_refresh_active'] = False
346
+ # Create downloadable file
347
+ transcript_file = create_transcript_file(job.transcript_text, job_id)
348
+ return (
349
+ f"βœ… Transcription completed in {processing_time}",
350
+ job.transcript_text,
351
+ gr.update(visible=True, value=transcript_file),
352
+ f"Processed: {job.original_filename}",
353
+ gr.update(visible=False), # Hide auto-refresh status
354
+ stats_display
355
+ )
356
+ elif job.status == 'failed':
357
+ # Job failed, stop auto-refresh
358
+ job_state['auto_refresh_active'] = False
359
+ return (
360
+ f"❌ Transcription failed after {processing_time}",
361
+ "",
362
+ gr.update(visible=False),
363
+ f"Error: {job.error_message[:100]}..." if job.error_message else "Unknown error",
364
+ gr.update(visible=False),
365
+ stats_display
366
+ )
367
+ elif job.status == 'processing':
368
+ # Still processing, continue auto-refresh
369
+ auto_refresh_active = job_state.get('auto_refresh_active', False)
370
+ return (
371
+ f"πŸ”„ Processing... ({processing_time} elapsed)\nπŸ“‘ Auto-refreshing every 10 seconds...",
372
+ "",
373
+ gr.update(visible=False),
374
+ f"Converting and analyzing: {job.original_filename}",
375
+ gr.update(visible=True, value="πŸ”„ Auto-refresh active") if auto_refresh_active else gr.update(visible=False),
376
+ stats_display
377
+ )
378
+ elif job.status == 'completed' and (not job.transcript_text or not job.transcript_text.strip()):
379
+ # Job marked as completed but transcript not yet available - keep refreshing
380
+ auto_refresh_active = job_state.get('auto_refresh_active', False)
381
+ return (
382
+ f"πŸ”„ Finalizing transcript... ({processing_time} elapsed)\nπŸ“‘ Auto-refreshing every 10 seconds...",
383
+ "",
384
+ gr.update(visible=False),
385
+ f"Retrieving results: {job.original_filename}",
386
+ gr.update(visible=True, value="πŸ”„ Auto-refresh active") if auto_refresh_active else gr.update(visible=False),
387
+ stats_display
388
+ )
389
+ else: # pending
390
+ # Still pending, continue auto-refresh
391
+ auto_refresh_active = job_state.get('auto_refresh_active', False)
392
+ return (
393
+ f"⏳ Queued for processing... ({processing_time} waiting)\nπŸ“‘ Auto-refreshing every 10 seconds...",
394
+ "",
395
+ gr.update(visible=False),
396
+ f"Waiting: {job.original_filename}",
397
+ gr.update(visible=True, value="πŸ”„ Auto-refresh active") if auto_refresh_active else gr.update(visible=False),
398
+ stats_display
399
+ )
400
+
401
+ except Exception as e:
402
+ print(f"❌ Error checking job status: {str(e)}")
403
+ return (
404
+ f"Error checking status: {str(e)}",
405
+ "",
406
+ gr.update(visible=False),
407
+ "",
408
+ gr.update(visible=False),
409
+ gr.update()
410
+ )
411
+
412
+ def should_auto_refresh(job_state, user):
413
+ """Check if auto-refresh should be active"""
414
+ if not user or not job_state or not job_state.get('auto_refresh_active', False):
415
+ return False
416
+
417
+ if 'current_job_id' not in job_state:
418
+ return False
419
+
420
+ job_id = job_state['current_job_id']
421
+ job = transcription_manager.get_job_status(job_id)
422
+
423
+ if not job or job.user_id != user.user_id:
424
+ return False
425
+
426
+ # Continue auto-refresh until job is completed AND transcript is available, or job failed
427
+ if job.status == 'failed':
428
+ return False
429
+ elif job.status == 'completed' and job.transcript_text and job.transcript_text.strip():
430
+ return False
431
+ else:
432
+ return True
433
+
434
+ def auto_refresh_status(job_state, user):
435
+ """Auto-refresh function that only runs when needed"""
436
+ if should_auto_refresh(job_state, user):
437
+ return check_current_job_status(job_state, user)
438
+ else:
439
+ return (
440
+ gr.update(), # No change to status_display
441
+ gr.update(), # No change to transcript_output
442
+ gr.update(), # No change to download_file
443
+ gr.update(), # No change to job_info
444
+ gr.update(visible=False), # Hide auto-refresh indicator
445
+ gr.update() # No change to user stats
446
+ )
447
+
448
+ def stop_auto_refresh(job_state, user):
449
+ """Manually stop auto-refresh"""
450
+ if job_state:
451
+ job_state['auto_refresh_active'] = False
452
+ if user:
453
+ print(f"⏹️ [{user.username}] Auto-refresh stopped by user")
454
+ return gr.update(visible=False)
455
+
456
+ # History Functions
457
+ def get_user_history_table(user, show_all_user_transcriptions=False):
458
+ """Get user history as a formatted table - PDPA compliant"""
459
+ if not user:
460
+ return []
461
+
462
+ try:
463
+ if show_all_user_transcriptions:
464
+ # Show ALL transcriptions for current user
465
+ jobs = transcription_manager.get_user_history(user.user_id, limit=100)
466
+ else:
467
+ # Show recent transcriptions for current user
468
+ jobs = transcription_manager.get_user_history(user.user_id, limit=20)
469
+
470
+ if not jobs:
471
+ return []
472
+
473
+ # Create table data
474
+ table_data = []
475
+
476
+ for job in jobs:
477
+ # Format datetime
478
+ try:
479
+ created_time = datetime.fromisoformat(job.created_at)
480
+ formatted_date = created_time.strftime("%Y-%m-%d %H:%M")
481
+ except:
482
+ formatted_date = job.created_at[:16]
483
+
484
+ # Status with emoji
485
+ status_display = format_status(job.status)
486
+
487
+ # Processing time
488
+ time_display = format_processing_time(job.created_at, job.completed_at)
489
+
490
+ # Job ID display (shortened for table)
491
+ job_id_display = job.job_id[:8] + "..." if len(job.job_id) > 8 else job.job_id
492
+
493
+ # Language
494
+ language_display = ALLOWED_LANGS.get(job.language, job.language)
495
+
496
+ # Create download status
497
+ if job.status == 'completed' and job.transcript_text:
498
+ download_link = f"πŸ“„ Available"
499
+ elif job.status == 'processing':
500
+ download_link = "πŸ”„ Processing..."
501
+ elif job.status == 'pending':
502
+ download_link = "⏳ Queued..."
503
+ elif job.status == 'failed':
504
+ download_link = "❌ Failed"
505
+ else:
506
+ download_link = "⚠️ Unknown"
507
+
508
+ table_data.append([
509
+ formatted_date,
510
+ job.original_filename,
511
+ language_display,
512
+ status_display,
513
+ time_display,
514
+ job_id_display,
515
+ download_link
516
+ ])
517
+
518
+ return table_data
519
+
520
+ except Exception as e:
521
+ print(f"❌ Error loading user history: {str(e)}")
522
+ return []
523
+
524
+ def refresh_history_and_downloads(user, show_all_user_transcriptions=False):
525
+ """Refresh history table and create download files"""
526
+ if not user:
527
+ return (
528
+ [],
529
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False),
530
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False),
531
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False),
532
+ gr.update(visible=False),
533
+ gr.update()
534
+ )
535
+
536
+ try:
537
+ if show_all_user_transcriptions:
538
+ jobs = transcription_manager.get_user_history(user.user_id, limit=100)
539
+ else:
540
+ jobs = transcription_manager.get_user_history(user.user_id, limit=20)
541
+
542
+ # Get table data
543
+ table_data = get_user_history_table(user, show_all_user_transcriptions)
544
+
545
+ # Create download files for completed jobs
546
+ download_updates = []
547
+ completed_jobs = [job for job in jobs if job.status == 'completed' and job.transcript_text]
548
+
549
+ for i in range(10): # We have 10 download file components
550
+ if i < len(completed_jobs):
551
+ job = completed_jobs[i]
552
+ # Create transcript file
553
+ transcript_file = create_transcript_file(job.transcript_text, job.job_id)
554
+
555
+ # Create label with filename and job info
556
+ label = f"πŸ“„ {job.original_filename} ({job.created_at[:10]})"
557
+
558
+ download_updates.append(
559
+ gr.update(visible=True, value=transcript_file, label=label)
560
+ )
561
+ else:
562
+ download_updates.append(gr.update(visible=False))
563
+
564
+ # Get updated user stats
565
+ stats_display = get_user_stats_display(user)
566
+
567
+ return [table_data] + download_updates + [stats_display]
568
+
569
+ except Exception as e:
570
+ print(f"❌ Error refreshing user history: {str(e)}")
571
+ return (
572
+ [],
573
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False),
574
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False),
575
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False),
576
+ gr.update(visible=False),
577
+ gr.update()
578
+ )
579
+
580
+ def on_history_refresh_click(user, show_all_user_transcriptions):
581
+ """Manual history refresh"""
582
+ if user:
583
+ print(f"πŸ”„ [{user.username}] User refreshed history (show_all: {show_all_user_transcriptions})")
584
+ return refresh_history_and_downloads(user, show_all_user_transcriptions)
585
+
586
+ def on_history_tab_select(user):
587
+ """Auto-refresh history when history tab is selected"""
588
+ if user:
589
+ print(f"πŸ“š [{user.username}] History tab opened, refreshing data...")
590
+ return refresh_history_and_downloads(user, show_all_user_transcriptions=False)
591
+
592
+ # PDPA Compliance Functions
593
+ def export_user_data(user):
594
+ """Export user data for GDPR compliance"""
595
+ if not user:
596
+ return "❌ Please log in to export your data", gr.update(visible=False)
597
+
598
+ try:
599
+ export_data = transcription_manager.export_user_data(user.user_id)
600
+
601
+ # Create export file
602
+ os.makedirs("temp", exist_ok=True)
603
+ filename = f"temp/user_data_export_{user.user_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
604
+
605
+ with open(filename, "w", encoding="utf-8") as f:
606
+ json.dump(export_data, f, indent=2, ensure_ascii=False)
607
+
608
+ print(f"πŸ“¦ [{user.username}] Data export created")
609
+ return "βœ… Your data has been exported successfully", gr.update(visible=True, value=filename, label="Download Your Data Export")
610
+
611
+ except Exception as e:
612
+ print(f"❌ Error exporting user data: {str(e)}")
613
+ return f"❌ Export failed: {str(e)}", gr.update(visible=False)
614
+
615
+ def update_marketing_consent(user, marketing_consent):
616
+ """Update user's marketing consent"""
617
+ if not user:
618
+ return "❌ Please log in to update consent"
619
+
620
+ try:
621
+ success = transcription_manager.update_user_consent(user.user_id, marketing_consent)
622
+ if success:
623
+ user.marketing_consent = marketing_consent
624
+ print(f"πŸ“§ [{user.username}] Marketing consent updated: {marketing_consent}")
625
+ return f"βœ… Marketing consent updated successfully"
626
+ else:
627
+ return "❌ Failed to update consent"
628
+ except Exception as e:
629
+ return f"❌ Error: {str(e)}"
630
+
631
+ def delete_user_account(user, confirmation_text):
632
+ """Delete user account and all data"""
633
+ if not user:
634
+ return "❌ Please log in to delete account", None, gr.update(visible=True), gr.update(visible=False)
635
+
636
+ if confirmation_text != "DELETE MY ACCOUNT":
637
+ return "❌ Please type 'DELETE MY ACCOUNT' to confirm", user, gr.update(visible=False), gr.update(visible=True)
638
+
639
+ try:
640
+ success = transcription_manager.delete_user_account(user.user_id)
641
+ if success:
642
+ print(f"πŸ—‘οΈ [{user.username}] Account deleted successfully")
643
+ return "βœ… Your account and all data have been permanently deleted", None, gr.update(visible=True), gr.update(visible=False)
644
+ else:
645
+ return "❌ Failed to delete account", user, gr.update(visible=False), gr.update(visible=True)
646
+ except Exception as e:
647
+ return f"❌ Error: {str(e)}", user, gr.update(visible=False), gr.update(visible=True)
648
+
649
+ def on_user_login(user):
650
+ """Update UI components when user logs in"""
651
+ if user:
652
+ return gr.update(value=user.marketing_consent)
653
+ else:
654
+ return gr.update(value=False)
655
+
656
+ def check_system_status():
657
+ """Check if the system is properly initialized"""
658
+ try:
659
+ # Test database connection
660
+ if transcription_manager and transcription_manager.db:
661
+ # Try a simple database operation
662
+ test_stats = transcription_manager.db.get_user_stats("test_user_id")
663
+ print("βœ… System initialization successful")
664
+ return "πŸ‘€ Please log in to view your statistics..."
665
+ else:
666
+ print("❌ System initialization failed - transcription manager not available")
667
+ return "❌ System initialization failed - please check configuration"
668
+ except Exception as e:
669
+ print(f"❌ System initialization error: {str(e)}")
670
+ return f"❌ System error: {str(e)}"
671
+
672
+ def create_transcript_file(transcript_text, job_id):
673
+ """Create a downloadable transcript file"""
674
+ os.makedirs("temp", exist_ok=True)
675
+ filename = f"temp/transcript_{job_id}.txt"
676
+ with open(filename, "w", encoding="utf-8") as f:
677
+ f.write(transcript_text)
678
+ return filename
679
+
680
+ # Enhanced CSS with authentication styling
681
+ enhanced_css = """
682
+ /* Main container styling */
683
+ .gradio-container {
684
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
685
+ font-family: 'Segoe UI', system-ui, sans-serif;
686
+ color: #212529;
687
+ }
688
+
689
+ /* Card styling */
690
+ .gr-box {
691
+ background: white;
692
+ border: 1px solid #dee2e6;
693
+ border-radius: 12px;
694
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
695
+ padding: 20px;
696
+ margin: 10px 0;
697
+ }
698
+
699
+ /* Button styling */
700
+ .gr-button {
701
+ background: linear-gradient(135deg, #007bff, #0056b3);
702
+ border: none;
703
+ border-radius: 8px;
704
+ color: white;
705
+ font-weight: 500;
706
+ padding: 12px 24px;
707
+ transition: all 0.2s ease;
708
+ box-shadow: 0 2px 4px rgba(0,123,255,0.2);
709
+ }
710
+
711
+ .gr-button:hover {
712
+ background: linear-gradient(135deg, #0056b3, #004085);
713
+ transform: translateY(-1px);
714
+ box-shadow: 0 4px 8px rgba(0,123,255,0.3);
715
+ }
716
+
717
+ .gr-button[variant="secondary"] {
718
+ background: linear-gradient(135deg, #6c757d, #495057);
719
+ }
720
+
721
+ .gr-button[variant="secondary"]:hover {
722
+ background: linear-gradient(135deg, #495057, #343a40);
723
+ }
724
+
725
+ /* Login/Register button styling */
726
+ .auth-button {
727
+ background: linear-gradient(135deg, #28a745, #1e7e34);
728
+ min-width: 120px;
729
+ }
730
+
731
+ .auth-button:hover {
732
+ background: linear-gradient(135deg, #1e7e34, #155724);
733
+ }
734
+
735
+ .danger-button {
736
+ background: linear-gradient(135deg, #dc3545, #c82333);
737
+ }
738
+
739
+ .danger-button:hover {
740
+ background: linear-gradient(135deg, #c82333, #a71e2a);
741
+ }
742
+
743
+ /* Input styling */
744
+ .gr-textbox, .gr-dropdown, .gr-file {
745
+ border: 2px solid #e9ecef;
746
+ border-radius: 8px;
747
+ background: white;
748
+ color: #212529;
749
+ transition: border-color 0.2s ease;
750
+ }
751
+
752
+ .gr-textbox:focus, .gr-dropdown:focus {
753
+ border-color: #007bff;
754
+ box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
755
+ }
756
+
757
+ /* Status styling */
758
+ .status-display {
759
+ background: linear-gradient(135deg, #e3f2fd, #bbdefb);
760
+ border-left: 4px solid #2196f3;
761
+ padding: 15px;
762
+ border-radius: 0 8px 8px 0;
763
+ margin: 10px 0;
764
+ }
765
+
766
+ .success-status {
767
+ background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
768
+ border-left-color: #4caf50;
769
+ }
770
+
771
+ .error-status {
772
+ background: linear-gradient(135deg, #ffebee, #ffcdd2);
773
+ border-left-color: #f44336;
774
+ }
775
+
776
+ /* Auto-refresh indicator styling */
777
+ .auto-refresh-indicator {
778
+ background: linear-gradient(135deg, #fff3cd, #ffeaa7);
779
+ border: 1px solid #ffeaa7;
780
+ border-radius: 6px;
781
+ padding: 8px 12px;
782
+ font-size: 12px;
783
+ color: #856404;
784
+ text-align: center;
785
+ animation: pulse 2s infinite;
786
+ }
787
+
788
+ @keyframes pulse {
789
+ 0% { opacity: 1; }
790
+ 50% { opacity: 0.7; }
791
+ 100% { opacity: 1; }
792
+ }
793
+
794
+ /* User stats styling */
795
+ .user-stats {
796
+ background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
797
+ border: 1px solid #c8e6c9;
798
+ border-radius: 6px;
799
+ padding: 8px 12px;
800
+ font-size: 12px;
801
+ color: #2e7d32;
802
+ text-align: center;
803
+ font-weight: 500;
804
+ }
805
+
806
+ /* Authentication form styling */
807
+ .auth-form {
808
+ background: white;
809
+ border: 2px solid #007bff;
810
+ border-radius: 12px;
811
+ padding: 25px;
812
+ box-shadow: 0 4px 12px rgba(0,123,255,0.15);
813
+ }
814
+
815
+ /* Privacy notice styling */
816
+ .privacy-notice {
817
+ background: linear-gradient(135deg, #e3f2fd, #bbdefb);
818
+ border: 1px solid #2196f3;
819
+ border-radius: 8px;
820
+ padding: 15px;
821
+ font-size: 14px;
822
+ color: #1976d2;
823
+ margin: 10px 0;
824
+ }
825
+
826
+ /* PDPA section styling */
827
+ .pdpa-section {
828
+ background: linear-gradient(135deg, #fff3cd, #ffeaa7);
829
+ border: 1px solid #ffc107;
830
+ border-radius: 8px;
831
+ padding: 15px;
832
+ margin: 10px 0;
833
+ }
834
+
835
+ /* History table styling */
836
+ .history-table {
837
+ background: white;
838
+ border: 1px solid #dee2e6;
839
+ border-radius: 8px;
840
+ font-size: 14px;
841
+ }
842
+
843
+ .history-table thead th {
844
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
845
+ color: #495057;
846
+ font-weight: 600;
847
+ padding: 12px;
848
+ border-bottom: 2px solid #dee2e6;
849
+ }
850
+
851
+ .history-table tbody tr {
852
+ cursor: pointer;
853
+ transition: background-color 0.2s ease;
854
+ }
855
+
856
+ .history-table tbody tr:hover {
857
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
858
+ }
859
+
860
+ .history-table tbody tr:nth-child(even) {
861
+ background: #f8f9fa;
862
+ }
863
+
864
+ .history-table tbody tr:nth-child(even):hover {
865
+ background: linear-gradient(135deg, #e9ecef, #dee2e6);
866
+ }
867
+
868
+ .history-table tbody td {
869
+ padding: 10px;
870
+ border-bottom: 1px solid #dee2e6;
871
+ vertical-align: middle;
872
+ }
873
+
874
+ /* Tab styling */
875
+ .tab-nav {
876
+ background: white;
877
+ border-bottom: 2px solid #dee2e6;
878
+ border-radius: 8px 8px 0 0;
879
+ }
880
+
881
+ /* Header styling */
882
+ .main-header {
883
+ background: white;
884
+ border: 1px solid #dee2e6;
885
+ border-radius: 12px;
886
+ padding: 25px;
887
+ text-align: center;
888
+ margin-bottom: 20px;
889
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
890
+ }
891
+
892
+ .main-header h1 {
893
+ color: #007bff;
894
+ margin-bottom: 10px;
895
+ font-size: 2.2em;
896
+ font-weight: 600;
897
+ }
898
+
899
+ .main-header p {
900
+ color: #6c757d;
901
+ font-size: 1.1em;
902
+ margin: 0;
903
+ }
904
+ """
905
+
906
+ # Create the main interface
907
+ with gr.Blocks(
908
+ theme=gr.themes.Soft(
909
+ primary_hue="blue",
910
+ secondary_hue="gray",
911
+ neutral_hue="gray",
912
+ font=["system-ui", "sans-serif"]
913
+ ),
914
+ css=enhanced_css,
915
+ title="πŸŽ™οΈ Azure Speech Transcription - Secure & PDPA Compliant"
916
+ ) as demo:
917
+
918
+ # Global state
919
+ current_user = gr.State(None)
920
+ job_state = gr.State({})
921
+
922
+ # Header
923
+ with gr.Row():
924
+ gr.HTML("""
925
+ <div class="main-header">
926
+ <h1>πŸŽ™οΈ Azure Speech Transcription</h1>
927
+ <p>Secure, PDPA-compliant transcription service with user authentication and privacy protection</p>
928
+ </div>
929
+ """)
930
+
931
+ # User stats display
932
+ user_stats_display = gr.Textbox(
933
+ label="",
934
+ lines=1,
935
+ interactive=False,
936
+ show_label=False,
937
+ placeholder="πŸ‘€ Please log in to view your statistics...",
938
+ elem_classes=["user-stats"]
939
+ )
940
+
941
+ # Authentication Section
942
+ with gr.Column(visible=True, elem_classes=["auth-form"]) as auth_section:
943
+ gr.Markdown("## πŸ” Authentication Required")
944
+ gr.Markdown("Please log in or create an account to use the transcription service.")
945
+
946
+ with gr.Tabs() as auth_tabs:
947
+ # Login Tab
948
+ with gr.Tab("πŸ”‘ Login") as login_tab:
949
+ with gr.Column():
950
+ login_email = gr.Textbox(
951
+ label="Email or Username",
952
+ placeholder="Enter your email or username"
953
+ )
954
+ login_password = gr.Textbox(
955
+ label="Password",
956
+ type="password",
957
+ placeholder="Enter your password"
958
+ )
959
+
960
+ with gr.Row():
961
+ login_btn = gr.Button("πŸ”‘ Login", variant="primary", elem_classes=["auth-button"])
962
+
963
+ login_status = gr.Textbox(
964
+ label="",
965
+ show_label=False,
966
+ interactive=False,
967
+ placeholder="Enter your credentials and click Login"
968
+ )
969
+
970
+ # Register Tab
971
+ with gr.Tab("πŸ“ Register") as register_tab:
972
+ with gr.Column():
973
+ reg_email = gr.Textbox(
974
+ label="Email",
975
+ placeholder="Enter your email address"
976
+ )
977
+ reg_username = gr.Textbox(
978
+ label="Username",
979
+ placeholder="Choose a username (3-30 characters, alphanumeric and underscore)"
980
+ )
981
+ reg_password = gr.Textbox(
982
+ label="Password",
983
+ type="password",
984
+ placeholder="Create a strong password (min 8 chars, mixed case, numbers)"
985
+ )
986
+ reg_confirm_password = gr.Textbox(
987
+ label="Confirm Password",
988
+ type="password",
989
+ placeholder="Confirm your password"
990
+ )
991
+
992
+ gr.Markdown("### πŸ“‹ Privacy & Data Consent")
993
+
994
+ with gr.Column(elem_classes=["privacy-notice"]):
995
+ gr.Markdown("""
996
+ **Privacy Notice**: By creating an account, you acknowledge that:
997
+ - Your data will be stored securely in user-separated Azure Blob Storage
998
+ - Transcriptions are processed using Azure Speech Services
999
+ - You can export or delete your data at any time
1000
+ - We comply with GDPR and data protection regulations
1001
+ """)
1002
+
1003
+ gdpr_consent = gr.Checkbox(
1004
+ label="I consent to the processing of my personal data as described in the Privacy Notice (Required)",
1005
+ value=False
1006
+ )
1007
+ data_retention_consent = gr.Checkbox(
1008
+ label="I agree to data retention for transcription service functionality (Required)",
1009
+ value=False
1010
+ )
1011
+ marketing_consent = gr.Checkbox(
1012
+ label="I consent to receiving marketing communications (Optional)",
1013
+ value=False
1014
+ )
1015
+
1016
+ with gr.Row():
1017
+ register_btn = gr.Button("πŸ“ Create Account", variant="primary", elem_classes=["auth-button"])
1018
+
1019
+ register_status = gr.Textbox(
1020
+ label="",
1021
+ show_label=False,
1022
+ interactive=False,
1023
+ placeholder="Fill out the form and agree to the required consents to create your account"
1024
+ )
1025
+
1026
+ login_after_register = gr.Button(
1027
+ "πŸ”‘ Go to Login",
1028
+ visible=False,
1029
+ variant="secondary"
1030
+ )
1031
+
1032
+ # Main Application (visible only when logged in)
1033
+ with gr.Column(visible=False) as main_app:
1034
+
1035
+ # Logout button
1036
+ with gr.Row():
1037
+ with gr.Column(scale=3):
1038
+ pass
1039
+ with gr.Column(scale=1):
1040
+ logout_btn = gr.Button("πŸ‘‹ Logout", variant="secondary")
1041
+
1042
+ # Main transcription interface
1043
+ with gr.Tab("πŸŽ™οΈ Transcribe"):
1044
+ with gr.Row():
1045
+ # Left column - Input settings
1046
+ with gr.Column(scale=1):
1047
+ gr.Markdown("### πŸ“ Upload File")
1048
+
1049
+ file_upload = gr.File(
1050
+ label="Audio or Video File",
1051
+ type="filepath",
1052
+ file_types=[
1053
+ ".wav", ".mp3", ".ogg", ".opus", ".flac", ".wma", ".aac",
1054
+ ".m4a", ".amr", ".webm", ".speex",
1055
+ ".mp4", ".mov", ".avi", ".mkv", ".wmv", ".flv", ".3gp"
1056
+ ]
1057
+ )
1058
+
1059
+ with gr.Row():
1060
+ language = gr.Dropdown(
1061
+ choices=[(v, k) for k, v in ALLOWED_LANGS.items()],
1062
+ label="Language",
1063
+ value="en-US"
1064
+ )
1065
+ audio_format = gr.Dropdown(
1066
+ choices=AUDIO_FORMATS,
1067
+ value="wav",
1068
+ label="Output Format"
1069
+ )
1070
+
1071
+ gr.Markdown("### βš™οΈ Settings")
1072
+
1073
+ with gr.Row():
1074
+ diarization_enabled = gr.Checkbox(
1075
+ label="Speaker Identification",
1076
+ value=True
1077
+ )
1078
+ speakers = gr.Slider(
1079
+ minimum=1,
1080
+ maximum=10,
1081
+ step=1,
1082
+ value=2,
1083
+ label="Max Speakers"
1084
+ )
1085
+
1086
+ with gr.Row():
1087
+ timestamps = gr.Checkbox(
1088
+ label="Timestamps",
1089
+ value=True
1090
+ )
1091
+ profanity = gr.Dropdown(
1092
+ choices=["masked", "removed", "raw"],
1093
+ value="masked",
1094
+ label="Profanity Filter"
1095
+ )
1096
+
1097
+ with gr.Row():
1098
+ punctuation = gr.Dropdown(
1099
+ choices=["automatic", "dictated", "none"],
1100
+ value="automatic",
1101
+ label="Punctuation"
1102
+ )
1103
+ lexical = gr.Checkbox(
1104
+ label="Lexical Form",
1105
+ value=False
1106
+ )
1107
+
1108
+ submit_btn = gr.Button(
1109
+ "πŸš€ Start Transcription",
1110
+ variant="primary",
1111
+ size="lg"
1112
+ )
1113
+
1114
+ # Right column - Results
1115
+ with gr.Column(scale=1):
1116
+ gr.Markdown("### πŸ“Š Status")
1117
+
1118
+ # Auto-refresh indicator
1119
+ auto_refresh_status_display = gr.Textbox(
1120
+ label="",
1121
+ lines=1,
1122
+ interactive=False,
1123
+ show_label=False,
1124
+ visible=False,
1125
+ elem_classes=["auto-refresh-indicator"]
1126
+ )
1127
+
1128
+ status_display = gr.Textbox(
1129
+ label="",
1130
+ lines=3,
1131
+ interactive=False,
1132
+ show_label=False,
1133
+ placeholder="Upload a file and click 'Start Transcription' to begin...\nStatus will auto-refresh every 10 seconds during processing.\nYour data is stored in your private user folder for PDPA compliance."
1134
+ )
1135
+
1136
+ job_info = gr.Textbox(
1137
+ label="",
1138
+ lines=1,
1139
+ interactive=False,
1140
+ show_label=False,
1141
+ placeholder=""
1142
+ )
1143
+
1144
+ with gr.Row():
1145
+ refresh_btn = gr.Button(
1146
+ "πŸ”„ Check Status",
1147
+ variant="secondary"
1148
+ )
1149
+ stop_refresh_btn = gr.Button(
1150
+ "⏹️ Stop Auto-Refresh",
1151
+ variant="secondary"
1152
+ )
1153
+
1154
+ gr.Markdown("### πŸ“„ Results")
1155
+
1156
+ transcript_output = gr.Textbox(
1157
+ label="Transcript",
1158
+ lines=12,
1159
+ interactive=False,
1160
+ placeholder="Your transcript with speaker identification and precise timestamps (HH:MM:SS) will appear here..."
1161
+ )
1162
+
1163
+ download_file = gr.File(
1164
+ label="Download",
1165
+ interactive=False,
1166
+ visible=False
1167
+ )
1168
+
1169
+ # History tab
1170
+ with gr.Tab("πŸ“š My History"):
1171
+ gr.Markdown("### πŸ“‹ Your Transcription History & Downloads")
1172
+ gr.Markdown("*View your personal transcription history and download completed transcripts (PDPA compliant - only your data)*")
1173
+
1174
+ with gr.Row():
1175
+ refresh_history_btn = gr.Button(
1176
+ "πŸ”„ Refresh My History & Downloads",
1177
+ variant="primary"
1178
+ )
1179
+ show_all_user_checkbox = gr.Checkbox(
1180
+ label="Show All My Transcriptions (not just recent 20)",
1181
+ value=False
1182
+ )
1183
+
1184
+ history_table = gr.Dataframe(
1185
+ headers=["Date", "Filename", "Language", "Status", "Duration", "Job ID", "Download"],
1186
+ datatype=["str", "str", "str", "str", "str", "str", "str"],
1187
+ col_count=(7, "fixed"),
1188
+ row_count=(15, "dynamic"),
1189
+ wrap=True,
1190
+ interactive=False,
1191
+ elem_classes=["history-table"]
1192
+ )
1193
+
1194
+ # Download Files Section
1195
+ gr.Markdown("### πŸ“₯ Download Your Completed Transcripts")
1196
+ gr.Markdown("*Your available transcript downloads will appear below after refreshing*")
1197
+
1198
+ # Container for dynamic download files
1199
+ with gr.Column():
1200
+ download_file_1 = gr.File(label="", visible=False, interactive=False)
1201
+ download_file_2 = gr.File(label="", visible=False, interactive=False)
1202
+ download_file_3 = gr.File(label="", visible=False, interactive=False)
1203
+ download_file_4 = gr.File(label="", visible=False, interactive=False)
1204
+ download_file_5 = gr.File(label="", visible=False, interactive=False)
1205
+ download_file_6 = gr.File(label="", visible=False, interactive=False)
1206
+ download_file_7 = gr.File(label="", visible=False, interactive=False)
1207
+ download_file_8 = gr.File(label="", visible=False, interactive=False)
1208
+ download_file_9 = gr.File(label="", visible=False, interactive=False)
1209
+ download_file_10 = gr.File(label="", visible=False, interactive=False)
1210
+
1211
+ # PDPA Compliance Tab
1212
+ with gr.Tab("πŸ”’ Privacy & Data"):
1213
+ gr.Markdown("### πŸ”’ GDPR & Data Protection")
1214
+ gr.Markdown("Manage your personal data and privacy settings in compliance with data protection regulations.")
1215
+
1216
+ with gr.Column(elem_classes=["pdpa-section"]):
1217
+ gr.Markdown("#### πŸ“Š Data Export")
1218
+ gr.Markdown("Download all your personal data including transcriptions, account info, and usage statistics.")
1219
+
1220
+ export_btn = gr.Button("πŸ“¦ Export My Data", variant="primary")
1221
+ export_status = gr.Textbox(
1222
+ label="",
1223
+ show_label=False,
1224
+ interactive=False,
1225
+ placeholder="Click 'Export My Data' to download your complete data archive"
1226
+ )
1227
+ export_file = gr.File(
1228
+ label="Your Data Export",
1229
+ visible=False,
1230
+ interactive=False
1231
+ )
1232
+
1233
+ with gr.Column(elem_classes=["pdpa-section"]):
1234
+ gr.Markdown("#### πŸ“§ Marketing Consent")
1235
+ gr.Markdown("Update your preferences for receiving marketing communications.")
1236
+
1237
+ marketing_consent_checkbox = gr.Checkbox(
1238
+ label="I consent to receiving marketing communications",
1239
+ value=False
1240
+ )
1241
+ update_consent_btn = gr.Button("βœ… Update Consent", variant="secondary")
1242
+ consent_status = gr.Textbox(
1243
+ label="",
1244
+ show_label=False,
1245
+ interactive=False,
1246
+ placeholder="Update your marketing consent preferences"
1247
+ )
1248
+
1249
+ with gr.Column(elem_classes=["pdpa-section"]):
1250
+ gr.Markdown("#### ⚠️ Account Deletion")
1251
+ gr.Markdown("""
1252
+ **Warning**: This action is irreversible and will permanently delete:
1253
+ - Your user account and profile
1254
+ - All transcription history and files
1255
+ - All data stored in Azure Blob Storage
1256
+ - Usage statistics and preferences
1257
+ """)
1258
+
1259
+ deletion_confirmation = gr.Textbox(
1260
+ label="Type 'DELETE MY ACCOUNT' to confirm",
1261
+ placeholder="Type the exact phrase to confirm account deletion"
1262
+ )
1263
+ delete_account_btn = gr.Button(
1264
+ "πŸ—‘οΈ Delete My Account",
1265
+ variant="stop",
1266
+ elem_classes=["danger-button"]
1267
+ )
1268
+ deletion_status = gr.Textbox(
1269
+ label="",
1270
+ show_label=False,
1271
+ interactive=False,
1272
+ placeholder="Account deletion requires confirmation text"
1273
+ )
1274
+
1275
+ # Auto-refresh timer
1276
+ timer = gr.Timer(10.0)
1277
+
1278
+ # Event handlers
1279
+
1280
+ # Authentication events
1281
+ login_btn.click(
1282
+ login_user,
1283
+ inputs=[login_email, login_password],
1284
+ outputs=[login_status, current_user, auth_section, main_app, user_stats_display]
1285
+ ).then(
1286
+ on_user_login,
1287
+ inputs=[current_user],
1288
+ outputs=[marketing_consent_checkbox]
1289
+ ).then(
1290
+ lambda user: ("", "") if user else (gr.update(), gr.update()), # Clear login fields on success
1291
+ inputs=[current_user],
1292
+ outputs=[login_email, login_password]
1293
+ )
1294
+
1295
+ register_btn.click(
1296
+ register_user,
1297
+ inputs=[reg_email, reg_username, reg_password, reg_confirm_password,
1298
+ gdpr_consent, data_retention_consent, marketing_consent],
1299
+ outputs=[register_status, login_after_register]
1300
+ ).then(
1301
+ lambda status: ("", "", "", "", False, False, False) if "βœ…" in status else (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()),
1302
+ inputs=[register_status],
1303
+ outputs=[reg_email, reg_username, reg_password, reg_confirm_password, gdpr_consent, data_retention_consent, marketing_consent]
1304
+ )
1305
+
1306
+ login_after_register.click(
1307
+ lambda: (gr.update(selected=0), ""), # Switch to login tab (index 0) and clear status
1308
+ outputs=[auth_tabs, register_status]
1309
+ )
1310
+
1311
+ logout_btn.click(
1312
+ logout_user,
1313
+ outputs=[current_user, login_status, auth_section, main_app, user_stats_display]
1314
+ )
1315
+
1316
+ # Transcription events
1317
+ submit_btn.click(
1318
+ submit_transcription,
1319
+ inputs=[
1320
+ file_upload, language, audio_format, diarization_enabled,
1321
+ speakers, profanity, punctuation, timestamps, lexical, current_user
1322
+ ],
1323
+ outputs=[status_display, transcript_output, download_file, job_info, job_state, auto_refresh_status_display, user_stats_display]
1324
+ )
1325
+
1326
+ refresh_btn.click(
1327
+ lambda job_state, user: (
1328
+ print("πŸ”„ User manually checked status") if user else None,
1329
+ check_current_job_status(job_state, user)
1330
+ )[1],
1331
+ inputs=[job_state, current_user],
1332
+ outputs=[status_display, transcript_output, download_file, job_info, auto_refresh_status_display, user_stats_display]
1333
+ )
1334
+
1335
+ stop_refresh_btn.click(
1336
+ stop_auto_refresh,
1337
+ inputs=[job_state, current_user],
1338
+ outputs=[auto_refresh_status_display]
1339
+ )
1340
+
1341
+ # Auto-refresh timer event
1342
+ timer.tick(
1343
+ auto_refresh_status,
1344
+ inputs=[job_state, current_user],
1345
+ outputs=[status_display, transcript_output, download_file, job_info, auto_refresh_status_display, user_stats_display]
1346
+ )
1347
+
1348
+ # History events
1349
+ refresh_history_btn.click(
1350
+ on_history_refresh_click,
1351
+ inputs=[current_user, show_all_user_checkbox],
1352
+ outputs=[
1353
+ history_table,
1354
+ download_file_1, download_file_2, download_file_3, download_file_4, download_file_5,
1355
+ download_file_6, download_file_7, download_file_8, download_file_9, download_file_10,
1356
+ user_stats_display
1357
+ ]
1358
+ )
1359
+
1360
+ show_all_user_checkbox.change(
1361
+ lambda user, show_all: (
1362
+ print(f"πŸ‘οΈ User toggled show all personal transcriptions: {show_all}") if user else None,
1363
+ refresh_history_and_downloads(user, show_all)
1364
+ )[1],
1365
+ inputs=[current_user, show_all_user_checkbox],
1366
+ outputs=[
1367
+ history_table,
1368
+ download_file_1, download_file_2, download_file_3, download_file_4, download_file_5,
1369
+ download_file_6, download_file_7, download_file_8, download_file_9, download_file_10,
1370
+ user_stats_display
1371
+ ]
1372
+ )
1373
+
1374
+ # PDPA compliance events
1375
+ export_btn.click(
1376
+ export_user_data,
1377
+ inputs=[current_user],
1378
+ outputs=[export_status, export_file]
1379
+ )
1380
+
1381
+ update_consent_btn.click(
1382
+ update_marketing_consent,
1383
+ inputs=[current_user, marketing_consent_checkbox],
1384
+ outputs=[consent_status]
1385
+ )
1386
+
1387
+ delete_account_btn.click(
1388
+ delete_user_account,
1389
+ inputs=[current_user, deletion_confirmation],
1390
+ outputs=[deletion_status, current_user, auth_section, main_app]
1391
+ )
1392
+
1393
+ # Auto-hide/show speakers slider
1394
+ diarization_enabled.change(
1395
+ lambda enabled: gr.update(visible=enabled),
1396
+ inputs=[diarization_enabled],
1397
+ outputs=[speakers]
1398
+ )
1399
+
1400
+ # Load user stats on app start and verify system is ready
1401
+ demo.load(
1402
+ lambda: (
1403
+ print("πŸš€ PDPA-Compliant Azure Speech Transcription Service Started..."),
1404
+ check_system_status()
1405
+ )[1],
1406
+ outputs=[user_stats_display]
1407
+ )
1408
+
1409
+ # Info section
1410
+ with demo:
1411
+ gr.HTML("""
1412
+ <div style="background: white; border: 1px solid #dee2e6; border-radius: 12px; padding: 20px; margin-top: 20px; color: #212529;">
1413
+ <h3 style="color: #007bff; margin-top: 0;">πŸ“‹ How to Use</h3>
1414
+ <ol style="line-height: 1.6;">
1415
+ <li><strong>Register/Login:</strong> Create an account or log in with existing credentials</li>
1416
+ <li><strong>Upload:</strong> Select your audio or video file</li>
1417
+ <li><strong>Configure:</strong> Choose language and enable speaker identification</li>
1418
+ <li><strong>Start:</strong> Click "Start Transcription" - status will auto-update every 10 seconds</li>
1419
+ <li><strong>Download:</strong> Get your transcript with speaker identification and timestamps</li>
1420
+ <li><strong>Manage:</strong> Use Privacy & Data tab to export or delete your data</li>
1421
+ </ol>
1422
+
1423
+ <h3 style="color: #007bff;">🎡 Supported Formats</h3>
1424
+ <p><strong>Audio:</strong> WAV, MP3, OGG, OPUS, FLAC, WMA, AAC, M4A, AMR, WebM, Speex</p>
1425
+ <p><strong>Video:</strong> MP4, MOV, AVI, MKV, WMV, FLV, 3GP</p>
1426
+
1427
+ <h3 style="color: #007bff;">πŸ”’ Security & Privacy Features</h3>
1428
+ <ul style="line-height: 1.6;">
1429
+ <li><strong>βœ… User Authentication:</strong> Secure registration and login system</li>
1430
+ <li><strong>βœ… Password Security:</strong> Strong password requirements and secure hashing</li>
1431
+ <li><strong>βœ… User-Separated Storage:</strong> Each user has isolated folders in Azure Blob Storage</li>
1432
+ <li><strong>βœ… GDPR Compliance:</strong> Full data export and account deletion capabilities</li>
1433
+ <li><strong>βœ… Consent Management:</strong> Granular consent controls for data processing</li>
1434
+ <li><strong>βœ… Privacy by Design:</strong> Users can only access their own data</li>
1435
+ <li><strong>βœ… Audit Trail:</strong> Comprehensive logging for compliance and security</li>
1436
+ <li><strong>βœ… Data Retention:</strong> Clear data retention policies and user control</li>
1437
+ </ul>
1438
+
1439
+ <h3 style="color: #007bff;">🎯 Enhanced Features</h3>
1440
+ <ul style="line-height: 1.6;">
1441
+ <li><strong>Enhanced Timestamps:</strong> Precise timing for each speaker segment (HH:MM:SS format)</li>
1442
+ <li><strong>Better Speaker Diarization:</strong> Improved speaker identification with timestamps</li>
1443
+ <li><strong>Personal Statistics:</strong> Real-time usage tracking and analytics</li>
1444
+ <li><strong>Complete History:</strong> View all your transcriptions or recent ones</li>
1445
+ <li><strong>Direct Downloads:</strong> Easy access to completed transcriptions</li>
1446
+ <li><strong>Data Export:</strong> Download all your data in JSON format</li>
1447
+ <li><strong>Account Management:</strong> Full control over your account and data</li>
1448
+ </ul>
1449
+
1450
+ <h3 style="color: #007bff;">πŸ’‘ Tips</h3>
1451
+ <ul style="line-height: 1.6;">
1452
+ <li>Use a strong password with mixed case letters, numbers, and symbols</li>
1453
+ <li>WAV files process fastest for transcription</li>
1454
+ <li>Enable speaker identification for meetings and interviews</li>
1455
+ <li>Auto-refresh continues until transcript is fully retrieved</li>
1456
+ <li>Visit Privacy & Data tab to manage your data and consent preferences</li>
1457
+ <li>You can export all your data or delete your account at any time</li>
1458
+ <li>All your files are stored in your private, secure user folder</li>
1459
+ <li>Your data is protected according to GDPR and privacy regulations</li>
1460
+ </ul>
1461
+ </div>
1462
+ """)
1463
+
1464
+ if __name__ == "__main__":
1465
+ print("πŸš€ Starting Secure PDPA-Compliant Azure Speech Transcription Service...")
1466
+ demo.launch(
1467
+ server_name="0.0.0.0",
1468
+ server_port=7861,
1469
+ share=False,
1470
+ show_error=True
1471
+ )
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio
2
+ python-dotenv
3
+ azure-storage-blob