Upload projects
Browse files- .env +17 -0
- Developer.md +1904 -0
- USER.md +459 -0
- app_core.py +1133 -0
- gradio_app.py +1471 -0
- 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
|